Skip to content

第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 阶段二:查询理解,把一句话拆成可检索任务

用户的问题通常混合了多个子问题。本章的例子至少包含两个:

  1. 新版企业版套餐的私有化部署是否包含 SSO。
  2. 旧版专业版客户升级到新版企业版如何收费。

如果把这两个问题混成一个向量,召回结果会被平均化。更好的做法是把它们拆成结构化查询:

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_idvisibilitydoc_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 同步是可选交付项,不应混同为默认包含。旧版专业版客户升级企业版时,通常按剩余合同金额折抵,再对新增模块按企业版当前报价补差。现有材料没有给出特殊折扣或审批例外,如果涉及大客户折扣,需要查询销售审批规则。

这个答案有三个特点:

  1. 分别回答了两个子问题。
  2. 把默认包含和可选交付区分开。
  3. 对证据不足的部分没有编造。

生成后还需要校验。最基本的校验包括:

  • 答案中的关键结论是否都有引用。
  • 引用是否来自本次上下文中的证据。
  • 引用文档是否对用户可见。
  • 答案是否引入证据外的价格、日期、人名、承诺。
  • 多个证据冲突时是否按版本、权威性处理。

校验可以由规则、模型或人工介入完成。高风险系统里,校验失败不应该把答案直接返回给用户,而应该降级:

引用不是为了好看,而是为了让答案进入可审计状态。没有引用的 RAG,很难和普通聊天区分开。

2.10 日志与反馈:让每次回答变成训练样本

RAG 请求结束后,系统应该保存完整链路日志。最少包括:

类别字段
请求原始问题、用户、租户、时间、会话 ID
查询理解子问题、改写查询、实体、任务类型
召回每路召回结果、分数、耗时
权限过滤前后数量、过滤原因
排序rerank 分数、最终排名、覆盖的子问题
上下文进入 prompt 的证据 ID、token 数、截断原因
生成模型、参数、输出、引用
校验是否通过、失败原因、降级策略
反馈用户点赞/点踩、追问、人工标注

这些日志有三个用途。

第一,调试单次错误。用户投诉某个答案错了,你可以回放链路,判断是找不到、找错、塞不下还是答不准。

第二,构建评估集。被点踩的问题、人工修正的问题、频繁追问的问题,都是高价值评估样本。RAG 的评估集不应该凭空编,而应该来自真实流量。

第三,驱动知识治理。如果很多问题都找不到答案,可能不是检索差,而是知识库没有对应文档;如果某份旧文档经常被召回导致错误,就应该下线或标记过期;如果某类问题总是需要人工补充,说明文档结构需要改。

这也是 RAG 与传统 FAQ 的区别:RAG 系统不是写完就完,它应该在使用中暴露知识库的缺口。

2.11 在线链路与离线链路的数据契约

本章一直在讲在线请求,但在线链路能做什么,取决于离线索引时保存了什么。

如果离线阶段只存了 textembedding,在线阶段就无法按版本排序、无法显示页码、无法过滤权限、无法合并相邻 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 用于上下文组织和引用展示。
  • versioneffective_date 用于新旧政策判断。
  • acl 用于权限过滤。
  • prev_chunk_idnext_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)——决定整体延迟的那条链。方法:

  1. 给每个阶段的 P50/P99 延迟打 trace 字段
  2. 画 dependency graph
  3. 用 longest path 算法找关键路径
  4. 优化关键路径的节点、其他非关键节点可以牺牲一点

典型发现: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_idresolved_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 / ELKtrace 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 understandingrewrite prompt / 模型 / 策略
权限过滤时机 / 下推方式
多路召回增减路 / 每路 top_k
Rerank模型版本 / top_k / 微调版本
证据组织合并 / 扩展 / 去重 策略
Context packing顺序 / 压缩 / 格式
GenerationLLM 版本 / 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 请求可以被看作一条证据流水线:

  1. 接收问题时收齐用户、租户、时间和会话上下文。
  2. 查询理解把自然语言问题拆成可检索的结构化任务。
  3. 权限上下文先定义候选边界,避免无权材料进入模型。
  4. 多路召回用不同机制提高覆盖率。
  5. 重排序把候选变成真正能回答问题的证据。
  6. 证据组织把 chunk 还原成文档结构和子问题结构。
  7. 上下文打包在 token 预算内最大化证据密度。
  8. 生成与校验保证答案基于证据、可引用、可审计。
  9. 日志与反馈把每次回答变成系统改进的材料。

下一章我们反过来看:当这条链路某个环节出错时,RAG 会以哪些方式失败。

基于 VitePress 构建