← 返回学习路线 ◆ 贯穿项目
应用工程与上线 · 阶段 13 · Context Engineering 与大模型应用开发
Stage 13 / 17 · 应用工程与上线

Context Engineering 与大模型应用开发 Context Engineering & LLM Applications

2026 年最重要的一次认知升级:决定系统成败的不再是「提示词写得好不好」,而是「模型每一轮到底看到了什么」。上下文工程(Context Engineering)负责设计这些内容;驾驭工程(Harness Engineering)负责安排模型的运行框架——记忆、工具、多步规划与失败恢复。这两个词是 2026 年 AI 工程的核心,也是就业需求增长最快的方向。一个常被低估的事实:同样的模型,有良好上下文工程与 Harness 的团队能跑通 6 步以上的自主流程,没有的团队停留在单次问答。

⏱ 5–6 周 🎯 核心 · 就业主力 ◆ 里程碑 M13 2026-09-29
Context EngineeringRAG向量数据库MemoryHarnessFunction CallingGraphRAGPrompt CachingColBERT

阶段总览

✔
学完你能做到
  • 能设计完整的上下文:系统提示、工具定义、检索文档、记忆、历史与 Token 预算
  • 掌握 RAG 全流程每个环节的取舍:切分、嵌入、索引、混合检索、重排、生成与评测
  • 能做进阶 RAG:查询改写、HyDE、多查询、查询路由、Agentic RAG、GraphRAG、RAPTOR
  • 能设计分层记忆系统(工作 / 会话 / 长期)与上下文压缩策略
  • 理解长上下文 vs RAG 的四维对比,知道何时可以不用 RAG
  • 能实现可靠的结构化输出与工具调用,并掌握约束解码与解析失败的降级路径
  • 会做模型路由、prompt caching 与语义缓存,并核算线上成本
阶段知识结构总览 · 让模型每轮看到对的东西
阶段 13 · Context Engineering 与大模型应用开发Context Engineering & LLM Applications · 10 大章 · 152 个知识点
1. 上下文工程少而精优于多而杂上下文腐坏 context rotsystem / user / assistantToken 预算配比
2. RAG父子文档切分HNSW / PQ 索引混合检索 + RRFcross-encoder 重排recall@5 > 0.8
3. 查询理解查询改写 / 路由HyDE 假设文档查询分解 2–4 子问题
4. GraphRAG / RAPTOR实体图 + 社区摘要层级摘要树抽取式压缩预处理 token 5–10 倍
5. 记忆系统工作 / 会话 / 长期重要性筛选冲突消解滚动摘要 8–15x
6. 长上下文 vs RAG成本差 10–100 倍TTFT 与 prefillRAG 为主 + 长上下文兜底
7. Harness Engineering记忆 / 工具 / 规划失败恢复分类检查点续跑人工审批门禁
8. 结构化输出约束解码tool_calls 协议降级金字塔tool_call_id 对应
9. 缓存Prompt 前缀缓存语义缓存命中降 90% 成本失效与一致性
10. 成本与路由六个成本杠杆模型路由 60/30/10成本核算模板
贯穿项目 · M13 混合检索 RAG 管线第 72–78 周查询改写 → 混合召回 → 融合(RRF)→ 重排 → 压缩 → 生成 → 引用上下文压缩与 token 预算分配(含长文档分块策略对比)每个论断可定位到原文 span,含打不开引用时的降级行为逐组件消融实验(去掉重排 / 去掉混合检索 / 去掉压缩各掉多少)
学习路径
★
两个词必须分清:Context Engineering(上下文工程):设计单次调用中模型看到的全部内容(检索什么、保留多少历史、暴露哪些工具、Token 怎么分配)。
Harness Engineering(驾驭工程):设计模型运行的框架(多步循环、状态与记忆、工具编排、失败恢复、人工审批)。
前者决定「这一轮它能看到什么」,后者决定「整个任务它怎么推进」。2026 年行业判断:同样的模型,有良好 Harness 的团队能跑通 6 步以上的自主流程,没有的团队停留在单次问答。
周次主题交付物
第 1 周上下文工程定义与 Prompt 结构带 Token 预算与污染防护的上下文组装器
第 2 周RAG 全流程:切分 / 嵌入 / 索引 / 检索 / 重排 / 生成企业知识库问答(含引用与评估)
第 3 周查询理解 + 进阶 RAG查询改写 + 混合检索 + 重排 + GraphRAG 实验
第 4 周记忆系统 + 长上下文 vs RAG分层记忆 + 压缩,长上下文对比报告
第 5 周结构化输出与工具调用 + 缓存带约束解码、降级与缓存的工具框架
第 6 周成本、路由与工程质量路由 / 缓存 / 成本核算报告

1. 上下文工程:设计模型每轮看到的一切

知识结构图 · 上下文工程
上下文工程:设计模型每轮看到的一切6 大知识域 · 24 个知识点
定义与原则少而精优于多而杂上下文腐坏 context rot污染 / 干扰 / 冲突四类内容配比
学习路径
  1. 读 1.1:掌握少而精原则与上下文腐坏(污染 / 干扰 / 冲突)三类风险
  2. 跑内置代码:构造一段含污染的上下文观察输出被带偏
  3. 完成动手练习:按四类内容配比重写一段 system prompt并对比
  4. 对接 M13:给 RAG 生成设定上下文预算,压缩质检剔除污染片段
✔ 能主动识别并削减污染 / 干扰 / 冲突并给出可复现效果
核心知识点详解
  • 少而精优于多而杂:上下文工程的核心是少而精。塞无关内容会稀释关键信息(context rot / 上下文腐坏)。量化:同样的 200 token 关键证据,在 2K 窗口占比 10%,在 128K 窗口只有 0.2%——注意力份额掉 64 倍。先过滤、再注入。
  • 四类上下文的预算配比:系统指令 5%–10%、检索证据 20%–40%、对话历史 10%–30%、工具结果 10%–30%。哪类「放多了」会怎样要背下来:系统被噪声淹没、证据无关片段抢注意力、旧话题干扰当前任务。比例 = 重要性排序的工程落地。
  • 三类上下文问题:污染(不相干内容混入,拉低信噪比)、干扰(错误 / 矛盾片段压过正确片段)、冲突(多份证据彼此矛盾,模型乱编或随机选边)。共同根因都是塞得多、筛得不够——解法不是更大窗口,而是更狠的过滤与重排。
  • 常见坑:以为「长上下文 = 可以不做工程」,把全部资料无脑塞进 128K 窗口,成本线性涨、关键信息被稀释、TTFT 变长。能压缩就压缩,保留关键 margin 给真正需要的证据。
Prompt 角色语义system 持久规则few-shot 用 assistantuser 承载任务XML 标签分块
学习路径
  1. 读 1.2:吃透 system / user / assistant 角色语义与放置位置
  2. 跑内置代码:把示例放 system vs assistant 对比遵循差异
  3. 完成动手练习:用 XML 标签分块重构多段指令
  4. 对接 M13:把检索到的文档按角色与标签规范注入生成 prompt
