跳转至

典型题深度演练 · 对话与 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 的通信量:

\[\text{Comm}_{\text{TP}} = 2 \times (4 \times d_{\text{model}} \times \text{rank} \times 2 \text{ bytes})\]

其中 \(d_{\text{model}}\) 是隐藏维度,rank 是 TP 卡数。 70B 模型 \(d_{\text{model}} \approx 8192\), 4 卡 TP,每层通信量约 524 KB. 80 层则约 42 MB 每 token.

KV 缓存 (KV cache) 显存公式:

\[ \text{KV cache memory} = 2 \times 2 \times n_{\text{layers}} \times d_{\text{model}} \times n_{\text{heads}} \times \text{seq\_len} \times 2 \text{ bytes} \]

其中第 1 个 2 是 K 和 V,第 2 个 2 是每头的维度。 实际计算:

\[M_{\text{kv}} = 2 \times n_{\text{layers}} \times d_{\text{model}} \times n_{\text{heads}} \times \text{seq\_len} \times \text{dtype\_bytes}\]

用 70B 模型举例:\(n_{\text{layers}} = 80\), \(d_{\text{model}} = 8192\), \(n_{\text{heads}} = 64\), seq_len = 4096, FP16 (2 bytes):

\[M_{\text{kv}} = 2 \times 80 \times 8192 \times 64 \times 4096 \times 2 = 2 \times 80 \times 8192 \times 64 \times 4096 \times 2 = 2 \times 80 \times 8192 \times 64 \times 4096 \times 2\]

逐步计算: $\(= 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 倍吞吐。

\[ \text{Throughput}_{\text{continuous}} = \frac{\sum_{i=1}^{N} \text{seq\_len}_i}{\max_i \text{seq\_len}_i \times \text{batch\_size}} \times \text{Throughput}_{\text{single}} \]

在短请求多的场景,连续批处理优势更明显——因为长请求不会拖慢短请求。

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 (忠实度):生成的回答是否完全基于检索到的上下文,不包含幻觉。

\[\text{Faithfulness} = \frac{|\text{claims supported by context}|}{|\text{total claims in answer}|}\]

其中 claims 是回答中的独立陈述。 例如"公司年假是 15 天,需要工作满 1 年"包含 2 个 claims,如果两个都能在检索到的上下文中找到,Faithfulness = 1.0.

Answer Relevance (回答相关性):回答是否回答了用户的问题。

Context Precision (上下文精确率):检索到的上下文中,有多少是真正相关的。

RAGAS 的总体分:

\[RAGAS = \frac{\text{Faithfulness} + \text{Answer Relevance} + \text{Context Precision}}{3}\]

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 的损失函数 (对比学习):

\[\mathcal{L} = -\log \frac{e^{\text{sim}(q, p^+)}}{e^{\text{sim}(q, p^+)} + \sum_{j=1}^{N} e^{\text{sim}(q, p_j^-)}}\]

其中 \(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 输出一个相关度分数:

\[s(q, d) = \text{sigmoid}(W \cdot \text{BERT}([CLS] q [SEP] d [SEP]))\]

其中 \(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 倍:每个请求独立处理,完成即加入新请求,不等待长请求。

参考文献

  1. 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 原论文,提出检索 + 生成的端到端框架.)

  2. 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 检索核心.)

  3. Nogueira, R., & Cho, K. (2019). Passage Re-ranking with BERT. arXiv:1901.04085. (monoBERT Cross-encoder 重排序,RAG 排序阶段核心.)

  4. 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 分页管理.)

  5. 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.)

  6. Anthropic. (2024). Prompt Caching with Claude. Anthropic Blog, August 2024. (Prompt Cache 实现,共享前缀 KV cache,降低延迟和成本.)

  7. Lin, T.-Y., Goyal, P., Girshick, R., He, K., & Dollár, P. (2017). Focal Loss for Dense Object Detection. ICCV 2017. (Focal Loss 原论文,可迁移到意图分类极不平衡场景.)

  8. Devlin, J., Chang, M.-W., Lee, K., & Toutanova, K. (2019). BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. NAACL 2019. (BERT 原论文,意图分类和槽位填充的基础模型.)

  9. Huyen, C. (2022). Designing Machine Learning Systems. O'Reilly. (ML 系统设计综合参考,涵盖对话系统、RAG、LLM Serving.)

  10. vLLM 官方文档. https://docs.vllm.ai/ (vLLM 部署和配置指南.)