典型题深度演练 · 对话与 LLM 系统¶
一句话总结: 3 道 mock 覆盖客服机器人 (意图+槽位+对话管理)、ChatGPT 类通用对话 (LLM serving 全链路)、RAG 系统 (检索+生成+评估),每道 45 分钟完整时间盒 + 公式 + 白板图 + 中文圈真实案例。
前两节 (03/04) 讲的是"推荐排序"和"风险防御"——都是围绕用户主动消费或被动遭遇某一场景。 这一节换到"对话",核心是 AI 系统如何理解用户自然语言并生成合理回复。 我们会发现,第 02 节的 8 步框架在对话系统里同样适用,但每个环节有完全不同的挑战:意图必须精确,推理延迟必须低,回复必须忠实于事实。 这一节我们用 3 道题来磨练这些能力.
0. 全章概览:3 道题的认知地图¶
对话与 LLM 系统是 2024-2025 年面试的绝对热点——几乎所有已知公司 (字节豆包、阿里通义、百度文心、DeepSeek、MiniMax、智谱 GLM、月之暗面 Kimi) 都在招 LLM 应用/系统方向的人。 面试官会从 3 个角度出题:
| 类型 | 原型 | 核心能力 | 中文圈代表 |
|---|---|---|---|
| 客服机器人 | 意图分类 + 槽位填充 + 对话状态管理 | 基于规则到 BERT 到 LLM 的演进 | 阿里小蜜 / 京东 JIMI / 中国移动智能客服 |
| 通用对话系统 | ChatGPT 类 LLM Serving | 推理优化 + 安全 + 系统架构 | DeepSeek / 通义千问 / Kimi |
| RAG 系统 | 检索增强生成 | 索引 + 检索 + 排序 + 生成 + 评估 | 百度文心知识库 / 字节豆包 RAG |
这 3 道题层层递进:客服机器人考"传统 NLP 基本功",通用对话考"LLM 系统", RAG 考"如何让 LLM 接地气". 很多面试官会从题 1 问到题 3,考察候选人的知识广度。
题 1:客服机器人系统¶
1.1 题目陈述¶
"设计一个客服机器人系统. 以电商客服为例,帮用户查询订单、退换货、咨询商品信息。 要求能理解用户意图、提取关键信息 (订单号/商品名),并自动生成回复。 当机器人无法处理时,转人工客服."
这是 NLP 面试中最经典的"综合题"——它把意图分类 (Intent Classification)、槽位填充 (Slot Filling)、对话状态管理 (Dialogue State Management,简称对话状态跟踪)、回复生成 (Response Generation) 全部串起来。 而且,面试官会追问"从规则到 BERT 再到 LLM 怎么演进"——这是考察候选人是否经历过 NLP 技术栈升级。
1.2 45 分钟时间盒¶
| 时间 | Step | 必答内容 | 关键产出 |
|---|---|---|---|
| 0:00-0:05 | Clarify | 业务范围?离线/在线?技术栈?转人工条件? | 需求边界 |
| 0:05-0:10 | Metrics | FCR + CSAT + 转人工率 + 意图准确率 + 槽位 F1 | 5 层指标 |
| 0:10-0:15 | Data | 对话日志 + 知识库 + 商品库 + 人工客服话术 | 数据流图 |
| 0:15-0:20 | Features | 用户画像 + 对话历史 + 上下文特征 + 情绪特征 | 特征分类 |
| 0:20-0:30 | Model | 意图分类 + 槽位填充 + 对话管理 + 回复生成 四段式 | 完整架构图 |
| 0:30-0:35 | Training | 数据标注 + 数据增强 + 在线学习 + 冷启动 | 训练方案 |
| 0:35-0:42 | Deployment | 实时推理 (< 500ms) + 降级 + 转人工路由 | 服务架构 |
| 0:42-0:45 | Monitor + 反问 | 转人工率监控 + 情绪检测 + 冷启动策略;反问"什么时候应该拒绝机器人回复?" | 监控 + 追问 |
1.3 Step 1 — Clarify (0:00-0:05)¶
满分话术 (向面试官确认):
"业务范围:电商客服,核心场景——订单查询、退换货、商品咨询、投诉。 这些是高频场景,覆盖 80% 用户请求。
离线/在线:在线实时,用户在等,期望 < 500ms 出回复。 离线批量复盘用于模型更新。
技术栈:用户说中文,混有口语化表达 (如"我的快递咋还没到")、错别字、中英混合。 需要中文 NLP 全栈能力。
转人工条件:机器人置信度低 (< 0.7)、用户情绪激烈 (愤怒/投诉)、用户要求"转人工". 这 3 个条件触发转人工。
冷启动:新业务上线,没有历史对话数据。 先用规则引擎 + 人工标注对话模板,积累 1 万条后再上模型."
1.4 Step 2 — Metrics (0:05-0:10)¶
客服机器人有 5 层指标,面试时至少答出前 3 层:
1.4.1 业务层指标¶
首次接触解决率 (First Contact Resolution, FCR) — 用户一次对话就解决需求,不重复提问的概率。 行业电商客服 FCR 目标 70-80%.
客户满意度 (Customer Satisfaction, CSAT) — 对话结束后用户打分 (1-5 分), 4 分以上算满意。 目标 ≥ 4.2.
转人工率 — 机器人处理的对话中,最终转到人工客服的比例。 越低越好。 行业目标 15-25%.
1.4.2 模型层指标¶
意图分类准确率:多分类,覆盖 10-20 个意图。 准确率 ≥ 95%.
槽位填充 F1:实体抽取 F1 分数。 订单号/商品名/日期等,F1 ≥ 90%.
回复 BLEU/Rouge-L:生成回复与标准回复的相似度。 辅助指标,不高。
1.4.3 系统层指标¶
P50/P99 延迟:上线前 < 500ms P99,上线后 < 1s P99.
吞吐量:每秒处理请求数 (QPS). 单机 ≥ 100 QPS.
1.5 Step 3-4 — 数据 + 特征 (0:10-0:20)¶
1.5.1 数据源¶
# 客服机器人数据源
kf_data_sources = {
"对话日志": {
"source": "MySQL / Kafka 实时流",
"fields": ["session_id", "user_id", "agent_id", "timestamp",
"user_message", "bot_reply", "intent", "slots",
"is_handoff", "user_rating"],
"volume": "每天 100 万+ 对话",
},
"知识库": {
"source": "内部文档 / 帮助中心 / FAQ",
"fields": ["question", "answer", "category", "keywords"],
"volume": "1 万+ 条 QA",
},
"商品库": {
"source": "电商 ERP 系统",
"fields": ["product_id", "name", "category", "specs",
"price", "stock", "status"],
},
"人工客服话术": {
"source": "人工客服聊天记录",
"fields": ["query", "agent_response", "resolution"],
"purpose": "训练机器人回复生成, 学习优秀客服的回复方式",
},
}
1.5.2 特征工程¶
# 客服机器人特征
kf_features = {
"用户画像": {
"vip_level": "会员等级 (高等级用户容忍度更高, 可降低转人工阈值)",
"order_count": "历史订单数",
"return_rate": "退货率 (高退货率用户需要谨慎处理)",
"complaint_count": "历史投诉次数",
},
"对话历史": {
"turn_count": "当前轮次 (3 轮以上未解决 → 转人工)",
"previous_intents": "本轮已出现的意图序列",
"previous_slots": "已收集到的槽位信息",
"resolution_status": "用户是否表达过"算了""升级"等情绪词",
},
"语言特征": {
"text_length": "用户消息长度 (短消息可能是简单的'查订单', 长消息可能是复杂投诉)",
"has_question_mark": "是否含问号",
"sentiment_score": "情感分数 (-1 到 1, 负值可能触发转人工)",
"words_per_second": "打字速度 (极快可能是机器人)",
},
"上下文特征": {
"topic": "当前话题 (如订单/退货/投诉)",
"is_first_turn": "是否首轮对话",
"last_handoff_reason": "上次转人工的原因 (如果本次对话之前转过)",
},
}
1.6 Step 5 — Model (0:20-0:30) ★ 本节核心¶
1.6.1 整体架构:四段式¶
客服机器人 四段式架构
┌────────────────────────────────────────────────────────────────┐
│ │
│ 意图分类 (Intent) 槽位填充 (Slot) 对话管理 (DST) 回复生成 (NLG) │
│ ───────────── ──────────── ────────────── ───────────── │
│ ① 规则引擎 ① 规则 ① 槽位跟踪 ① 模板回复 │
│ ② BERT 微调 ② BERT NER ② 对话状态机 ② 检索式 │
│ ③ LLM 少样本 ③ LLM 信息抽取 ③ LLM 状态管理 ③ 生成式 │
│ │
│ 输出: 用户意图 输出: 实体键值对 输出: 当前状态 输出: 回复文本 │
│ (查订单/退货/投诉) (订单号: 12345) (已确认订单, (好的, 已查 │
│ 待查状态) 到您的订单…) │
│ │
└────────────────────────────────────────────────────────────────┘
面试官为什么喜欢这道题:四段式架构让你把 NLP 四大子任务都展示一遍,而且能答出"规则 → BERT → LLM"的演进逻辑,说明你有技术视野。
1.6.2 意图分类:规则 → BERT → LLM¶
第一代 — 规则引擎 (Rule-based):
# 规则引擎意图分类 (关键词匹配 + 正则)
import re
class RuleIntentClassifier:
"""基于规则的首轮意图分类, 冷启动用."""
def __init__(self):
self.rules = {
"查订单": [
r"(查|看|找|查查|看看).*(订单|快递|物流|包裹|到哪)",
r"(订单|快递|物流|包裹).*(号|编号|号码)",
r"我的.*(订单|快递|物流|包裹)",
],
"退货": [
r"(退|退货|退款|退换)",
r"(不想要|不满意|有问题|坏了)",
r"7天.*退货",
],
"投诉": [
r"(投诉|举报|差评|给差评|投诉到)",
r"(客服|态度|服务).*(差|不好|恶劣)",
r"(315|12315|消费者协会)",
],
"商品咨询": [
r"(多大|多少钱|什么颜色|尺寸|材质|能用)",
r"(质量|好不好|怎么样|评价|推荐)",
r"(搭配|配什么|适合)",
],
"转人工": [
r"(人工|转人工|找人工|客服|在线客服|人工客服)",
r"(转|接).*(人工|客服)",
],
}
def predict(self, text: str) -> tuple:
"""返回 (意图, 置信度)."""
for intent, patterns in self.rules.items():
for pattern in patterns:
if re.search(pattern, text):
return intent, 0.9 # 规则命中, 置信度高
return "未知", 0.0
缺点:规则引擎覆盖有限,用户说"我昨天买的手机怎么还没到"就匹配不到"查订单". 冷启动时勉强可用,但一旦积累 1 万 + 标注数据,必须上模型。
第二代 — BERT 微调:
# BERT 意图分类 (PyTorch)
from transformers import BertModel, BertTokenizer
import torch
import torch.nn as nn
class IntentClassifier(nn.Module):
"""BERT 微调意图分类, 支持 10-20 个电商意图."""
def __init__(self, model_name="bert-base-chinese", num_intents=12):
super().__init__()
self.bert = BertModel.from_pretrained(model_name)
self.dropout = nn.Dropout(0.1)
self.classifier = nn.Linear(self.bert.config.hidden_size, num_intents)
def forward(self, input_ids, attention_mask):
outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask)
# 用 [CLS] token 的表示做分类
pooled = outputs.last_hidden_state[:, 0, :] # [CLS] token
pooled = self.dropout(pooled)
logits = self.classifier(pooled)
return logits
# 训练: 每轮对话用户首句作为输入, 意图标签作为输出
# 数据: 1 万+ 人工标注对话
# 推理: 单条 < 30ms (T4 GPU), INT8 量化后 < 10ms
# 评估: 意图准确率 ≥ 95%, 混淆矩阵重点看"查订单"和"退货"之间的混淆
BERT 的不可替代优势:用户说"我昨天买的手机怎么还没到"——BERT 能理解"查订单"意图,因为"还没到"隐含了"物流查询"语义。 这是规则引擎做不到的。
第三代 — LLM 少样本:
LLM 时代,意图分类可以用 In-context Learning (ICL) 做:
# LLM 少样本意图分类 (Prompt 模板)
INTENT_PROMPT = """你是一个电商客服意图分类器. 请判断用户的意图属于以下哪一类:
- 查订单: 查询物流、订单状态、快递信息
- 退货: 退货、退款、退换货
- 投诉: 投诉商家、客服投诉
- 商品咨询: 咨询商品信息、价格、规格
- 转人工: 要求转人工客服
- 其他
用户: {user_message}
意图:"""
# 调用 LLM 获取回复, 解析意图
# 优点: 零标注, 灵活, 支持任意意图
# 缺点: 延迟高 (500ms-2s), 成本高, 不稳定
演进路径总结:
| 方案 | 准确率 | 延迟 | 标注成本 | 维护成本 | 适用阶段 |
|---|---|---|---|---|---|
| 规则引擎 | 70-80% | < 5ms | 低 | 高 | 冷启动 |
| BERT 微调 | 95%+ | < 30ms | 中 (1 万+) | 低 | 上线后 |
| LLM 少样本 | 90-95% | 500ms-2s | 低 | 低 | 新意图探索 |
面试官追问: "BERT 和 LLM 意图分类你应该选哪个?"
✅ 答:上线初期用 LLM 少样本快速验证,用 LLM 标注数据蒸馏到 BERT 模型,线上用 BERT 推理 (低成本低延迟), LLM 只做兜底和冷启动。 这是蒸馏 (distillation) 的典型应用。
1.6.3 槽位填充 (Slot Filling)¶
槽位填充本质是命名实体识别 (Named Entity Recognition, NER),但对话场景的 NER 更依赖上下文:
# BERT 槽位填充 (NER 序列标注)
from transformers import BertForTokenClassification
class SlotFiller(nn.Module):
"""BERT 序列标注, 提取槽位值."""
def __init__(self, model_name="bert-base-chinese", num_labels=9):
# 标签: O(非实体), B-order(订单号开始), I-order(订单号内部),
# B-prd(商品名开始), I-prd(商品名内部),
# B-date(日期开始), I-date(日期内部),
# B-price, I-price
super().__init__()
self.bert = BertForTokenClassification.from_pretrained(
model_name, num_labels=num_labels
)
def forward(self, input_ids, attention_mask, labels=None):
outputs = self.bert(
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels, # 训练时提供标签
)
return outputs # loss, logits, ...
# 推理: 输入"查一下订单 12345 到哪里了"
# 输出: "查一下订单 [B-order]1[I-order]2[I-order]3[I-order]4[I-order]5 到哪里了"
# 解析: 订单号 = "12345"
# 关键: 槽位值需要和对话状态管理联动
# 例如用户说"帮我查一下", 上一轮已收集到"订单号=12345",
# 这一轮不需要用户重复说订单号
1.6.4 对话状态管理 (Dialogue State Management)¶
对话状态管理 (DST) 负责跟踪多轮对话中已收集到什么信息、还缺什么信息:
# 对话状态管理 (Dialogue State Tracker)
class DialogueStateTracker:
"""跟踪对话状态, 决定下一步动作."""
def __init__(self):
self.state = {
"intent": None, # 当前意图
"slots": {}, # 已收集的槽位
"required_slots": {}, # 当前意图所需槽位
"turn": 0, # 当前轮次
"history": [], # 对话历史
"handoff_needed": False, # 是否需要转人工
}
# 每个意图需要的槽位
self.intent_requirements = {
"查订单": {"order_id": "订单号"},
"退货": {"order_id": "订单号", "reason": "退货原因"},
"投诉": {"order_id": "订单号", "description": "投诉描述"},
"商品咨询": {"product_name": "商品名"},
}
def update(self, intent, slots):
"""更新对话状态."""
self.state["turn"] += 1
self.state["intent"] = intent or self.state["intent"]
self.state["slots"].update(slots)
self.state["required_slots"] = self.intent_requirements.get(
self.state["intent"], {}
)
def get_missing_slots(self):
"""返回还缺哪些槽位."""
required = set(self.state["required_slots"].keys())
collected = set(self.state["slots"].keys())
missing = required - collected
return {k: v for k, v in self.state["required_slots"].items()
if k in missing}
def next_action(self):
"""决定下一步动作: 追问 / 执行 / 转人工."""
if self.state["turn"] > 5:
# 5 轮还没解决, 转人工
return "handoff", "不好意思, 已为您转接人工客服"
missing = self.get_missing_slots()
if missing:
# 追问缺失的槽位
slot_name = list(missing.values())[0]
return "ask_slot", f"请问您的{slot_name}是?"
# 全部收集完成, 执行业务查询
return "execute", None
1.6.5 回复生成 (Response Generation)¶
回复生成有 3 种方案,按复杂度递增:
1. 模板回复 (最简单,90% 场景够用):
# 模板回复
TEMPLATES = {
"查订单_成功": "好的, 查到您的订单 {order_id} 当前状态为: {status}. 预计 {eta} 送达.",
"查订单_失败": "查询订单 {order_id} 失败, 请确认订单号是否正确.",
"退货_成功": "已为您提交退货申请, 退货单号 {return_id}. 快递员将上门取件.",
"退货_失败": "抱歉, 订单 {order_id} 已超出退货时效 (7 天), 无法办理退货.",
"投诉_登记": "已记录您的投诉, 客服专员将在 24 小时内联系您.",
"商品咨询_回复": "{product_name} 当前价格为 ¥{price}, 库存 {stock} 件.",
}
2. 检索式回复 (BERT 语义匹配,从知识库找最相似 QA):
# 检索式回复: 用 BERT 做语义匹配
from sentence_transformers import SentenceTransformer, util
class RetrievalResponse:
"""从知识库检索最相似的 QA 对."""
def __init__(self, knowledge_base):
self.encoder = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
# 预编码知识库所有问题
self.kb_questions = [item["question"] for item in knowledge_base]
self.kb_answers = [item["answer"] for item in knowledge_base]
self.kb_embeddings = self.encoder.encode(
self.kb_questions, convert_to_tensor=True
)
def retrieve(self, query, top_k=1):
"""检索最相似的问题."""
query_emb = self.encoder.encode(query, convert_to_tensor=True)
scores = util.cos_sim(query_emb, self.kb_embeddings)[0]
top_idx = scores.argsort(descending=True)[:top_k]
return [
{"question": self.kb_questions[i],
"answer": self.kb_answers[i],
"score": float(scores[i])}
for i in top_idx
]
3. 生成式回复 (LLM 生成,最新方案):
# 生成式回复: LLM + 检索结果 + 对话历史
GENERATE_PROMPT = """你是一个电商客服助手. 请根据以下信息生成回复:
对话历史:
{history}
知识库相关回答:
{kb_context}
用户当前问题: {user_message}
请生成专业、友好、简洁的回复. 如果无法回答, 主动提出转人工.
回复:"""
1.6.6 转人工 (Handoff) 策略¶
转人工是客服机器人的"安全阀". 面试官特别关注:候选人是否知道不是所有问题都该机器人回答.
# 转人工判定逻辑
class HandoffDecision:
"""什么时候转人工."""
@staticmethod
def should_handoff(intent_result, slot_result, dialog_state, user_sentiment):
"""转人工条件."""
reasons = []
# 条件 1: 意图置信度低
if intent_result.confidence < 0.7:
reasons.append("意图不明确")
# 条件 2: 用户情绪激烈
if user_sentiment < -0.5:
reasons.append("用户情绪负面")
# 条件 3: 用户明确要求转人工
if intent_result.intent == "转人工":
reasons.append("用户要求转人工")
# 条件 4: 多轮未解决
if dialog_state.turn > 5:
reasons.append("超过 5 轮未解决")
# 条件 5: 敏感场景 (投诉/纠纷)
if intent_result.intent == "投诉":
reasons.append("投诉场景需人工")
# 条件 6: 模型不确定 (LLM 复审)
if intent_result.intent == "其他":
reasons.append("模型无法识别意图")
return len(reasons) > 0, reasons
1.7 Step 6-8 — 训练 + 部署 + 监控 (0:30-0:45)¶
| Step | 客服机器人强相关 | 数字 / 工具 |
|---|---|---|
| Training | 数据标注 (1 万+) + 数据增强 (回译/替换) + 在线学习 (新话术冷启动) | BERT 微调:单 GPU 1-2 小时;数据增强:EDA 方法 |
| Deployment | 实时推理 (< 500ms),降级 (BERT → 规则引擎 → 转人工),转人工路由 | Triton 推理服务器 + 知识库 (Elasticsearch) |
| Monitor | 转人工率 + 用户满意度 + 意图漂移 (新话术一周变一次) + 冷启动自动标注 | Prometheus + Grafana + 对话日志回溯 |
面试官追问: "怎么处理冷启动?"
✅ 答:新业务上线,没有历史对话。 先用规则引擎 (关键词) 跑 1 周,收集 1 万 + 对话。 然后人工标注 2 千条,训练 BERT 意图分类。 上线后,用 LLM 做兜底——当 BERT 置信度 < 0.8 时,走 LLM 判断。 LLM 的判断结果自动回流做"伪标注",积累到 5 千条后重新训练 BERT. 这就是自训练 (self-training) 循环.
1.8 中文圈案例:阿里小蜜 / 京东 JIMI / 中国移动¶
1.8.1 阿里小蜜 (Alibaba XiaoMi)¶
阿里小蜜是中文圈最复杂的客服机器人之一,覆盖淘宝/天猫/菜鸟/支付宝多个场景:
- 量级:每天处理数千万次对话,覆盖 200+ 意图
- 架构:规则引擎 + BERT 意图分类 + 知识图谱 + 多轮对话管理
- 转人工率:目标 < 20%,实际约 15%
- 特色:跨场景上下文传递——用户在淘宝聊天窗口问"这个什么时候到",小蜜知道用户刚买什么商品、什么物流
1.8.2 京东 JIMI¶
京东 JIMI (JD Intelligent Marketing Interaction) 是京东自研的客服机器人:
- 特色:强售后 + 强物流一体化——京东自营物流状态可直接查询
- 技术:基于 BERT 的意图分类 + 槽位填充,对话管理用有限状态机 (FSM)
- 转人工策略:投诉 + 纠纷自动转人工,退货低风险场景自动处理
1.8.3 中国移动 10086 智能客服¶
中国移动的智能客服覆盖 10086 热线 + 在线客服:
- 特色:多轮对话 + 业务办理 (查话费/流量/账单)
- 挑战:方言识别 + 老人语音 (口语化严重)
- 方案: ASR (语音转文字) + 意图分类 + 业务系统对接
1.9 学员常见卡顿点 + 解套¶
| 卡顿点 | 解套 |
|---|---|
| 只答模型不答规则 | 强调规则引擎是冷启动 + 兜底方案,冷启动 1 万条之前全靠规则 |
| 不会槽位填充 | 槽位填充 = NER 序列标注,和意图分类共享 BERT 底层 |
| 不提对话状态管理 | DST 跟踪已收集/缺失的槽位,决定"追问/执行/转人工" |
| 不提转人工策略 | 6 个条件:置信度低 / 情绪负面 / 用户要求 / 多轮 / 投诉 / 未知意图 |
| 不会冷启动 | 规则 (1 周) → 标注 (2 千) → BERT (上线) → LLM 兜底 → 自训练循环 |
| 不提 FCR | FCR 是客服最重要的业务指标,目标 70-80% |
| 不提 CSAT | 用户满意度 4+/5,转人工后 CSAT 通常更高 |
| BERT vs LLM 不分场景 | 线上用 BERT (低成本低延迟), LLM 做冷启动 + 兜底 |
1.10 关键追问 cheat sheet¶
| 追问 | 标准答案 |
|---|---|
| 意图分类怎么做? | 规则 (冷启动) → BERT (上线) → LLM (兜底),蒸馏 LLM 到 BERT |
| 槽位填充怎么实现? | BERT NER 序列标注,共享意图分类的 BERT 底层 |
| 多轮对话怎么管理? | DST 跟踪槽位,缺什么追问什么,填完执行,5 轮转人工 |
| 什么时候转人工? | 6 条件:低置信度 / 负面情绪 / 用户要求 / 多轮 / 投诉 / 未知 |
| 冷启动怎么处理? | 规则引擎跑 1 周 → 人工标注 2 千 → BERT 上线 → LLM 自训练 |
| FCR 是什么? | 首次接触解决率,一次对话解决需求的比例,目标 70-80% |
| 阿里小蜜怎么做? | 200+ 意图,规则+BERT+知识图谱,转人工率 < 20% |
| BERT vs LLM 选哪个? | BERT 线上 (低延迟低成本的 95% 场景), LLM 冷启动 + 兜底 |
题 2: ChatGPT 类通用对话系统¶
2.1 题目陈述¶
"设计一个 ChatGPT 类通用对话系统的 Serving 架构. 用户发消息后,系统在 1-3 秒内返回回复。 需要考虑模型推理优化、安全层、内容审核、负载均衡、流式输出."
这是 LLM 系统面试的核心题——几乎每个招 LLM 工程师的团队 (字节豆包、阿里通义、DeepSeek、智谱、MiniMax) 都会出这道题。 难点在于:这不是简单的"模型调 API",而是从用户请求到 LLM 回复的全链路系统设计,涉及负载均衡、速率限制、推理优化、安全过滤。
2.2 45 分钟时间盒¶
| 时间 | Step | 必答内容 | 关键产出 |
|---|---|---|---|
| 0:00-0:05 | Clarify | 模型规模?并发?延迟要求?安全要求?流式? | 需求边界 |
| 0:05-0:10 | Metrics | TTFT + TPOT + P99 延迟 + throughput + 安全拦截率 | 5 层指标 |
| 0:10-0:15 | Data | 请求数据 + 用户画像 + 对话历史 + 安全策略 | 数据流图 |
| 0:15-0:20 | Features | 用户特征 + 对话特征 + 语义特征 + 非常规提示 (jailbreak) 特征 | 特征分类 |
| 0:20-0:32 | Model | LLM Serving 全链路: LB → Rate Limiter → Router → Prompt Preprocessor → LLM Server → Post-processor | 系统架构图 |
| 0:32-0:38 | Training | 模型蒸馏 + 量化 + 提示词优化 | 优化方案 |
| 0:38-0:43 | Deployment | 多机多卡部署 + 弹性伸缩 + 降级 | 服务架构 |
| 0:43-0:45 | Monitor + 反问 | 安全拦截率 + 延迟异常 + 内容敏感性;反问"如何检测模型幻觉?" | 监控 + 追问 |
2.3 Step 1 — Clarify (0:00-0:05)¶
"模型规模:假设 70B 参数模型,FP16 推理,单卡 A100 80GB 装不下 (70B × 2 bytes = 140 GB). 需要 2-4 张 A100 做张量并行 (TP).
并发:假设 1000 QPS,峰值 3000 QPS. 需要多机多 TensorRT-LLM / vLLM 实例。
延迟要求:用户期望 1-3 秒首 token 延迟 (TTFT),后续 token 50ms/个 (TPOT).
安全要求:必须过滤暴力/色情/政治敏感/越狱 (jailbreak) 内容。 输入输出都要过滤。
流式:支持 SSE (Server-Sent Events) 流式输出,用户看到逐个 token 生成."
2.4 Step 2 — Metrics (0:05-0:10)¶
2.4.1 延迟指标¶
TTFT (Time to First Token,首 token 延迟):从用户发送请求到收到第一个 token 的时间。 目标:P50 < 500ms, P99 < 2s.
TPOT (Time per Output Token,每 token 延迟):生成每个后续 token 的时间。 目标:< 50ms/token.
E2E 延迟:完整回复延迟。 目标:100 token 回复 < 5s.
2.4.2 吞吐指标¶
吞吐量 (Throughput):每秒生成 token 数。 单机 A100 80GB 70B 模型,约 1000-2000 tokens/s.
QPS (Queries Per Second):每秒请求数。 目标:1000 QPS.
2.4.3 安全指标¶
安全拦截率:系统拦截的违规内容占全部违规内容的比例。 目标:> 99%.
误拦截率:正常内容被误拦截的比例。 目标:< 0.1%.
越狱检测率:检测到越狱尝试的比例。 目标:> 95%.
2.5 Step 5 — Model (0:20-0:32) ★ 本节核心¶
2.5.1 整体架构:全链路 7 层¶
ChatGPT 通用对话系统 全链路架构
┌──────────────────────────────────────────────────────────────────┐
│ │
│ 用户请求 │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ ① LB │ 负载均衡器 (Nginx / Envoy), 轮询/LRU 路由 │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ ② Rate │ 速率限制器 (Token Bucket / Sliding Window), 每用户限流 │
│ │ Limiter│ 100 QPS, 全局 3000 QPS │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ ③ Router │ 路由器 (模型选择): 短文本 → 小模型, 复杂推理 → 大模型, │
│ │ │ 敏感主题 → 安全模型, 多轮长对话 → 长上下文模型 │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ ④ Prompt │ 提示词预处理器: 系统提示词注入 + 对话历史拼接 + │
│ │ Preproc│ 上下文窗口截断 + Prompt Cache 命中检查 │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ ⑤ Safety │ 输入安全层: 内容审核 + 越狱检测 + PII 脱敏 │
│ │ Input │ 拒绝违规请求, 不让 LLM 看到有害内容 │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ ⑥ LLM │ LLM 推理服务器 (vLLM/SGLang/TensorRT-LLM): │
│ │ Server│ 连续批处理 + KV Cache + PagedAttention │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ ⑦ Safety │ 输出安全层: 内容审核 + 事实校验 + 拒绝有害回复 │
│ │ Output │ 质量过滤 (重复/无意义/空洞) │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ 用户收到回复 (流式 SSE) │
│ │
│ 延迟预算: LB 5ms + RateLimit 2ms + Router 5ms + PromptPrep 10ms │
│ + SafetyIn 50ms + LLM Server 500ms(TTFT) + SafetyOut 50ms = 622ms │
└──────────────────────────────────────────────────────────────────┘
2.5.2 LLM Serving 核心技术¶
问题: 70B 模型,FP16,一张 A100 80GB 装不下。 必须用张量并行 (Tensor Parallelism, TP) 把模型切到多卡上。
张量并行 (TP):把每层矩阵切分到多张 GPU 上。 例如,4 卡 TP,每张卡只存 1/4 的权重。 前向传播时,每张卡计算自己的部分,用 all-reduce 通信汇总结果。
流水线并行 (PP):把层分段,每段在不同 GPU 上。 例如,70B 模型 80 层,4 卡 PP,每卡 20 层。
TP 通信量数学:
对于 Transformer 层,前向传播需要一次 all-reduce 同步注意力输出,一次 all-reduce 同步 FFN 输出。 每次 all-reduce 的通信量:
其中 \(d_{\text{model}}\) 是隐藏维度,rank 是 TP 卡数。 70B 模型 \(d_{\text{model}} \approx 8192\), 4 卡 TP,每层通信量约 524 KB. 80 层则约 42 MB 每 token.
KV 缓存 (KV cache) 显存公式:
其中第 1 个 2 是 K 和 V,第 2 个 2 是每头的维度。 实际计算:
用 70B 模型举例:\(n_{\text{layers}} = 80\), \(d_{\text{model}} = 8192\), \(n_{\text{heads}} = 64\), seq_len = 4096, FP16 (2 bytes):
逐步计算: $\(= 2 \times 80 \times 8192 \times 64 \times 4096 \times 2\)$ $\(= 2 \times 80 \times 8192 \times 64 \times 8192\)$ $\(= 2 \times 80 \times 8192 \times 524288\)$ $\(= 2 \times 80 \times 4,294,967,296\)$ $\(= 2 \times 343,597,383,680\)$ $\(= 687,194,767,360 \text{ bytes} \approx 640 \text{ GB}\)$
640 GB 显存 才能存下 70B 模型单条 4096 token 会话的 KV 缓存!这解释了为什么 LLM serving 的核心瓶颈是显存,而不是算力。
解决方案: - PagedAttention (vLLM):把 KV 缓存分页,像虚拟内存一样管理,减少碎片,提升 2-4 倍批大小 - KV cache 共享:前缀相同的请求共享 KV 缓存 (Prompt Cache) - KV cache 量化: INT8 量化 KV 缓存,省 50% 显存
2.5.3 连续批处理 (Continuous Batching)¶
传统批处理:等一批请求全部完成后,一起处理。 短请求必须等长请求,浪费 GPU 算力。
连续批处理 (continuous batching):每个请求独立处理,请求完成后立即加入新请求。 理论上,连续批处理比传统批处理提升 2-4 倍吞吐。
在短请求多的场景,连续批处理优势更明显——因为长请求不会拖慢短请求。
2.5.4 vLLM / SGLang / TensorRT-LLM 配置¶
# vLLM 服务配置示例
from vllm import LLM, SamplingParams
# 配置 vLLM 推理引擎
llm = LLM(
model="/path/to/model-70B",
tensor_parallel_size=4, # 4 卡 TP
dtype="bfloat16",
max_model_len=8192, # 最大上下文长度
gpu_memory_utilization=0.90, # GPU 显存利用率
enable_prefix_caching=True, # 前缀缓存, 共享 KV cache
max_num_seqs=256, # 最大并发序列数
block_size=16, # PagedAttention 块大小
)
# 采样参数
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=1024,
stop=["\n\n", "用户:", "---"],
)
# 推理
outputs = llm.generate(prompts, sampling_params)
# SGLang 服务配置示例
# SGLang 在多轮对话场景有优势: 预填充和解码阶段分离, 提升效率
# 启动命令:
# python -m sglang.launch_server \
# --model-path /path/to/model-70B \
# --tp 4 \
# --port 30000 \
# --host 0.0.0.0 \
# --enable-prefix-caching \
# --mem-fraction-static 0.88 \
# --max-lora-rank 64
# 客户端调用
import sglang as sgl
@sgl.function
def chat(system, user_message):
sgl.system(system)
sgl.user(user_message)
sgl.assistant(sgl.gen("answer", max_tokens=1024))
# 流式调用
state = chat.run(
system="你是一个有帮助的助手",
user_message="什么是注意力机制?",
stream=True,
)
for out in state.text_iter():
print(out, end="", flush=True)
# TensorRT-LLM 配置 (最大吞吐场景)
# 适用: 高并发, 需要极致性能
# 缺点是模型转换时间长 (几小时), 不支持动态模型更新
# 模型转换步骤:
# 1. 将 HuggingFace 模型转换为 TensorRT-LLM 格式
# 2. 配置 INT8/FP8 量化
# 3. 设置 TP+PP 策略
# 启动命令:
# mpirun -n 4 --allow-run-as-root \
# python tensorrt_llm_example.py \
# --engine_dir /path/to/trt_engines \
# --max_batch_size 128 \
# --max_input_len 4096 \
# --max_output_len 1024
三选一场景:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 快速原型 / 中小规模 | vLLM | 最爱,部署简单,兼容 HuggingFace 模型 |
| 多轮对话 / 中等规模 | SGLang | 预填充/解码分离,多轮对话效率高 |
| 高并发生产 / 大流量 | TensorRT-LLM | 极致吞吐,但模型转换复杂 |
| 研究 / 快速迭代 | vLLM | 灵活,支持新模型快 |
2.5.5 安全层 (Safety Layer)¶
安全层是 LLM 系统的"门卫",分输入和输出两层:
输入安全层 (Pre-filter):
# 输入安全层: 内容审核 + 越狱检测
class InputSafetyLayer:
"""过滤用户输入, 防止有害内容进入 LLM."""
def __init__(self):
self.content_moderation = load_moderation_model()
self.jailbreak_detector = load_jailbreak_model()
self.pii_detector = load_pii_model() # PII: 个人信息
def check(self, user_message: str) -> dict:
"""返回审核结果."""
result = {
"is_safe": True,
"reasons": [],
"action": "pass", # pass / block / warn
}
# 1. 内容审核 (暴力/色情/政治敏感)
mod_result = self.content_moderation.predict(user_message)
if mod_result.is_unsafe:
result["is_safe"] = False
result["reasons"].append(f"内容违规: {mod_result.category}")
result["action"] = "block"
# 2. 越狱检测
jailbreak_check = self.jailbreak_detector.detect(user_message)
if jailbreak_check.is_attack:
result["is_safe"] = False
result["reasons"].append(f"越狱尝试: {jailbreak_check.technique}")
result["action"] = "block"
# 3. PII 脱敏 (不阻止, 但脱敏)
pii_result = self.pii_detector.detect(user_message)
if pii_result.has_pii:
result["reasons"].append(f"检测到PII: {pii_result.types}")
# 脱敏处理, 不阻止
result["masked_message"] = self.pii_detector.mask(user_message)
return result
输出安全层 (Post-filter):
# 输出安全层: 过滤有害回复
class OutputSafetyLayer:
"""过滤 LLM 生成的回复, 防止有害内容输出."""
def __init__(self):
self.content_moderation = load_moderation_model()
self.quality_filter = load_quality_model()
def check(self, output_text: str, input_text: str) -> dict:
"""返回审核结果."""
result = {
"is_safe": True,
"reasons": [],
"action": "pass",
}
# 1. 内容审核
mod_result = self.content_moderation.predict(output_text)
if mod_result.is_unsafe:
result["is_safe"] = False
result["reasons"].append(f"输出违规: {mod_result.category}")
result["action"] = "block"
result["replacement"] = "抱歉, 我无法生成该内容."
# 2. 质量过滤 (重复/无意义/空洞)
quality = self.quality_filter.score(output_text)
if quality < 0.3:
result["warnings"].append("回复质量低, 建议重新生成")
# 3. 事实性检查 (简单版)
# 实际场景用 RAG 或外部知识库做事实校验
return result
2.6 Step 6-8 — 训练 + 部署 + 监控 (0:32-0:45)¶
| Step | LLM 对话系统强相关 | 数字 / 工具 |
|---|---|---|
| Training | 模型蒸馏 (小模型模仿大模型) + 量化 (FP16 → INT8 → FP8) + 提示词优化 | 蒸馏:1/10 成本;量化:INT8 省 50% 显存,1-2% 精度损失 |
| Deployment | 多机多卡 TP + PP + 弹性伸缩 (K8s HPA) + 降级 (小模型替代大模型) | Kubernetes + Docker + 推理路由器 |
| Monitor | TTFT + TPOT + P99 + 安全拦截率 + 错误率 + 幻觉检测 | Prometheus + Grafana + 告警规则 |
2.7 中文圈案例:DeepSeek / 通义千问 / Kimi¶
2.7.1 DeepSeek¶
DeepSeek (深度求索) 是中文圈最受关注的 LLM 公司之一,以其在推理和成本效率上的突破闻名:
- 模型: DeepSeek-V2/V3 系列,MoE 架构,总参数量 671B,激活参数 37B
- 部署:公开报告中提到通过 MoE 稀疏激活和 KV cache 压缩,推理成本显著低于同规模稠密模型
- 特色:开源模型权重,公开技术报告详细说明训练和推理优化方法,在中文圈和全球开发者社区均有大量部署实例
2.7.2 通义千问 (Qwen)¶
阿里云的通义千问 (Qwen) 系列:
- 模型: Qwen2.5 系列,0.5B-72B 多种规格
- 部署:阿里云百炼平台提供 SaaS 服务,支持 vLLM / SGLang 部署
- 特色:多语言强 (英文/中文/代码),长上下文 (128K tokens)
2.7.3 Kimi (月之暗面)¶
月之暗面 (Moonshot AI) 的 Kimi:
- 模型:长上下文 (200 万字,即约 200 万 tokens 上下文窗口)
- 部署:自研推理引擎,针对长上下文优化
- 特色:长文档对话、文件读取、联网搜索
2.8 学员常见卡顿点 + 解套¶
| 卡顿点 | 解套 |
|---|---|
| 只答模型不答系统 | 全链路 7 层:LB → Rate Limit → Router → PromptPrep → SafetyIn → LLM → SafetyOut |
| 不会 KV cache 公式 | \(M_{\text{kv}} = 2 \times n_{\text{layers}} \times d_{\text{model}} \times n_{\text{heads}} \times \text{seq\_len} \times 2\), 70B 4096 token = 640 GB |
| 不会 TP/PP 通信量 | TP 每层 2 次 all-reduce,每次约 \(4 \times d_{\text{model}}\) 元素;PP 通信量小,但流水线气泡 |
| 不提连续批处理 | 传统批处理等长请求,连续批处理独立处理,提升 2-4 倍吞吐 |
| 不提安全层 | 输入和输出双层安全过滤,内容审核 + 越狱检测 + PII 脱敏 |
| 不提流式输出 | SSE 流式,用户看到逐个 token 生成,提升体验 |
| vLLM vs SGLang vs TRT-LLM 分不清 | vLLM (快速原型) / SGLang (多轮对话) / TRT-LLM (高吞吐生产) |
| 不提 Prefix Cache | 共享前缀的请求共享 KV cache,节省大量显存 |
2.9 关键追问 cheat sheet¶
| 追问 | 标准答案 |
|---|---|
| KV cache 显存怎么算? | \(M_{\text{kv}} = 2 \times n_{\text{layers}} \times d_{\text{model}} \times n_{\text{heads}} \times \text{seq\_len} \times 2\), 70B 4096 token = 640 GB |
| 连续批处理是什么? | 每个请求独立处理,完成即加入新请求,提升 2-4 倍吞吐 |
| vLLM 的核心创新? | PagedAttention:把 KV cache 分页管理,减少显存碎片,提升批大小 |
| 安全层怎么设计? | 输入安全 (内容审核 + 越狱检测 + PII) + 输出安全 (内容审核 + 质量过滤) |
| 怎么选推理引擎? | 快速原型用 vLLM,多轮对话用 SGLang,高吞吐生产用 TensorRT-LLM |
| 流式输出怎么实现? | SSE (Server-Sent Events),逐个 token 返回 |
| 什么时候用小模型? | 短文本/简单任务用小模型 (Qwen 0.5B/7B),复杂推理用大模型 |
| 如何降级? | 大模型超时/报错 → 降级到小模型 → 降级到缓存回复 → 返回错误提示 |
题 3: RAG (Retrieval-Augmented Generation) 系统¶
3.1 题目陈述¶
"设计一个 RAG 系统,让 LLM 在回答问题时能检索外部知识库。 以企业内部知识库为例,员工问'公司年假政策是什么',系统需要从知识库中找到相关文档,并基于文档生成回答."
RAG 是 2024-2025 年面试的必考题——几乎每个 LLM 应用团队都在做 RAG. 面试官的核心关注点:候选人是否理解检索 + 生成的端到端 pipeline,以及如何评估 RAG 效果。
3.2 45 分钟时间盒¶
| 时间 | Step | 必答内容 | 关键产出 |
|---|---|---|---|
| 0:00-0:05 | Clarify | 知识库规模?文档类型?实时性?准确率要求? | 需求边界 |
| 0:05-0:10 | Metrics | 检索 P@K + RAGAS 指标 (Faithfulness/Answer Relevance/Context Precision) | 评估体系 |
| 0:10-0:15 | Data | 知识库文档 + 对话日志 + 用户反馈 | 数据流图 |
| 0:15-0:20 | Indexing | Chunking 策略 + Embedding 模型 + 向量数据库 | 索引方案 |
| 0:20-0:30 | Retrieval | Dense + Sparse + Hybrid 检索 + Re-ranking (Cross-encoder) | 检索架构 |
| 0:30-0:35 | Generation | Context injection + Citation + Faithfulness 控制 | 生成方案 |
| 0:35-0:42 | Evaluation | 离线评估 (RAGAS) + 在线评估 (A/B test) + 人工评估 | 评估方案 |
| 0:42-0:45 | Monitor + 反问 | 检索效果漂移 + 用户反馈回流;反问"什么时候 RAG 不如直接让 LLM 回答?" | 监控 + 追问 |
3.3 Step 1 — Clarify (0:00-0:05)¶
"知识库规模:假设 100 万份文档,每份 500-5000 字,中英文混合。 需要定期更新 (每天增量).
文档类型:政策文档 (PDF/Word)、FAQ、内部 Wiki、邮件。 结构化和非结构化都有。
实时性:知识库更新后,1 小时内生效。 需要增量索引。
准确率要求:回答必须基于检索到的文档,不能凭空生成。 关键场景 (政策/合规) 需要引用原文。
用户:企业内部员工,中文为主,混有英文术语."
3.4 Step 2 — Metrics (0:05-0:10)¶
3.4.1 检索指标¶
P@K (Precision at K):检索返回的前 K 个结果中,相关文档的比例。 目标:P@5 ≥ 0.8.
Recall@K:检索返回的前 K 个结果中,覆盖了多少相关文档。 目标:Recall@10 ≥ 0.9.
MRR (Mean Reciprocal Rank):第一个相关结果排名的倒数均值。 目标:MRR ≥ 0.9.
3.4.2 RAGAS 指标¶
检索增强生成评估 (Retrieval-Augmented Generation Assessment, RAGAS) 是 Shahul Es 等人 2023 年提出的 RAG 评估框架,核心有 3 个指标:
Faithfulness (忠实度):生成的回答是否完全基于检索到的上下文,不包含幻觉。
其中 claims 是回答中的独立陈述。 例如"公司年假是 15 天,需要工作满 1 年"包含 2 个 claims,如果两个都能在检索到的上下文中找到,Faithfulness = 1.0.
Answer Relevance (回答相关性):回答是否回答了用户的问题。
Context Precision (上下文精确率):检索到的上下文中,有多少是真正相关的。
RAGAS 的总体分:
3.5 Step 3-4 — 数据 + 索引 (0:10-0:20)¶
3.5.1 数据源¶
# RAG 数据源
rag_data_sources = {
"知识库文档": {
"type": "PDF/Word/Markdown/HTML",
"volume": "100 万文档",
"update": "每日增量更新",
},
"FAQ 库": {
"type": "结构化 (Question-Answer)",
"volume": "5 万对",
"update": "每周更新",
},
"对话日志": {
"type": "用户与 RAG 系统的交互日志",
"volume": "每天 10 万+ 对话",
"purpose": "评估 + 反馈回流",
},
"用户反馈": {
"type": "点赞/点踩/人工标注",
"volume": "少量 (1%)",
"purpose": "在线评估 + 模型微调",
},
}
3.5.2 Chunking 策略¶
Chunking 是 RAG 最容易被忽视但影响最大的环节。 切得太大,检索精度下降;切得太小,上下文不完整。
三种 Chunking 策略:
# 三种 Chunking 策略
# 1. Fixed-size Chunking (固定大小切分)
# 简单, 但可能切断完整段落
def fixed_chunk(text, chunk_size=512, overlap=50):
tokens = tokenize(text)
chunks = []
for i in range(0, len(tokens), chunk_size - overlap):
chunk = tokens[i:i + chunk_size]
chunks.append(detokenize(chunk))
return chunks
# 2. Semantic Chunking (语义切分)
# 按段落/句子边界切分, 保持语义完整
def semantic_chunk(text):
# 按段句切分, 保持段落完整
paragraphs = text.split("\n\n")
chunks = []
for para in paragraphs:
if len(tokenize(para)) > 512:
# 长段落按句子切分
sentences = split_sentences(para)
for sent in sentences:
chunks.append(sent)
else:
chunks.append(para)
return chunks
# 3. Hierarchical Chunking (层级切分) — 推荐方案
# 父文档 + 子段落 + 摘要, 检索时先匹配摘要, 再返回父文档
class HierarchicalChunker:
"""层级切分: 文档 → 摘要 → 段落 → 句子."""
def __init__(self, llm):
self.llm = llm # 用 LLM 生成摘要
def chunk(self, document):
# 1. 生成文档摘要 (用 LLM)
summary = self.llm.generate(f"总结以下文档: {document[:2000]}")
# 2. 按段落切分
paragraphs = document.split("\n\n")
# 3. 按句子切分长段落
segments = []
for para in paragraphs:
if len(para) > 500:
segments.extend(split_sentences(para))
else:
segments.append(para)
return {
"summary": summary,
"segments": segments,
"document_id": hash(document),
}
推荐方案:层级切分 (Hierarchical). 检索时先用摘要匹配,找到最相关的文档,再在文档内按段落检索。 既保证检索精度,又保证上下文完整。
3.5.3 Embedding 模型选择¶
中文场景推荐:
| 模型 | 维度 | 适用场景 | 备注 |
|---|---|---|---|
| BGE (BAAI/bge-large-zh-v1.5) | 1024 | 通用中文,开源 | 检索任务 SOTA |
| text2vec-large-chinese | 1024 | 通用中文,开源 | 腾讯出品 |
| m3e-base | 768 | 中英混合 | 兼顾中英文 |
| OpenAI text-embedding-3-small | 1536 | 英文为主 | 商用,需 API |
3.6 Step 5 — Retrieval (0:20-0:30) ★ 本节核心¶
3.6.1 整体架构¶
RAG 系统 检索 → 排序 → 生成 架构
┌──────────────────────────────────────────────────────────────────┐
│ │
│ 用户查询 │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ ① 查询重写 │ 把用户问题改写为检索友好的查询, 支持多轮对话中的查询理解 │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ ② 混合检索 (Hybrid Retrieval) │ │
│ │ ┌──────────┐ ┌───────────┐ │ │
│ │ │ Dense │ │ Sparse │ │ │
│ │ │ BGE 编码 │ │ BM25 关键词 │ │ │
│ │ │ 语义匹配 │ │ 精确匹配 │ │ │
│ │ └────┬─────┘ └─────┬─────┘ │ │
│ │ │ │ │ │
│ │ └──────┬───────┘ │ │
│ │ ▼ │ │
│ │ Top-100 候选文档 │ │
│ └──────────────┬────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ ③ Re-ranking │ 用 Cross-encoder 精排, 从 Top-100 选 Top-5 │
│ │ (Cross-encoder) │ │
│ │ monoBERT 重排序 │ │
│ └────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ ④ 上下文注入 │ 把 Top-5 文档拼入 prompt, 带引用格式 │
│ └────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ ⑤ LLM 生成回答 │ 基于检索到的上下文生成回答, 带引用标注 │
│ └────────┬───────────┘ │
│ │ │
│ ▼ │
│ 用户收到回复 (带引用) │
│ │
│ 延迟预算: 查询重写 20ms + 检索 50ms + Re-rank 100ms + 生成 500ms │
│ = 670ms (P50) │
└──────────────────────────────────────────────────────────────────┘
3.6.2 稠密检索 (Dense Retrieval)¶
Dense Passage Retrieval (DPR, Karpukhin 2020) 是稠密检索的代表工作。 核心思想:用双编码器 (dual-encoder) 分别编码问题和段落,用余弦相似度匹配。
DPR 的损失函数 (对比学习):
其中 \(q\) 是问题编码,\(p^+\) 是正例段落编码,\(p_j^-\) 是负例段落编码。 目标是让正例的相似度远大于所有负例。
# DPR 检索器
from sentence_transformers import SentenceTransformer
class DenseRetriever:
"""稠密检索: 用 BGE 双编码器."""
def __init__(self, model_name="BAAI/bge-large-zh-v1.5"):
self.encoder = SentenceTransformer(model_name)
self.documents = [] # 文档列表
self.embeddings = None # 预计算文档嵌入
def index_documents(self, documents):
"""批量索引文档."""
self.documents = documents
texts = [doc["text"] for doc in documents]
# 批量编码, 3000 文档/秒 (A100)
self.embeddings = self.encoder.encode(
texts, convert_to_tensor=True, show_progress_bar=True
)
def retrieve(self, query, top_k=20):
"""检索最相似的文档."""
query_emb = self.encoder.encode(query, convert_to_tensor=True)
# 余弦相似度
scores = (self.embeddings @ query_emb.T).squeeze()
top_idx = scores.argsort(descending=True)[:top_k]
return [
{"doc": self.documents[i], "score": float(scores[i])}
for i in top_idx
]
3.6.3 稀疏检索 (Sparse Retrieval)¶
BM25 是经典的稀疏检索算法,基于关键词匹配。 适合:专有名词 (公司名/政策名/产品名),精准匹配场景。
密集检索 vs 稀疏检索对比:
| 维度 | Dense (稠密) | Sparse (稀疏) |
|---|---|---|
| 原理 | 语义匹配 | 关键词匹配 |
| 优点 | 理解同义词,语义相关 | 精确匹配,专有名词 |
| 缺点 | 语义漂移,专有名词弱 | 同义词不匹配 |
| 资源 | 需要 GPU,存储量大 | 只需 CPU,存储小 |
| 适用 | 开放域 QA,语义检索 | 专有名词,精确匹配 |
3.6.4 混合检索 (Hybrid Retrieval)¶
混合检索 = Dense + Sparse,加权融合:
# 混合检索: Dense + Sparse 加权融合
class HybridRetriever:
"""混合检索: Dense 语义 + Sparse 关键词."""
def __init__(self, dense_weight=0.7):
self.dense_retriever = DenseRetriever()
self.sparse_retriever = BM25Retriever()
self.dense_weight = dense_weight
def retrieve(self, query, top_k=100):
"""混合检索, 返回 Top-K 个候选."""
# 1. Dense 检索: 语义匹配
dense_results = self.dense_retriever.retrieve(query, top_k * 2)
# 2. Sparse 检索: 关键词匹配
sparse_results = self.sparse_retriever.retrieve(query, top_k * 2)
# 3. 分数归一化 (Min-Max)
def normalize(results):
scores = [r["score"] for r in results]
min_s, max_s = min(scores), max(scores)
for r in results:
r["score"] = (r["score"] - min_s) / (max_s - min_s + 1e-8)
return results
dense_results = normalize(dense_results)
sparse_results = normalize(sparse_results)
# 4. 加权融合
combined = {}
for r in dense_results:
combined[r["doc"]["id"]] = {
"doc": r["doc"],
"score": self.dense_weight * r["score"],
}
for r in sparse_results:
doc_id = r["doc"]["id"]
if doc_id in combined:
combined[doc_id]["score"] += (1 - self.dense_weight) * r["score"]
else:
combined[doc_id] = {
"doc": r["doc"],
"score": (1 - self.dense_weight) * r["score"],
}
# 5. 排序取 Top-K
results = sorted(combined.values(), key=lambda x: x["score"], reverse=True)
return results[:top_k]
3.6.5 Re-ranking (Cross-encoder)¶
检索阶段用双编码器 (Dense + Sparse) 快速找到 Top-100 候选,排序阶段用交叉编码器 (Cross-encoder) 精排。
Cross-encoder: monoBERT (Nogueira & Cho, 2019):
# Cross-encoder Re-ranking
from sentence_transformers import CrossEncoder
class ReRanker:
"""Cross-encoder 精排: 输入 (query, document), 输出相关度分数."""
def __init__(self, model_name="BAAI/bge-reranker-v2-m3"):
# 中文 Cross-encoder 重排序模型
self.model = CrossEncoder(model_name)
def rerank(self, query, candidates, top_k=5):
"""从 Top-100 候选中选择 Top-5."""
# 生成 (query, doc) 对
pairs = [(query, c["doc"]["text"]) for c in candidates]
# Cross-encoder 打分
scores = self.model.predict(pairs)
# 按分数排序
for i, c in enumerate(candidates):
c["rerank_score"] = float(scores[i])
candidates.sort(key=lambda x: x["rerank_score"], reverse=True)
return candidates[:top_k]
Cross-encoder 的分数公式:
对于每个 (query, document) 对,Cross-encoder 输出一个相关度分数:
其中 \(W\) 是线性分类层的权重。 交叉编码器比双编码器精度高 5-10%,但速度慢 10-100 倍,所以只在 Top-100 候选上做精排。
3.7 Step 5 — Generation (0:30-0:35)¶
3.7.1 上下文注入 + 引用¶
# RAG 生成: 上下文注入 + 引用
RAG_PROMPT = """你是一个企业内部知识库助手. 请根据以下检索到的文档回答用户问题.
检索到的文档:
{contexts}
用户问题: {question}
要求:
1. 回答必须基于检索到的文档, 不能凭空生成
2. 在回答中引用来源, 格式为 [来源: 文档名]
3. 如果检索到的文档不足以回答问题, 明确说"我无法从知识库中找到相关信息"
4. 回答简洁, 使用中文
回答:"""
# 上下文注入: 把 Top-5 文档拼入 prompt
def build_context(documents):
"""构建上下文文本."""
contexts = []
for i, doc in enumerate(documents):
source = doc["doc"].get("source", f"文档{i+1}")
text = doc["doc"]["text"]
contexts.append(f"[{source}]\n{text}")
return "\n\n---\n\n".join(contexts)
3.7.2 忠实度 (Faithfulness) 控制¶
RAG 的核心挑战是让 LLM 忠实于检索到的上下文,而不是凭记忆生成. 控制方法:
# Faithfulness 控制: 让 LLM 只基于上下文回答
class FaithfulnessController:
"""控制 LLM 生成忠实于上下文的回答."""
def __init__(self, llm):
self.llm = llm
def generate(self, question, contexts):
"""生成忠实回复."""
# 1. 构建 prompt (强制要求基于上下文)
prompt = self._build_prompt(question, contexts)
# 2. 生成初始回答
response = self.llm.generate(prompt)
# 3. Faithfulness 检查 (用少样本)
check_prompt = f"""判断以下回答中的每个陈述是否能在上下文中找到支持.
回答: {response}
上下文: {contexts}
请输出每个陈述的分析:
1. "..." — 支持 / 不支持 / 部分支持
2. "..." — 支持 / 不支持 / 部分支持
..."""
check_result = self.llm.generate(check_prompt)
# 4. 如果有不支持的陈述, 重新生成或删除
if "不支持" in check_result:
# 重新生成, 更严格约束
prompt = self._build_prompt(question, contexts, strict=True)
response = self.llm.generate(prompt)
return response
3.8 RAGAS 评估代码¶
# RAGAS 评估: 离线评估 RAG 系统
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
)
from datasets import Dataset
# 准备评估数据集
eval_data = {
"question": [
"公司年假政策是什么?",
"如何申请报销?",
"远程办公需要什么条件?",
],
"answer": [
"公司年假为 15 天, 工作满 1 年后可享受.",
"通过报销系统提交申请, 附上发票和审批单.",
"需要经理批准, 并确保网络连接稳定.",
],
"contexts": [
[
"年假政策: 员工工作满 1 年后, 每年可享受 15 天带薪年假.",
"年假申请需提前 3 天提交.",
],
[
"报销流程: 登录报销系统, 填写报销单, 上传发票和审批单.",
"报销审批周期: 5 个工作日.",
],
[
"远程办公政策: 员工可申请远程办公, 需经理批准.",
"远程办公要求: 稳定的网络连接, 每日在线会议.",
],
],
}
# 创建 Dataset
dataset = Dataset.from_dict(eval_data)
# 计算 RAGAS 指标
scores = evaluate(
dataset,
metrics=[
faithfulness,
answer_relevancy,
context_precision,
context_recall,
],
)
print(scores)
# 输出示例:
# {'faithfulness': 0.92, 'answer_relevancy': 0.88,
# 'context_precision': 0.85, 'context_recall': 0.90}
3.9 Prompt Cache / Semantic Cache¶
RAG 系统的一个优化点是缓存——相同问题重复出现时,不需要重新检索和生成:
# 语义缓存 (Semantic Cache)
class SemanticCache:
"""语义缓存: 相似问题直接返回缓存答案."""
def __init__(self, embedding_model, similarity_threshold=0.95):
self.encoder = SentenceTransformer(embedding_model)
self.threshold = similarity_threshold
self.cache = {} # query_embedding -> answer
self.cache_embeddings = []
def get(self, query):
"""检查缓存中是否有相似问题."""
query_emb = self.encoder.encode(query, convert_to_tensor=True)
if len(self.cache_embeddings) == 0:
return None
# 余弦相似度搜索
scores = (self.cache_embeddings @ query_emb.T).squeeze()
if len(scores.shape) == 0:
scores = scores.unsqueeze(0)
max_score = scores.max()
if max_score > self.threshold:
idx = scores.argmax().item()
return list(self.cache.values())[idx]
return None
def put(self, query, answer):
"""存入缓存."""
query_emb = self.encoder.encode(query, convert_to_tensor=True)
self.cache[query] = answer
self.cache_embeddings.append(query_emb)
3.10 中文圈案例:百度文心知识库 / 字节豆包 RAG¶
3.10.1 百度文心一言知识库¶
百度文心一言的知识库功能是其 RAG 能力的体现:
- 搜索引擎天生就是 RAG 引擎:百度搜索 + 文心一言 = 天然 RAG 架构
- 知识库:企业可以上传文档,构建专属知识库
- 检索:百度搜索的海量索引 + 语义匹配
3.10.2 字节豆包 RAG¶
字节跳动的豆包大模型通过火山引擎提供 RAG 服务:
- 知识库管理:支持多种格式文档上传,自动 chunking 和索引
- 检索增强:结合搜索和语义匹配,实现混合检索
- 引用溯源:在回答中标注信息来源于哪个文档
3.11 学员常见卡顿点 + 解套¶
| 卡顿点 | 解套 |
|---|---|
| 只答 LLM 不答检索 | RAG 的核心是检索,检索不好,LLM 再好也没用 |
| 不会 Chunking | 三种策略:Fixed / Semantic / Hierarchical,推荐 Hierarchical |
| 不提混合检索 | Dense (语义) + Sparse (关键词) 互补,加权融合 |
| 不提 Re-ranking | Cross-encoder 精排 Top-100 → Top-5,提升精度 5-10% |
| 不会 RAGAS | Faithfulness + Answer Relevance + Context Precision,三指标平均 |
| 不提 Faithfulness 控制 | LLM 会凭记忆生成,必须强制基于上下文 + 引用 |
| 不提缓存 | 语义缓存,相似问题直接返回,节省 90% 检索成本 |
| 不会 DPR 公式 | \(\mathcal{L} = -\log \frac{e^{\text{sim}(q, p^+)}}{e^{\text{sim}(q, p^+)} + \sum e^{\text{sim}(q, p_j^-)}}\) |
3.12 关键追问 cheat sheet¶
| 追问 | 标准答案 |
|---|---|
| Chunking 怎么选? | Fixed (简单) / Semantic (语义完整) / Hierarchical (推荐,摘要+段落) |
| Dense vs Sparse 区别? | Dense 语义匹配,Sparse 关键词匹配;混合检索最优 |
| Cross-encoder 怎么用? | 检索后精排,比双编码器精度高 5-10%,但慢 10-100 倍 |
| RAGAS 指标有哪些? | Faithfulness + Answer Relevance + Context Precision |
| Faithfulness 怎么保证? | 强制基于上下文 + 引用 + 少样本检查 + 不符合重新生成 |
| 语义缓存怎么设计? | Embedding 相似度 > 0.95 直接返回缓存 |
| DPR 损失函数? | 对比学习:\(\mathcal{L} = -\log \frac{e^{\text{sim}(q, p^+)}}{e^{\text{sim}(q, p^+)} + \sum e^{\text{sim}(q, p_j^-)}}\) |
| 什么时候 RAG 不如直接 LLM? | 常识性问题 (LLM 已学会)、创造性问题、实时性要求高时 |
📌 本节要点¶
- 3 道题覆盖对话系统核心:客服机器人 (传统 NLP 基本功) + ChatGPT 类通用对话 (LLM Serving 系统) + RAG (检索增强生成),层层递进。
- 客服机器人 4 段式:意图分类 + 槽位填充 + 对话状态管理 + 回复生成,从规则引擎到 BERT 到 LLM 的演进路径。
- 转人工 6 条件:置信度低 / 情绪负面 / 用户要求 / 多轮 / 投诉 / 未知意图,这是客服机器人安全阀。
- KV Cache 显存公式: \(M_{\text{kv}} = 2 \times n_{\text{layers}} \times d_{\text{model}} \times n_{\text{heads}} \times \text{seq\_len} \times 2\), 70B 模型 4096 token 需约 640 GB 显存。
- LLM Serving 全链路 7 层: LB → Rate Limiter → Router → Prompt Preprocessor → Safety Input → LLM Server → Safety Output.
- vLLM / SGLang / TensorRT-LLM 三选一: vLLM (快速原型) / SGLang (多轮对话) / TRT-LLM (高吞吐生产).
- RAG 检索 → 排序 → 生成:混合检索 (Dense + Sparse) → Cross-encoder Re-ranking → 上下文注入 + 引用。
- RAGAS 评估 3 指标: Faithfulness (忠实度) + Answer Relevance (回答相关性) + Context Precision (上下文精确率).
- DPR 对比学习损失: \(\mathcal{L} = -\log \frac{e^{\text{sim}(q, p^+)}}{e^{\text{sim}(q, p^+)} + \sum_{j=1}^{N} e^{\text{sim}(q, p_j^-)}}\),让正例远大于负例。
- 连续批处理提升吞吐 2-4 倍:每个请求独立处理,完成即加入新请求,不等待长请求。
参考文献¶
-
Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., Küttler, H., Lewis, M., Yih, W., Rocktäschel, T., Riedel, S., & Kiela, D. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. NeurIPS 2020. arXiv:2005.11401. (RAG 原论文,提出检索 + 生成的端到端框架.)
-
Karpukhin, V., Oğuz, B., Min, S., Lewis, P., Wu, L., Edunov, S., Chen, D., & Yih, W. (2020). Dense Passage Retrieval for Open-Domain Question Answering. EMNLP 2020. arXiv:2004.04906. (DPR 双编码器稠密检索,RAG 检索核心.)
-
Nogueira, R., & Cho, K. (2019). Passage Re-ranking with BERT. arXiv:1901.04085. (monoBERT Cross-encoder 重排序,RAG 排序阶段核心.)
-
Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H., Gonzalez, J. E., Zhang, H., & Stoica, I. (2023). Efficient Memory Management for Large Language Model Serving with PagedAttention. SOSP 2023. arXiv:2309.06180. (vLLM PagedAttention 核心论文,KV cache 分页管理.)
-
Es, S., James, J., Espinosa-Anke, L., & Schockaert, S. (2023). Ragas: Automated Evaluation of Retrieval Augmented Generation. arXiv:2309.15217. (RAGAS 评估框架,Faithfulness / Answer Relevance / Context Precision.)
-
Anthropic. (2024). Prompt Caching with Claude. Anthropic Blog, August 2024. (Prompt Cache 实现,共享前缀 KV cache,降低延迟和成本.)
-
Lin, T.-Y., Goyal, P., Girshick, R., He, K., & Dollár, P. (2017). Focal Loss for Dense Object Detection. ICCV 2017. (Focal Loss 原论文,可迁移到意图分类极不平衡场景.)
-
Devlin, J., Chang, M.-W., Lee, K., & Toutanova, K. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. NAACL 2019. (BERT 原论文,意图分类和槽位填充的基础模型.)
-
Huyen, C. (2022). Designing Machine Learning Systems. O'Reilly. (ML 系统设计综合参考,涵盖对话系统、RAG、LLM Serving.)
-
vLLM 官方文档. https://docs.vllm.ai/ (vLLM 部署和配置指南.)