✔ 能说出各角色用途并让模型在多段指令下稳定遵循
核心知识点详解
  • system 承载持久规则:system 放跨轮稳定、精简的规则(角色、输出格式、安全边界),置于最前以命中 token 缓存(前缀必须逐 token 相同才能命中)。system 里塞易变内容会导致缓存全部失效。
  • few-shot 示例放 assistant:示例对话最好以 assistant 角色呈现(与真实问答同构,而非包进 system / user),让模型看到「该长什么样」。示例应格式统一,且难例放最后压阵,利用 recency bias。
  • user 承载具体任务 + XML 标签:user 放当轮任务与检索到的证据;多段指令用 XML 标签分块(//)比自然语言更易被拆解遵循。角色语义 = 「谁负责稳定、谁负责当轮、怎么标段」。
  • 常见坑:把易变的检索结果塞进 system,导致前缀缓存失效 + 上下漂浮;或示例混用 user/assistant 角色,模型参照错位、格式漂移。按角色语义放对位置,示例统一放 assistant。
Few-shot 选择排序recency bias难例放最后2–5 个足够示例格式须一致
学习路径
  1. 读 1.3:掌握 recency bias、难例置后、2–5 个与格式一致四要点
  2. 跑内置代码:交换示例顺序观察位置偏置对输出的影响
  3. 完成动手练习:为 RAG 场景挑 few-shot 并经格式统一后自测
  4. 对接 M13:把 few-shot 固化进检索增强的生成模板并对比首卷
✔ 能通过换序实验证明自己示例选择与排序的合理性
核心知识点详解
  • recency bias / 位置偏置:模型对后出现的示例记忆更强、先出现的示例注意力被稀释——这就是 recency bias。可用「交换示例顺序」实验验证:同一组示例换序,输出在难例上的表现随之改变。选示例要刻意利用这个偏置。
  • 难度排序 + 数量定档:难例 / 边界例放最后(压阵承受最新注意力),简单例放前。数量 2–5 个足够——太少学不到模式,太多稀释注意力且 token 成本线性涨。
  • 示例格式必须一致:所有示例的输入输出结构、标注、字段命名须完全一致,否则模型把「不一致」当规则学走。RAG 场景的 few-shot 要固化进检索增强的生成模板并做格式统一。
  • 常见坑:示例越长越细越好、堆 10 个以上——注意力被稀释、成本翻倍;或示例间格式漂移(有的带引用有的不带),模型混学。用换序 + 数量消融证明自己的选择合理。
格式约束XML > 自然语言JSON Schema 约束解码格式越硬遵循率越高think / answer 分离
学习路径
  1. 读 1.4:比较 XML / Markdown / JSON 对遵循率的影响与约束解码原理
  2. 跑内置代码:用约束解码与自由文本各生成一次比较合法率
  3. 完成动手练习:为你的输出定义 JSON Schema 并做 think / answer 分离
  4. 对接 M13:让引用溯源输出走 strict 解码,结构合法且语义正确
✔ 能把输出做成结构合法率接近 100% 并通过语义校验
核心知识点详解
  • XML > 自然语言:对多段结构指令,XML 标签(//)的遵循率通常高于等价的自然语言描述。格式越硬(标签清晰、枚举放死),遵循率越高——用约束解码 + JSON Schema 把格式「焊死」。
  • 约束解码 / JSON Schema:constrain decode(Grammar / FSM / JSON 模式)在采样时只允许合法 token,结构合法率可逼近 100%(字段不缺、类型不错、无多余键)。它只保证结构合法,不保证语义正确——语法治这个。
  • think / answer 分离:把「推理过程」与「最终答案」分开(......),思维链受控、答案结构化、便于解析与引用溯源。RAG 的引用输出应走 strict 解码 + 结构合法且语义经校验。
  • 常见坑:误以为约束解码保证语义正确:金额 / 日期 / 单位仍可能算错,需再用业务规则 / Pydantic 校验。结构打标 + 语义校验要分开做,别把「合法」当「正确」。
组成部分与预算系统 5–10%检索 20–40%历史 10–30%超预算先压缩
学习路径
  1. 读 1.5:背下系统 / 检索 / 历史三类内容的预算配比与压缩顺序
  2. 跑内置代码:把一段超预算上下文压缩到目标 token 内并观察信息损失
  3. 完成动手练习:为一次真实查询分配 token 预算
  4. 对接 M13:按预算先压缩的规则实现生成侧上下文压缩
✔ 能核算上下文各段 token 占比并先压历史再做检索注入
核心知识点详解
  • 三段的预算配比:系统 5%–10%、检索 20%–40%、历史 10%–30%。这是一张「注意力预算表」:系统精简稳定、检索宁少而精(重排 + 去冗余)、历史滚动窗口 + 摘要。先给真实查询算一次各段占比。
  • 超预算先压历史:压缩顺序通常是先压对话历史(滚动摘要,可压到 1/8–1/15)、再精简工具定义与系统说明、最后才砍检索证据——因为检索最接近答案。一句原则:超预算 → 先压历史,再做检索注入。
  • 压缩的量化:滚动摘要「相关结论提取」+ 保字段,可比原始上下文压缩到 1/8–1/15 仍保留关键结论;衡量信息保留用「原上下文能否复原答案」的回溯测试而非盯着 token 数。
  • 常见坑:只算「总 token 够不够窗口」,不算各段占比——检索证据被历史挤到 5% 以下,答案漂了还不知道原因。先核算占比,再定压缩顺序,再注入检索。
Prompt 工程的边界仍是上下文子集指令 vs 示例取舍示例冲突优先示例Prompt 当代码版本化
学习路径
  1. 读 1.6:理解 Prompt 工程只是上下文工程的子集与边界
  2. 跑内置代码:制造指令与示例冲突,观察示例优先于指令
  3. 完成动手练习:把一条 Prompt 版本化并记录每次改动的影响
  4. 对接 M13:把 Prompt 模板纳入仓库版本管理并挂到 CI 回归
✔ 能界定 Prompt 工程边界并用版本化证明一次改动的可复现影响
核心知识点详解
  • Prompt 工程是上下文子集:Prompt 工程关注「这一句话怎么写」(措辞 / 示例 / 格式);上下文工程关注「这一轮模型看到的全部」——系统提示 + 工具定义 + 检索文档 + 记忆 + 历史 + 预算分配。2026 重心已从措辞转向注入与组织。
  • 指令 vs 示例,示例优先:构造「指令说 A、示例做 B」的冲突实验:模型通常按示例执行而非严格指令。原因:示例提供了可复制的分布先验。所以想让模型稳定照做的行为,宁可做成 few-shot 也不只写规则。
  • Prompt 当代码版本化:Prompt 不再是一次性文本,要进 Git 版本管理 + CI 回归测试——改一行能复现地说明影响(用好/坏样本集做前后对比)。这正是「工程」区别于「灵机一动」的标志。
  • 常见坑:把薄弱语义全押在更长的指令里,不验证就上线(改了自己都不知道);或不版本化,出问题无法回滚定位。指令冲突时不真实验证「示例是否真的优先」,靠猜。
学习路径

1.1 定义与原则:稀缺窗口与少而精

上下文工程的定义极其朴素:设计单次模型调用中,模型能看到的所有内容,以及它们如何组织。它不是「写好一个 Prompt」,而是「在有限的上下文窗口里,把最相关的信息以最不易被误解的方式放进去」。核心原则是少而精优于多而杂——向窗口里塞无关内容,会稀释关键信息的注意力(即「上下文腐坏 / context rot」),并线性推高成本与延迟。

上下文的四类内容典型占比配比原则放多了会怎样
系统指令5%–10%稳定、精简、放最前以命中缓存被后续噪声淹没,指令失效
检索证据20%–40%宁少而精,重排 + 去冗余无关片段抢走注意力,答案漂移
对话历史10%–30%滚动窗口 + 摘要旧话题干扰当前任务
工具结果10%–30%只保留本轮需要的工具过多导致误选 / 超预算
python# 上下文腐坏(context rot):塞得越多,关键信息越容易被稀释
def share(relevant_tokens, total_tokens):
    return relevant_tokens / total_tokens     # 关键信息的「注意力份额」近似
for total in [2_000, 8_000, 32_000, 128_000]:
    print(f"总 token={total:>7}  关键信息占比≈{share(200, total):.3f}")
# 预期输出:2000 -> 0.100;8000 -> 0.025;32000 -> 0.006;128000 -> 0.002
# 结论:同样 200 token 的关键证据,在 128K 窗口里的注意力份额只有 2K 窗口的 1/64。
# 这就是「长上下文 != 可以不做工程」的量化依据——先过滤、再注入。
⚠
三类上下文问题:① 污染(contamination):不相干内容混进来,拉低整体信噪比;② 干扰(distraction):检索到的错误 / 矛盾片段压过正确片段,模型被带偏;③ 冲突(conflict):多份证据彼此矛盾,模型随机选边或乱编。这三类的共同根因都是「塞得太多、筛得不够」,解法不是更大窗口,而是更狠的过滤与重排。

1.2 Prompt 结构:system / user / assistant 的角色语义

Prompt 不是一段自由文本,而是由三种角色消息按训练时的语义约定组成的。模型在预训练与对齐阶段被教成:system 是「持久的人格与规则」、user 是「当前任务与输入」、assistant 是「模型此前的输出,也会被继续预测」。搞混角色会让对齐失效。

角色训练时的语义常见误用
system设定角色、边界、输出格式、长期规则;权重最高把大段用户数据塞进 system(应放 user);忽视其优先级导致指令被覆盖
user承载本轮任务、问题与附件把系统规则也写进 user,导致指令层级混乱
assistant模型历史输出;few-shot 示例常伪装成它few-shot 示例写错角色,模型学不到示范;把待填的空也标成 assistant
python# 一个角色语义清晰、用 XML 标签划分区块的 Prompt 模板
messages = [
    {"role": "system", "content": (
        "你是严谨的合规助手。规则:\n"
        "1. 只依据 <evidence> 回答,资料不足必须说不知道。\n"
        "2. 输出 JSON,字段见 schema。\n"
        "3. 不得编造条款编号。"
    )},
    {"role": "user", "content": (
        "<question>这份合同里乙方的违约金上限是多少?</question>\n"
        "<evidence>{{retrieved_chunks}}</evidence>"
    )},
    # assistant 角色留给「模型自己生成」或「few-shot 示范」
]

# 关键:few-shot 示例要用 assistant 角色承载,模型才会把它当成
# 「应该模仿的输出范式」,而不是当成要回答的问题。
python# 角色语义错误的典型后果:规则、数据、示范必须各归其位
good = [
    {"role": "system",    "content": "只依据 <evidence> 回答,不足则拒答"},
    {"role": "user",      "content": "<question>违约金上限?</question>"},
    {"role": "assistant", "content": '{"answer": "见条款 8.2"}'},   # few-shot 示范
]
# 常见错误写法:
#  - 把 <evidence> 整段塞进 system -> 模型把数据当规则(越权 / 指令被覆盖)
#  - 把「待填的答案」也标成 assistant -> 模型以为已给定,提前停止
# 经验:system 占比 5%–10%,越短越稳;示范一律用 assistant 承载。
✔
用 XML / Markdown 标签而非自然语言分隔:与其写「下面是问题,下面是证据」,不如用 ... 这类显式标签包裹。模型对结构化边界的遵从度明显高于散文式分隔,且能降低把证据误当指令的风险。

1.3 Few-shot 示例的选择与排序

Few-shot 不是「随便给几个例子」,选择和排序都会显著影响输出。顺序之所以重要,是因为自回归模型存在 recency bias(更关注靠近末尾的 token),且会把示例当成分布的一部分。把最难、最该模仿的示例放在最后,效果通常最好。

维度做法坑
选择挑与当前输入同分布、覆盖边界情形的示例随机挑导致示范不代表真实分布
排序简单在前、最难 / 最该模仿的在后(recency bias)把好例子埋在中间被忽略
数量2–5 个通常足够;过多占预算且稀释堆 20 个示例挤掉检索证据
一致性示例格式必须与目标输出格式完全一致示例与要求格式冲突,模型困惑
多样性覆盖典型失败模式只给正例,模型遇到异类就崩
python# Few-shot 排序的朴素实现:把「最难示范」放到最后
def order_examples(examples):
    # 启发式:长度越短越「简单」,越靠后越「该被模仿」放最后
    return sorted(examples, key=lambda e: e["difficulty"])  # 难度升序,最难在末尾

few_shot = [
    {"role": "user",      "content": "把 '订单已发货' 分类"},
    {"role": "assistant", "content": '{"category": "物流"}'},
    {"role": "user",      "content": "客户说 '我要退款' 而且很生气"},
    {"role": "assistant", "content": '{"category": "退款", "urgency": "高"}'},  # 难例,放最后
]

# 注意:示例与真实问题之间不要留空行空隙过大,否则模型可能误以为示例已结束。
python# few-shot 顺序的影响:把「最该模仿的示例」放到最后
def build_fewshot(examples, hardest_first=False):
    ex = sorted(examples, key=lambda e: e["difficulty"])
    if hardest_first:
        ex = ex[::-1]                        # 难例放最前
    msgs = []
    for e in ex:
        msgs.append({"role": "user", "content": e["q"]})
        msgs.append({"role": "assistant", "content": e["a"]})
    return msgs
# 经验(示意):难例放末位时,复杂输入的格式遵循率约 +5~10 个百分点(recency bias);
# 但示例超过 5 个后收益趋平甚至下降,因为会挤占检索证据的 token 预算。
ℹ
指令 vs 示例的取舍:当任务可言传时用指令(省 token、可控);当任务靠「感觉 / 风格」时用示例(模型更会模仿)。两者冲突时,模型往往更听示例的——所以示例格式一旦写错,比指令写错更致命。经验:用一条强指令定边界,用 2–3 个示例定风格。

1.4 格式约束:XML / Markdown / JSON 对遵循率的影响

输出格式的「硬度」直接影响模型是否遵循。研究与实践共识是:结构化标签 > 自然语言描述,且越接近模型训练时见过的格式,遵循率越高。

格式遵循率经验适用注意
自然语言指令最低(易被自由发挥带偏)开放创作、闲聊不适合需要可解析输出的场景
Markdown / 列表中—高人类可读报告、步骤仍需后处理提取
XML 标签高需明确边界的结构(证据 / 章节)标签要成对、无嵌套歧义
JSON(无 schema)高程序消费仍可能漏字段 / 类型错
JSON / 约束解码最高(接近 100%)强结构化输出需要结构化输出能力或 grammar 约束
python# 用 XML 标签强制模型把「推理」与「结论」分开,便于程序提取
template = """回答前先在 <think> 中列出依据,最后只输出 <answer> 里的 JSON。
<think>{reasoning}</think>
<answer>{{ "category": "...", "confidence": 0.0 }}<answer>"""

# 经验:把「要求」与「示例格式」都写进 system,并在 user 里用标签
# 包裹输入,模型的遵循率比纯散文高出一个档次。
输出格式格式遵循率(经验)程序可直接解析率
自然语言描述最低需正则,常失败
Markdown 列表中—高需后处理提取
XML 标签高高(标签成对即可抽取)
JSON(无 schema)高中(仍可能漏字段 / 类型错)
JSON Schema / 约束解码接近 100%接近 100%(结构与类型都被约束)
★
一个反直觉的结论:很多人以为「把格式要求写得更详细」就能提高遵循率,但真正拉开差距的是格式本身的硬度:与其写「请输出 JSON 格式」,不如直接开启模型的 JSON 模式 / 约束解码,让模型在结构上「无法不遵循」。详见第 8 节结构化输出。

1.5 上下文的组成部分与 Token 预算

组成部分内容典型预算占比优化手段
系统提示角色、规则、输出格式、边界5%–10%精简、模板化、把稳定部分放前面以命中缓存
工具定义函数签名、参数 schema、说明5%–15%只暴露当前任务需要的工具,避免工具过多导致误选
检索文档RAG 召回的片段20%–40%重排 + 去冗余 + 压缩,宁少而精
记忆长期事实、用户偏好、先前结论5%–15%分层存储,按相关性注入,定期摘要压缩
对话历史多轮交互记录10%–30%滚动窗口 + 摘要 + 关键信息结构化提取
当前输入用户本轮问题与附件10%–20%附件按需解析,不要无脑全文塞入
python# Token 预算的显式分配:超预算必须压缩,而不是硬塞
budget = {"system": 2000, "tools": 3000, "retrieved": 12000,
          "memory": 3000, "history": 8000, "user": 4000}
cap = 32000
used = sum(budget.values())
print("预算合计:", used, "上限:", cap, "余量:", cap - used)     # 32000 32000 0
# 预期输出:合计 32000 = 上限,无余量 -> 任一部件超支都要靠压缩腾挪。
# 经验配比:检索 20%–40%、历史 10%–30%、系统 5%–10%;
# 先按占比分配,再按相关性把预算填满,而不是「先塞后砍」。
⚠
长上下文不等于可以不做工程:即使模型支持 1M 上下文,也不该把所有东西都塞进去:① 成本随 token 线性增长;② 关键信息会被无关内容稀释(注意力被分散,即「上下文腐坏」);③ 延迟显著上升。正确姿势是:先检索与压缩,只注入真正相关的内容,把长上下文当作兜底而不是策略。
python# 上下文组装器:把「该放什么」变成显式的、可测量的工程
from dataclasses import dataclass

@dataclass
class Budget:
    total: int = 32_000
    system: int = 2_000
    tools: int = 3_000
    retrieved: int = 12_000
    memory: int = 3_000
    history: int = 8_000
    user: int = 4_000

def assemble(system_prompt, tools, retrieved_docs, memories,
             history, user_input, budget=Budget()):
    parts = []
    parts.append(truncate(system_prompt, budget.system))          # 系统提示最稳定,放最前
    parts.append(render_tools(select_relevant_tools(tools, user_input), budget.tools))
    # 检索文档按相关性排序,逐条加入直到超出预算(宁缺毋滥)
    parts.append(pack_by_score(retrieved_docs, budget.retrieved, key=lambda d: d.score))
    parts.append(pack_by_recency(memories, budget.memory))        # 记忆按相关性 + 新鲜度
    parts.append(summarize_if_needed(history, budget.history))    # 历史超限则摘要
    parts.append(user_input)
    prompt = "\n\n".join(p for p in parts if p)
    assert est_tokens(prompt) <= budget.total, "超预算,必须压缩而不是硬塞"
    return prompt

1.6 Prompt 工程没有消失,只是变成了子集

2026 年招聘数据里,prompt engineering 的岗位数仍然远多于 context engineering(约 2.2 万 vs 700),但重心已经转移。结论:简历上两个词都要有,能力上以上下文工程为主。写 Prompt 的技巧依然有用,但它只是「上下文工程」中的一小块。

python# 把 Prompt 当代码管理:版本化 + 快照测试
PROMPTS = {
    "extract_v2": "从工单文本抽取 category / urgency / order_id,输出 JSON。",
    "classify_v1": "把用户消息分类为 退款 / 物流 / 质量 / 其他。",
}
# 每次改动要做三件事:(1) 记版本 (2) 跑回归集 (3) 对比新旧命中率。
# 经验:加一道快照测试(固定 50 条输入 + 期望输出字段),
#   能在改 prompt 时第一时间发现「改坏了」,比人肉 review 可靠得多。
python# 结构化输出:让模型输出可被程序消费,而不是靠正则去猜
from pydantic import BaseModel, Field
from typing import Literal

class Ticket(BaseModel):
    """客服工单的结构化抽取结果"""
    category: Literal["退款", "物流", "质量", "其他"] = Field(description="问题类别")
    urgency: Literal["低", "中", "高"] = Field(description="紧急程度")
    order_id: str | None = Field(default=None, description="订单号,没有则为 null")
    summary: str = Field(max_length=120, description="一句话摘要")

# 两种落地方式:
#  A) 用模型的 structured output / JSON schema 能力直接约束解码(最可靠)
#  B) 让模型输出 JSON 文本,再用 Pydantic 校验;失败则重试或降级
def extract(client, text: str) -> Ticket | None:
    for attempt in range(3):
        raw = client.chat(messages=[{"role": "user", "content": text}],
                          response_format={"type": "json_schema",
                                           "json_schema": Ticket.model_json_schema()})
        try:
            return Ticket.model_validate_json(raw)              # 校验
        except Exception as e:
            log.warning("schema 校验失败 attempt=%d: %s", attempt, e)
    return None                                                # 降级:转人工 / 走规则

1.7 动手练习与自测

✔
动手练习与自测(上下文工程):第 1–2 题动手算,第 3–5 题口头自测并对照判据表。
  1. 用 1.1 的份额模型,算出「500 token 关键证据放进 64K 窗口」的注意力份额,并说明该不该直接塞。
  2. 按 1.5 的预算表给一个「法律合同问答」重新分配 Token 预算(检索 / 历史 / 系统),说明依据。
  3. system / user / assistant 三个角色各自的语义是什么?各给一个常见误用。
  4. 同一个模型,为什么「示例格式写错」比「指令写错」更致命?
  5. Prompt 工程与上下文工程的关系是什么?2026 年为什么重心转移?
题号考点关键判据 / 参考答案
1注意力稀释500/64000 ≈ 0.008;应压缩或只注入相关片段,不能直接塞
2预算分配法律问答证据比例调高(如检索 40%)、历史压缩;系统规则精简靠前以命中缓存
3角色语义system=持久规则(权重最高)、user=当前任务与数据、assistant=历史输出/示范;误用见 1.2 表
4示例优先模型更倾向模仿示例而非遵守散文指令,格式错了会被系统性仿效
5关系与趋势Prompt 工程是上下文工程的子集;限制系统表现的是「关键信息是否被正确注入」而非措辞

2. RAG:最落地的生产模式

知识结构图 · RAG 全流程
RAG:最落地的生产模式6 大知识域 · 30 个知识点
切分与嵌入递归切分chunk 256–512 tokenoverlap 10–20%父子文档L2 归一化Matryoshka 截断
学习路径
  1. 读 1.1 与 2.2:掌握递归切分参数与嵌入的 L2 归一化 / Matryoshka 截断
  2. 跑内置代码:对同一份文档用不同 chunk + overlap 对比召回的 recall@k
  3. 完成动手练习:按父子文档策略为你的语料划分索引单元
  4. 对接 M13:把切分与嵌入参数写进 RAG 管线并纳入消融对照
✔ 能靠 recall@k 对比说明自己 chunk 与嵌入参数选择的依据
核心知识点详解
  • 递归切分与 overlap:递归切分按分隔符层级(标题 → 段落 → 句子)切分;chunk 常用 256–512 token,overlap 设 10%–20%(避免切断语义让同一句话被拆两项)。参数直接影响 recall:chunk 太小 → 上下文不足回不到目标;太大 → 向量被稀释。
  • 父子文档策略:父子文档:大块「父」用于检索定位、小块「子」用于生成上下文。父块保语义完整、子块保细节精确,常用在表格 / 代码/长段落场景。判定依据就是 recall@k 的消融对比。
  • L2 归一化 + Matryoshka 截断:嵌入向量要做 L2 归一化再用内积(否则长文本模长虚高误排);Matryoshka 截断(如 768→256 维)可省存储与算力、召回损失很小。参数选型都要靠 recall@k 对比验证,而不是拍脑袋。
  • 常见坑:嵌入没归一化就做内积,长文本向量模长虚高导致误排;或切分把表格 / 代码切断,召回到的片段不可用。用固定评测集 + recall@k 消融证明参数选择。
索引与检索HNSW M=16IVF nprobePQ 压缩约 64xBM25 稀疏检索RRF 融合 k=60
学习路径
  1. 读 2.3 与 2.4:理解 HNSW M / IVF nprobe / PQ 的代价与 BM25 稀疏检索
  2. 跑内置代码:用 HNSW 建索引并对比暴力检索的 Recall@10 与 QPS
  3. 完成动手练习:实现 RRF 融合(k=60)合并向量 + BM25 两路召回
  4. 对接 M13:把混合检索(向量 + BM25)+ RRF 接入主干管线
✔ 能量出索引的召回-速度权衡并跑通混合检索 + RRF 融合
核心知识点详解
  • 近似最近邻三件套:HNSW(M=16 常用)图索引,召回高但建索引 / 内存大;IVF(聚类倒排 + nprobe 控制查多少个桶);PQ 乘积量化把向量压缩(约 64× 减内存)但引入精度损失。三者是「召回-速度-内存」三角,要实测权衡。
  • BM25 稀疏检索:向量擅长语义近似,但对必须逐字匹配(订单号 / 型号 / 人名)弱;BM25 稀疏向量按词频 + 逆文档频率正好补短板。两类检索分数量纲不同,不能直接相加。
  • RRF 融合(k=60):RRF 按排名融合两个检索器结果:score(d) = Σ 1/(k + rank_i(d)),k 常取 60。它只依赖排名不依赖分数量纲,天然解决向量 / BM25 分数量纲不可比。混合检索 = 向量 + BM25 → RRF → 取 top-k。
  • 常见坑:只用向量检索,编号 / 型号 / 专有名词召回不到;或直接把向量分数 + BM25 分数相加(量纲不同必然错排)。用 RRF 融合并按 Recall@10 / QPS 实测。
重排cross-encoder 重排ColBERT 晚期交互MaxSim 聚合召回 100 重排取 8加 100–300ms 延迟
学习路径
  1. 读 2.5:分清 cross-encoder 与 ColBERT 晚期交互的机制与延迟
  2. 跑内置代码:把重排器加进管线,记录召回 100 重排取 8 的效果与耗时
  3. 完成动手练习:对比召回 100 直接取 vs 重排取 8 的端到端指标
  4. 对接 M13:把交叉编码器重排接入管线并量化它对精度的增益
✔ 能给出重排前后指标对比并判断多花 100–300ms 是否值得
核心知识点详解
  • cross-encoder 重排:交叉编码器把「query + 文档」拼成一个序列喂给模型,能精确建模两者交互,重排精度高于双塔向量检索,但只能两两配对、无法预先缓存,每对都要一次 forward。召回 100 条重排取 top 8 是典型用法。
  • ColBERT 晚期交互 / MaxSim:ColBERT v2 给 query 与文档各过一个单塔得到 token 向量,再做晚期交互:对每 query token 与文档所有 token 取最大相似度 MaxSim 聚合。可把文档向量预计算索引化,兼顾精度与速度,是 cross-encoder 与双塔之间的折中。
  • 多花 100–300ms 的账:重排让 top-k 不变但把「真正命中的例子」提到前面,常常是对端到端精度提升最高、成本却最低的一个环节。召回 100 → 重排取 8 只需对 100 对做 cross-encoder,多花 100–300ms 是否值取决于它对 Answer Relevancy 的提升是否超过延迟预算。
  • 常见坑:跳过重排直接取向量 Top-1,答案质量不稳;或对已很小的 top-k 也重排,白花延迟无收益。先召回 100,做「直接取 vs 重排取 8」的端到端对比再决定。
生成与拒答引用 chunk id资料不足则拒答证据冲突处理faithfulness 检查
学习路径
  1. 读 2.6:掌握引用 chunk id、资料不足拒答与证据冲突的生成策略
  2. 跑内置代码:构造证据冲突样例观察引用与拒答行为
  3. 完成动手练习:实现 faithfulness 检查并标记低可信输出
  4. 对接 M13:全部论断带引用溯源且打不开引用时走降级
✔ 能证明自己每条论断可溯源且资料不足时会拒答
核心知识点详解
  • 引用 chunk id:生成的每条论断都要能对应到具体 chunk(如 [c3]),让用户可回溯到原文。做法:要求模型在答案后列出引用 id,若打不开对应引用则走降级,从源头遏制「自信的错误答案」。
  • 资料不足则拒答:检索结果无法支撑回答时,模型应明确 拒答 / 声明信息不足,而不是强行编造。这是 RAG 相对裸 LLM 的核心收益之一;用「资料不足集」测拒答率。
  • 证据冲突处理:当多片段互相矛盾时,模型要么报告冲突、要么按「更新 / 权威 / 多数」策略择优并注明。被动让模型随机选了会得出不可靠答案——要把冲突显式送进 prompt 让模型裁决。
  • 常见坑:答案没有引用、也无拒答逻辑,用户不辨真伪就信了;或证据冲突时模型随机选边,连自己都不知道依据哪段。加 faithfulness 检查 + 低可信标记,凡引用即溯源、无支撑即拒答。
全流程七环节解析 / 切分 / 嵌入索引 / 检索 / 重排生成与引用recall@5 > 0.8
学习路径
  1. 读 2.7:串起解析→切分→嵌入→索引→检索→重排→生成七环节
  2. 跑内置代码:把整条管线端到端跑通并看各环节耗时分布
  3. 完成动手练习:用 recall@5 > 0.8 作为检索质量红线做一次体检
  4. 对接 M13:交付完整 pipeline 并给出承载全部环节的组件清单
✔ 能把七环节串成可执行管线并说明每个环节的输入输出
核心知识点详解
  • 七环节流水:解析 → 切分 → 嵌入 → 索引 → 检索 → 重排 → 生成与引用。每个环节的输入输出要能讲清:解析出纯文本 → 切分成 chunk → 嵌入成向量 → 建索引 → 混合检索召回 → 重排精选 → 生成带引用的答案。一条管线就是这七步的接线。
  • retrieval 红线 recall@5 > 0.8:离线体检可以用 recall@5 > 0.8 作为检索质量红线:前 5 个召回里应覆盖到目标片段 ≥ 80% 的查询。没到这个线,重排再好也救不了——先修检索再谈上层。
  • 各环节耗时分布:端到端跑一遍并看各环节耗时分布:常见大头是 prefill(检索结果的解码)与检索本身。先用 profile 找出瓶颈,再针对性优化(缓存、并行、压缩),不要上来就换模型。
  • 常见坑:端到端只测最终答案,不分开测检索——出问题无法定位是召回不够还是生成臆造;或没有 recall 红线 QA 就上线。先按七环节逐级给输入输出,再用 recall@5 体检。
进阶与评测查询改写 / HyDE多跳分解Agentic RAGGraphRAGRAPTOR 摘要树Recall@k / MRR
学习路径
  1. 读 2.8 与 2.9:了解进阶结构与检索 / 生成分离评测口径
  2. 跑内置代码:先用 Recall@k / MRR 测检索侧,再单独测生成侧
  3. 完成动手练习:判断当前任务是否需要进阶结构,给出依据
  4. 对接 M13:落地逐组件消融表,说明每个组件的边际贡献
✔ 能把检索与生成分开评测并产出可解释的消融结果
核心知识点详解
  • 检索侧 vs 生成侧分开评:检索侧指标:Recall@k(目标片段是否被召回)、MRR(命中的排名多靠前)、Hit Rate;生成侧:Faithfulness(是否臆造)、Answer Relevancy(是否切题)。分开评测才能定位问题在「没召回」还是召回了但「说错」。
  • 进阶结构按需上:基线 RAG 打底后再按需叠加:查询改写 / HyDE 提升短 query、多跳分解解决多步、Agentic RAG 让 agent 自主检索、GraphRAG 做全局总结、RAPTOR 摘要树做层级概览。盲目全上只会增加延迟与复杂度。
  • 消融表证明边际贡献:落地时逐组件做消融(开 / 关某一组件,其余不变)并记录指标变化——证明每个组件的边际贡献,而不是「一招大杂烩」说不清谁起了作用。示例表:基线 | +重排 | +RRF | +HyDE 各列 recall / faithfulness。
  • 常见坑:只评估最终答案一个数,出问题无法定位在检索还是生成;或把所有进阶模块堆上去不加消融,指标涨了却不知道是哪个起效。分开评 + 逐组件消融是硬要求。
学习路径

2.1 切分:策略与经验值

切分(chunking)是 RAG 质量的上游阀门——切坏了,后面重排也救不回来。没有「唯一正确」的切法,只有「适配文档结构与查询方式」的切法。

策略做法适用代价 / 坑
固定长度每 N 个 token 一刀切纯文本、无结构容易在句 / 表中间切断
递归切分按段落 → 句子 → 词的层级切多数通用文档仍可能切断语义单元
语义切分按 embedding 相似度断层切长文、主题跳跃大慢、对阈值敏感
父子文档大块存、小块检索,召回后回贴大块需要完整上下文生成存储翻倍
小块检索大块生成检索用小块保精度,生成用大块保完整表格 / 长段落需两套索引关联

经验值:通用文档 chunk size 取 256–512 token,overlap 取 10%–20%(如 256-token 块配 10%–15% 重叠 ≈ 25–40 token)。代码 / 法律 / 表格类文档可放到 512–1024 token 以减少结构切断。overlap 太小会丢失跨块语义,太大则冗余与成本上升。父子文档是当前生产里性价比最高的折中:检索用 128–256 token 的小块保证精度,召回后换成 1–2k token 的父块送给生成模型,兼顾召回率与上下文完整。

python# 递归切分 + 小块检索大块生成(父子文档)的朴素实现
def recursive_split(text, chunk=400, overlap=60):
    # 按段落优先,再按句子,最后按词兜底
    seps = ["\n\n", "\n", "。", " "]
    pieces = [text]
    for sep in seps:
        out = []
        for p in pieces:
            if len(p) <= chunk:
                out.append(p); continue
            for sub in p.split(sep):
                out.append(sub)
        pieces = out
    # 滑动窗口合并 + overlap
    chunks, buf = [], ""
    for p in pieces:
        if len(buf) + len(p) <= chunk:
            buf += p + " "
        else:
            chunks.append(buf.strip()); buf = buf[-overlap:] + p + " "
    if buf: chunks.append(buf.strip())
    return chunks

# 父子映射:每个小块的 metadata 记录它所属的父块 id,
# 检索命中后回查父块,把父块(更完整)喂给生成模型。
python# chunk 大小对召回的影响(经验曲线)
def recall(chunk_size):
    if chunk_size < 128:   return 0.72    # 太碎,语义不完整
    if chunk_size <= 512:  return 0.86    # 甜点区
    if chunk_size <= 1024: return 0.80    # 偏大,噪声增多
    return 0.70                           # 过大,关键句被稀释
for cs in [64, 128, 256, 512, 1024, 2048]:
    print(f"chunk={cs:>5}  recall@5≈{recall(cs):.2f}")
# 预期输出:64->0.72;128/256/512->0.86;1024->0.80;2048->0.70
# 结论:通用文档 256–512 token 最稳;父子文档用 128–256 小块检索更精。
⚠
切分最常见的两个错误:① 把一张表格或一段流程切成两半——检索到也不可用,生成模型拿到的片段缺行缺列;② overlap 设 0——相邻块语义断裂,跨块问题必漏。解决:对表格 / 代码用「结构感知切分」(按行 / 按函数),对长文用父子文档。

2.2 嵌入:模型选择与相似度

嵌入模型把文本压成固定维度向量,其质量直接决定召回上限。选择时要看三个维度:维度、多语言支持、领域适配。

嵌入模型维度多语言备注
bge-m31024强(100+ 语言)支持稠密 + 稀疏 + 多向量,检索全能选手
gte-Qwen21536 / 3584强中文场景常用,需部署
e5-mistral-7b4096中质量高但模型大、慢
voyage-31024强托管 API,按量计费
text-embedding-3-large3072(可截断到 256)中支持 Matryoshka,可压缩
ColBERT Late Interaction每 token 独立向量取决于底座检索精度高,存储大(见 2.5)

归一化与相似度:嵌入向量几乎总要先 L2 归一化到单位球。归一化后,余弦相似度 = 点积(内积),所以「余弦 vs 内积」之争在归一化后等价——但如果你忘了归一化,直接做内积就会被向量模长带偏(长文本向量模更大,内积虚高)。务必统一在入库和查询时都归一化。

pythonimport numpy as np

def l2_normalize(v):
    return v / (np.linalg.norm(v, axis=-1, keepdims=True) + 1e-8)

def cosine(a, b):
    a, b = l2_normalize(a), l2_normalize(b)
    return float(np.dot(a, b))            # 归一化后等于内积

# 嵌入与生成模型「不同源」的注意事项:
#  - 检索用 bge-m3,生成用 GPT / Gemini 是常态,二者语义空间不同,
#    这没问题(检索只看嵌入空间内部一致性)。
#  - 但如果你用生成模型的 embedding 做检索,注意它可能未针对检索优化,
#    召回率远低于专用嵌入模型。不要为了省一个服务而牺牲召回。
python# 忘记归一化的代价:内积被向量模长带偏
import numpy as np
a = np.array([0.1, 0.2])      # 短文本方向向量(模长小)
b = np.array([10.0, 20.0])    # 长文本,方向相同、模长大
print("未归一化内积:", float(a @ b))                          # 5.0(虚高)
print("余弦相似度:", float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b))))  # 1.0
# 预期输出:内积 5.0;余弦 1.0(方向完全一致)。
# 结论:入库与查询都要 L2 归一化;归一化后「余弦 = 内积」,
#   否则长文本向量模长虚高,会把不相关的长文档排到前面。
✔
维度不是越高越好:高维(4096)模型质量好,但存储与检索成本也高。Matryoshka 表示(如 text-embedding-3)允许你截断到 256 维而不损失太多召回,是成本与质量的实用折中。中文场景务必实测,不要只看英文榜单——很多英文 SOTA 模型在中文短 query 上并不占优。

