
这不是一篇简单的“调API”教程,而是一份从架构设计到生产部署的完整工程实践记录。
去年年底,团队接到了一个挑战:为一家人力资源SaaS服务商搭建一套AI智能面试对话平台。需求很直接——用AI替代第一轮结构化面试,候选人通过网页与AI面试官对话,系统自动评估并生成报告。
听起来并不复杂?OpenAI的API加上一个网页聊天界面似乎就能搞定。
但真正的挑战在于:并发峰值500+面试同时进行,每场面试需要30-40轮有深度的追问对话,响应延迟要控制在2秒内,并且面试数据不能出域(私有化部署要求)。
经过三个月的迭代,我们交付了一套基于微服务架构的AI面试平台。本文将完整复盘其中的技术选型、架构设计、关键实现与踩坑经验。
先明确业务边界:
┌─────────────────────────────────────────────────────┐
│ 面试全流程 │
│ 候选人端 → AI面试官 → 对话引擎 → 评估中心 → 报告 │
└─────────────────────────────────────────────────────┘核心能力拆解为三个维度:
能力域 | 具体需求 |
|---|---|
对话智能 | 多轮追问、情绪感知、防作弊检测、多语言支持 |
高并发 | 500路并发面试,每路独立会话状态 |
数据安全 | 私有化部署、对话脱敏、审计日志 |
可观测 | 全链路追踪、模型耗时监控、Token消耗统计 |
选型结论:Go + Java 混合栈
为什么不用单一语言?Go的并发模型在处理大量长连接时优势明显,而Java生态在复杂业务建模和批处理上更成熟。二者通过gRPC通信,各取所长。
选型结论:多模型路由 + 本地化小模型
场景 | 模型方案 |
|---|---|
核心对话 | 私有化部署的Qwen-72B(4bit量化) |
追问生成 | 混元/GLM-4(API调用,备用) |
情绪识别 | 自研200M参数BERT变体(CPU推理) |
防作弊 | 本地规则引擎 + 轻量分类器 |
核心原则:主模型本地化保证数据安全,辅助模型用云端API降本。实际运行中,约70%的请求走本地Qwen,30%路由到云端(当本地负载过高或模型不确定时)。
服务注册/配置: Nacos + Apollo
链路追踪: Jaeger (自定义Span)
监控: Prometheus + Grafana
日志: ELK (结构化日志)
消息队列: RabbitMQ (异步评估任务)
缓存: Redis (会话状态 + 限流)
数据库: PostgreSQL (主) + TiDB (历史归档)
容器编排: K8s (自建机房)
网关: Kong (自定义插件) ┌─────────────┐
│ Kong网关 │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
┌────▼───┐ ┌────▼───┐ ┌────▼───┐
│会话管理 │ │对话引擎 │ │ 评估 │
│(Go) │ │(Go) │ │(Java) │
└────┬───┘ └────┬───┘ └────┬───┘
│ │ │
└────────────┼────────────┘
│
┌──────▼──────┐
│ 模型网关 │
│ (路由/降级) │
└──────┬──────┘
│
┌─────────────────┼─────────────────┐
│ │ │
┌────▼────┐ ┌─────▼─────┐ ┌─────▼─────┐
│本地Qwen │ │ 云端API │ │ 小模型 │
│(GPU集群) │ │(备用) │ │(CPU) │
└──────────┘ └───────────┘ └───────────┘决策1:会话状态完全外置
所有面试会话状态存于Redis,服务节点无状态。好处是扩缩容随心所欲,坏处是Redis一旦抖动全盘受影响。我们的补偿方案:本地内存做L1缓存(TTL 5秒),Redis做L2,配合Circuit Breaker。
决策2:流式对话的双重保障
AI对话采用SSE(Server-Sent Events)流式输出,但网络环境复杂,我们同时提供了:
决策3:异步评估解耦
面试结束后,评估任务丢入RabbitMQ,由Java服务异步消费。评估结果通过Webhook通知业务方,同时支持主动查询。
这是整个系统的心脏,处理一次用户输入到AI响应的完整链路:
// 对话处理核心流程(简化版)
func (e *Engine) ProcessMessage(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
// 1. 获取会话上下文(含历史对话、候选人画像、面试大纲)
session, err := e.sessionMgr.Get(ctx, req.SessionID)
if err != nil {
return nil, err
}
// 2. 并发执行:主模型推理 + 辅助检测
var (
mainResp <-chan *ModelResponse
emotion <-chan *EmotionResult
risk <-chan *RiskResult
)
g := errgroup.WithContext(ctx)
// 主模型调用(支持流式/非流式)
g.Go(func() error {
mainResp = e.modelGateway.Chat(ctx, buildPrompt(session, req.Message))
return nil
})
// 情绪检测(轻量模型,快速返回)
g.Go(func() error {
emotion = e.emotionDetector.Analyze(ctx, req.Message)
return nil
})
// 风险检测(敏感词、作弊特征)
g.Go(func() error {
risk = e.riskDetector.Check(ctx, req.Message, session)
return nil
})
// 等待所有任务完成或超时
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
if err := g.Wait(); err != nil {
// 降级处理:主模型失败时返回预设话术
return e.fallbackResponse(session), nil
}
// 3. 结果聚合与后处理
response := &ChatResponse{
Message: <-mainResp,
Emotion: <-emotion,
RiskScore: <-risk,
// 注入元数据用于评估
Metadata: buildMetadata(session, req),
}
// 4. 异步更新会话(写入Redis + 发送变更事件)
go e.sessionMgr.UpdateAsync(session.ID, response)
return response, nil
}踩坑提醒:errgroup 配合 context 超时时,要小心子goroutine泄漏。我们给每个子任务额外加了一个 done channel 做强制回收。
这部分经历了5次大版本迭代。最终稳定的Prompt结构:
【系统指令】
你是一名资深技术面试官,面试岗位:{position},级别:{level}。
面试风格:{style}(温和/严格/引导型)
当前面试阶段:{stage}(开场/技术深挖/软技能/收尾)
【候选人背景】
{resume_summary}
【面试大纲】
{outline}
【已问问题与回答摘要】
{qa_history}
【当前问题】
候选人说:{current_answer}
【任务】
1. 判断候选人的回答是否完整,如不完整则追问(追问规则:{rules})
2. 如完整,根据大纲提出下一个问题
3. 生成对当前回答的即时评价(用于内部记录,不展示给候选人)
【输出格式】
{
"should_follow_up": bool,
"follow_up_question": string | null,
"next_question": string | null,
"instant_comment": string,
"confidence_score": float
}关键优化点:
type ModelGateway struct {
localClient *ollama.Client // 本地Qwen
cloudClients map[string]APIClient // 多家云API
router *Router
fallbackChain []string // 降级链路
}
func (g *ModelGateway) Chat(ctx context.Context, req *ModelRequest) <-chan *ModelResponse {
respCh := make(chan *ModelResponse, 1)
go func() {
defer close(respCh)
// 根据路由策略选择模型
target := g.router.Select(req)
for _, model := range target {
resp, err := g.callModel(ctx, model, req)
if err == nil {
respCh <- resp
return
}
// 记录失败,尝试下一个
log.Warnw("model call failed", "model", model, "error", err)
metrics.RecordModelFail(model)
}
// 全部失败,返回兜底
respCh <- g.getFallbackResponse(req)
}()
return respCh
}路由策略支持:
面试结束后,评估服务需要综合分析30-40轮对话,生成结构化报告:
@Service
public class EvaluationEngine {
@Async("evaluationExecutor")
public CompletableFuture<EvaluationReport> evaluate(EvaluationTask task) {
// 1. 加载完整对话记录
List<DialogueTurn> turns = dialogueRepository.findBySessionId(task.getSessionId());
// 2. 多维度评估(并行计算)
CompletableFuture<TechnicalScore> techFuture =
assessTechnical(turns, task.getPosition());
CompletableFuture<CommunicationScore> commFuture =
assessCommunication(turns);
CompletableFuture<PersonalityFit> personalityFuture =
assessPersonality(turns);
CompletableFuture<List<RiskIndicator>> riskFuture =
assessRisks(turns);
// 3. 综合评分(带权重)
return CompletableFuture.allOf(techFuture, commFuture, personalityFuture, riskFuture)
.thenApply(v -> {
TechnicalScore tech = techFuture.join();
CommunicationScore comm = commFuture.join();
PersonalityFit personality = personalityFuture.join();
List<RiskIndicator> risks = riskFuture.join();
return EvaluationReport.builder()
.overallScore(calculateOverall(tech, comm, personality))
.technical(tech)
.communication(comm)
.personality(personality)
.risks(risks)
.strengths(extractStrengths(turns))
.weaknesses(extractWeaknesses(turns))
.recommendation(generateRecommendation(tech, comm, personality))
.build();
});
}
private TechnicalScore assessTechnical(List<DialogueTurn> turns, String position) {
// 使用另一个轻量模型(或规则引擎)做技术点覆盖度分析
// 提取候选人提到的技术关键词,与岗位要求比对
// 同时评估回答深度(通过追问次数和回答长度加权)
}
}上线初期,P99延迟高达10.3秒,完全不可用。以下是优化路径:
优化手段 | 效果 |
|---|---|
Qwen-72B 4bit量化 + vLLM框架 | 推理速度提升3倍 |
启用Continuous Batching | GPU利用率从35%→78% |
Prompt缓存(前缀缓存) | 首Token延迟从1.2s→0.3s |
投机采样(draft model) | 生成速度提升40% |
最终,模型推理平均耗时从4.2秒降到0.9秒。
评估报告生成涉及大量对话记录查询,优化方式:
dialogue_{session_id_hash % 64})pg_stat_statements找到慢查询,添加复合索引最终P99延迟稳定在1.5秒以内。
除了常规的QPS/延迟/错误率,我们还埋了AI专属指标:
// 业务指标
var (
// 模型Token消耗(按模型维度)
modelTokenUsage = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "ai_token_usage_total"},
[]string{"model", "type"}, // type: prompt/completion
)
// 追问率(反映对话深度)
followUpRate = prometheus.NewHistogram(
prometheus.HistogramOpts{Name: "dialogue_follow_up_rate"},
)
// 模型置信度分布
confidenceScore = prometheus.NewHistogram(
prometheus.HistogramOpts{Name: "model_confidence_score", Buckets: []float64{0.5, 0.7, 0.8, 0.9, 0.95}},
)
// 降级触发次数
fallbackTriggered = prometheus.NewCounter(
prometheus.CounterOpts{Name: "model_fallback_total"},
)
)使用Jaeger自定义Span,标记每个环节:
Interview Session (traceId)
├── Gateway Auth (span)
├── Session Load (span)
├── Model Inference (span)
│ ├── Prompt Build (span)
│ ├── Model Call (span)
│ │ ├── Queue Wait (span)
│ │ └── Inference (span)
│ └── Response Parse (span)
├── Emotion Detect (span)
├── Risk Check (span)
└── Session Save (span)这样能精确定位每一步的耗时瓶颈。
应对:
应对:
客户机房可能有A100、V100、甚至T4。我们做了:
应对:
go-redis的默认连接池改为自定义大小(根据CPU核心数*10)上线运行3个月,累计完成面试场次:
指标 | 数据 |
|---|---|
总面试场次 | 12,847场 |
平均对话轮次 | 34.6轮/场 |
平均面试时长 | 18.3分钟 |
追问率 | 62.7%(说明对话有深度) |
评估准确率(与人工评分对比) | 89.2% |
系统可用性 | 99.97% |
客户反馈的典型场景:一位候选人结束面试后,主动询问"AI面试官什么时候能完全替代人类?我觉得它问的问题比我遇到过的HR都深。"
下一阶段我们在探索:
最后想说:AI全栈工程师的核心竞争力,不在于会调用多少API,而在于能把这些API变成稳定、可靠、可扩展的生产系统。 希望这篇文章对正在探索AI应用落地的同行有所启发。
欢迎在评论区交流,也欢迎关注我的技术博客,后续会拆解更多细节模块的实现。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。