Appearance
第2章 从一次提问读懂 RAG 全链路
"A retrieval system is not a function call. It is a pipeline of decisions."
本章要点
- 一次 RAG 请求可以拆成八个阶段:接收问题、构造查询、权限上下文、多路召回、重排序、证据组织、上下文打包、生成与校验
- 每个阶段都要保留结构化中间结果,否则系统无法调试、评估和复盘
- 召回链路追求覆盖,排序链路追求精度,上下文链路追求可用证据密度
- RAG 的在线链路必须和离线索引链路通过稳定的数据契约连接
- 生产系统的关键不是"最后回答了什么",而是"答案如何由证据一步步推导出来"
2.1 从一个问题开始
本章用一个具体问题贯穿整条链路:
"新版企业版套餐里,私有化部署是否包含 SSO?如果客户已经买了旧版专业版,升级时怎么收费?"
这不是一个适合直接丢给大模型的问题。它包含多个隐含条件:
- "新版企业版套餐" 指的是哪个版本的定价文档。
- "私有化部署" 可能在产品规格、交付手册、合同模板里都有描述。
- "SSO" 可能写作 SAML、OIDC、单点登录、企业身份源。
- "旧版专业版" 涉及历史套餐映射。
- "升级时怎么收费" 可能出现在销售 FAQ、价格表、商务审批规则里。
如果你只做一次向量搜索,系统可能召回一份包含 "SSO" 的登录文档,却漏掉套餐价格表;也可能召回旧版专业版 FAQ,却没有看到新版企业版规格;还可能把"私有化部署支持 LDAP"误读成"包含 SSO"。
这个问题需要的不是一个搜索动作,而是一条证据流水线。
这张图里有两个容易被低估的角色。
第一个是 查询理解。用户的问题往往不是检索系统想要的形态。检索系统需要实体、同义词、时间范围、任务类型和候选子问题。直接用原问题做 embedding,等于把所有查询理解压力都交给向量模型。
第二个是 观测与评估。RAG 系统必须保存每一步的中间结果:改写后的查询是什么、召回了哪些候选、哪些被权限过滤、reranker 为什么把某段排第一、最终 prompt 里到底包含哪些证据。没有这条日志,线上答错时你只能猜。
2.2 阶段一:接收问题,不要急着检索
在线链路的入口通常收到的不只是用户输入的一句话,还包括会话、用户、租户、产品上下文和调用场景。
json
{
"question": "新版企业版套餐里,私有化部署是否包含 SSO?如果客户已经买了旧版专业版,升级时怎么收费?",
"user": {
"id": "u_123",
"tenant": "acme",
"roles": ["sales", "enterprise"]
},
"conversation": {
"session_id": "s_456",
"previous_topic": "2026 pricing migration"
},
"runtime": {
"locale": "zh-CN",
"now": "2026-04-24"
}
}这些字段会影响后续决策:
roles决定能不能看内部销售折扣规则。tenant决定能不能检索某个客户的合同条款。previous_topic帮助解析"新版"和"旧版"。now影响时间过滤,避免旧政策压过新政策。
一个常见错误,是把 RAG 入口设计成 retrieve(question: string)。这个接口太窄,导致后续系统只能从字符串里猜上下文。生产级接口至少应该接收:
| 字段 | 用途 |
|---|---|
| 原始问题 | 保留用户真实表达,用于回答和审计 |
| 会话摘要 | 处理追问、省略和指代 |
| 用户身份 | 权限过滤和个性化 |
| 租户/空间 | 多租户隔离 |
| 当前时间 | 时间敏感知识排序 |
| 任务类型 | 问答、总结、对比、排障、代码定位 |
| 期望输出 | 是否需要引用、表格、步骤、JSON |
RAG 的第一步不是 embedding,而是把一次请求的上下文边界收清楚。
2.3 阶段二:查询理解,把一句话拆成可检索任务
用户的问题通常混合了多个子问题。本章的例子至少包含两个:
- 新版企业版套餐的私有化部署是否包含 SSO。
- 旧版专业版客户升级到新版企业版如何收费。
如果把这两个问题混成一个向量,召回结果会被平均化。更好的做法是把它们拆成结构化查询:
json
{
"intent": "policy_lookup",
"entities": {
"new_plan": "企业版",
"old_plan": "专业版",
"deployment": "私有化部署",
"feature": ["SSO", "SAML", "OIDC", "单点登录"],
"topic": "升级收费"
},
"sub_queries": [
{
"id": "q1",
"query": "新版 企业版 私有化部署 SSO SAML OIDC 单点登录 是否包含",
"expected_doc_types": ["pricing", "product_spec", "enterprise_delivery"]
},
{
"id": "q2",
"query": "旧版 专业版 升级 新版 企业版 收费 迁移 价格 规则",
"expected_doc_types": ["pricing", "sales_faq", "migration_policy"]
}
],
"time_preference": "latest",
"answer_style": "with_citations"
}这里的查询理解可以由规则、轻量模型或大模型完成。不要一开始就默认必须用大模型。很多场景里,规则足够稳定:
- 如果用户输入包含错误码,保留原样并走关键词召回。
- 如果输入包含代码符号,保留大小写和命名边界。
- 如果问题包含"最新、现在、当前",添加时间优先信号。
- 如果问题包含"对比、区别、差异",拆成多个对象分别检索。
大模型适合处理更复杂的语义改写,比如省略、指代、多轮追问和跨语言同义词。但大模型改写也要有约束:它可以扩展同义词,不能凭空添加实体;它可以拆子问题,不能改变用户意图。
查询理解阶段的输出应该被记录下来。很多 RAG 错误不是召回器的问题,而是查询改写把问题带偏了。比如把"旧版专业版"改写成"专业服务版",后续所有召回都会错。
2.4 阶段三:权限上下文,先设边界再找证据
权限是 RAG 和普通搜索最大的区别之一。
普通搜索可以把所有候选都找出来再展示;RAG 不能这样做。因为候选一旦进入大模型上下文,就已经被模型读取。即使最终答案不引用,也可能在生成中泄露。
权限过滤有三种常见位置:
检索前过滤 的优点是安全边界清晰、候选规模小;缺点是过滤条件必须能被索引支持。如果你用一个共享向量索引,却没有把 tenant_id、visibility、doc_type 做成可过滤字段,就只能在召回后过滤,容易出现 top-k 被过滤光的问题。
检索后过滤 的优点是灵活,可以调用权限服务做复杂判断;缺点是浪费召回名额。假设 top-20 里有 18 条用户无权访问,过滤后只剩 2 条,正确证据可能排在原始第 50 名,根本没有被取出。
生成后校验 只能作为最后防线,不能作为主要权限机制。它可以检查答案引用是否都属于可见文档,但不能保证模型没有吸收过无权上下文。
生产系统通常三者都要有:
| 阶段 | 负责内容 |
|---|---|
| 检索前 | 租户、知识库、空间、密级这类粗粒度过滤 |
| 检索后 | 文档 ACL、用户组、时间窗口这类精确过滤 |
| 生成后 | 引用完整性、答案是否包含无来源敏感内容 |
权限不是第 7 章才出现的问题。从在线链路第一个请求对象开始,权限上下文就必须存在。
2.5 阶段四:多路召回,不要让一种相似度决定一切
本章问题里同时有语义概念和精确词:
- "私有化部署" 是语义概念,可能写成 on-prem、私有部署、专有化交付。
- "SSO" 是缩写,可能写成 SAML、OIDC、单点登录。
- "旧版专业版" 是产品版本名,精确匹配很重要。
- "升级收费" 可能出现在价格表、迁移政策、销售 FAQ。
单一路线很难覆盖这些情况。向量召回擅长语义相似,但可能错过精确符号;BM25 擅长关键词匹配,但不理解同义表达;结构化过滤能保证文档类型和时间范围,但不能判断语义相关性。
更可靠的召回通常是多路的:
候选合并不是简单拼列表。你需要处理:
- 同一个 chunk 被多路召回,如何合并分数。
- 同一文档多个相邻 chunk 命中,是否提升整段权重。
- BM25 分数和向量相似度量纲不同,如何归一化。
- 某一路召回为空,是否触发降级策略。
- 候选数量过多,如何控制 rerank 成本。
一个实用做法是先保留每路召回的独立分数,再在后续排序阶段统一处理:
json
{
"chunk_id": "c_789",
"doc_id": "pricing_2026_enterprise",
"text": "企业版私有化部署包含 SAML/OIDC 单点登录...",
"scores": {
"dense": 0.83,
"bm25": 12.4,
"metadata_boost": 1.2
},
"matched_queries": ["q1"],
"metadata": {
"doc_type": "pricing",
"version": "2026.04",
"effective_date": "2026-04-01"
}
}不要过早把这些分数压成一个数字。调试时你需要知道某个候选为什么出现:是语义相似、关键词命中、时间加权,还是图谱关系带出来的。
2.6 阶段五:重排序,把候选变成证据
召回阶段的目标是"别漏",重排序阶段的目标是"别乱"。
多路召回后,候选列表可能有几十到几百个 chunk。它们大多和问题有一点关系,但不是都能作为答案证据。重排序要判断的是:这个 chunk 对当前问题是否有直接回答价值。
对本章问题,候选可以分成四类:
| 候选类型 | 例子 | 排序倾向 |
|---|---|---|
| 直接证据 | "企业版私有化部署包含 SAML/OIDC SSO" | 应排前 |
| 条件证据 | "旧版专业版升级按剩余合同金额折抵" | 应排前 |
| 背景材料 | "SSO 配置步骤" | 可保留但不优先 |
| 噪声材料 | "登录页支持企业 Logo" | 应排后 |
重排序可以用 cross-encoder reranker、LLM 打分、规则加权,或组合方案。无论用哪种,都要注意一个边界:reranker 评估的是"query 与 passage 的相关性",不等于"passage 的事实优先级"。
比如旧版文档和新版文档都高度相关,但新版应该优先。这需要 metadata 参与排序:
一个成熟排序器通常不是单一模型,而是一组信号:
- 语义相关性:候选是否回答当前子问题。
- 关键词覆盖:关键实体是否出现。
- 文档权威性:正式协议高于评论、草稿、历史讨论。
- 时间有效性:最新版本高于旧版本,但历史问题例外。
- 结构位置:标题、表格、FAQ 答案位置通常比正文旁支更重要。
- 子问题覆盖:一个候选覆盖 q1,另一个覆盖 q2,不能只取 q1 的前几条。
排序输出也应该保留解释字段。哪怕只是内部日志,也能帮助排查:
json
{
"chunk_id": "c_789",
"rank": 1,
"rerank_score": 0.91,
"reasons": ["direct_answer", "latest_version", "official_pricing_doc"],
"covers": ["q1"]
}这类结构化信息后续还会用于上下文打包:覆盖不同子问题的证据都要进入 prompt,而不是只按全局分数截断。
2.7 阶段六:证据组织,chunk 不是最终单位
RAG 系统离线索引时通常以 chunk 为单位,但在线回答时不应该机械地以 chunk 为单位消费证据。
原因很简单:chunk 是索引单位,不一定是理解单位。
一个价格表可能被切成多个 chunk:
- chunk A:企业版功能列表。
- chunk B:私有化部署说明。
- chunk C:SSO 说明。
- chunk D:升级计费规则。
如果检索返回 B、C、D,直接按三个片段拼 prompt,模型可能不知道它们都属于同一份 2026 价格政策,也可能看不到表格列头。证据组织阶段要把 chunk 还原成更适合阅读的结构。
常见操作包括:
- 相邻合并:同一文档连续 chunk 命中时合并,保留标题层级。
- 窗口扩展:命中 chunk 前后各取一段,补齐上下文。
- 表格还原:把单元格与表头、行头一起提供。
- 子问题分组:把 q1 证据和 q2 证据分开,避免模型混淆。
- 冲突标注:旧版和新版材料同时出现时标明版本和生效时间。
组织后的证据包可以长这样:
json
{
"evidence_groups": [
{
"sub_query": "q1",
"question": "企业版私有化部署是否包含 SSO",
"evidence": [
{
"source": "2026 企业版价格与功能表",
"doc_id": "pricing_2026_enterprise",
"version": "2026.04",
"quote": "企业版私有化部署包含 SAML/OIDC 单点登录...",
"location": "功能矩阵 / 身份认证"
}
]
},
{
"sub_query": "q2",
"question": "旧版专业版升级如何收费",
"evidence": [
{
"source": "旧套餐迁移 FAQ",
"doc_id": "migration_faq_2026",
"version": "2026.04",
"quote": "旧版专业版客户升级企业版时,按剩余合同金额折抵...",
"location": "FAQ / 计费规则"
}
]
}
]
}这一步把"候选文本"变成"可用证据"。没有它,模型拿到的只是碎片。
2.8 阶段七:上下文打包,在 token 预算内保留证据密度
上下文打包是 RAG 系统最容易被低估的环节。
很多实现直接把 top-k chunk 用分隔符拼起来:
text
参考资料:
1. ...
2. ...
3. ...
请回答用户问题。这对简单问答可以工作,但复杂问题会出问题。一个好的上下文包至少要包含:
- 原始问题和改写后的子问题。
- 每组证据对应哪个子问题。
- 每段证据的来源、版本、时间和位置。
- 冲突材料的优先级说明。
- 明确的回答约束:只基于证据,不足则说明不足。
更结构化的 prompt 片段可能是:
text
用户问题:
新版企业版套餐里,私有化部署是否包含 SSO?如果客户已经买了旧版专业版,升级时怎么收费?
请分别回答两个子问题,并为每个结论标注引用编号。
证据 A(回答子问题 q1)
来源:2026 企业版价格与功能表,版本 2026.04,位置:功能矩阵 / 身份认证
内容:企业版私有化部署包含 SAML/OIDC 单点登录;LDAP 同步作为可选交付项。
证据 B(回答子问题 q2)
来源:旧套餐迁移 FAQ,版本 2026.04,位置:FAQ / 计费规则
内容:旧版专业版客户升级企业版时,按剩余合同金额折抵;新增模块按企业版当前报价补差。
约束:
- 不要使用证据外的价格或折扣信息。
- 如果结论依赖销售审批规则,请说明材料不足。上下文打包的关键指标不是 token 数越少越好,而是 单位 token 的证据密度。低价值材料会挤占高价值材料,让模型在长上下文里迷路。
第 16 章会专门讨论上下文打包策略,包括最大边际相关性、证据分组、摘要压缩、引用保真和长上下文模型下的排序衰减。本章只需要建立一个原则:检索结果不能原样塞给模型,必须被组织成模型可执行的证据包。
2.9 阶段八:生成、引用与校验
到了生成阶段,大模型的任务不是自由发挥,而是基于证据完成受约束的综合。
对本章问题,一个理想答案应该类似:
新版企业版的私有化部署包含 SSO,具体支持 SAML/OIDC 单点登录;LDAP 同步是可选交付项,不应混同为默认包含。旧版专业版客户升级企业版时,通常按剩余合同金额折抵,再对新增模块按企业版当前报价补差。现有材料没有给出特殊折扣或审批例外,如果涉及大客户折扣,需要查询销售审批规则。
这个答案有三个特点:
- 分别回答了两个子问题。
- 把默认包含和可选交付区分开。
- 对证据不足的部分没有编造。
生成后还需要校验。最基本的校验包括:
- 答案中的关键结论是否都有引用。
- 引用是否来自本次上下文中的证据。
- 引用文档是否对用户可见。
- 答案是否引入证据外的价格、日期、人名、承诺。
- 多个证据冲突时是否按版本、权威性处理。
校验可以由规则、模型或人工介入完成。高风险系统里,校验失败不应该把答案直接返回给用户,而应该降级:
引用不是为了好看,而是为了让答案进入可审计状态。没有引用的 RAG,很难和普通聊天区分开。
2.10 日志与反馈:让每次回答变成训练样本
RAG 请求结束后,系统应该保存完整链路日志。最少包括:
| 类别 | 字段 |
|---|---|
| 请求 | 原始问题、用户、租户、时间、会话 ID |
| 查询理解 | 子问题、改写查询、实体、任务类型 |
| 召回 | 每路召回结果、分数、耗时 |
| 权限 | 过滤前后数量、过滤原因 |
| 排序 | rerank 分数、最终排名、覆盖的子问题 |
| 上下文 | 进入 prompt 的证据 ID、token 数、截断原因 |
| 生成 | 模型、参数、输出、引用 |
| 校验 | 是否通过、失败原因、降级策略 |
| 反馈 | 用户点赞/点踩、追问、人工标注 |
这些日志有三个用途。
第一,调试单次错误。用户投诉某个答案错了,你可以回放链路,判断是找不到、找错、塞不下还是答不准。
第二,构建评估集。被点踩的问题、人工修正的问题、频繁追问的问题,都是高价值评估样本。RAG 的评估集不应该凭空编,而应该来自真实流量。
第三,驱动知识治理。如果很多问题都找不到答案,可能不是检索差,而是知识库没有对应文档;如果某份旧文档经常被召回导致错误,就应该下线或标记过期;如果某类问题总是需要人工补充,说明文档结构需要改。
这也是 RAG 与传统 FAQ 的区别:RAG 系统不是写完就完,它应该在使用中暴露知识库的缺口。
2.11 在线链路与离线链路的数据契约
本章一直在讲在线请求,但在线链路能做什么,取决于离线索引时保存了什么。
如果离线阶段只存了 text 和 embedding,在线阶段就无法按版本排序、无法显示页码、无法过滤权限、无法合并相邻 chunk、无法判断文档类型。很多 RAG 系统后期难以演进,根因是最初的索引 schema 太贫瘠。
一个更合理的 chunk schema 至少包含:
json
{
"chunk_id": "c_789",
"doc_id": "pricing_2026_enterprise",
"text": "企业版私有化部署包含 SAML/OIDC 单点登录...",
"embedding": [0.012, -0.084],
"metadata": {
"tenant_id": "global",
"space_id": "sales_enablement",
"doc_type": "pricing",
"title": "2026 企业版价格与功能表",
"section_path": ["功能矩阵", "身份认证"],
"version": "2026.04",
"effective_date": "2026-04-01",
"source_url": "https://...",
"acl": ["role:sales", "role:enterprise"],
"prev_chunk_id": "c_788",
"next_chunk_id": "c_790"
}
}这里每个字段都服务在线链路:
doc_type用于文档权威性排序。section_path用于上下文组织和引用展示。version、effective_date用于新旧政策判断。acl用于权限过滤。prev_chunk_id、next_chunk_id用于窗口扩展和相邻合并。source_url用于答案引用。
离线和在线之间的契约一旦设计好,后续系统才有迭代空间。否则每加一个功能都要重建索引,甚至重新解析全部文档。
2.12 各阶段的降级与 fallback:链路必须有退路
前 11 节描述了 RAG 的正常流水线。真实生产里、每一个阶段都可能在任何一刻失败——Embedding 服务超时、向量库连接池耗尽、rerank GPU OOM、LLM 返回 429。一个没有 fallback 的 RAG 链路等于脆弱的串联系统:一环断、整条崩。成熟的 RAG 设计里、每个阶段都有降级路径、确保用户拿到可用的答案而非 5xx 错误。
降级的基本原则
三条原则:
- 部分可用优于全部不可用:能返回"不完美但有用"的答案、就不要给 500
- 降级要可观测:每次降级记一次、不要静默
- 降级不能悄悄跨越安全边界:权限过滤绝不能因为"降级"被跳过
各阶段的具体降级
查询理解失败:LLM 改写超时、或解析出错。降级:直接用原 query 进入下游。代价是 recall 可能略降、但不阻塞链路。
权限过滤失败:权限服务不可达。绝不降级——直接 5xx 给用户"暂时无法服务"。权限是安全底线、不能因故障放水。
多路召回失败:
- Dense 召回超时 → 只用 BM25 的结果
- BM25 失败 → 只用 dense
- 两路都失败 → 看是否能用 metadata filter 精确召回兜底、否则走 query cache
- 全部失败 → fallback 到静态"抱歉暂时无法回答"
Rerank 失败:用融合后的 RRF 分数直接排序、跳过 cross-encoder。recall 还在(召回没变)、只是精度降一档、远比"不答"强。
上下文打包 OOM:context 总长度超限。按 rerank 分数降序、截到 token 预算内。最关键的证据留着、末尾弱候选丢掉。
LLM 生成失败:
- 主模型超时 → 重试一次(短 timeout)
- 仍失败 → 切 fallback 模型(Sonnet → Haiku)
- Fallback 也失败 → 返回"基于检索到的证据(不生成自然语言)"——把 top-3 chunk 原文给用户看
- 最坏 → 静态回复"系统繁忙、稍后再试"、带 request_id 供事后查
熔断器(circuit breaker)模式
单次失败能重试、持续失败要熔断——防止雪崩:
- 某服务 1 分钟内错误率 > 50% → 熔断 30 秒、新请求直接走降级
- 30 秒后半开状态、放一小部分流量探活
- 探活成功率 > 80% → 关闭熔断、恢复正常
- 仍失败 → 继续熔断
关键服务(LLM、向量库、Embedding)都要有熔断器。没有熔断、一个慢服务会拖垮整个链路的并发池。
用户可见 vs 内部降级
不是所有降级都要告诉用户:
- 透明降级:LLM fallback 到小模型、答案质量略降——不明示,静默执行(但内部有 log)
- 轻提示:权限过滤后候选太少、只能基于 2 条证据回答——答案加 "本次基于有限资料"
- 明示降级:全链路都挂、走静态回复——明确告诉用户 "系统繁忙"
用户看到太多"降级"标记反而怀疑系统整体稳定性。内部观测一定要全、外部提示要有选择。
Fallback 链的编排
复杂降级需要有序执行。一个典型 LLM 生成的 fallback 链:
python
async def generate_with_fallback(prompt, context):
for attempt in [
(sonnet, timeout=3), # 首选
(sonnet, timeout=8), # 重试更长 timeout
(haiku, timeout=5), # 降模型
(cached_similar_answer, None), # 查 cache 里类似答案
(static_fallback, None), # 最后静态回复
]:
try:
result = await try_generate(attempt, prompt, context)
if result:
return result
except Exception as e:
log_fallback(attempt, e)
raise SystemDegraded("all fallbacks exhausted")每一步都 log、便于事后分析哪级 fallback 被触发最多。
降级率的观测
生产 RAG 看板必备:
fallback_rate_by_stage:每个阶段的降级触发率、理想 < 1%full_pipeline_success_rate:完整正常流程的比例、理想 > 95%user_visible_degradation:用户看到降级标记的比例fallback_answer_quality:降级路径的答案质量(可以用 mini gold set 跑)
任一指标持续超标——说明上游服务稳定性不够、或降级策略本身有问题。降级不是"平时不看、出事再说"——是常态运营的一部分。
常见反模式
- 没有 fallback:一旦失败就 500、用户体验差
- 降级逻辑藏在 try-except:没 log、没指标、事后查不到为什么
- 权限也降级:故障时放水——合规事故
- fallback 链太长:5 级 fallback 每级 3 秒、累计 15 秒才给用户兜底——不如早放弃
- cache 当万能 fallback:命中率低、冷启动场景没数据
降级设计是 RAG 可用性的工程基础——每加一个新阶段就问"这个阶段挂了怎么办"。答案不能是"阻塞等它恢复"。
2.13 并行化与流水线优化:挤出每一毫秒
前面 12 节把 RAG lifecycle 按八个阶段串讲、看起来是一条顺序流水线。事实上多数阶段有并行机会——默认串行跑就是浪费延迟。生产 RAG 的 P50 延迟 2 秒以内基本靠并行化、不靠单点优化。这节梳理每个阶段的并行化空间、以及识别关键路径的方法。
默认串行 vs 并行后的 lifecycle
两者对用户的感受差一倍——不是单点优化能做到的、是整条链路的并发设计。
各阶段的并行机会
查询理解 + 权限上下文:两者都只依赖原始请求、互不依赖——可以并行。Python 里用 asyncio.gather:
python
qu_task = rewrite_query(raw_query, conv_context)
perm_task = resolve_user_context(user_id, tenant_id)
qu_result, perm_result = await asyncio.gather(qu_task, perm_task)300ms + 50ms 串行 = 350ms、并行 = 300ms(用较慢的那个)。
多路召回:dense / BM25 / metadata 三路互不依赖、必须并行。串行会拖到每路延迟相加、并行取最慢那路:
python
dense_task = dense_retrieve(query)
bm25_task = bm25_retrieve(query)
meta_task = metadata_filter(query)
dense, bm25, meta = await asyncio.gather(dense_task, bm25_task, meta_task)召回从 150-200ms 压到 80-100ms。
Rerank + Context pack:rerank 必须在召回完成后、但可以边 rerank 边初步组织证据——当 rerank 打完分、pack 只等最后几毫秒就能完成。这种 pipeline 对长 rerank 特别值。
LLM 流式生成:不等完整答案——第一个 token 出来就开始流、用户感受的是 TTFT、不是总延迟。1500ms 总生成 + 500ms TTFT、用户感觉"快"。
关键路径识别
并行化的核心是找关键路径(critical path)——决定整体延迟的那条链。方法:
- 给每个阶段的 P50/P99 延迟打 trace 字段
- 画 dependency graph
- 用 longest path 算法找关键路径
- 优化关键路径的节点、其他非关键节点可以牺牲一点
典型发现:LLM 生成 + rerank 占总延迟 70-80%——优化这两个最值、其他非关键路径的优化 ROI 低。
推测执行(speculative execution)
进阶并行——某些后续阶段提前启动、失败就废:
- 提前暖 LLM:query 刚进来、context 还没齐、先把 system prompt 送到 LLM 建立 cache、完整 prompt 来时续上——省 100-200ms
- 提前召回:rewrite 还没完、先用原 query 跑一次召回、rewrite 完再补——命中 cache 时白赚
- 并行多模型:同一 prompt 同时发给两个 LLM、取快的——成本翻倍但 p99 降半
推测执行浪费少量资源换延迟——对延迟敏感场景(对话 TTFT < 500ms)值得。
流式的依赖解耦
LLM 流式生成时、后续阶段也能流式启动:
- Grounding 验证:每 token 出来就验证、不等完整答案
- Citation 解析:看到
[doc-就解析、不等完整 - UI 渲染:流式传给前端、边出边显示
这种"pipeline parallelism"让TTFT 成为用户感知的延迟、而不是总延迟。生产 RAG 几乎都要实现流式。
依赖的显式管理
并行化靠显式声明依赖——用 Python asyncio / Rust async 的 task graph、或专门的 workflow 引擎(Prefect / Temporal):
python
graph = Graph()
graph.add_node("qu", rewrite_query, deps=["raw_query"])
graph.add_node("perm", resolve_perm, deps=["user_ctx"])
graph.add_node("dense", dense_retrieve, deps=["qu", "perm"])
graph.add_node("bm25", bm25_retrieve, deps=["qu", "perm"])
graph.add_node("rerank", rerank, deps=["dense", "bm25"])
graph.add_node("generate", llm_gen, deps=["rerank"])
result = await graph.execute()这种显式 graph 让并行机会一目了然、新同学改代码也不会误改成串行。
并行化的反模式
- 盲目 await:每个 await 都串行、没用 gather
- 隐式依赖错误:声明了并行但代码里悄悄依赖了、出奇怪 bug
- 并行但资源不够:一股脑并行 10 个 LLM 调用、rate limit 爆炸
- 忘了 cancel:某并行任务失败、其他任务不取消、浪费算力
并行化是谨慎工程——只在明确独立的阶段并行、加 timeout 和 cancel、永远测延迟分布而非均值。
量化改进的衡量
优化前后对比要看:
- P50 / P99 TTFT:主要指标
- P99 端到端:长尾
- CPU / GPU 峰值使用率:并行多的时候可能资源瓶颈
- LLM token 使用量:推测执行可能多费 token
没有对比衡量、优化可能只是把延迟从 A 阶段挪到 B 阶段、总量不变。
2.14 多轮对话的 lifecycle 变形
前面 13 节讲的都是单轮 RAG——一次请求一个完整答案。真实生产里大量 RAG 嵌在多轮对话里:客服聊天、代码助手、Agent 任务。多轮不是"单轮重复 N 次"——它引入了跨请求状态、指代解析、上下文累积等新问题。单轮 lifecycle 的某些阶段需要调整、还要加新阶段。这节把多轮的 lifecycle 变形讲清楚。
单轮和多轮的结构差异
多轮版增加了四个步骤:加载 session / 指代解析 / 上下文合并 / 更新 session。每个都对检索质量有结构性影响。
Session state 的管理
Session 是多轮的核心——保存跨轮信息:
python
{
"session_id": "sess-abc",
"user_id": "u-123",
"created_at": "2026-04-25T10:00",
"turns": [
{"role": "user", "content": "企业版 SSO 怎么配置", "ts": "10:00"},
{"role": "assistant", "content": "配置步骤是...", "refs": ["doc-1"], "ts": "10:01"},
{"role": "user", "content": "那价格呢", "ts": "10:02"}, # 当前轮
],
"extracted_entities": ["企业版", "SSO"],
"active_topic": "企业版 SSO",
"summary_so_far": "用户在了解企业版 SSO 的配置和定价"
}存储选择:
- Redis:快、适合短会话(< 1 小时)
- Postgres:持久化、适合跨天 session
- Vector DB:如果要对历史对话做语义检索
关键:session 数据和用户数据必须隔离(session 是临时、per-request;用户 profile 是持久)。
指代解析的位置
"那价格呢" 里的 "那" 指什么?必须在 QU 之前解析清楚:
python
def resolve_coreference(current_query, session):
if not has_reference_words(current_query): # 没"那""它""上面说的"
return current_query
# 用 LLM 或规则解析
prompt = f"""
对话历史:{session.turns[-5:]}
当前用户问题:{current_query}
改写成独立问题、不依赖历史。
"""
resolved = llm_rewrite(prompt)
return resolved # "企业版 SSO 的价格"位置放在 QU 之前——QU 才能拿到独立 query 做扩展 / 拆解。放错位置(比如放在检索后)、QU 就已经基于歧义 query 跑偏了。
上下文累积的 compacting
对话越长、session 里累积越多——但不是所有都要喂 LLM。三层 compacting:
- 最近 N 轮原文(通常 3-5 轮):完整保留、保证近期细节
- 中期摘要(N 到 2N 轮):LLM 压缩成 200 token 摘要
- 早期标签(更老的):只保留 "讨论过 SSO""涉及企业版" 这类标签
这让 session 可以跑 100+ 轮仍然不爆 context——近事完整、远事只保关键脉络。
多轮的权限特殊性
权限在多轮更复杂——session 里的旧内容可能在权限变更后成"违禁":
- 第 1 轮用户问"企业版 SSO"、拿到答案 A(时当时有权)
- 用户权限降级(不再有 SSO 访问权)
- 第 3 轮用户问"刚才说的那个怎么配"、session 里还有答案 A
怎么办?
- 严格模式:权限变更后、session 全清。简单但用户体验差
- 回溯过滤:session 每次加载重新过权限、无权内容标记为"[已屏蔽]"
- 透明模式:告诉用户"之前的答案因权限变更不再适用"
金融 / 医疗场景选严格模式、一般场景选回溯过滤。
多轮的缓存差异
单轮 cache key 常是 hash(query)——多轮不够:
- 同 query "那价格呢" 在两个不同 session 下含义完全不同
- Cache key 必须含
session_id或resolved_query
正确做法:cache 最终解析后的 query + 证据、而不是原始输入。
多轮的 fallback 特殊性
单轮 fallback(§2.12)处理当前请求失败。多轮里还要处理:
- 历史污染:早期某轮给了错答案、后续 session 里引用这个错答案
- 级联错误:第 5 轮错、第 6 轮基于第 5 轮的信息继续错、第 7 轮更错
多轮 fallback 策略:
- 错误标记:某轮确认错了、session 里标注、后续不再引用
- session 截断:严重错误时清空 session、重新开始
- Turn-level rollback:让用户撤销最近几轮、从中间点继续
多轮评估的联动
评估单轮看 recall / faithfulness——多轮加看:
- Session coherence:整个会话逻辑连贯吗
- Fact retention:早期说过的事实、后期正确引用吗
- Reference resolution accuracy:指代解析对吗
§20.16 多轮评估专门讲——这里强调:多轮 lifecycle 的每个变形阶段、都对应一个新的失败模式和评估维度。
长 session 的架构挑战
Session 长到 50+ 轮时、架构压力增加:
- Context token 累积:即使 compacting、长 session 的 context 仍比短 session 长 3-5 倍
- Session 状态大小:几 MB 级
- Memory 的联动(ch18):长 session 应沉淀到 long-term memory
架构上:session 是短期(小时-天)、memory 是长期(月-年)。两层衔接的规则要清楚——哪些 session 信息值得升级到 memory。
Agent 场景的 lifecycle
多轮是"用户和系统交替"——Agent 场景是"LLM 内部自主多步":
text
单轮 Agent: user 问 → LLM 决定调 RAG → 答
多步 Agent: user 问
→ LLM 决定第 1 次 RAG (query A)
→ 看结果不够
→ LLM 决定第 2 次 RAG (query B)
→ 看结果够了
→ 综合答每次 RAG 都是一次完整 lifecycle——和多轮对话的 session 叠加、复杂度再升。§18 memory 和 ch19 Agentic RAG 展开这部分。
多轮 lifecycle 的实现复杂度
相比单轮、多轮实现增加:
- Session 存储 + 并发控制(同 session 并发请求要排队)
- 指代解析服务
- Context compacting 的后台 job
- 多轮 eval 的 gold set + 评估框架
典型工程成本:从单轮升到多轮、整个 RAG 系统代码量 +40-60%、运维复杂度翻倍。不是小升级——确认业务真需要多轮再上。
单轮优先、多轮按需
一个经常被忽视的真相:很多场景不需要多轮——
- API 形式的 RAG(开发者调用):每次独立 query
- 搜索框式产品:用户重新输入
- 短 FAQ 场景:一问一答就够
强制上多轮反而:延迟升、成本升、出 bug。先做单轮、看业务是否真需要再上多轮——避免过度工程。
2.15 端到端 trace 的设计:让每次请求都可观测
前面章节多次提到 "trace"——作为 debug / 归因 / audit 的前提。但具体 trace 长什么样、怎么设计、存哪里——需要专门讲清楚。一个完整的 trace 系统让 RAG 从"盲盒"变成"玻璃盒"——每次请求的每个步骤都可追溯。这节给出 RAG trace 设计的实用指南。
Trace 的作用
一份好的 trace 同时满足这六种用途——不是为每个用途单独做记录。
Trace 应覆盖的数据
完整的 RAG 请求 trace:
json
{
"trace_id": "tr-abc123",
"request_id": "req-xyz",
"user_id": "u-456",
"tenant_id": "acme",
"timestamp": "2026-04-25T10:00:00Z",
"total_duration_ms": 1850,
"stages": {
"query_understanding": {
"duration_ms": 280,
"input": "企业版 SSO 怎么配",
"output": {"rewritten": "...", "entities": [...]},
"model": "haiku",
"cost_usd": 0.0002
},
"retrieval": {
"duration_ms": 95,
"paths": {
"dense": {"candidates": 50, "top_scores": [0.89, 0.84, ...]},
"bm25": {"candidates": 50, "top_scores": [...]}
},
"fusion": "RRF",
"final_top_k_ids": ["doc-1-c7", "doc-3-c2", ...]
},
"rerank": {
"duration_ms": 320,
"model": "bge-reranker-v2-m3",
"top_5_after_rerank": [{"id": "doc-1-c7", "score": 0.94}, ...]
},
"generation": {
"duration_ms": 1150,
"model": "sonnet-4.6",
"prompt_tokens": 3200,
"completion_tokens": 520,
"cost_usd": 0.0176,
"prompt_cache_hit": true
}
},
"response": {
"answer": "企业版 SSO 的配置步骤...",
"citations": ["doc-1-c7", "doc-3-c2"],
"confidence": 0.87
},
"user_feedback": null // 稍后异步填充
}关键字段:每阶段的输入输出 + 延迟 + 成本 + 模型版本——少一项都会让 debug 复杂。
Trace ID 的传递
一个请求经过多个服务(gateway → QU → retrieval → rerank → LLM)——trace_id 必须全链路透传:
python
# Gateway 生成
trace_id = generate_trace_id()
request.headers["X-Trace-Id"] = trace_id
# 每个下游服务取出、带入自己的日志
@app.post("/retrieve")
async def retrieve(query, request):
trace_id = request.headers["X-Trace-Id"]
logger.bind(trace_id=trace_id).info("starting retrieval")
# ...
# 调下游时继续传
async with httpx.AsyncClient() as client:
await client.post("/rerank", headers={"X-Trace-Id": trace_id})OpenTelemetry / Jaeger 等标准可以自动做这件事——不要自己发明。
Span 的设计
每个阶段产生一个 span:
python
with tracer.start_span("query_understanding") as span:
span.set_attribute("model", "haiku")
span.set_attribute("input.query", query[:200]) # 截断防太长
result = rewrite_query(query)
span.set_attribute("output.rewritten", result.rewritten[:200])
span.set_attribute("output.entities_count", len(result.entities))注意:
- 不 log 敏感原文:用户 query / 答案可能含 PII、要脱敏或只 log 前几个字
- 量化指标:记数值(token 数、分数、耗时)、不记完整文本
- 加 tag:model 版本、prompt 模板版本、feature flag 状态
Trace 的存储
Trace 数据量大——需要专门存储:
| 存储 | 优点 | 缺点 |
|---|---|---|
| Jaeger / Tempo | 专业 tracing 系统、查询快 | 存储时效短(通常 7-30 天) |
| 对象存储(S3)+ 索引 | 长期便宜 | 查询慢 |
| ClickHouse | 既能分析又能查单次 | 运维重 |
| 混合:热数据 Jaeger + 冷数据 S3 | 平衡 | 复杂 |
生产推荐混合——近 7 天 Jaeger(快速 debug)+ 长期 S3(审计合规)。
采样策略
100% 采样成本高——生产常用采样:
- 正常请求:1-10% 采样
- 错误请求:100% 采样(遇到就记)
- 慢请求(> P95):100% 采样(长尾 debug)
- 用户反馈差的请求:100%(feedback 回来时 query 老 trace)
让 trace 存储可承担、同时关键 case 不丢。
从 trace 到 debug
有 trace、debug 流程化:
python
def debug_request(trace_id):
trace = trace_store.get(trace_id)
# 1. 总览
print(f"Total duration: {trace.total_duration_ms}ms")
print(f"User satisfied: {trace.user_feedback}")
# 2. 每阶段
for stage_name, stage in trace.stages.items():
print(f"[{stage_name}] {stage.duration_ms}ms")
if stage.duration_ms > thresholds[stage_name]:
print(f" ⚠️ slower than expected")
# 3. 输入输出
print(f"Original query: {trace.stages.query_understanding.input}")
print(f"Rewritten: {trace.stages.query_understanding.output.rewritten}")
print(f"Retrieved IDs: {trace.stages.retrieval.final_top_k_ids}")
print(f"Answer: {trace.response.answer[:200]}")单次 debug 从"翻日志几小时"变成"看 trace 几分钟"——效率天差地别。
Trace 的可观测性用法
除单次 debug、trace 是可观测性的数据源:
- 聚合分析:统计每阶段平均延迟、找瓶颈
- 异常检测:某阶段延迟 / 成本 / 失败率异常 → 自动告警(§3.12)
- A/B 对比:两 cohort 的 trace 对比、看指标差异
- 成本分析:trace 里的 cost_usd 字段汇总、即 per-tenant 账单(§21.18)
一份 trace、多种用途——这是 investment 的价值。
敏感数据的处理
Trace 里不能存:
- 原始 query 全文(可能含 PII)
- 答案全文(同上)
- 用户个人信息(姓名、电话、地址)
正确做法:
- 截断:前 100-200 字 + "..."
- 脱敏:用 Presidio 等工具 mask PII
- hash:query 的 hash 用于去重 / cache、hash 不可逆
- 分层存:普通 trace 脱敏、合规 audit 专门加密存全文
和应用日志的区别
Trace 和应用日志不是一回事:
| 维度 | 应用日志 | Trace |
|---|---|---|
| 粒度 | 每行日志 | 每请求完整 |
| 结构 | 自由文本 | 结构化 JSON |
| 跨服务 | 每服务独立 | 全链路串联 |
| 查询 | grep / ELK | trace ID 查 |
| 保留 | 几天 | 月-年 |
两者互补——trace 做请求粒度分析、log 做细节排查。
Trace 的版本化
随着 RAG 迭代、trace schema 会变:
json
{
"schema_version": "v3", // trace 格式版本
"stages": {...}
}查询 / 分析工具按版本处理——兼容老 trace。变更时加版本号、不静默改。
实施成本
完整 trace 系统的工程投入:
- OpenTelemetry 集成:1-2 人周
- Trace 存储(Jaeger / Tempo):1 人周
- 采样策略:3-5 人天
- Debug UI:1-2 人周
- 合规 audit 能力:1-2 人周
总计 1-2 人月起步。但后续每次事故 debug、每次优化分析都受益——回本 2-3 次事件。
Trace 的组织意义
好 trace 不只是技术工具——是组织协作的基础:
- 产品问 "为什么这个 query 答错"——给 trace_id 能答
- 销售问 "某客户的使用模式"——trace 聚合能分析
- 合规问 "能否重现 3 月的请求"——trace 里找
没 trace 时、每个问题都是"要一周 debug"——有 trace 时、"5 分钟答复"。这个差距决定团队响应速度。
从 0 到 1 的建议
小项目起步时、不用一开始就完美的 trace——渐进:
- MVP:只记 request_id + 关键字段的 plain log
- Stage 1:加 structured logging(JSON 格式)
- Stage 2:用 OpenTelemetry 建 spans、接 Jaeger
- Stage 3:完整 trace schema + 合规 audit
每个阶段有价值——不要等"完美方案"再开始。
反模式
- 无 trace:每次 debug 从头翻
- 有 trace 无 ID 透传:各服务的 log 无法关联
- log 全文本:trace 失去结构、查询慢
- 不做采样:存储爆炸、成本高
- 敏感数据明文:合规事故
和其他章节的配合
Trace 是 RAG 所有可观测性的共同数据源:
- §3.12 异常检测的输入
- §20.18 评估驱动优化的 badcase 源
- §21.18 多租户成本归因
- §22.14 合规审计
- §22.15 事故响应
没有 trace、这些都做不成——所以 trace 是优先级最高的基础设施。
2.16 Lifecycle 的 A/B 实验工程:每一阶段都能对照迭代
前面讲了 lifecycle 的各阶段——但每次改动怎么验证效果?RAG 的 lifecycle 有 8 个阶段、每阶段都可能 A/B——如果没有统一的实验平台、每次都从头搭、工程成本高且结果不可信。这节讲 RAG lifecycle 的 A/B 工程化——让实验成为团队的日常能力。
为什么 A/B 是 RAG 的核心能力
A/B 是 RAG 唯一可靠的改进验证方式——offline eval 不够、A/B 才是真相。
A/B 的各阶段维度
RAG lifecycle 的每个阶段都能 A/B:
| 阶段 | 可 A/B 的东西 |
|---|---|
| Query understanding | rewrite prompt / 模型 / 策略 |
| 权限 | 过滤时机 / 下推方式 |
| 多路召回 | 增减路 / 每路 top_k |
| Rerank | 模型版本 / top_k / 微调版本 |
| 证据组织 | 合并 / 扩展 / 去重 策略 |
| Context packing | 顺序 / 压缩 / 格式 |
| Generation | LLM 版本 / prompt / temperature |
| Citation | 格式 / 粒度 |
一个成熟 RAG 团队同时跑 5-10 个实验——每个阶段都有实验。
实验设计原则
单变量原则:一次实验只改一个变量。两个变量同时改,出问题不知道哪个导致的。
样本量足够:看检测目标 effect size——小改动需要大样本、大改动小样本够。
实验时间:至少 1-2 周(排除 novelty effect)、关键改动 1 个月。
分层随机化:按 user_id hash 分组、保证同 user 看同一实验。
按改动大小分级
- 小改动(prompt 微调 / top_k 变 1):1 周、10k 样本
- 中改动(换 rerank 模型):2 周、50k 样本
- 大改动(重构架构):1 月 + 、100k+ 样本
- 超大改动(换 LLM 厂商):几月、大量监控
实验时长不是一刀切——匹配改动。
指标选择
RAG A/B 的关键指标分层:
L1 工程指标:
- P99 延迟
- 错误率
- 成本 per request
L2 质量指标:
- Faithfulness
- Answer relevance
- Recall@k(若 gold set 有)
L3 业务指标:
- CTR
- 采纳率
- 用户满意度评分
- 复用率
每次 A/B 至少看三层的几个指标——不是只看一个。
指标的主次关系
- 主指标(primary metric):决定 A/B 胜负的
- guardrail 指标:不许降(如延迟 / 错误率)
- 诊断指标:帮助理解发生了什么
典型配置:
yaml
experiment: new_rerank_v2
primary: answer_relevance
guardrails:
- latency_p99 < 3s
- error_rate < 1%
diagnostics:
- recall@10
- citation_grounding_rate
- user_feedback胜负看 primary、任一 guardrail 突破就判负。
实验平台的基本要求
工具必备:
- 分桶(bucketing):按 user_id 稳定分组
- Feature flag:控制哪些用户进哪组
- Metric 采集:每次请求打点、关联到实验组
- 统计引擎:计算 effect size、confidence interval
- Dashboard:实时看实验状态
开源选择:
- Growthbook:开源轻量
- LaunchDarkly:商用成熟
- Statsig:新兴、快
- 自建:简单的可以自己做
建议:小项目用 Growthbook、大项目上 LaunchDarkly / Statsig。
实验的统计学
关键概念:
- 样本量计算:基于 effect size + confidence + power
- p-value:结果显著吗(常用 p < 0.05)
- Confidence interval:效果区间
- Bonferroni correction:多个 metric 同时测时修正
工程师不用精通统计——但要知道别不看数据就下结论。工具能帮做计算。
多变量实验 (MAB)
N 个版本同时跑、自动把流量导向好的——multi-armed bandit:
python
# 简化版 Thompson sampling
for each request:
version = sample_from_posterior(arms_performance)
result = serve(version)
update_posterior(version, result)适合:有多个候选要快速决定的场景(如 5 个不同 prompt)。
缺点:比 A/B 复杂、统计解读麻烦——谨慎用。
实验中的常见问题
- SRR 污染(sample ratio mismatch):实验组流量应 50/50、实际偏——要查 bucketing bug
- Novelty effect:新版本前几天 CTR 高、后来回落
- Primacy effect:新老用户对新版本感受不同
- External shock:实验期间有外部事件(营销 / 新闻)影响
每种问题有对应处理——实验不是"跑完看数字"那么简单。
实验的组织流程
成熟团队的流程:
每一步有 owner 和产出——不是"改代码跑就完事"。
实验的文化
让 A/B 成为团队习惯:
- 默认 A/B:任何改动都假设 A/B、除非明显不需要
- 文档化:每个实验的 hypothesis / design / result 存档
- 学习:失败的 A/B 也有价值(知道什么不行)
- 分享:跨团队分享经验
没这个文化——A/B 是个别人的事、大多数改动仍拍脑袋。
A/B 的 cost
A/B 不是免费:
- 平台搭建:Growthbook 等 1-2 人周起步
- 每次实验:设计 + 实施 + 分析 1-3 人天
- 基础设施:metric 采集 / 存储的长期成本
但 A/B 的错误决策成本——上线一个变差的改动、影响 100% 用户——远大于实验成本。A/B 是省钱的方式。
快速实验的哲学
好的 RAG 团队的 A/B 节奏:
- 每周启动 2-5 个新实验
- 每月决定 10+ 个实验的胜负
- 每季度做几个大实验
数量 > 质量——跑多了自然有突破。纠结每个实验设计完美——不如多跑几个。
实验的局限
A/B 不是万能:
- 生态 / 长期效应:单次 A/B 看不到(需要长期 observational)
- 少量用户的 high-stakes 场景:样本不够、难统计显著
- 副作用:某实验单独好、组合起来坏
长期指标 / 罕见事件的 A/B 特别难——要用其他方法(longitudinal analysis)。
RAG 特有的 A/B 挑战
相对普通 Web A/B、RAG 有特殊:
- 指标多维:质量 + 延迟 + 成本同时看、比单纯 CTR 复杂
- 用户感知延迟:好答案几秒后出——用户行为信号慢
- 评估主观:什么是"好答案"难量化
- 变动传染:改 chunking 影响 rerank 影响 generation——变量多
RAG 团队要比普通 A/B 更耐心 + 更严格。
从 A/B 到持续实验平台
进阶方向——Continuous experimentation:
- 所有新 feature 默认走实验
- 自动化统计分析
- 结果进 changelog / 团队 dashboard
- AI 建议下一个可实验的点
这种平台化的 RAG 开发 = 每个改动都是数据验证的——质量持续提升。
A/B 和 ch22 §22.17 KPI 的关系
ch22 §22.17 讲 KPI——A/B 是实现 KPI 改善的工具:
- KPI 是目标(如满意度 +5%)
- A/B 是手段(每次改动验证是否朝 KPI 方向)
- 组合起来:数据驱动的 roadmap
A/B 文化的转变
团队从"拍脑袋 + 盲改"转到"假设 + A/B"需要时间:
- 管理层相信 data > opinion
- 工程师习惯列 hypothesis
- 产品会设计 experiment
- 决策基于实验结果
这个转变 6 月到 2 年——坚持很重要。中间可能有人觉得慢——但长期看、质量提升快得多。
2.17 本章小结
一次 RAG 请求可以被看作一条证据流水线:
- 接收问题时收齐用户、租户、时间和会话上下文。
- 查询理解把自然语言问题拆成可检索的结构化任务。
- 权限上下文先定义候选边界,避免无权材料进入模型。
- 多路召回用不同机制提高覆盖率。
- 重排序把候选变成真正能回答问题的证据。
- 证据组织把 chunk 还原成文档结构和子问题结构。
- 上下文打包在 token 预算内最大化证据密度。
- 生成与校验保证答案基于证据、可引用、可审计。
- 日志与反馈把每次回答变成系统改进的材料。
下一章我们反过来看:当这条链路某个环节出错时,RAG 会以哪些方式失败。