2.3 索引:HNSW / IVF / PQ

当文档量到十万级以上,暴力遍历所有向量(flat search)不可行,必须上近似最近邻(ANN)索引。主流三者各有取舍。

索引关键参数取舍
HNSWM(每层连边数,默认 16)、efConstruction(建图质量,100–400)、efSearch(查询召回,50–200)召回高、查询快;内存占用大(图结构),M 越大越准越占内存
IVFnlist(聚类中心数)、nprobe(查几个簇)内存省、可扩展;nprobe 太小召回掉,太大变慢
PQ(乘积量化)m(子量化段数)、bits(每段位数)把 4096 维压到几十字节,内存降 10–16 倍;有精度损失
Flat(暴力)无100% 召回;仅适合 < 10 万级
python# HNSW 的实用参数配置(以 hnswlib 为例)
import hnswlib

p = hnswlib.Index(space="cosine", dim=1024)
p.init_index(max_elements=1_000_000, ef_construction=200, M=16)
p.add_items(vectors, ids)
p.set_ef(64)                       # 查询时的 efSearch:越大召回越高、越慢

# 经验:
#  M=16 是默认甜点;要更高召回可试 M=32(内存约翻倍)。
#  ef_construction=200 平衡建图质量与建索引时间。
#  efSearch=50~200:召回率不足先调它,再考虑加大 M。
#  PQ 通常叠加在 HNSW 上做「压缩存储」,牺牲一点精度换内存。
python# HNSW vs PQ 的内存量级:为什么上亿向量必须量化
n, dim = 1_000_000, 1024
raw  = n * dim * 4 / 1e9        # float32 原始向量
pq   = n * 64 / 1e9            # PQ 压到 64 字节/向量
hnsw = raw * 1.5              # HNSW 图结构额外开销
print(f"原始向量 {raw:.1f} GB;HNSW 约 {hnsw:.1f} GB;PQ 约 {pq:.2f} GB")
# 预期输出:原始向量 4.1 GB;HNSW 约 6.1 GB;PQ 约 0.06 GB(压缩约 64x)
# 结论:内存吃紧时用 PQ/SQ,代价是召回微降;M=16 是默认甜点,要更高召回再加大 M。
ℹ
什么时候上 PQ:当向量库内存吃紧(上亿向量、4096 维)时,用 PQ / SQ(标量量化)把每个向量从 16KB 压到 32–64 字节,内存降 1–2 个数量级。代价是召回微降,对大多数 QA 场景可接受。很多托管向量库(Qdrant / Pinecone)已默认开量化。

2.4 检索:稠密 / 稀疏 / 混合 + RRF

检索是 RAG 的第一道关口,直接决定「该不该被看见的材料有没有被看见」。三种范式各有盲区。

检索范式擅长盲区
稠密(向量)语义相近、同义改写、概念匹配精确术语、编号、型号、人名(需逐字匹配)
稀疏(BM25)精确关键词、专有名词、编号语义泛化、同义改写
混合(两者融合)同时覆盖语义与精确实现稍复杂,需融合策略
python# 混合检索 + RRF 融合 + 重排:RAG 质量提升最明显的三步组合
from rank_bm25 import BM25Okapi
import numpy as np

def rrf_fuse(vec_rank, bm25_rank, k=60):
    """RRF:只依赖排名不依赖分数,天然解决两种检索器分数不可比的问题。
       公式:score(d) = sum_{i in {vec,bm25}} 1 / (k + rank_i(d))"""
    scores = {}
    for r, doc_id in enumerate(vec_rank):  scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + r + 1)
    for r, doc_id in enumerate(bm25_rank): scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + r + 1)
    return [d for d, _ in sorted(scores.items(), key=lambda x: -x[1])]

def retrieve(query, chunks, embedder, vdb, reranker, top_k=6, recall_k=60):
    qv = embedder.encode(query)
    vec_hits = vdb.search(qv, k=recall_k)                       # 1) 向量召回(语义)
    bm = BM25Okapi([c.tokenized for c in chunks])
    bm_hits = np.argsort(-bm.get_scores(query.split()))[:recall_k]  # 2) 关键词召回(精确)
    fused = rrf_fuse([h.id for h in vec_hits], [chunks[i].id for i in bm_hits])
    cands = [chunk_by_id(chunks, i) for i in fused[:recall_k]]
    return reranker.rerank(query, cands)[:top_k]                # 3) 重排(性价比最高)
python# RRF 数值演示:两路检索分数不可比,但排名可以融合
def rrf(rank_lists, k=60):
    s = {}
    for rl in rank_lists:
        for r, d in enumerate(rl):
            s[d] = s.get(d, 0) + 1 / (k + r + 1)
    return sorted(s, key=s.get, reverse=True)
vec  = ["D3", "D1", "D5"]      # 向量召回顺序
bm25 = ["D1", "D2", "D3"]      # 关键词召回顺序
print("融合结果:", rrf([vec, bm25]))
# 预期输出:融合结果: ['D1', 'D3', 'D2', 'D5']
# D1 两路都靠前 -> 排第一;D3 次之。k 常取 60,只依赖排名、绕开量纲问题。
★
为什么 RRF 是默认融合公式:向量检索返回的是 cosine 分数(0–1),BM25 返回的是无界词频分数,两者量纲不同,不能直接相加。RRF(Reciprocal Rank Fusion,k 常取 60)只用到排名不依赖分数,绕开了量纲问题,且对两个检索器一视同仁。实践里 RRF 的融合效果常常优于手工加权求和。

2.5 重排:cross-encoder 与 ColBERT 晚期交互

召回阶段为了快,用的是「独立编码」的向量;重排阶段为了准,用的是「query 与文档联合编码」的 cross-encoder,或保留 token 级交互的 ColBERT。

方法机制精度延迟 / 成本
cross-encoderquery+doc 拼一起过一遍 transformer最高慢(不能预计算,每对现算),贵
ColBERT v2保留每 token 向量,late interaction 用 MaxSim 聚合高(接近 cross-encoder)中等,token 向量存储大,可量化
无重排直接取召回 Top-1最低最快
两阶段(召回 + 轻重排)向量召回 50–100 → 轻量重排 Top 5–8高性价比最高
python# ColBERT v2 晚期交互(late interaction):检索时延后到 token 级
# 思想:query 与 doc 各自先编码成 token 向量(可预计算、入库),
# 查询时再用 MaxSim 聚合,兼顾「可预计算」与「token 级精度」。
import numpy as np

def maxsim(q_tokens, d_tokens):
    """对 query 每个 token,取它与 doc 所有 token 的最大余弦,再求和。"""
    s = 0.0
    for q in q_tokens:                      # query token 向量(已归一化)
        sims = q @ d_tokens.T               # 与 doc 所有 token 的余弦
        s += sims.max()                     # MaxSim:最匹配的 doc token
    return s

# 召回阶段:只用 doc 的「单向量」(如 CLS 或平均)做粗排取 Top 50;
# 重排阶段:对这 50 个再算 MaxSim 精排。这是 ColBERT 快且准的关键。
python# 召回 + 重排的收益:为什么「召回 100 重排取 8」性价比最高
def precision_at_k(topk, relevant):
    return sum(1 for d in topk if d in relevant) / len(topk)
relevant = {"A", "B", "C"}
recall_stage = ["x1", "x2", "A", "x3", "x4", "B", "x5", "C"]   # 召回顺序不理想
after_rerank = ["A", "B", "C", "x1", "x2", "x3"]
print("重排前 Top-3 精确率:", round(precision_at_k(recall_stage[:3], relevant), 2))  # 0.33
print("重排后 Top-3 精确率:", round(precision_at_k(after_rerank[:3], relevant), 2))  # 1.00
# 预期输出:0.33 -> 1.00。重排把相关三条提到最前,直接决定最终答案质量。
# 延迟提示:cross-encoder 逐对现算,Top-100 重排约加 100–300ms,召回窗口别开太大。
✔
重排的性价比公式:行业共识:先用便宜的向量检索召回到 50–100 条,再用 cross-encoder 重排取 Top 5–8。这一步对最终质量的影响通常最大、性价比最高。但要注意延迟:cross-encoder 是逐对现算,Top-100 重排可能加 100–300ms,因此召回窗口别开太大;ColBERT v2 因 token 向量可预计算,重排更快但存储更大。

2.6 生成:引用、拒答与证据冲突

检索到好材料只是半场。生成阶段要解决三件事:让答案可追溯(引用)、资料不足时老实说不知道(拒答)、多份证据矛盾时怎么办(冲突)。

问题做法失败表现
引用 / 归因要求输出每段答案对应的 chunk id,并回链原文答案无出处,无法核验
拒答策略明确「资料不足则回答不知道」,并设置信阈值硬编、幻觉式作答
证据冲突让模型先列出矛盾点,再给最可能结论并标注不确定随机站队、掩盖矛盾
长答案忠实要求「每句话都能在证据中找到依据」夹带外部知识(closed-book 偷跑)
python# 带引用与拒答的生成 Prompt 设计
SYSTEM = (
    "你是基于资料的助手。规则:\n"
    "1. 只使用 <evidence> 中的信息,不得引入外部知识。\n"
    "2. 每个结论后用 [n] 标注来源 chunk 编号。\n"
    "3. 若资料无法回答,输出 '资料不足,无法确定'。"
)

def build(user_q, chunks):
    ev = "\n".join(f"[{i}] {c.text}" for i, c in enumerate(chunks))
    return [
        {"role": "system", "content": SYSTEM},
        {"role": "user",   "content": f"<question>{user_q}</question>\n<evidence>{ev}</evidence>"},
    ]

# 证据冲突时:让模型先输出 <conflict> 段列出矛盾,再给结论;
# 程序侧可据 conflict 标记决定是否升级人工。
python# 忠实度(faithfulness)检查:答案里的每个事实是否能在证据中找到
def faithfulness(claims, evidences):
    ev = " ".join(evidences)
    supported = sum(1 for c in claims if any(k in ev for k in c["keywords"]))
    return supported / len(claims)
claims = [{"keywords": ["8.2", "违约金"]}, {"keywords": ["不可抗力"]}]
ev = ["条款 8.2 约定违约金上限为合同金额的 20%"]
print("忠实度:", round(faithfulness(claims, ev), 2))    # 0.50
# 预期输出:0.50 —— 第二条「不可抗力」在证据里找不到,判定为「偷跑 / 幻觉」。
# 实践:把忠实度纳入评测集,专门抓 closed-book 偷跑(答案看似合理但证据里没有)。
⚠
生成阶段最隐蔽的失败:模型在「closed-book 模式」下偷偷用训练知识回答,无视你给的证据——表现为答案看起来合理,但引用的 chunk 里根本没有对应内容。解法:在 system 里强制「只依据 evidence」,并在评测里加一道「忠实度(faithfulness)」指标(见 2.9),专门抓这类偷跑。

2.7 全流程七环节(串起来看)

解析与切分
把文档转成文本并切分。按语义结构切优于固定长度切;加 10%–20% 重叠避免切断语义;保留元数据(来源、页码、章节)用于引用与过滤。
嵌入
选择嵌入模型时看维度、多语言、领域适配。中文场景要实测,不要只看英文榜单。入库与查询都归一化。
存储与索引
已有 Postgres 用 pgvector;自托管与混合检索用 Qdrant;托管无服务用 Pinecone;内置混合检索用 Weaviate。
检索
混合检索是关键:向量(语义)+ BM25(精确术语 / 编号 / 专有名词)并用,再 RRF 融合。只用向量常常漏掉精确匹配。
重排
先用便宜的向量检索召回到 50–100 条,再用 cross-encoder 重排取 Top 5–8。这一步性价比最高。
生成
把片段与问题一起给模型,要求「只能基于给定资料回答、资料不足就说不知道」,并输出引用。
评估
分别测检索质量与生成质量。这是 RAG 项目成败的分水岭(见 2.9)。
python# 七个环节的最小可运行骨架(把全流程串起来)
def rag_answer(q):
    qs   = rewrite(q)                    # 1) 查询改写
    docs = hybrid_retrieve(qs, k=60)     # 2) 向量 + BM25 混合召回
    docs = rrf_fuse(docs)                # 3) RRF 融合
    top  = rerank(q, docs)[:6]           # 4) cross-encoder 重排取 Top-6
    ctx  = pack_with_citations(top)      # 5) 带引用拼装
    ans  = llm.generate(q, ctx)          # 6) 生成(只依据证据)
    return ans, evaluate(ans, top)       # 7) 检索侧 + 生成侧分别评估
# 目标基线:recall@5 > 0.8,faithfulness > 0.9。
# 七步里「混合检索 + 重排 + 评估」三步对最终质量影响最大。
★
RAG 最常见的四种失败:① 切分不合理:把一张表格切成两半;② 只用向量检索:遇到订单号、型号就失效;③ 不做重排:Top-1 常常不是最优;④ 没有评估:demo 能用,上线后大量「自信的错误答案」。第四点最致命。

2.8 进阶 RAG:从检索到推理

进阶技术做法解决的问题代价
查询改写 / 扩展让模型把用户问题改写成更适合检索的多个查询口语化提问与文档用词不一致额外一次模型调用
HyDE先生成假设性答案,再用它检索问题太短、语义信息不足可能放大幻觉方向
多跳检索(Multi-hop)把复杂问题拆成子问题,逐步检索需要跨文档推理的问题延迟与调用次数增加
Agentic RAG让 Agent 自主决定检索什么、检索几轮、何时停止复杂且不确定的信息需求成本与不可控性上升
GraphRAG构建实体-关系图,做图上的多跳检索全局性、总结性、跨实体关联问题构建成本高、维护复杂
上下文压缩检索后对片段做摘要或抽取关键句注入的 token 过多、关键信息被稀释压缩可能丢信息,需评估
自适应检索先用小模型判断是否需要检索简单问题不必检索,省成本与延迟分类器误判的风险
阶段(渐进叠加)叠加技术典型收益代价
① 基础 RAG混合检索 + 重排recall 相对纯向量 +10~20 个点一次重排延迟 100–300ms
② + 查询理解查询改写 / HyDE / 多查询短 query 召回 +5~15 个点额外 1 次模型调用
③ + 多跳 / 分解子问题拆分逐步检索多跳 / 跨文档问题可答延迟与调用数增加
④ + Agentic / Graph自主多轮 + 实体关系图复杂 / 全局总结类问题成本与不可控性显著上升
✔
选择顺序建议:不要一开始就上 GraphRAG 或 Agentic RAG。按这个顺序渐进:基础 RAG(含混合检索 + 重排)→ 查询改写 → 评估体系 → 多跳 → Agentic / Graph。多数业务场景在「基础 RAG + 重排 + 严格评估」这一步就能达到可用质量,盲目上复杂架构只会增加不可控性。

2.9 评测:检索侧与生成侧分开测

RAG 最致命的错误是「只测最终答案」。检索坏了还是生成坏了,必须用两套指标分别定位。

层面指标含义怎么算
检索Recall@k前 k 个结果是否包含全部相关文档命中相关数 / 总相关数
检索Hit Rate@k前 k 个里是否至少命中一个至少命中一次的 query 占比
检索MRR第一个相关结果的平均排名倒数mean(1 / rank_of_first_relevant)
生成Faithfulness(忠实度)答案是否只来自证据、无臆造LLM-as-Judge / 归因比对
生成Answer Relevancy答案是否切题LLM-as-Judge 打分
生成Context Precision送入的证据里相关的是否排前面基于证据相关性的排序质量
生成Context Recall回答问题所需的证据是否都被召回所需证据 / 被召回的相关证据
python# 用 Ragas 风格分别评估检索与生成(概念性伪代码)
def eval_rag(questions, golden, pipeline):
    ret, gen = [], []
    for q in questions:
        ctx = pipeline.retrieve(q, k=5)
        ans = pipeline.generate(q, ctx)
        # 检索侧:ground-truth 相关 chunk 是否在前 5
        ret.append(recall_at_k(ctx, golden.relevant[q], k=5))
        # 生成侧:答案是否能在 ctx 中找到依据(忠实度)
        gen.append(faithfulness(ans, ctx))
    return {"recall@5": mean(ret), "faithfulness": mean(gen)}

# 实践:先确保 recall@5 > 0.8,再优化 faithfulness。
# 若 recall 低 → 改切分 / 嵌入 / 检索;若 faithfulness 低 → 改生成指令 / 加引用约束。
python# 检索侧指标的最小实现:Recall@k / Hit Rate@k / MRR
def recall_at_k(ret, rel, k):
    return len(set(ret[:k]) & set(rel)) / len(rel)
def hit_rate_at_k(ret, rel, k):
    return 1.0 if set(ret[:k]) & set(rel) else 0.0
def mrr(ret, rel):
    for i, d in enumerate(ret):
        if d in rel:
            return 1.0 / (i + 1)
    return 0.0
ret = ["x1", "x2", "A", "x3", "B"]
rel = {"A", "B"}
print("Recall@5:", recall_at_k(ret, rel, 5))     # 1.0
print("Hit@3:", hit_rate_at_k(ret, rel, 3))      # 1.0
print("MRR:", round(mrr(ret, rel), 3))           # 0.333
# 预期输出:1.0 / 1.0 / 0.333(第一个相关项排在第 3 位)。
# 实践:先确保 recall@5 > 0.8,再优化 faithfulness;否则在错的地方使劲。
⚠
只评估答案的人会永远修错地方:如果 recall 只有 0.4,再好的生成模型也答不对——因为材料根本没被召回。反之若 recall 高但答案错,是生成指令或冲突处理的问题。两套指标分开看,优化方向才不会南辕北辙。

2.10 动手练习与自测

✔
动手练习与自测(RAG):第 1–3 题动手跑代码,第 4–6 题口头自测并对照判据表。
  1. 用 2.4 的 rrf 函数,把 k 从 60 改为 10,观察融合结果顺序是否变化,并解释 k 的作用。
  2. 用 2.9 的指标代码构造一组「召回有但排序靠后」的数据,算出 MRR < 0.5 并说明该调什么。
  3. 给自己的一小段文本实现父子文档切分(小块 128、父块 1024),统计召回 20 条时命中父块数。
  4. RAG 效果差时,如何用两套指标判断是检索问题还是生成问题?
  5. 为什么 RRF 优于「向量分数 + BM25 分数直接相加」?
  6. 什么时候该上 GraphRAG,什么时候基础 RAG + 重排就够?
题号考点关键判据 / 参考答案
1RRF 的 kk 越小越强调头部排名;k 大则接近平均。顺序可能微调,但双路都靠前的仍居首
2MRR 定位相关项排名 3 -> MRR≈0.33;属于排序问题,优先加 cross-encoder 重排
3父子文档小块保精度、父块保完整;命中后回贴父块,验证生成侧上下文更完整
4故障定位recall 低=检索问题(切分/嵌入/检索);recall 高但答案错=生成问题(指令/冲突)
5RRF 优势cosine 与 BM25 量纲不可比,直接相加失衡;RRF 只用排名、一视同仁
6GraphRAG 边界全局总结 / 跨实体关联才上;它预处理 token 是基础版 5–10 倍且维护复杂

3. 查询理解:在检索之前先读懂问题

知识结构图 · 查询理解
查询理解:在检索之前先读懂问题3 大知识域 · 10 个知识点
改写与路由查询改写多查询扩展 3–5查询路由查询分类
学习路径
  1. 读 3.1:理解查询改写、多查询扩展与路由各自解决什么
  2. 跑内置代码:把一条 query 扩成 3–5 路后再检索,对比单路召回
  3. 完成动手练习:为任务配置查询路由与分类规则
  4. 对接 M13:把改写 + 多查询并入混合检索的召回前处理
✔ 能量出多查询扩展对召回的增益并说明路由分类依据
核心知识点详解
  • 查询改写 / 多查询扩展:改写在检索前把 query 改得更可检索(补全指代、去口语);多查询扩展把一条 query 扩成 3–5 个变体分别检索再合并去重,能显著提升召回的 recall(示例可比单路 recall 高 10%+)。增益越低越说明基路已健康。
  • 查询路由 / 分类:按「要检索 / 要工具 / 要长上下文 / 要纯知识」等类别做路由 / 分类,把 query 分到不同执行路径。分类依据要可解释:关键词 / embedding 相似度 / 少量分类器,别拍脑袋。
  • 多查询的取舍:多查询扩展成本线性涨(n 路检索 × n 次),但通常先合并去重、只把 top 合并进重排。要实测:加多查询后 recall 涨多少,延迟涨多少,收益是否值回成本。
  • 常见坑:无条件全量多查询(几倍延迟只换 1% recall);或路由分类规则拍脑袋、遇到新 query 分错路径导致答案路径不对。先量增益、把路由依据写进文档。
HyDE 假设文档假设答案嵌入短 query 召回 +13幻觉带偏风险
学习路径
  1. 读 3.2:理解 HyDE 用假设答案嵌入提升短 query 召回的原理与风险
  2. 跑内置代码:对比直接嵌入 vs HyDE 在短 query 上的 recall 提升
  3. 完成动手练习:定位哪类 query 该用 HyDE 并写出降级条件
  4. 对接 M13:按需启用 HyDE 并在消融里确认净增益
✔ 能量出 HyDE 在短 query 的增益并识别带偏场景
核心知识点详解
  • HyDE 原理:HyDE(Hypothetical Document Embedding):先让 LLM 根据短 query 生成一段假设答案,再用这段假设答案的嵌入去检索。假设答案包含更丰富的语义,通常比裸 query 更贴近目标文档,短 query 召回到 recall 可提升 13 个百分点量级。
  • 为什么只适合短 / 模糊 query:短 query 信息熵低,加假设答案能大幅补全语义;若 query 本身已很具体、冗余文本多,HyDE 收益变小甚至为负。要按 query 区分:短 / 模糊 query 触发 HyDE,长 / 明确 query 不触发。
  • 幻觉带偏风险:Biggest risk 是假设答案带偏——LLM 先入为主的错误会污染嵌入,把检索拉到错误片区。缓解:把假设答案与 query 本身都参与检索、或对高风险 query 禁用。净增益要消融确认。
  • 常见坑:把 HyDE 当万能提升,长 query 也套导致延迟翻倍收益为负;或忽略「假设答案可能带偏」,不设降级条件就让错误先入为主。按 query 特征开启 + 消融验证净增益。
查询分解子问题 2–4 个延迟 0.4 + 0.6n静态分解 vs Agentic
学习路径
  1. 读 3.3:理解查询分解的子问题数与延迟 0.4 + 0.6n 的权衡
  2. 跑内置代码:跑一次多跳问题分解并逐子查询检索汇总
  3. 完成动手练习:对比静态分解与 Agentic 分解的延迟与准确率
  4. 对接 M13:为多跳问答案例配置查询分解并计入延迟预算
✔ 能量化分解带来的延迟代价与多跳准确率收益
核心知识点详解
  • 分解子问题 2–4 个:多跳问题先拆成 2–4 个子问题分别检索再汇总回答,能显著提升多跳 / 复杂查询的准确率。子问题太少不够细、太多引入冗余与错误累积——2–4 是常用档。
  • 延迟代价公式 0.4 + 0.6n:分解的延迟近似 0.4 + 0.6n(基准单跳约 0.4,每个子问题额外约 0.6 单位):n=1 约 1.0,n=3 约 2.2——延迟约随子问题数线性涨。要量化:分解把准确率提多少,买这个延迟值不值。
  • 静态 vs Agentic 分解:静态分解:一次把主问题拆成固定子问题并行检索;Agentic 分解:agent 逐步决定下一步查什么、用到哪再做。Agentic 更灵活但延迟更高、需 harness 支撑;静态更可控、适合有固定模板的多跳。
  • 常见坑:对单跳简单问题也强行分解,白白加延迟无收益;或分解出的子问题检索结果互相冲突,汇总时模型乱编。先测「该问题是否需要多跳」,再算 0.4+0.6n 的延迟账。
学习路径

3.1 查询改写、多查询扩展与路由

用户的原始问题往往不适合直接检索:口语化、太短、隐含多意图。查询理解的目标是把「人怎么问」转成「检索器怎么找」。

技术做法解决的问题代价
查询改写模型把问题改写成规范的检索式用词不一致、歧义一次调用
多查询扩展生成 3–5 个角度的查询,分别检索后合并单 query 覆盖不全N 倍检索成本
查询路由先判断「该检索还是不检索 / 检索哪个库」简单问题不必检索,省成本分类器误判风险
查询分类区分事实 / 总结 / 比较 / 闲聊不同问题用不同管线一次调用
python# 多查询扩展 + 路由:把一个问题变成多个检索,再合并去重
def expand_queries(q, llm, n=3):
    prompt = f"为检索生成 {n} 个不同角度的查询,覆盖问题可能的意图:\n{q}"
    return llm.generate(prompt).splitlines()[:n]

def route(q, llm):
    # 返回 "retrieve" / "long_context" / "chat"
    return llm.classify(q, labels=["retrieve", "long_context", "chat"])

def retrieve_multi(q, chunks, k=6):
    qs = expand_queries(q, llm)
    hits = []
    for sub in qs:
        hits += vector_search(sub, chunks, k=k)
    # 按 id 去重,保留最高分(或用 RRF 重新融合多 query 结果)
    return dedupe_by_score(hits)[:k]

# 路由:若 route(q)=="chat",直接回答不检索,省下检索 + 重排成本。
python# 多查询扩展后去重:按 doc id 保最高分(或再 RRF 融合)
def merge_multi_query(hits_per_query, top_k=6):
    best = {}
    for hits in hits_per_query:
        for rank, h in enumerate(hits):
            s = 1 / (rank + 1)
            if h["id"] not in best or s > best[h["id"]]["s"]:
                best[h["id"]] = {"s": s, "doc": h}
    ranked = sorted(best.values(), key=lambda x: -x["s"])
    return [b["doc"] for b in ranked[:top_k]]
# 经验:多查询扩到 3–5 个收益趋平;每个 query 各取 Top-6 再合并,
#   比单 query 取 Top-30 覆盖更全(不同角度命中不同片段)。
ℹ
什么时候该路由掉检索:纯闲聊、需要模型固有知识就能答的、或文档全集已小于上下文窗口的问题,不必检索。多一次检索就多一次延迟与成本,路由分类器能砍掉相当比例的无效检索——但要设 fallback:分类不确定时默认走检索。

3.2 HyDE:假设文档嵌入

HyDE(Hypothetical Document Embeddings)用一个反直觉但有效的技巧解决「短 query 语义信息不足」:先让模型生成一段「假设的答案文档」,再用这段文档(而非原问题)去做向量检索。因为假设答案的语义密度远高于短问题,检索召回往往更好。

python# HyDE:用「假设答案」去检索,而非用短问题
def hyde_retrieve(q, embedder, vdb, k=6):
    # 1) 让模型先编一个「如果资料里有答案,它会长什么样」
    hypo_doc = llm.generate(f"假设资料中存在该问题的答案,写出那段答案:\n{q}")
    # 2) 用这段假设文档的嵌入去检索(而不是用原 query 的嵌入)
    qv = embedder.encode(hypo_doc)
    return vdb.search(qv, k=k)

# 为什么有效:短 query(如「违约金上限」)语义稀疏,和长文档向量距离大;
# 而假设答案文档复述了相关概念,与真文档更近。
python# HyDE 的效果对比(示意数字):短 query 上提升明显
def recall_direct(): return 0.58     # 直接用短 query 的向量检索
def recall_hyde():   return 0.71     # 用假设答案文档的向量检索
print("直接检索 recall@5:", recall_direct())              # 0.58
print("HyDE recall@5:", recall_hyde())                    # 0.71
print("提升:", round(recall_hyde() - recall_direct(), 2)) # +0.13
# 结论:短 query(语义稀疏)受益最大;但假设答案若含错误事实会带偏检索,
#   所以 HyDE 只用于召回,最终答案仍严格基于真实召回材料。
⚠
HyDE 的坑:假设答案可能包含错误事实,而这些错误会「带偏」检索方向——它把检索变成「找和这个(可能错误的)说法相似的文档」。因此 HyDE 在事实型短查询上提升明显,在需要严格准确的场景要慎用,或只用假设文档做召回、最终答案仍严格基于真实召回材料。

3.3 查询分解:把复杂问题拆成可检索的子问题

当问题需要跨文档、多步推理(「A 公司的供应商 B 在 2025 年的合规记录如何?」),单跳检索答不全。查询分解让模型先规划子问题,分别检索再汇总。

python# 查询分解 + 顺序 / 并行检索 + 汇总
def decompose(q, llm):
    plan = llm.generate(f"把问题拆成 2-4 个可独立检索的子问题:\n{q}")
    return [line for line in plan.splitlines() if line.strip()]

def answer_complex(q, chunks, llm):
    subs = decompose(q, llm)
    evidence = []
    for sub in subs:                          # 简单场景可并行检索
        ev = retrieve(sub, chunks, k=4)
        evidence.append(f"子问题: {sub}\n" + format_chunks(ev))
    # 最后把所有子证据汇总回答原问题
    return llm.generate(f"基于下列证据回答原问题:\n{q}\n\n{evidence}")

# 注意:分解粒度要适中。拆太粗(1 个)等于没拆;拆太细(>5)延迟爆表且
# 子答案之间容易矛盾。
python# 查询分解的可控性:子问题数量与延迟、矛盾之间的权衡
def latency(n): return 0.4 + 0.6 * n     # 规划 0.4s + 每个子检索约 0.6s(示意)
for n in [1, 3, 5, 9]:
    print(f"子问题数={n}  预计延迟≈{latency(n):.1f}s")
# 预期输出:1->1.0s;3->2.2s;5->3.4s;9->5.8s
# 结论:拆太粗(n=1)=没拆;拆太细(n>5)延迟爆表且子答案互相矛盾,甜点 2–4。
✔
分解 vs Agentic RAG:查询分解是「一次性静态规划子问题」,成本低、可控;Agentic RAG 是「边检索边决定下一步」,更灵活但成本与不可控性高。多数业务先用分解就够了,只有当问题真的高度不确定、需要多轮试探时才上 Agentic。

3.4 动手练习与自测

✔
动手练习与自测(查询理解):第 1–2 题动手算,第 3–5 题口头自测并对照判据表。
  1. 用 3.1 的 merge_multi_query 给两路召回结果去重排序,检查是否出现「同一文档被多次命中」。
  2. 用 3.3 的延迟模型,找出 n 取多少时延迟仍可接受(阈值 3s),并说明为什么不再往上加。
  3. 查询改写 / 多查询 / 路由分别解决什么问题?各自的代价是什么?
  4. HyDE 为什么对短 query 有效?它的主要风险是什么?
  5. 查询分解与 Agentic RAG 的取舍是什么?什么场景才值得上 Agentic?
题号考点关键判据 / 参考答案
1多查询去重同一 doc 保最高分;合并后覆盖比单 query Top-30 更全
2分解粒度n<=4 时延迟 <=2.8s;再大延迟线性上升且子答案易矛盾
3查询理解三件套改写解决用词不一致、多查询解决覆盖不全、路由解决「该不该检索」
4HyDE 机制假设答案语义密度高、与真文档更近;风险是假设事实错误会带偏检索
5分解 vs Agentic分解=静态、便宜、可控;Agentic=动态、贵、适合高度不确定的多轮试探

4. 进阶结构:GraphRAG / RAPTOR / 压缩

知识结构图 · GraphRAG / RAPTOR / 压缩
进阶结构:GraphRAG / RAPTOR / 压缩3 大知识域 · 13 个知识点
GraphRAG实体-关系三元组Leiden 社区检测社区摘要预处理 token 5–10 倍全局总结场景
学习路径
  1. 读 4.1:吃透实体-关系抽取 → Leiden 社区 → 摘要的 GraphRAG 链路
  2. 跑内置代码:在小语料上建图并生成社区摘要,观察预处理 token 放大 5–10 倍
  3. 完成动手练习:判断任务是否需要全局总结场景,给出依据
  4. 对接 M13:仅在全局关联型查询上旁路启用 GraphRAG 并对比收益
✔ 能说明 GraphRAG 适合全局总结并量化它的成本放大
核心知识点详解
  • GraphRAG 链路:实体-关系抽取 → 构建图 → Leiden 社区检测 → 对社区生成摘要 → 查询时按社区检索。它擅长跨实体关联类和全局总结类查询(「整体趋势是什么」「A 与 B 有什么联系」),这是传统向量 RAG 的弱项。
  • 成本放大 5–10 倍:GraphRAG 预处理 token 放大 5–10 倍:实体/关系抽取、社区摘要都要对全量文本反复过 LLM。构建成本显著高于普通索引,且建图是离线的。所以只在真需要全局总结时启用。
  • 何时该用 GraphRAG:查询是「全局总结 / 跨实体关系 / 需要聚合多段落」→ 值得;若全是「定位某一句话」的局部查询,二维向量 RAG 就够,GraphRAG 纯属浪费。按查询类型路由。
  • 常见坑:在所有查询旁路无脑开 GraphRAG,成本涨 5–10 倍、延迟翻倍收益有限;或抽取质量差(实体识别不准)导致图结构噪声大。只在全局关联型查询上旁路启用并对比收益。
RAPTOR 摘要树递归聚类摘要叶子给细节上层给概览构建成本低于 GraphRAG
学习路径
  1. 读 4.2:理解递归聚类摘要的层级树如何兼顾细节与概览
  2. 跑内置代码:构建一层摘要树并观察叶子 vs 上层对回答的影响
  3. 完成动手练习:对比 RAPTOR 与 GraphRAG 的构建成本与适用任务
  4. 对接 M13:为长文档问答选用 RAPTOR 或 GraphRAG 并在消融里自证
✔ 能说明摘要树在细节-概览间的取舍并给出构建成本对比
核心知识点详解
  • 递归聚类摘要:RAPTOR 自底向上把段落按 embedding 聚类、对每簇生成摘要,逐层构建层级摘要树:叶子是原始细节、上层是概览摘要。检索时可去合适层级取,兼顾细节与概览的连续光谱。
  • 叶子给细节、上层给概览:查具体事实去叶子(原始 chunk),查全局概览去上层(聚类摘要)。关键设计是「检索时选哪一层」——按 query 需求决定,不是每层都查。
  • 构建成本 < GraphRAG:RAPTOR 只做「聚类 + 摘要」,不做实体 / 关系抽取,构建成本显著低于 GraphRAG——适合长文档问答,而 GraphRAG 更适合多文档全局总结。两者按「单长文档 vs 跨文档关系」取舍。
  • 常见坑:查询细节却命中上层摘要(信息丢失);或每层都查导致 token 膨胀。先明确 query 要细节还是要概览,再选层,并用构建成本对比决定上哪套。
上下文压缩抽取式 vs 摘要式上下文无关化指代消解结构化提取压缩红线
学习路径
  1. 读 4.3:分清抽取式与摘要式压缩及指代消解的作用
  2. 跑内置代码:对同一长文档走两种压缩并对比信息保留
  3. 完成动手练习:定义自己的压缩红线(保留哪些字段)
  4. 对接 M13:把上下文压缩 + token 预算分配接入生成侧
✔ 能对比压缩前后信息保留并守住在自己的压缩红线上
核心知识点详解
  • 抽取式 vs 摘要式压缩:抽取式:按规则 / 模型选保留句子,不改变原文(信息保真、可溯源);摘要式:让 LLM 重新组织,压缩率更高但可能引入偏差。抽取保真性强、摘要省 token——按场景选,两者可混用。
  • 上下文无关化 / 指代消解:摘要丢掉上文后,「它 / 该方案 / 上述」等指代会悬空。压缩时必须做上下文无关化:把指代展开成具体实体(「该公司」→「Acme Corp」),否则压缩后的上下文无法独立理解。
  • 结构化提取 + 压缩红线:把结论 / 关键字段按结构化提取(如保留日期、金额、名称、结论四字段),其余压缩。同时设压缩红线:必须保留哪些字段 / 结构,红线以下不压缩。衡量保留用「压缩后能否仍答出原问题」。
  • 常见坑:只盯压缩后 token 数,不验证信息保留(重要字段丢了);或压缩时未做指代消解,摘要上下文悬空无法独立使用。先定红线再压缩,用回溯问答验证。
学习路径

4.1 GraphRAG:实体图 + 社区摘要

GraphRAG 由微软提出,针对的是基础 RAG 答不好的两类问题:全局性总结(「这份语料整体讲了什么?」)和跨实体关联(「A 和 B 通过哪些事件关联?」)。做法是先把文档抽取成「实体-关系」图,再对图做社区检测(Leiden),为每个社区生成摘要,查询时既做向量检索也做图遍历。

维度基础 RAGGraphRAG
擅长局部事实、点查全局总结、跨实体关联、多跳
索引成本低(嵌入即可)高(抽取 + 建图 + 社区摘要,token 消耗大)
查询成本低中—高(图遍历 + 多社区摘要)
维护增量嵌入简单图更新复杂,需重算受影响社区
适用绝大多数 QA企业知识库、法规、研报等需「鸟瞰」的场景
python# GraphRAG 的核心构建步骤(概念性)
def build_graphrag(docs, llm):
    # 1) 抽取 (实体, 关系, 描述) 三元组
    triplets = [extract_triplets(d, llm) for d in docs]
    graph = build_kg(triplets)                  # 实体-关系图
    # 2) 社区检测(Leiden),把图分成语义社区
    communities = leiden_communities(graph)
    # 3) 为每个社区生成摘要(这一步消耗大量 token)
    summaries = {c: llm.summarize(community_text(c)) for c in communities}
    return graph, communities, summaries

# 查询:
#  - 局部问题:图 + 向量检索定位相关实体与社区摘要
#  - 全局问题:直接聚合多个社区摘要做鸟瞰式回答
python# GraphRAG 的构建 token 消耗:为什么它「贵」在预处理
docs, chunks_per_doc = 1000, 20
extract_tokens = docs * chunks_per_doc * 300     # 每块抽取三元组的 token 量级
summary_tokens = 500_000                          # 社区摘要(示意)
print("抽取 token 量级:", f"{extract_tokens:.0e}")                    # 6e+06
print("总计(含摘要):", f"{extract_tokens + summary_tokens:.0e}")      # 7e+06
# 预期输出:抽取约 6e6、总计约 7e6 token。
# 对比基础 RAG(仅嵌入)几乎为零 -> GraphRAG 预处理 token 可能是基础版的 5–10 倍。
# 结论:只在需要「全局总结 / 跨实体关联」时才上,且要接受维护成本。
⚠
GraphRAG 的代价:社区摘要的构建要在预处理阶段把所有文档「讲一遍」,token 消耗可能是基础 RAG 的 5–10 倍,且增量更新麻烦。只有当你的场景确实需要「全局总结 / 跨实体关联」时才值得——否则基础 RAG + 重排 + 严格评估通常就够。

4.2 RAPTOR:层级摘要树

RAPTOR 用另一种方式解决全局问题:把检索到的 chunk 递归聚成簇,为每个簇生成摘要,再聚再摘要,形成一棵从细到粗的摘要树。查询时同时检索叶子(细节)和上层节点(概括),兼顾局部与全局。

python# RAPTOR 层级摘要树的构建(概念性)
def build_raptor(chunks, embedder, llm):
    layer = chunks
    tree = [layer]
    while len(layer) > 1:
        # 1) 对当前层做聚类(如高斯混合 / 层次聚类)
        clusters = cluster(layer, embedder, k=auto_k(layer))
        # 2) 每个簇生成一条摘要,作为上一层节点
        layer = [llm.summarize(texts(c)) for c in clusters]
        tree.append(layer)                      # 越往上越概括
    return tree                                # tree[0]=最细, tree[-1]=根摘要

# 查询:在每一层都检索最相关的节点,把所有命中(细 + 粗)喂给生成模型。
python# RAPTOR 摘要树的层级结构:越往上越概括
def build_layers(n_chunks, branching=5):
    layers = []
    while n_chunks > 1:
        n_chunks = max(1, n_chunks // branching)   # 每层按分支因子聚簇
        layers.append(n_chunks)
    return layers
print("层级节点数(自底向上):", build_layers(1000))
# 预期输出:约 [200, 40, 8, 1](5 层左右)
# 查询时在每一层都检索:叶子给细节、上层给概览,兼顾局部与全局。
# 构建成本远低于 GraphRAG(只需聚类 + 逐簇摘要)。
ℹ
RAPTOR vs GraphRAG:两者都解决「全局 / 总结」类问题。RAPTOR 基于语义聚类的树,构建相对便宜、适合「从细节到概览」的连续光谱;GraphRAG 基于实体关系图,在「明确跨实体关联」上更强但构建更贵。实践中可先用 RAPTOR,遇到强关联查询再补 GraphRAG。

4.3 上下文压缩与摘要记忆

当检索或历史过长,直接全塞会稀释注意力并推高成本。上下文压缩的几种手段各有取舍。

python# 摘要式压缩:历史超限时滚动摘要,而不是无限堆积
def compress_history(history, llm, max_turns=6):
    if len(history) <= max_turns:
        return history
    old, recent = history[:-max_turns], history[-max_turns:]
    summary = llm.summarize(render(old))        # 把旧对话压成一段
    return [{"role": "system", "content": f"<history_summary>{summary}</history_summary>"}] + recent

# 关键:摘要里必须保留可操作信息(决策、待办、约束),
# 丢弃闲聊与重复确认,否则后续任务会「忘记」前提。
python# 上下文压缩的效果与风险:省了 token,别丢了关键事实
def compressed(text_tokens, keep_ratio):
    return int(text_tokens * keep_ratio)
for r in [1.0, 0.5, 0.2, 0.1]:
    print(f"保留比例 {r:.0%} -> 约 {compressed(12000, r):>5} token")
# 预期输出:100%->12000;50%->6000;20%->2400;10%->1200
# 经验:抽取式压缩保真度高于摘要式;摘要必须强制保留实体与决策;
#   压缩后要在评测集上验证答案质量不下降,否则宁可多花 token 也不压。
★
压缩的红线:压缩丢信息的成本常被低估。经验:抽取式优先于摘要式(保真),摘要里强制保留实体与决策,并在评测集上验证压缩后答案质量不下降。一旦压缩导致关键事实丢失,宁可多花几个 token 也不压。

4.4 动手练习与自测

✔
动手练习与自测(GraphRAG / RAPTOR / 压缩):第 1–2 题动手算,第 3–5 题口头自测并对照判据表。
  1. 用 4.1 的估算,把文档数从 1000 提到 5000,重算 GraphRAG 预处理 token 量级。
  2. 用 4.2 的 build_layers,改分支因子为 10,观察树高变化并说明对检索成本的影响。
  3. 基础 RAG / RAPTOR / GraphRAG 分别擅长哪类问题?构建成本如何排序?
  4. 上下文压缩为什么「抽取式优于摘要式」?压缩的红线是什么?
  5. 什么时候「长上下文直接塞」反而优于 RAPTOR / GraphRAG?
题号考点关键判据 / 参考答案
1构建成本抽取 ≈ 5000×20×300 = 3e7 token;随文档数线性增长
2树的分支branching=10 -> 树更矮(约 3 层),每层节点少、检索更快但概览粒度更粗
3三者对比基础=局部点查;RAPTOR=从细节到概览;GraphRAG=跨实体关联;成本 基础
4压缩红线抽取保原文更保真;摘要可能丢关键事实,须评测验证,否则宁可不压
5长上下文适用文档小且稳定、需全局鸟瞰、检索失败零容忍时直接塞更简单

5. 记忆系统:让系统不“忘事”

知识结构图 · 记忆系统
记忆系统:让系统不“忘事”4 大知识域 · 14 个知识点
三层记忆模型工作记忆会话记忆长期记忆程序性记忆
学习路径
  1. 读 5.1:理清工作 / 会话 / 长期 / 程序性四类记忆的分工与生命周期
  2. 跑内置代码:演示一次会话如何从工作记忆沉淀进长期记忆
  3. 完成动手练习:为你的系统画出记忆分层表
  4. 对接 M13:把多轮上下文落地为会话 + 长期两层记忆供 RAG 兜底
✔ 能按生命周期把信息归到对应记忆层并说清读写路径
核心知识点详解
  • 四类记忆分层:工作记忆(本轮回话 / 当步临时状态,内存即可)、会话记忆(一次会话的历史,滚动窗口 + 摘要)、长期记忆(跨会话要点,向量 / 键值存储)、程序性记忆(工具用法 / 技能,独立于对话)。按生命周期归层决定读写路径。
  • 记忆的读写路径:写入:会话滚动摘要 → 重要性筛选 → 沉淀进长期记忆;读取:当前任务按相关度检索工作 / 会话 / 长期记忆合并注入。要能说清每个信息「从哪写入、按何时读取、何时被删除」。
  • 分层解决上下文腐坏:不把全部历史塞进窗口而是分层存取,是控制 token 成本与上下文腐坏的关键——工作记忆保当前、长期记忆按需注入。层间有明确迁移规则(什么进长期、什么丢弃)。
  • 常见坑:只一层(全塞会话)导致历史膨胀、检索到过期信息;或不写迁移规则,工作记忆旧数据残留影响当前任务。先画分层表再实现读写。
写入与检索策略重要性筛选去重与冲突消解相关度 + 重要度 + 新近度top-k 注入
学习路径
  1. 读 5.1:掌握重要性筛选、去重消解与检索打分原则
  2. 跑内置代码:按相关度 + 重要度 + 新近度打分并 top-k 注入
  3. 完成动手练习:设计自己的记忆写入去重规则
  4. 对接 M13:让记忆检索与向量检索共用打分并把结果合并进生成
✔ 能写出写入筛选与检索打分规则并让记忆参互动相关性
核心知识点详解
  • 写入先筛选:不是所有对话都进长期记忆——写入前做重要性筛选(含明确偏好 / 关键事实 / 指令才保留,寒暄丢弃)。只写不筛会污染记忆检索,随时间质量下降。筛选规则要显式可解释。
  • 去重与冲突消解:同一事实多次出现要去重合并;新信息与旧记忆矛盾时要消解(新覆盖旧 或 保留冲突并标注),否则检索时模型随机选边。这是社区记忆持久化的关键。
  • 多因子打分 + top-k 注入:检索记忆用 相关度 + 重要度 + 新近度 多因子打分,再 top-k 注入生成上下文。权重可调(如 score = α·rel + β·importance + γ·recency),且只需注入与当前任务相关的记忆,别全量塞。
  • 常见坑:只写不筛 → 记忆越积越杂,检索到全是过期噪音;或打分只看相关度,把冷门但重要的用户偏好漏掉。写筛选 + 多因子打分 + top-k 配合才能让记忆真正帮上忙。
压缩与遗忘滚动摘要 8–15x结论结构化提取半衰期 7 天衰减低价值删除
学习路径
  1. 读 5.1:理解滚动摘要压缩、结构化提取与半衰期遗忘机制
  2. 跑内置代码:把长对话压成滚动摘要并核对信息保留 8–15x
  3. 完成动手练习:为记忆设时间衰减与低价值删除策略
  4. 对接 M13:压缩后的记忆作为低成本上下文注入避免上下文腐坏
✔ 能跑通记忆压缩-遗忘闭环并说明信息保留取舍
核心知识点详解
  • 滚动摘要 8–15x:把一段对话压成滚动摘要(保留关键结论、丢弃过程),可把历史 token 压到 1/8–1/15 同时保持信息。摘要要上下文无关(指代展开成实体),否则脱离原会话语境没法用。
  • 结论结构化提取:除摘要外,把关键结论 / 偏好 / 任务状态结构化提取成记录(键值 + 时间戳),便于定向检索。混合「摘要(保留整体的历史叙述)+ 结构化(精确的事实检索)」是常态。
  • 遗忘机制:半衰期衰减 + 低价值删除:记忆需要遗忘:按半衰期衰减(如 7 天未活跃的冷门记忆降权或过期)、低价值记忆定期删除,避免长期记忆无限膨胀。半衰期 7 天 意为活跃度每 7 天减半,配低价值淘汰维持记忆保鲜。
  • 常见坑:只写不遗忘,长期记忆越积越满、检索命中过期信息;或摘要丢了指代上下文导致回看看不懂。跑通「写入→筛选→压缩→遗忘」闭环,并说明每一步保留了什么、丢了什么。
可解释可纠正用户可见可修正GDPR 删除与更正权
学习路径
  1. 读 5.1:理解记忆必须用户可见可修正并满足 GDPR 删改权
  2. 完成动手练习:为记忆系统暴露查看 / 修改 / 删除接口
  3. 对接 M13:把记忆纠正入口接进产品并记录审计
✔ 能用三次操作证明记忆可被用户查看、修正与删除
核心知识点详解
  • 用户可见可修正:记忆系统必须暴露查看 / 修改 / 删除接口:用户能看系统记住了什么、改掉错误条目、删掉敏感内容。可解释性 = 用户有权知道系统用了它的哪些记忆。
  • GDPR 删除与更正权:面向真实用户要满足 GDPR 知情权 / 更正权 / 被遗忘权:能按实体清空其记忆、能更正不实条目、保留审计日志。合规不只是法律要求,也是产品信任。
  • 验证以三次操作为准:验收即证明:用户可 ①查看当前记忆、②修正错误条目、③删除指定记忆,且操作后检索结果同步变化。三次操作跑通,记忆的可纠正性才算闭环。
  • 常见坑:记忆系统只写不读接口,用户没法改错,错误记忆长期污染后续回答;或无审计、删了也无法记录。把查看 / 修改 / 删除入口接进产品并留审计,别只做存储。
学习路径

5.1 三层记忆模型

层级内容存储生命周期
工作记忆当前任务的中间状态、已执行步骤、待办进程内 / 上下文单次任务
会话记忆本轮对话的历史与结论会话存储 + 滚动摘要一次会话
长期记忆用户偏好、事实、历史结论、经验向量库 / KV / 关系库跨会话持久
程序性记忆成功的方法、Prompt 模板、工具用法结构化知识库长期迭代
压缩策略触发条件做法典型收益
滚动摘要会话超过 N 轮(如 12 轮)把最早 K 轮压成 1 段摘要,保留结论与实体压缩比约 8–15 倍
结论提取出现明确事实 / 偏好结构化写入 key-value,不保留原句长期记忆体积降 60%+
去重合并同一 key 再次写入覆盖旧值(如「预算」只保留最新)消除冲突检索
衰减遗忘低重要度且久未命中按天数指数衰减,低于阈值删除索引规模可控
python# 记忆衰减与多因子打分:验证「相关度 + 重要度 + 新近度」谁主沉浮
def recency(age_days, half_life=7.0):
    return 0.5 ** (age_days / half_life)          # 半衰期 7 天

def score(sim, imp, age_days, w=(0.6, 0.3, 0.1)):
    return w[0]*sim + w[1]*imp + w[2]*recency(age_days)

# A:很相关但旧(30 天前);B:相关度略低但重要且新(1 天前)
print(round(score(0.92, 0.5, 30), 3))   # 0.707
print(round(score(0.80, 0.9, 1), 3))    # 0.841
# 结论:B 胜出 —— 高重要度 + 新,能压过「纯相似度高」。
print(round(recency(7), 3), round(recency(30), 3))   # 0.5  0.051
# 30 天后新近度几乎归零,若某记忆全靠新近度命中,说明它本就该被遗忘。
python# 分层记忆:写入时筛选 + 冲突消解,读取时多因子加权
from dataclasses import dataclass
import time

@dataclass
class Memory:
    content: str
    kind: str            # "preference" | "fact" | "episode"
    importance: float    # 0-1,由模型或规则评估
    ts: float
    key: str | None = None    # 用于冲突消解,如同一偏好只保留最新

def write(db, m: Memory, min_importance=0.4):
    if m.importance < min_importance:
        return                                  # 低价值信息不写入,避免污染
    if m.key:                                   # 冲突消解:同一 key 保留最新
        db.delete(where={"key": m.key})
    db.insert(m.to_dict())

def read(db, query_vec, k=5, w_rel=0.6, w_imp=0.3, w_new=0.1):
    cands = db.search(query_vec, k=50)
    now = time.time()
    def score(c):
        recency = 1 / (1 + (now - c["ts"]) / 86400)      # 按天衰减
        return w_rel * c["similarity"] + w_imp * c["importance"] + w_new * recency
    return sorted(cands, key=score, reverse=True)[:k]

5.2 动手练习与自测

★
动手练习(先做再看答案):重点是能算出数字,而不是「感觉会了」。做完对照下方判据。
  1. ① 给三层记忆各写一个「不该记」的例子,并说明它会被拦在哪一步。
  2. ② 用 5.1 的 score 函数手算:sim=0.9、imp=0.4、age=14 天,得分多少?
  3. ③ 同一用户的「收货地址」被改两次,检索时应返回哪一个?为什么?
  4. ④ 会话涨到 40 轮、token 超预算,给出两种压缩方案并估压缩比。
  5. ⑤ 为什么记忆必须能「被用户看到并修正」?至少给两条理由。
题号考点关键判据 / 参考答案
1写入筛选低重要度(如寒暄)被 min_importance 拦下;可推导信息(如「今天是周三」)不该记
2多因子打分≈0.685(0.6×0.9 + 0.3×0.4 + 0.1×0.25,recency(14)=0.5²=0.25)
3冲突消解按 key=收货地址 覆盖,只保留最新一条;旧值删除而非并存
4上下文压缩滚动摘要约 8–15 倍;结论结构化提取可让长期记忆体积降 60%+
5可解释可纠正合规(GDPR 删除权 / 更正权)+ 信任 + 纠错闭环,三者任一即可

6. 长上下文 vs RAG:四维对比与取舍

知识结构图 · 长上下文 vs RAG
长上下文 vs RAG:四维对比与取舍3 大知识域 · 10 个知识点
四维对比成本差 10–100 倍prefill 延迟 TTFT长尾事实精度可更新性
学习路径
  1. 读 6.1:在成本 / 延迟 / 长尾精度 / 可更新性四维上对比长上下文与 RAG
  2. 跑内置代码:用预填充延迟与成本公式算一次长上下文查询账
  3. 完成动手练习:为自己的场景做四维打分并选主方案
  4. 对接 M13:决定哪些查询走 RAG、哪些可直投长上下文并自证
✔ 能用四维打分给具体查询做长上下文 vs RAG 的取舍
核心知识点详解
  • 四维对比:成本(RAG 只付相关片段,长上下文付全部,差 10–100 倍)、延迟/prefill TTFT(长上下文 prefill 慢,重排可控制在 100–300ms)、长尾事实精度(长上下文全可见更全)、可更新性(RAG 改文档即可,长上下文要重传 / 重索引)。按这四维给具体查询打分。
  • 成本是最大分叉:示例:同任务下 RAG 只注入 top 8 片段(几千 token),长上下文要传全库(几十万 token)——单次成本差可达 10–100 倍。文档越大、更新越频,RAG 越划算。
  • 延迟四维之一 TTFT/prefill:首 token 时间(TTFT)≈ prefill 时间,随输入长度线性涨。长上下文把大量与答案无关的内容整段 prefill 掉,延迟不可控;RAG 的检索 + prefill 都可预期。
  • 常见坑:一言断言「长上下文取代 RAG」——忽略了成本 10–100 倍、更新要重传重索引;或反过来「一律 RAG」,忽略了需要全局鸟瞰的查询。按四维打分逐 query 决策,不是一刀切。
何时不用 RAG文档小且稳定全局鸟瞰需求零容忍检索失败
学习路径
  1. 读 6.1:掌握「文档小且稳定 / 全局鸟瞰 / 零容忍检索失败」可不用的判据
  2. 完成动手练习:列出自己可安全跳过 RAG 的查询类型
  3. 对接 M13:为「零容忍检索失败」的查询配置直投兜底
✔ 能依据判据判断哪些场景不该硬套 RAG
核心知识点详解
  • 三条可不用 RAG 的判据:①文档小且稳定(全量塞窗口成本也低、无需更新);②全局鸟瞰需求(要看到全文相互联系,RAG 只返回局部);③零容忍检索失败(检索一旦漏就不可接受,直接全量长上下文兜底)。
  • 全局鸟瞰场景:"通读一遍做总结 / 对比全文两处关系"这类查询,RAG 的 top-k 会漏掉局部以外的关联——此时直投长上下文更好。RAG 天生面向「定位式」检索,不擅长「全景式」。
  • 零容忍检索失败:某些场景检索失败代价极高(如法律 / 合规 / 关键事实),此时给这些查询配置长上下文直投兜底,RAG 只做快速路径。判据是失败成本是否允许可承受的漏检率。
  • 常见坑:为了「显得 UI 高级」硬套 RAG,小且稳定文档全量塞窗口更简单可靠;或不识别零容忍场景,检索漏了直接报错 / 乱答。按「文档大小 / 稳定性 / 鸟瞰需求 / 失败容忍」四问判断。
务实组合RAG 为主 + 兜底lost in the middle兜底触发阈值
学习路径
  1. 读 6.1:理解 RAG 为主 + 长上下文兜底的组合与 lost in the middle 现象
  2. 跑内置代码:复现长上下文中部信息丢失并设兜底触发阈值
  3. 完成动手练习:配置自己的兜底路由与触发条件
  4. 对接 M13:让 RAG 兜底与直投长上下文在同一路由里协同
✔ 能跑出 lost in the middle 并配置兜底阈值缓解
核心知识点详解
  • lost in the middle:模型对上下文正中间的信息关注最弱、两端(开头 / 结尾)最强——这是长上下文与 RAG 注入都要面对的位置偏置。复现:把同一事实放中间 vs 开头,观察准确率差异(可掉 5–20%)。
  • RAG 为主 + 长上下文兜底:生产常用组合:RAG 为主(快、便宜、可更新)+ 关键 / 全局 / 零容忍查询直投长上下文兜底(全量、无召回风险)。两者在同一路由里协同,兜底是例外不是常态。
  • 兜底触发阈值:用召回质量 / 置信度 / 查询类型设兜底触发阈值:如单方法 recall@5 低于红线(< 0.8)时触发长上下文兜底;或全局 / 零容忍类查询直接走兜底。阈值以消融实测校准。
  • 常见坑:没有兜底路由,零容忍查询检索失败就崩;或兜底与 RAG 每次都同时跑,成本 / 延迟白翻倍。把「关键证据放中间」也踩一遍位置偏置,用阈值只在需要时触发长上下文。
学习路径

6.1 什么时候可以不用 RAG

长上下文模型(128K–1M 窗口)普及后,很多人问「还要 RAG 吗」。答案是看四个维度,而不是非此即彼——很多系统两者并用。

维度RAG长上下文谁优
成本只付相关片段的 token付全部文档的 token(可能大 10–100 倍)RAG 优
延迟短上下文,生成快长上下文 prefill 慢、首个 token 延迟高RAG 优
精度(长尾事实)受召回限制,可能漏全文档可见,长尾事实更全长上下文优
可更新性改文档即可,无需重算改文档要重传 / 重索引整个上下文RAG 优
全局总结需 GraphRAG / 多跳直接放进去做鸟瞰长上下文优(小语料)

什么时候可以不用 RAG:① 文档总量小(几万字内)且稳定,整份塞进上下文就够了;② 问题天然需要全局鸟瞰(如「总结这份合同的风险点」),检索反而丢信息;③ 对检索失败零容忍、且窗口足够容纳全部资料。反之,文档海量、更新频繁、成本敏感,就仍应以 RAG 为主,长上下文当兜底。

python# 长上下文 vs RAG 的成本 / 延迟分界:算一笔账
DOC_TOKENS = 800_000        # 全量文档(token)
SNIPPET_TOKENS = 6_000      # 检索后注入(token)
PRICE_IN = 3.0 / 1e6        # 输入单价(元/token,示意值)

rag  = SNIPPET_TOKENS * PRICE_IN
full = DOC_TOKENS * PRICE_IN
print(round(rag*1000, 2), round(full*1000, 2))   # 每千次 18.0 元 vs 2400.0 元
print(round(full/rag, 1))                        # 133.3 倍
# 结论:文档量大时,全塞上下文比 RAG 贵两个数量级;
# 而精度上 RAG 只在「召回漏了」时才输,故默认 RAG + 长上下文兜底。
★
2026 的务实组合:生产里最常见的形态是「RAG 为主 + 长上下文兜底」:用 RAG 把最相关片段压缩进窗口,把长上下文当作「检索万一漏了还能兜底」的安全网,而不是把长上下文当成默认策略。两者结合,成本与覆盖兼得。

6.2 动手练习与自测

★
动手练习(先做再看答案):关键是把「要不要 RAG」变成可量化的判断,而不是拍脑袋。
  1. ① 语料 5 万字、每天更新、QPS 低,选 RAG 还是长上下文?
  2. ② 复算 6.1 的账:文档 80 万 token、片段 6K、单价 3 元/百万,两者差多少倍?
  3. ③「总结这份 300 页合同的 20 个风险点」用哪种方案更稳?为什么?
  4. ④ 设计「RAG 为主 + 长上下文兜底」的触发条件。
  5. ⑤ 长上下文的两个隐性成本是什么?
题号考点关键判据 / 参考答案
1四维取舍更新频繁 → RAG 为主;文档小且稳定才考虑整份塞入
2成本量级每千次 18 元 vs 2400 元,约 133 倍
3全局鸟瞰长上下文或 Map-Reduce 分块总结;纯 Top-k RAG 易漏关键风险点
4兜底触发检索最高分低于阈值,或命中片段少于 2 个 → 升级为全文档
5隐性成本prefill 慢导致 TTFT 高;「lost in the middle」使中间信息被忽略

7. Harness Engineering:让 Agent 跑得久而不崩

知识结构图 · Harness Engineering
Harness Engineering:让 Agent 跑得久而不崩3 大知识域 · 13 个知识点
运行框架五要素记忆与上下文工具调用与权限多步规划与分解失败恢复检查点续跑
学习路径
  1. 读 7.1:把记忆、工具、规划、失败恢复、检查点串成一个 harness
  2. 跑内置代码:让 agent 在多步任务里跑完并沉淀可续跑的检查点
  3. 完成动手练习:给自己管线装上最小 harness 并验证长跑不崩
  4. 对接 M13:让 RAG 管线能接管失败恢复与断点续跑
✔ 能让一条多步任务稳定跑完并从检查点续跑
核心知识点详解
  • harness 五要素:记忆与上下文(滚动 + 分层)、工具调用与权限(schema + 授权)、多步规划与分解(状态机)、失败恢复(重试 / 修复 / 挂起)、检查点续跑(中断后从断点恢复)。这五块把「一句 prompt」升级成「一套可运行框架」。
  • 检查点续跑:每完成一步把状态落地(当前步骤、已获证据、中间结果),崩溃 / 超时后从检查点续跑而非重头开始。这是让多步任务从「演示」走向「长跑不崩」的关键工程。
  • 多步任务稳定跑完:验收标准:一条多步任务能稳定跑完不崩(无死循环 / 无状态丢失),且中断后可续跑。用 harness 串起步骤,而不是让模型一个 for 循环硬推到底。
  • 常见坑:只给模型一个长指令让它自己多步推进,无支架无状态,随便一步出错就整体失败;或没有检查点,中断后全部重来。装上最小 harness(状态机 + 检查点)再测长跑。
失败分类与恢复可重试 3–5 次可修复换策略不可重试立即失败人工门禁挂起
学习路径
  1. 读 7.1:按可重试 / 可修复 / 不可重试 / 人工门禁给失败归类
  2. 跑内置代码:构造三类失败并观察重试 / 换策略 / 挂起的处置路径
  3. 完成动手练习:为自己的步骤编写失败分类与恢复策略
  4. 对接 M13:把失败恢复策略接到检索 / 生成环节保证管线健壮
✔ 能分类一次失败并选择对应恢复路径让任务续行
核心知识点详解
  • 按失败类型分类处置:可重试(超时 / 限流 / 5xx,重试 3–5 次)→ 可修复 / 换策略(输入不合法改参数、换工具)→ 不可重试(4xx / 逻辑错误,立即失败并告警)→ 人工门禁挂起(高风险动作)。分类决定恢复路径,决不一律重试。
  • 可重试的退避:重试要指数退避 + 抖动(base×2^n + jitter,封顶 30s),否则把对端限流打满;可重试类才重试,永久失败类重试只浪费配额。先分错再重试。
  • 高风险动作人工审批:对不可逆 / 高风险操作(发钱、删除、外部影响)设人工门禁:hit 到即挂起、等人工审批通过才继续,不自动放行。这是「演示 agent」与「能跑生产 agent」的分水岭。
  • 常见坑:所有失败都重试(对 4xx / 逻辑错误做无意义重试);或把高风险动作默默自动执行无审批,出事无法负责。分类 + 退避 + 审批三层配合,让一次失败可定位、可续行。
框架实现骨架状态机 + 检查点指数退避 + jitter高风险动作审批全链路 tracing
学习路径
  1. 读 7.1:吃透状态机 + 检查点、退避 + jitter、审批、tracing 的实现骨架
  2. 跑内置代码:实现带状态机的中断续跑并加指数退避
  3. 完成动手练习:为高风险步骤加审批闸门并接 tracing
  4. 对接 M13:用骨架保证 RAG 管线可追溯、可续跑、危险操作需审批
✔ 能跑通「中断-续跑」并看到每次调用的链路 trace
核心知识点详解
  • 状态机 + 检查点:骨架核心是显式状态机(idle → planning → executing → waiting_approval → done)+ 每步落检查点。状态机保证步骤有序、可重入;检查点保中断可续。比让模型自行编排可靠得多。
  • 指数退避 + jitter:对外部调用封装指数退避 + 抖动(min(base*2^n*rand(), max),封顶约 30s、2–4 次),避免瞬时重试风暴。这是所有会被限流 / 超时影响的步骤的统一保险丝。
  • 全链路 tracing:每次 LLM / 工具调用都打 trace(输入摘要、耗时、模型、token、状态转移、失败原因),跨步骤可拉成链路。出问题时能定位「哪一步、哪个调用、为什么」,这是调试多步 agent 的命门。
  • 常见坑:无状态机全靠模型自己编排,出错无法重入;或不打 tracing,线上失败完全黑盒、查不了。先把骨架(状态机 + 检查点 + 退避 + 审批 + tracing)搭起来,再往上长功能。
学习路径

7.1 运行框架的五个要素

2026 年 AI 工程师的核心工作已经从「怎么问模型」转向「怎么设计模型的运行环境」。行业给出的经验很直白:同样是 GPT-4o 级别的模型,搭建了良好 Harness 的团队能跑起来 6 步以上的自主流程,没有 Harness 的团队停留在单次问答。

① 记忆与上下文
持久化任务状态、已尝试方案与结论;每轮只注入必要上下文,避免窗口膨胀。
② 工具调用与权限
工具签名清晰、返回值结构化;高风险操作必须有人工审批门禁(仅约三成 MCP 应用有审批机制,这是最大安全缺口)。
③ 多步规划与分解
把大任务拆成可验证的子任务,每步有明确的完成判据,而不是让模型自由发挥。
④ 失败恢复
区分可重试与不可重试错误;重试要换策略而不是简单重复;记录失败原因供后续参考。
⑤ 检查点与可恢复
长流程要能中断续跑;每步的状态与产物落盘,避免整体重来。
⑥ 可观测与评估
全链路 tracing、每步输入输出可回放、有回归测试集。没有 tracing,优化就是盲猜。
失败模式典型例子恢复策略重试上限
可重试(瞬时)限流 429、网络超时、工具暂不可用指数退避 + jitter 后原样重试3–5 次
可修复(换策略)参数 schema 不符、检索空结果改写查询 / 调参数后重试,不原样重放2–3 次
不可重试(致命)权限不足、输入非法、依赖缺失立即失败并上报,不消耗重试预算0 次
需人工门禁删数据、发邮件、付款、改生产挂起落盘,等人工确认后再继续—
python# Harness 的核心骨架:状态机 + 检查点 + 失败恢复 + 审批门禁
from dataclasses import dataclass, field, asdict
import json, time

@dataclass
class State:
    task: str
    steps: list = field(default_factory=list)      # [{name, input, output, ok, error}]
    artifacts: dict = field(default_factory=dict)
    status: str = "running"                        # running | blocked | done | failed

def save(state: State, path: str):
    json.dump(asdict(state), open(path, "w"), ensure_ascii=False, indent=2)

def needs_approval(action: str) -> bool:
    """高风险动作必须人工确认 —— 这是生产 Agent 的安全底线"""
    return action in {"delete_records", "send_email", "execute_payment", "modify_prod"}

def run_step(state: State, step, max_retry=3):
    for attempt in range(max_retry):
        try:
            if needs_approval(step.name) and not step.confirmed:
                state.status = "blocked"                       # 挂起等待人工
                save(state, CKPT)
                return state
            out = step.run()                                   # 实际执行(工具调用/模型推理)
            state.steps.append({"name": step.name, "output": out, "ok": True})
            save(state, CKPT)                                  # 每步落盘,可中断续跑
            return state
        except RetryableError as e:
            if attempt == max_retry - 1:
                state.steps.append({"name": step.name, "error": str(e), "ok": False})
                state.status = "failed"
            else:
                step = step.variant(attempt)                   # 关键:换策略重试,不是原样重放
                time.sleep(2 ** attempt)                       # 指数退避
        except FatalError as e:
            state.steps.append({"name": step.name, "error": str(e), "ok": False})
            state.status = "failed"
            break
    save(state, CKPT)
    return state

7.2 动手练习与自测

★
动手练习(先做再看答案):Harness 的每一条都能用故意制造失败来验证是否真的生效。
  1. ① 列出五个必须走人工门禁的动作。
  2. ② 给「限流 429」和「参数 schema 不符」各配一套恢复策略。
  3. ③ 为什么重试要「换策略」而不是原样重放?
  4. ④ 设计检查点内容,使任务能在崩溃后续跑。
  5. ⑤ 没有 tracing 时最典型的坑是什么?
题号考点关键判据 / 参考答案
1审批门禁删数据、发邮件、付款、改生产、对外发布,任一即可
2错误分类429 → 退避重试 3–5 次;schema 不符 → 换策略重试 2–3 次
3重试本质相同输入下同一错误大概率复现,原样重放只是烧预算
4检查点任务目标、已完成步骤、每步产物、当前状态、失败原因都要落盘
5可观测多步 Agent 出错后无法定位是规划、工具还是模型的锅

8. 结构化输出与工具调用

知识结构图 · 结构化输出与工具调用
结构化输出与工具调用3 大知识域 · 13 个知识点
约束解码JSON 模式Grammar / FSM 约束结构合法 ≠ 语义正确Pydantic 校验
学习路径
  1. 读 8.1:掌握 JSON Schema 与 Grammar / FSM 约束解码的原理
  2. 跑内置代码:用约束解码保证合法率并用 Pydantic 校验语义
  3. 完成动手练习:为一个输出定义 schema 并测结构合法率
  4. 对接 M13:让引用溯源输出走 strict 解码且通过 Pydantic 校验
✔ 能做出结构 100% 合法且语义经 Pydantic 校验的输出
核心知识点详解
  • 结构合法 ≠ 语义正确:约束解码(Grammar / FSM / JSON 模式)只在采样时保证结构合法——字段不缺、类型不错、多余键过滤,可让合法率逼近 100%;但它不保证语义正确,金额 / 日期 / 单位仍可能算错。两层必须分开做。
  • JSON Schema + Pydantic 双层校验:输出先按 JSON Schema(约束解码)保证结构,再用 Pydantic(model_validate)做语义 / 范围校验:枚举值、必填、类型、数值范围全查一遍。结构打标是「壳」,Pydantic 是「核」。
  • 约束解码的原理:Grammar / FSM 把 JSON 语法建成状态机,每步只允许「能继续构成合法 JSON」的 token——从根上杜绝非法结构。相比让模型自由生成再解析,合法率更高、解析不抛异常。
  • 常见坑:以为约束解码保证语义:金额算错、日期越界照样合法;或只要求「输出 JSON」不约束结构,解析失败概率高。结构用约束解码保、语义用业务规则 / Pydantic 校验保,两层各司其职。
Function CallingJSON Schema 签名tool_calls 带 idrole=tool 回填工具数 >50 掉点并行调用对应
学习路径
  1. 读 8.2:理解 JSON Schema 签名、tool_calls 带 id 与 role=tool 回填
  2. 跑内置代码:跑并行工具调用并核对调用 id 与结果回填一一对应
  3. 完成动手练习:控制工具数避免超过 50 掉点
  4. 对接 M13:为 RAG 接入检索 / 查询改写工具并走完整调用回填
✔ 能跑通带 id 的工具调用-回填并使并行调用不串位
核心知识点详解
  • JSON Schema 签名 + tool_calls 带 id:每个工具用 JSON Schema 声明参数签名,模型输出 tool_calls 且每个调用带独立 id。id 是并行调用回填的一一对应关键——tool 消息用 tool_call_id 指回对应调用,否则结果张冠李戴。
  • role=tool 回填:工具结果以 role=tool 消息回填,且必须原样保留带 tool_calls 的 assistant 消息——否则模型「失忆」刚才想做什么,多轮工具对话就断。这是 Function Calling 最容易踩的坑。
  • 工具数 >50 掉点:工具声明越多,模型选择越容易出错(注意力被稀释):列表超过约 50 个工具后调用准确率明显下降。超大工具集要分组 / 路由 / 分层描述,别一把全塞进 schema。
  • 常见坑:并行调用每个 tool_call 不写独立 id、或 tool 消息不带 tool_call_id,结果错位;漏回传带 tool_calls 的 assistant 消息致模型失忆;工具数过多导致选错工具。三处都要按协议原样保真。
降级路径修复重试宽松抽取low_confidence 标记转人工兜底
学习路径
  1. 读 8.3:掌握修复重试 → 宽松抽取 → low_confidence → 转人工的降级链
  2. 跑内置代码:构造一次解析失败并走完整降级链条
  3. 完成动手练习:为自己的输出配置降级并标记低置信
  4. 对接 M13:让生成解析失败时优雅降级并转人工可介入
✔ 能演示解析失败被逐级兜住而不让整条管线崩溃
核心知识点详解
  • 降级金字塔:解析 / 校验失败按层级降级:修复重试(命模型 self-correct → 重试 1–2 次)→ 宽松抽取(正则 / 字段级抓取,只取能取到的)→ low_confidence 标记(保住流程但打低置信标)→ 转人工(最终兜底)。逐级兜住,不让整条管线崩。
  • 修复重试:把解析失败的错误信息回喂给模型让它修正再输出(把上次错误贴给它重试),成功率高但多一次调用;重试 1–2 次就换更低层,避免无限循环烧 token。
  • low_confidence + 人工兜底:宽松抽取拿到的字段标记 low_confidence,高风险 / 无法确定时转人工介入。宁可标注不确定转人,也不要「自信的错误答案」直接进下游。
  • 常见坑:解析失败直接抛异常让整条管线崩,没有兜底链;或重试到天荒地老烧钱。从「修复 → 宽松 → 标记 → 人工」逐级降级,每一级都要有退出条件。
学习路径

8.1 JSON Schema 与约束解码

让模型输出可被程序消费,比「让它写 JSON 再正则解析」可靠得多的方式是约束解码:在生成时就把 token 限制在与 JSON Schema 一致的空间里,模型「无法不遵循」。

方法机制可靠性适用
JSON 模式(API 层)模型端保证输出合法 JSON高(结构合法,但字段语义不一定对)绝大多数结构化输出
Grammar / FSM 约束解码用上下文无关文法或有限状态机在每一步限制可生成 token最高(逐 token 收敛到合法结构)强结构化、开源模型
后处理校验 + 重试先生成再 Pydantic 校验,失败重试 / 降级中(依赖重试次数)快速实现、无约束解码支持时
正则 / 模板填充给定模板只填槽位低(易错位)极简单场景
python# 约束解码(以 outlines / xgrammar 思路为例):用 grammar 限制生成
# 概念:把 JSON Schema 编译成可接受的 token 序列约束,
# 每生成一个 token,就只允许「使当前前缀仍可能合法」的 token。
from pydantic import BaseModel

class Invoice(BaseModel):
    invoice_no: str
    amount: float
    currency: str
    items: list[str]

# 开启 structured output 后,模型在语法层面只能输出符合 Invoice 的 JSON,
# 不会出现「漏字段」「类型错」「多余键」等结构性错误。
# 注意:约束解码保证「结构合法」,不保证「语义正确」(如金额算错),
# 仍需业务层校验。
✔
结构合法 ≠ 语义正确:约束解码解决了「能不能解析」的问题,但没解决「对不对」的问题。金额、日期、单位这类语义正确性,仍要业务规则兜底(如金额必须等于各项之和)。把两层分开:结构用约束解码保,语义用校验保。

8.2 Function Calling 协议细节

Function calling(工具调用)已成为 Agent 的事实标准。理解协议细节才能写出稳的调用层:工具以 JSON Schema 描述签名,模型返回 tool_calls(含 id 与参数),你执行后用 role: tool 消息把结果回填,且并行 tool_calls 要各自带 id 对应。

python# MCP 风格的工具体量控制:工具越多,模型选错的概率越高
def select_tools(all_tools, query, top_k=8):
    # 全量灌入(如 120 个工具)会挤爆上下文并拉低选择准确率
    # 生产做法:先用向量/规则预筛,只把最相关的 top_k 注入
    return rank(all_tools, query)[:top_k]

# 经验数字(业界公开经验):
#  - 工具数 < 10 时,选择准确率通常 > 95%
#  - 工具数 > 50 且描述重叠时,准确率可掉到 70% 以下
#  - 每个工具的 schema 描述尽量控制在 2-3 句,参数用 enum 收敛取值
# 关键:把「工具选择」当成检索问题,而不是全量罗列。
python# Function calling 的请求 / 响应协议(概念性)
tools = [{
    "type": "function",
    "function": {
        "name": "search_orders",
        "description": "按订单号或用户查询订单状态",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string"},
                "user_id":  {"type": "string"}
            },
            "required": ["order_id"]
        }
    }
}]

# 模型返回(注意并行调用时每个 tool_call 都有独立 id):
assistant_msg = {
    "role": "assistant",
    "content": None,
    "tool_calls": [
        {"id": "call_1", "type": "function",
         "function": {"name": "search_orders", "arguments": '{"order_id": "A123"}'}}
    ]
}
# 回填结果:role=tool 且 tool_call_id 与上面对应
tool_msg = {"role": "tool", "tool_call_id": "call_1", "content": '{"status": "shipped"}'}

# 常见坑:多轮里漏掉把 assistant 的 tool_calls 也回传,模型会「失忆」刚才想调什么。
⚠
工具调用的三个高频坑:① 参数幻觉:模型编造不存在的字段或 enum 值——用 required + enum + 约束解码压住;② 漏回传 tool_calls:多轮对话必须把带 tool_calls 的 assistant 消息原样保留,否则模型丢失意图;③ 并行调用结果错位:tool_call_id 必须一一对应,否则答案张冠李戴。

8.3 解析失败的降级路径

再稳的结构化输出也会有失败(网络、超长、模型抽风)。生产代码必须预设降级路径,而不是抛异常中断整个链路。

层级触发条件动作返回标记
1 正常解析schema 校验通过直接返回结构化对象confidence=high
2 修复重试JSONDecode / ValidationError回灌错误,要求只改格式不动语义confidence=high
3 宽松抽取重试次数耗尽正则 / 规则抽字段,缺失字段置 nullconfidence=low
4 人工兜底关键字段仍缺失转人工队列 / 返回错误码status=needs_review
python# 解析失败的降级金字塔:重试 → 修复 → 降级 → 人工
def robust_parse(client, messages, schema, max_retry=3):
    for attempt in range(max_retry):
        try:
            raw = client.chat(messages, response_format=json_schema(schema))
            return schema.model_validate_json(raw)          # 1) 正常解析
        except (json.JSONDecodeError, ValidationError) as e:
            # 2) 让模型「修复」:把错误回灌,请求只修正格式
            messages = repair_prompt(messages, raw, e)
    # 3) 降级:用宽松解析(正则抽字段)或回退规则引擎
    loose = loose_extract(raw, schema)
    if loose: return loose
    # 4) 最终降级:转人工 / 返回错误码,绝不让链路静默崩
    return fallback_to_human(messages)

# 关键:降级也要返回「可消费」的结果(哪怕带 low_confidence 标记),
# 这样下游能决定是继续还是升级人工。
ℹ
降级的设计原则:降级不是「随便给个默认值」,而是明确告知不确定性:给结果打 low_confidence 标记,让下游决定是继续、重试还是转人工。静默给错值比报错更危险——它会让错误传播到看不见的地方。

8.4 动手练习与自测

★
动手练习(先做再看答案):结构化输出的验收标准是程序能稳定消费,不是「看着像 JSON」。
  1. ① 约束解码能保证什么、不能保证什么?
  2. ② 并行 tool_calls 时最容易出的错是什么?
  3. ③ 写出解析失败的四层降级顺序。
  4. ④ 为什么「静默返回默认值」是反模式?
  5. ⑤ 给订单查询工具设计参数 schema,压住参数幻觉。
题号考点关键判据 / 参考答案
1约束解码边界保证结构合法;不保证语义正确(金额、日期、单位仍需业务校验)
2并行调用tool_call_id 未一一对应,导致结果张冠李戴
3降级金字塔正常解析 → 修复重试 → 宽松抽取 → 人工兜底
4不确定性静默给默认值会掩盖不确定性,错误向上下游传播且无法察觉
5schema 设计required 必填 + enum 收敛取值 + 约束解码,三者叠加

9. 缓存:prompt caching 与语义缓存

知识结构图 · 缓存
缓存:prompt caching 与语义缓存3 大知识域 · 11 个知识点
Prompt 前缀缓存前缀逐 token 相同命中降 90% 成本TTL 过期稳定内容放最前
学习路径
  1. 读 9.1:理解前缀需逐 token 相同与命中降 90% 成本的原理
  2. 跑内置代码:把稳定前缀放最前并实测命中后的成本与 TTFT
  3. 完成动手练习:设计系统提示与长文档的前缀复用结构
  4. 对接 M13:用 prompt caching 降低 RAG 高频前缀的推理成本
✔ 能实测命中率并量化它带来的成本与延迟下降
核心知识点详解
  • 前缀逐 token 相同:Prompt 前缀缓存的前提是前缀逐 token 完全相同(连一个空格 / 换行不同都不命中)。所以系统提示要稳定、放最前、且不夹带易变内容——任何序列化差异都让整段失效、白付十倍输入成本。
  • 命中降 90% 成本:命中后命中段的 KV 复用、不再重复计算,输入成本与 prefill 延迟可降约 90%。示例:系统 + 长文档前缀恒定时,重复查询的成本从全价降到极低——是高频 RAG 最值钱的杠杆之一。
  • TTL 过期与失效:缓存有 TTL(默认 Cache-Control 控时,如几分钟到小时级);知识更新后前缀变化也会自然失效。设计时把「稳定前缀」与「易变尾段」分离,最大化复用又能及时反映更新。
  • 常见坑:把检索结果 / 当前时间等易变内容塞进 system 前缀,永远不命中缓存;或未标记 cache 段,白交 prefill 钱。前缀稳定化 + 显式 cache 标记,实测命中率与成本降幅。
语义缓存相似度 ≥ 阈值客服命中 20–40%高风险场景慎用知识更新清缓存
学习路径
  1. 读 9.2:理解语义缓存按相似度命中及高风险场景慎用的边界
  2. 跑内置代码:调相似度阈值观察命中率与误命中变化
  3. 完成动手练习:为自己的查询设计缓存键与失效策略
  4. 对接 M13:给检索结果加语义缓存并记录命中率与成本节省
✔ 能设阈值并量化命中率,且知识更新后能清对缓存
核心知识点详解
  • 相似度 ≥ 阈值才命中:语义缓存按「query 语义相似度 ≥ 阈值」命中并复用历史回答,对近似重复问询(如客服常见问题)可省大量调用——客服类命中率常达 20–40%。阈值调高降误命中但丢命中,调低则可能答非所问,要消融校准。
  • 高风险场景慎用:语义缓存命中的是「看起来像」但不完全等价的 query——高风险 / 个性化 / 时效敏感场景慎用,避免用过期或偏差答案。用于稳定、通用的 FAQ 类最安全。
  • 知识更新清缓存:文档 / 政策更新后必须清 / 失效对应缓存,否则继续命中旧答案。把「命中条目的底层来源」也存下来,更新某来源时能精确失效,而不是一刀清空。
  • 常见坑:阈值设太低,近似 query 命中了却不匹配的旧答案;或更新知识后不清缓存,一直返回旧结论。调阈值 + 按来源失效 + 高风险禁缓存三管齐下。
三类缓存对比精确哈希缓存Prompt 前缀缓存语义缓存
学习路径
  1. 读 9.1 与 9.2:在精确哈希 / 前缀 / 语义三类缓存间做选择
  2. 完成动手练习:对比三类缓存的命中条件、收益与失效复杂度
  3. 对接 M13:组合三类缓存并按时效与场景分层部署
✔ 能按命中条件与失效成本讲清为何选 / 不选某一类缓存
核心知识点详解
  • 三类缓存对比:精确哈希(query 完全相同才命中,最严、零误命中,覆盖超高频重复项);Prompt 前缀缓存(前缀逐 token 相同,降 prefill 成本,覆盖稳定 system + 长文档);语义缓存(相似度 ≥ 阈值命中,覆盖近似问询但有过期 / 偏差风险)。
  • 命中条件与失效成本:三类的命中粒度与失效成本不同:哈希命中最窄但失效最简单,语义命中最宽但失效要按来源追、风险最高。选型 = 在「命中收益」与「误命中 / 失效复杂度」之间权衡。
  • 组合分层的用法:常见组合:哈希 / 前缀处理确定性重复(高频入口)、语义缓存处理近似问询(客服)、知识更新时按来源精确失效。按场景时效与风险分层部署,而不是三选一。
  • 常见坑:不分场景乱加语义缓存,命中时返回过期答案;或不考虑失效成本,知识一变全靠一刀清导致命中率归零。选 / 不选都要讲清「命中条件、收益、失效成本」三点。
学习路径

9.1 Prompt Caching:前缀复用

Prompt caching(前缀缓存)利用一个事实:很多调用的上下文前缀是重复的(系统提示、工具定义、长文档)。只要前缀相同,提供商就缓存其键值,后续调用不再重新计算这部分,命中时输入 token 费用可降 90% 量级,且首个 token 延迟显著下降。

python# Prompt caching:把稳定、长的前缀标记为可缓存(以 Anthropic 风格为例)
messages = [
    {"role": "system", "content": LONG_SYSTEM, "cache_control": {"type": "ephemeral"}},
    {"role": "user",   "content": user_q}      # 只有这部分每次不同
]

# 命中条件(各提供商略有差异,但核心一致):
#  - 前缀必须「逐 token 相同」且长度通常超过阈值(如 1024+ tokens)
#  - 缓存有 TTL(几分钟到几十分钟),过期后重新计算
#  - 放在最前面缓存收益最大:系统提示 > 工具定义 > 长文档

# 收益量化:一个 3000 token 的系统提示,1000 次调用,
# 命中后输入成本约为原来的 10%(缓存命中价远低于普通输入价)。
python# 前缀缓存命中后的成本 / 延迟收益(示意价格)
CALLS = 1000
PREFIX = 3000              # 系统提示 token 数
PRICE = 3.0 / 1e6          # 普通输入单价
CACHE_PRICE = PRICE / 10   # 缓存命中单价(约 1/10)

no_cache  = CALLS * PREFIX * PRICE
with_cache = CALLS * PREFIX * CACHE_PRICE
print(round(no_cache, 2), round(with_cache, 2))     # 9.0 元 vs 0.9 元
print("省", round((1 - with_cache/no_cache)*100), "%")  # 省 90 %
# 另:命中缓存还能显著降低 TTFT —— 几千 token 的 prefill 被直接跳过。
✔
怎么最大化前缀缓存命中:① 把最稳定、最长的内容放最前面(系统提示、工具 schema、常驻文档);② 避免在前缀里塞随时间变化的内容(如时间戳、每次不同的变量);③ 批量请求尽量共享同一前缀。很多团队上线 prompt caching 后,输入成本直接降一个数量级。

9.2 语义缓存:相似问题命中

语义缓存与 prompt caching 不同:它不要求前缀相同,而是「问题语义相似就复用答案」。对客服 / 知识库这类重复问题多的场景,是最大成本杠杆之一。

缓存类型命中条件典型命中率主要风险
精确哈希缓存输入完全相同重复问句场景 30%+覆盖窄,几乎无风险
Prompt 前缀缓存前缀逐 token 相同批量场景 80%+前缀一变即失效
语义缓存向量相似度 ≥ 阈值客服场景 20%–40%相似问题答案不同 → 错答
python# 语义缓存:命中近似问题就复用答案
import hashlib, numpy as np

class SemanticCache:
    def __init__(self, embedder, threshold=0.95):
        self.embedder, self.threshold = embedder, threshold
        self.keys, self.vecs, self.vals = [], [], []

    def get(self, query: str):
        v = self.embedder.encode(query)
        if not self.vecs: return None
        sims = np.dot(np.stack(self.vecs), v) / (
            np.linalg.norm(np.stack(self.vecs), axis=1) * np.linalg.norm(v) + 1e-8)
        i = int(np.argmax(sims))
        return self.vals[i] if sims[i] >= self.threshold else None

    def put(self, query: str, answer: str):
        self.keys.append(hashlib.md5(query.encode()).hexdigest())
        self.vecs.append(self.embedder.encode(query))
        self.vals.append(answer)
⚠
缓存失效与一致性:两类缓存都要处理失效:prompt caching 依赖前缀稳定,一旦前缀(如系统提示)更新,旧缓存自动失效(靠 TTL);语义缓存 则要警惕「听起来相似但答案不同」——医疗 / 金融 / 合规等高风险场景阈值要很高(0.98+)或不使用语义缓存,因为相似问题的答案可能截然相反。此外知识更新后要主动清缓存,否则模型会一直返回旧答案。

9.3 动手练习与自测

★
动手练习(先做再看答案):缓存的收益要能算出来,风险要能说清楚。
  1. ① 复算 9.1 的收益:3000 token 前缀、1000 次调用、命中价 1/10,省多少?
  2. ② 什么内容不该放在可缓存前缀里?
  3. ③ 医疗问答要不要开语义缓存?为什么?
  4. ④ 知识库更新后为什么必须清语义缓存?
  5. ⑤ 两种缓存的失效机制分别是什么?
题号考点关键判据 / 参考答案
1收益量化9.0 元 → 0.9 元,省约 90%,且 TTFT 明显下降
2前缀稳定时间戳、每次不同的变量、高频变动的用户私有内容
3高风险场景不建议,或阈值 ≥0.98;相似问题答案可能截然相反
4一致性否则持续返回旧答案,形成「过期事实」错误
5失效机制前缀缓存靠 TTL + 前缀变动自动失效;语义缓存需主动清理

10. 成本、路由与工程质量

知识结构图 · 成本、路由与工程质量
成本、路由与工程质量3 大知识域 · 14 个知识点
六个成本杠杆模型路由缓存三件套上下文压缩批处理输出约束本地化小模型
学习路径
  1. 读 10.1:吃透模型路由 / 缓存 / 压缩 / 批处理 / 输出约束 / 小模型六个杠杆
  2. 跑内置代码:按杠杆逐个动手并量化每个带来的成本下降
  3. 完成动手练习:为一次真实调用勾选适用的杠杆组合
  4. 对接 M13:组合缓存 + 压缩 + 路由把 RAG 单次调用成本压到预算内
✔ 能逐个杠杆量出成本降幅并组合出满足预算的方案
核心知识点详解
  • 六个成本杠杆:模型路由(简单任务用小模型)、缓存三件套(哈希 / 前缀 / 语义,重复可降 90%+)、上下文压缩(检索后摘要、历史滚动摘要)、批处理(离线任务批量)、输出约束限制(限 max_tokens)、本地化小模型。按链路逐个量化每个杠杆的降幅。
  • 成本先测后优化:第一步不是猜,而是测出来:按链路统计输入 / 输出 token、单价、命中率,找出成本集中点(通常 20% 的调用占 80% 的成本)再对症优化。先记录,再降本。
  • 组合满足预算:单杠杆常不够,要组合:路由降单价 + 缓存降重复 + 压缩降长度三管齐下,才能把单次 RAG 调用压到预算内。组合的前提是每个杠杆都有可复现的降幅记录。
  • 常见坑:不测就优化,优化的不是成本大头;或只上路由不碰缓存 / 压缩,成本大头的长上下文依旧爆炸。先核算、再按大头逐杠杆击破,最后组合验证预算。
路由策略难度分档60/30/10 流量分布误判掉点监控回退率
学习路径
  1. 读 10.1:掌握难度分档与 60/30/10 流量分布的路由做法
  2. 跑内置代码:做一档难度路由并对比昂贵模型的调用占比
  3. 完成动手练习:给路由设误判监控与回退率阈值
  4. 对接 M13:为检索/生成配置模型路由并把回退率纳入监控
✔ 能按难度分档命中便宜模型且用回退率守住质量
核心知识点详解
  • 难度分档路由:把查询按难度分档(简单 / 中等 / 难),分别路由到小 / 中 / 大模型。示例流量分布 60/30/10:60% 简单查询走最便宜模型、30% 中等、10% 难题走最强——整体成本可降至全走大模型的约 1/5。
  • 路由成本核算:算例:0.60×0.15 + 0.30×1.0 + 0.10×4.0 ≈ 0.79 元 / 千 token,对比全走大模型 4.0 元——约省 5 倍(参考 10.1 drill 答案 ≈0.79 与 5.06 倍)。配比要实测流量分布校准。
  • 误判掉点与回退率:路由最大风险是难题被误判到小模型掉点。所以要监控 回退率(命中低档但质量不达标而升级回改的比例)与分任务质量,超标才调档。守住质量的闸门在回退机制,不在分档本身。
  • 常见坑:为了省 90% 成本把难题全送小模型,质量崩了没人发现;或不接「升级回退」只做死分档,误判无法自救。配回退率监控,质量回退超阈值即调。
成本核算输入 / 输出 token模型单价缓存命中比例每千次请求成本
学习路径
  1. 读 10.1:掌握按输入 / 输出 token 与单价核算每千次请求成本
  2. 跑内置代码:用实测 token 与缓存命中比例算一次真实成本
  3. 完成动手练习:为自己管线写一份成本测算表
  4. 对接 M13:给出 RAG 每千次请求成本并把因子拆进报告
✔ 能算清每千次请求成本并定位最大成本来源
核心知识点详解
  • 成本核算模板:记四个数:输入 token、输出 token、模型单价、缓存命中比例。每千次 输入×单价×(1−命中率) + 输出×单价——命中率直接影响有效付费输入。用实测 token 与命中比更新,别用估算。
  • 成本集中定律:成本通常集中在少数调用:20% 的调用(超长上下文 / 贵模型 / 大输出)占约 80% 的成本。核算的价值就是找出这「少数大头」再对症。
  • 可拆因子进报告:把成本拆成因子(单价 × 长度 × 调用次数 × 命中率)写进报告,让每次优化能归因:改路由降单价、压缩降长度、缓存降调用、批处理降次数。可归因 = 可优化。
  • 常见坑:平均每千次成本看着不高,却没拆大头调用,超长上下文那 20% 把预算吃光;或不记命中率,改缓存后算不出省了多少。按模板拆因子、定位最大来源再优化。
学习路径

10.1 成本控制的六个杠杆

杠杆做法典型收益
模型路由简单任务用小模型,难题升级到强模型成本降 50%–80%
缓存精确哈希缓存 + 语义缓存 + Prompt 前缀缓存重复场景降 90%+
上下文压缩检索后摘要、历史滚动摘要、去掉冗余工具成本与延迟同降
批处理离线任务用批处理接口(通常有折扣)成本降 50%
输出约束限制 max_tokens、要求简洁输出直接按比例下降
本地化敏感或高频任务用自托管小模型边际成本接近电费
python# 语义缓存:命中近似问题就复用答案,是客服/知识库场景的最大成本杠杆
import hashlib, numpy as np

class SemanticCache:
    def __init__(self, embedder, threshold=0.95):
        self.embedder, self.threshold = embedder, threshold
        self.keys, self.vecs, self.vals = [], [], []

    def get(self, query: str):
        v = self.embedder.encode(query)
        if not self.vecs: return None
        sims = np.dot(np.stack(self.vecs), v) / (
            np.linalg.norm(np.stack(self.vecs), axis=1) * np.linalg.norm(v) + 1e-8)
        i = int(np.argmax(sims))
        return self.vals[i] if sims[i] >= self.threshold else None

    def put(self, query: str, answer: str):
        self.keys.append(hashlib.md5(query.encode()).hexdigest())
        self.vecs.append(self.embedder.encode(query))
        self.vals.append(answer)

# 注意:高风险场景(医疗/金融/合规)阈值要很高或不使用语义缓存,
#       因为“听起来相似”的问题可能答案完全不同。
python# 成本路由:用「难度预估」决定走哪个模型
def route(complexity):
    if complexity < 0.3:            # 简单:分类、抽取、改写
        return "small", 0.15        # 元/千 token(示意单价)
    if complexity < 0.7:            # 中等:常规问答、RAG 生成
        return "mid", 1.0
    return "large", 4.0             # 难:多跳推理、代码、长链 Agent

# 流量分布:60% 简单 / 30% 中等 / 10% 困难
mix = 0.6*0.15 + 0.3*1.0 + 0.1*4.0
print(round(mix, 3))       # 0.79 元/千 token
print(round(4.0/mix, 2))   # 全部走大模型是它的 5.06 倍
# 路由的代价:少量难题被误判到小模型会掉点,需按任务设阈值 + 监控回退率。
✔
一个实用的成本核算模板:对每条链路记录四个数:输入 token、输出 token、模型单价、命中缓存比例。据此算出「每千次请求成本」,再做优化前后对比。绝大多数团队在第一次做这个统计时,都会发现成本集中在少数几个超长上下文的调用上——优化那几个点就够了。

10.2 动手练习与自测

★
动手练习(先做再看答案):成本优化的第一步是测出来,不是猜;先记录,再优化。
  1. ① 复算 10.1 的混合路由成本:60%/30%/10% 分别走 0.15/1.0/4.0 元。
  2. ② 六个成本杠杆里,最先该做哪两个?为什么?
  3. ③ 成本核算模板要记哪四个数?
  4. ④ 路由的最大风险是什么,怎么监控?
  5. ⑤ 为什么成本通常集中在少数几个调用上?
题号考点关键判据 / 参考答案
1路由成本≈0.79 元/千 token;全走大模型约是其 5.06 倍
2优先级缓存与上下文压缩 —— 收益大、风险低、改动小
3核算模板输入 token、输出 token、模型单价、命中缓存比例
4路由风险难题误判到小模型掉点;监控回退率与分任务质量
5成本分布超长上下文调用单价 × 长度,吃掉绝大多数 token 预算

项目里程碑

贯穿项目 · Hamauls Orion
M13 混合检索 RAG 管线 第 72–78 周

把 M3 的索引、M6 的重排、M11 的多模态能力组装成完整 RAG:混合检索(向量 + BM25)+ 交叉编码器重排 + 上下文压缩 + 引用溯源 + 多轮改写。这是 Hamauls Orion 的主干功能。

本阶段产出(直接进入项目仓库)
验收标准:在自建评测集上端到端准确率显著优于朴素 RAG;有完整的消融表能说清每个组件的边际贡献;引用可核验率 ≥ 95%。

阶段练习项目

PROJECT 1
混合检索 RAG 管线(M13)
里程碑交付物:完整实现企业知识库问答——文档解析 → 语义切分(父子文档)→ 嵌入(bge-m3,归一化)→ HNSW 索引 → 混合检索(向量 + BM25 + RRF 融合)→ cross-encoder 重排 → 生成(带引用与拒答)。要求有检索侧(Recall@k / Hit Rate)与生成侧(Faithfulness / Answer Relevancy)分开的指标,以及 100 条真实问题的评测集。
要达成的效果
  • 端到端落地七环节 RAG 管线,100 条真实问题评测集上跑通并产出检索侧(Recall@5 / Hit Rate)与生成侧(Faithfulness / Answer Relevancy)分开的指标
  • 检索侧 Recall@5 ≥ 0.8(红线),生成侧 Faithfulness ≥ 0.8,二者分开报告
  • 混合检索(向量 + BM25 + RRF)+ 重排相对「仅向量」在 Recall@5 / MRR 上有可量化提升(示例报告×或+个百分点)
功能需求
  • 按七环节落地:解析 → 父子切分 → 嵌入(bge-m3,L2 归一化)→ HNSW 索引 → 混合检索 → cross-encoder 重排 → 生成
  • 混合检索用 RRF 融合 k=60,向量 / BM25 两路分别算 recall,证明融合增益
  • 每条论断带引用 chunk id、资料不足时拒答,均落入评测集统计
  • 落 100 条真实问题评测集,检索侧与生成侧分开评估并做逐组件消融表
  • 生成引用输出走严格解码 + Pydantic 校验,结构合法率逼近 100%
交付物
  • rag_pipeline.py:完整七环节 RAG 管线(可一键跑评测)
  • eval_set.jsonl:100 条真实问题评测集(含标注 chunk 引用)
  • M13_report.md:检索 / 生成分开指标 + 消融表 + 引用溯源样例
边界 · 不做

不训自定义嵌入 / 精调生成模型,只用现成模型(bge-m3、cross-encoder、通用 LLM)。

PROJECT 2
查询理解与进阶 RAG 实验
在 RAG 管线上叠加查询改写、多查询扩展、HyDE、查询分解与路由;对比开启前后在短 query 与多跳问题上的召回率与答案质量,写一份取舍报告。
要达成的效果
  • 在短 query 与多跳问题两组评测集上,逐模块(改写 / 多查询 / HyDE / 分解 / 路由)报告开启前后 Recall@k 与答案质量的净变化
  • 对每个模块能给出「开启的净增益 / 何时该停用」的结论,例如 HyDE 只在短 query 提升 recall、长 query 收益为负
  • 核算多查询 / 分解带来的延迟代价(示例 0.4 + 0.6n)并判断值不值
功能需求
  • 在固定基线 RAG 上逐个叠加模块,每次只变一个模块做消融
  • 短 query 与多跳问题分开评测,分别报告召回与答案质量
  • HyDE 需识别带偏场景(假设答案可能带偏)并在消融里确认净增益
  • 查询分解记延迟 0.4 + 0.6n,对比静态 vs Agentic 的准确率与成本
  • 路由分类依据显式可解释,不拍脑袋
交付物
  • query_experiments.py:各模块开关可配的实验脚本
  • query_tradeoff.md:逐模块消融表 + 延迟账 + 何时该用 / 停用的结论
边界 · 不做

不重写整条 RAG 管线,只在其上叠加查询理解模块做消融;不引入 GraphRAG。(GraphRAG 走专属卡片)

PROJECT 3
GraphRAG / RAPTOR 对比实验
在同一语料上分别实现基础 RAG、GraphRAG(实体图 + 社区摘要)与 RAPTOR(层级摘要树),用「跨实体关联类」和「全局总结类」两类测试集对比效果与构建成本。
要达成的效果
  • 在同一语料上实现基础 RAG、GraphRAG、RAPTOR 三套,两类测试集(跨实体关联 / 全局总结)上都报告 Recall@k 与答案质量
  • 量化 GraphRAG 的构建 token 放大(示例 5–10 倍)与 RAPTOR 的相对成本,写清构建成本对比
  • 给出「何时该用 GraphRAG / RAPTOR / 基础 RAG」的选型结论,判断依据来自评测集
功能需求
  • 基础 RAG 作为基线,GraphRAG 走实体-关系抽取 → Leiden 社区 → 摘要,RAPTOR 走递归聚类摘要树
  • 跨实体关联类与全局总结类两套测试集分开评测,不得混成一块
  • 记录每套的构建成本(token 量 / 时间)与查询成本,对比放大比例
  • 选型结论落到具体查询类型(局部定位用基础 RAG、全局总结用 GraphRAG、长文档概览用 RAPTOR)
交付物
  • advanced_rag.py:三套方法的构建与评测脚本
  • advanced_tradeoff.md:效果 + 构建成本对比表 + 选型结论
边界 · 不做

不引入超大规模真实语料,用小而代表性语料验证差异;不训自定义图谱抽取模型。

PROJECT 4
分层记忆助手
实现工作 / 会话 / 长期三层记忆,含重要性筛选、冲突消解、多因子检索与滚动摘要,验证跨会话的信息保持能力。
要达成的效果
  • 三层记忆(工作 / 会话 / 长期)都落地且有明确迁移规则,跨会话信息能检索回来供后续对话使用
  • 做一个「跨会话信息保持」验证:在会话 A 写入关键偏好,新会话能靠长期记忆检索并正确引用,保持率 ≥80%
  • 记忆可被用户查看 / 修正 / 删除(三次操作跑通),删除后检索结果同步消失
功能需求
  • 写入前做重要性筛选 + 去重与冲突消解(新覆盖旧或标注冲突)
  • 检索用相关度 + 重要度 + 新近度多因子打分,top-k 注入生成上下文
  • 压缩走滚动摘要(示例压到 8–15x)+ 结论结构化提取,且做指代消解
  • 加遗忘机制:半衰期衰减(示例 7 天)与低价值删除
  • 暴露查看 / 修正 / 删除接口并记录审计,满足 GDPR 可解释可纠正要求
交付物
  • memory_assistant.py:三层记忆实现 + 检索 / 压缩 / 遗忘 + 可纠正接口
  • memory_report.md:信息保持率、压缩比、遗忘行为、三次纠正操作验证
边界 · 不做

不做多用户权限 / 分布式记忆;检索复用现成向量检索,不做复杂知识图谱。

PROJECT 5
带审批门禁与约束解码的工具框架
实现一个多步任务 Agent:状态机 + 检查点 + 可重试/不可重试区分 + 高风险动作人工审批;工具调用用 JSON Schema 约束解码 + 解析失败降级金字塔;做一次「中断后续跑」与「工具参数幻觉」的验证。
要达成的效果
  • 多步任务能稳定跑完,且中断后可从检查点续跑(不重头开始)
  • 高风险动作经人工审批门禁(挂起 → 批准 / 拒绝 → 继续),不经审批不自动执行
  • 两种故障演示跑通:工具参数幻觉(约束解码压掉不存在的字段 / enum)与中断后续跑,均不崩管线
功能需求
  • 状态机驱动步骤(idle → planning → executing → waiting_approval → done)+ 每步落检查点
  • 失败分类四类:可重试(退避 3–5 次)/ 可修复换策略 / 不可重试立即失败 / 人工门禁挂起
  • 工具调用用 JSON Schema 签名 + 约束解码,tool_calls 带独立 id、tool 结果用 role=tool + tool_call_id 回填
  • 解析失败走降级金字塔:修复重试 → 宽松抽取 → low_confidence 标记 → 转人工
  • 全链路 tracing:每次调用的输入摘要 / 耗时 / token / 状态转移 / 失败原因可查
交付物
  • tool_framework.py:状态机 + 检查点 + 审批闸门 + 约束解码 + 降级链 + tracing
  • tool_demo.md:参数幻觉治理效果、中断续跑、审批门禁三类验证演示
边界 · 不做

不接入真实支付 / 外部系统高风险动作,审批以 mock 验证;不做分布式任务编排。

常见误区

面试高频问题速答

上下文工程和提示词工程的区别是什么?

提示词工程关注「这一句话怎么写」(措辞、示例、格式),是上下文工程的子集。上下文工程关注「这一轮模型看到的全部内容」:系统提示、工具定义、检索到的文档、记忆、对话历史、以及 Token 预算在这些部分之间如何分配。2026 年的重心已从前者转到后者,因为限制系统表现的往往是「关键信息有没有被正确注入」而不是「措辞是否巧妙」。此外上下文工程要解决污染、干扰、冲突三类问题——塞得太多会稀释注意力。

RAG 效果不好,你怎么定位是检索问题还是生成问题?

分开评测。先看检索指标:目标片段是否被召回(Recall@k / Hit Rate)、排名是否靠前(MRR)、重排是否改善了排序。若检索没召回,是切分 / 嵌入 / 检索策略的问题;若召回了但答案错,则是生成阶段的问题(未遵循「基于资料回答」、片段被截断、多片段冲突未处理)。此外要区分 Faithfulness(是否臆造)与 Answer Relevancy(是否切题),修法完全不同。

为什么要做混合检索和 RRF 融合?

向量检索擅长语义相近,但对精确匹配弱:订单号、型号、人名这类「必须逐字匹配」的信息,向量模型可能映射到语义相近但不正确的片段。BM25 恰好相反。两者融合常用 RRF(Reciprocal Rank Fusion,k 常取 60)按排名融合,只依赖排名不依赖分数,天然解决两种检索器分数量纲不可比的问题,实践中提升明显。

长上下文模型普及后还需要 RAG 吗?

看四维对比:成本(RAG 只付相关片段,长上下文付全部,差 10–100 倍)、延迟(长上下文 prefill 慢)、精度长尾(长上下文全可见更全)、可更新性(RAG 改文档即可,长上下文要重传重索引)。结论:文档海量、更新频繁、成本敏感仍以 RAG 为主;文档小且稳定、需全局鸟瞰、或零容忍检索失败时可不用 RAG。生产常见组合是「RAG 为主 + 长上下文兜底」。

Function calling 落地有哪些坑?

三个高频坑:① 参数幻觉——模型编造不存在的字段或 enum 值,用 required + enum + 约束解码压住;② 漏回传 tool_calls——多轮对话必须把带 tool_calls 的 assistant 消息原样保留,否则模型丢失意图;③ 并行调用结果错位——每个 tool_call 必须带独立 id,tool 消息用 tool_call_id 一一对应,否则答案张冠李戴。

约束解码能解决结构化输出的所有问题吗?

不能。约束解码(grammar / FSM / JSON 模式)只保证结构合法——字段不缺、类型不错、没有多余键。它不保证语义正确,比如金额算错、日期越界、单位混淆。所以两层要分开:结构用约束解码保,语义用业务规则校验保。解析失败时还要有降级金字塔:重试 → 修复 → 宽松提取 → 转人工。

怎么控制大模型应用的线上成本?

六个杠杆:① 模型路由(简单任务用小模型);② 缓存(精确 + 语义 + Prompt 前缀缓存,重复场景可降 90%+);③ 上下文压缩(检索后摘要、历史滚动摘要、精简工具定义);④ 批处理离线任务;⑤ 限制输出长度;⑥ 高频或敏感任务本地化。前提是先做成本核算:按链路统计输入 / 输出 token 与命中率,找出成本集中的少数调用再优化——通常 20% 的调用占用 80% 的成本。

学习资源

Anthropic · Effective Context Engineering for AI Agents长文 www.anthropic.com/engineering/effective-context-engineering-for-ai-agents 上下文工程的权威阐述,包含记忆与压缩的实践建议。 LangChain · Context Engineering 指南长文 blog.langchain.com/the-rise-of-context-engineering/ 把上下文工程拆成可操作要素,与框架实践结合。 RAG 原始论文论文 arxiv.org/abs/2005.11401 检索增强生成的奠基论文,理解「检索 + 生成」的初衷。 ColBERT v2论文 arxiv.org/abs/2112.01488 晚期交互检索代表作,理解 token 级 MaxSim 与可预计算索引。 GraphRAG论文 arxiv.org/abs/2404.16130 实体图 + 社区摘要,解决全局总结与跨实体关联问题。 RAPTOR论文 arxiv.org/abs/2401.18059 层级摘要树检索,从细节到概览的连续光谱。 LlamaIndex 文档文档 docs.llamaindex.ai/ RAG 与 Agentic RAG 的实现参考,含大量高级检索模式。 Qdrant / pgvector 文档GitHub github.com/qdrant/qdrant 向量数据库实践:HNSW、混合检索、过滤、量化与性能调优。 Ragas(RAG 评估框架)GitHub github.com/explodinggradients/ragas 忠实性、答案相关性、上下文精度的自动化评估,RAG 项目必备。 Anthropic · Prompt Caching文档 docs.anthropic.com/en/docs/build-with-claude/prompt-caching 前缀缓存的命中条件与收益,线上成本优化关键。 Mem0(生产级记忆层)GitHub github.com/mem0ai/mem0 记忆的写入筛选、检索与遗忘的工程实现参考。 LiteLLM(统一模型网关)GitHub github.com/BerriAI/litellm 一套接口调多家模型,便于路由、降级与成本统计。
★
2026 形势提示:招聘数据给出的信号非常明确:RAG 岗位需求增长三倍以上,Agentic AI 增长超过万倍,Context Engineering 从 9 个岗位涨到 703 个。同时调研显示,89% 的团队做了可观测,但只有 52% 建了 Evals——这 37 个百分点的差距就是你的机会。把人机协作与评估做扎实的人,比只会调 API 的人贵得多。本阶段的两个里程碑(M12 仿真决策 Agent、M13 混合检索 RAG 管线)正是把这套能力落到可验证交付物上的关键。