Skip to content

第4章 生产级 RAG 架构:离线索引、在线检索与反馈闭环

"An architecture is a set of decisions that survive every reorganization." — Michael Nygard, Release It!

本章要点

  • 生产 RAG 由三条链路组成:离线索引(写)、在线检索(读)、反馈闭环(学)
  • 三条链路时序解耦:离线以批/流形式预计算,在线响应单次请求,反馈以小时/天为周期回流
  • 三条链路通过数据契约耦合:向量索引、chunk store、metadata schema、日志 schema 是四份必须稳定的契约
  • 架构演进顺序:先跑通在线链路 → 做好离线一致性 → 最后打通反馈闭环
  • 每条链路的 SLA 不同——离线讲吞吐和一致性、在线讲延迟和可用性、反馈讲准确性和时效性

4.1 为什么是三条链路

初学者画 RAG 架构图通常长这样:用户 → 检索器 → 大模型 → 用户。这张图完全忽略了知识是怎么进系统的、系统是怎么从错误里学习的。生产 RAG 的复杂度 80% 不在"检索 → 生成"这条显式链路上,而在两条隐式链路上:

  • 离线索引:把外部知识(Wiki、合同、代码、工单)持续转成可检索结构
  • 反馈闭环:把线上用户行为(点击、采纳、负反馈、显式评分)持续转化为离线优化的输入

三条链路合起来是一个闭环系统——在线依赖离线的输出、离线消费反馈的信号、反馈来自在线的行为。任何一条链路故障,另两条会在数天内退化。这也是为什么 Google/Bing/Amazon 的搜索架构都是三条链路并重而不是只谈检索——RAG 继承了搜索的这套架构 DNA。

这张图是本章的指南针。后续每一节都在给这张图填细节——哪一块是哪条链路、接口如何定义、故障如何隔离。

4.2 离线索引链路:批、流、增量

离线索引的目标:把外部知识转成稳定的、可检索的结构。"稳定"意味着下游在线链路可以对结构做强假设;"可检索"意味着结构支持向量检索、关键词检索和元数据过滤。

三种触发方式

  • 批(batch):每天/每周跑一次 Spark / Airflow job 把全量知识重建。适合月度变化的知识库(法规、产品规格)、小规模数据集(百万 chunk 以内)。优点是简单、强一致;缺点是时延大、资源浪费高。
  • 流(streaming):Kafka → 消费者 → 索引。文档变更事件实时推进索引。适合高频变化、强时效性(新闻、工单、代码仓库)。优点是分钟级延迟;缺点是运维复杂、需要处理乱序/重复。
  • 增量(incremental):批作为主链路 + 小批量增量补丁。适合绝大多数场景。比纯批更新及时、比纯流运维简单。第 8 章专门讨论增量索引。

选型决策树:知识库变化频率 > 每天 → 流或增量;< 每天 → 批;对外 SLA 要求分钟级新鲜度 → 流;可接受小时级 → 增量;可接受天级 → 批。

关键阶段的解耦

离线链路的五个阶段必须物理解耦——每阶段输出中间产物、可独立重跑:

  • 解析层:原始文档 → 结构化文本 + metadata。产物:parsed_doc.jsonl(每行一个文档的 {id, text, metadata})。
  • 分块层:结构化文本 → chunk 列表。产物:chunks.jsonl(每行一个 chunk 的 {doc_id, chunk_id, text, span, metadata})。
  • Embedding 层:chunk 文本 → 向量。产物:embeddings.parquet(chunk_id, vector, model_version)。
  • 索引层:向量 + metadata → 索引文件。产物:向量库快照(HNSW 图、IVF 中心、Flat 数组)+ 倒排索引 + metadata DB。
  • 发布层:索引文件 → 在线服务可加载的 artifact。产物:原子切换的版本化资源(S3/OSS/对象存储上的目录)。

每层产物持久化在对象存储里,失败时从最近成功的上一层恢复,不必整条重跑。这是数据管道里经典的分段可重入模式,和 Airflow/Prefect 的 DAG 执行模型完全对齐。

为什么一定要原子切换

索引更新不能在原地改——在线服务正在读的同时写是数据竞争的温床。正确姿势:新版本索引在新路径上构建完成后,把服务的 "current version pointer" 原子地指向新路径。版本切换后保留旧版本至少 24 小时作为回滚兜底。

工程实现有三种常见形态:

  • S3 符号链接:维护一个 index/current 指向 index/v42。服务启动时读 current。换版本时改 pointer。
  • 版本标签 + 热加载:服务定期轮询 "current_version" 这个键(Redis / etcd / S3 metadata),版本号变了就下载新索引热加载。
  • 蓝绿副本:两套服务各挂一个版本,LB 层切流。资源成本 2 倍但回滚最干净。

中小规模选 S3 符号链接最简单;超大规模选蓝绿副本最稳;混合选轮询热加载最省资源。

版本号策略与回滚

索引版本号不是递增数字那么简单——需要携带够多信息让运维能快速定位问题:v{YYYYMMDD}-{commit_sha}-{embedding_model},例如 v20260424-a1b2c3d-bge-m3-v1.5。三个字段分别对应:构建日期、索引 pipeline 代码版本、Embedding 模型版本。任何一个字段变了都意味着索引语义变了,在线端必须相应调整(尤其是 Embedding 模型变更必须重建全部向量,不能增量追加)。

回滚窗口推荐保留 3-7 天。回滚不是简单把 pointer 指回去——需要检查新产生的日志、反馈是否已经假设新索引存在。在写回滚脚本时就把这些依赖列出来,避免回滚本身带来新 bug。

离线链路的性能瓶颈

大规模知识库的离线索引经常卡在三个地方:

  • Embedding 吞吐:单张 A100 跑 bge-m3 大约 500-800 chunk/s(batch=32, seq_len=512)。100 万 chunk 的全量 Embedding 需要 20-30 分钟。bge-small-zh-v1.5 能到 3000 chunk/s,牺牲一点精度换吞吐。
  • 向量索引构建:HNSW 索引 100 万向量(768 维)构建时间约 15-30 分钟,内存占用约 3-5 GB。IVF-PQ 构建时间约等,但内存只要十分之一——大规模选 IVF-PQ,小规模选 HNSW。第 10 章展开。
  • I/O 传输:parquet 文件从对象存储下载到 GPU worker 的带宽。如果 worker 和存储不在同一 region,单纯传输可以吃掉 30% 的总时长。实操上把 Embedding pipeline 和对象存储放在同一 region 能立竿见影。

4.3 在线检索链路:延迟预算怎么花

在线链路的目标:把用户请求在 SLA 内转成带引用的答案。SLA 通常是 P99 延迟 < 2s(对话场景)或 < 5s(Agent 场景)。

延迟预算分配

假设总 SLA 是 P99 = 2000ms,典型预算分配:

阶段预算说明
查询理解(含 rewrite/拆解)150-300ms小模型或本地规则
多路召回(并行)80-150ms向量库 + BM25 并行
融合 + Rerank(cross-encoder)200-400ms瓶颈往往在此
Context pack20-50ms应该近乎零
LLM 生成(含 TTFT)800-1500ms首字 + 流式续写
网关/日志/其他100-200ms别轻视这块

两个易忽略的预算泄漏点:

  • 串行化:两路召回顺序调用而非并行,50+50=100ms 变成 80+80=160ms。用 asyncio.gather / tokio::join! 并行是基本功。
  • Rerank 批处理:cross-encoder 一次处理 50 个 (query, chunk) 对比 50 次单条调用快 5-10 倍(GPU 有效批大小)。

可用性设计

在线链路的所有调用都要有降级预案

  • 向量库不可用 → 只走 BM25
  • Rerank 服务超时 → 用融合分数直接排 top-5
  • LLM 超时 → 返回 "基于以下文档:[链接列表]",让用户自查

降级链路必须提前演练,不能等生产故障时才想。这和第 22 章讨论的产品化工程细节相关。

缓存:延迟和成本的共同敌人兼朋友

生产 RAG 的三层缓存各有作用:

  • 查询结果缓存(Redis,TTL 5-60 分钟):相同 query 在短时间内命中直接返回答案。缓存 key 是 hash(query + user_context + knowledge_version)——user_context 让个性化不被污染、knowledge_version 让索引更新时缓存自然失效。命中率和问题集中度强相关:客服场景可能 30%+,通用问答可能只有 2-5%。
  • Embedding 缓存(Redis,永久):相同文本的 embedding 复用。查询端的命中率取决于查询分布集中度;离线端的命中率在增量索引场景很高(只有少量 chunk 真的变了)。
  • Rerank 缓存(Redis,TTL 小时级):(query, chunk_id) → score 的小表。同一个用户的多轮对话里常见——用户追问时会重复命中上一轮的候选 chunk。

缓存命中带来的延迟降低通常在 100-500ms 量级。但缓存失效策略极其关键——错误的 TTL 会让用户拿到过期信息。策略:任何涉及"最新"的查询不走缓存(用 query classifier 识别)、任何用户反馈过的 query 清除缓存、任何知识库有更新的场景强制失效。

并行化的工程细节

并行召回看起来容易但坑点多。Python 侧:

  • asyncio.gather 时要注意异常传播——任一路失败默认整体失败。生产代码用 return_exceptions=True 单独处理每路。
  • 向量库和 BM25 通常是不同后端(Qdrant vs Elasticsearch),连接池要分开管理。
  • Rerank 如果调用外部 API(如 Cohere Rerank),要准备本地 fallback。

Rust/Go 等语言侧同理——tokio::join! / errgroup.Go 都需要精细处理部分成功场景。这和《Tokio 源码深度解析》第 14 章讨论的 select! 公平性是同一类问题。

4.4 反馈闭环:从日志到训练信号

反馈闭环是 RAG 三条链路里最常被忽略、却最决定长期质量的一条。没有反馈闭环的 RAG 系统在上线第 3 个月开始质量衰减——知识库更新了但检索器不适配、用户行为变了但 rerank 权重不更新、新的失败模式出现了但没人发现。

反馈信号有四种

  1. 隐式交互:点击、停留时长、是否采纳答案、是否转人工。量大、噪声高、需要大量样本才能用。
  2. 显式评分:赞/踩、1-5 星评分、"这个答案是否解决了你的问题"。量小、信号强、容易被老用户偏见扭曲。
  3. 人工标注:专家给 gold label、做错误归因。质量最高、成本最高、适合回归集。
  4. LLM-as-a-judge:用大模型对比回答 vs 证据自动打分。量大、中等质量、适合规模化监控。

生产系统应该四种信号同时收集。每种信号用不同的方式消费:隐式信号用于 rerank 权重微调、显式信号用于 prompt 迭代、人工标注用于模型训练、LLM judge 用于持续监控。

从 trace 到训练集:数据工程

反馈闭环的关键是把线上 trace 转成结构化训练集。第 3 章给的 trace schema 是每个请求的完整记录。从 trace 到训练集需要几步处理:

  • 去重:相同 query 在一天内命中多次,只保留一条
  • 标注:用户反馈或 LLM judge 打标
  • 采样:按问题类型、时间段、用户分层采样,避免头部用户主导训练信号
  • 负样本构造:仅有正例的训练集会过拟合——要从未命中的候选里构造难负例(hard negatives)

产出两类数据集:

  • Rerank 训练集(query, positive_chunk, negative_chunks[])——用于 fine-tune cross-encoder
  • RAG 回归集(query, gold_chunks[], expected_answer, acceptance_criteria)——用于每次迭代的质量门禁

第 20 章 RAG 评估会详细展开回归集的构造和维护。

反馈延迟的工程权衡

反馈闭环的时效性有一个本质张力:信号质量和时效成反比。

  • 实时信号(毫秒-秒):用户是否立刻 regenerate、是否关闭会话。噪声极大但即时。只适合做异常检测(点击率突降 = 上游出问题)。
  • 短期信号(分钟-小时):会话结束后的显式反馈、转人工率、后续问题的话题转移。质量中等,适合粗粒度监控。
  • 长期信号(天-周):用户留存、NPS、标注团队的深度 review。质量最高,适合模型训练样本。

生产 RAG 通常三种时效都用——实时信号驱动告警、短期信号驱动仪表板、长期信号驱动模型迭代。不要指望用长期信号做实时告警(太慢)、也不要用实时信号做训练样本(太嘈)。每种信号服务不同的决策层。

反馈数据的隐私和合规

反馈闭环里流动着用户请求的完整内容——这是合规敏感区

  • 用户请求可能含 PII:姓名、手机、身份证、工单号。trace 落盘前必须脱敏,或用能区分"原文"和"脱敏副本"的双存储方案(原文短 TTL、脱敏版长 TTL)。
  • 企业客户的查询本身是商业机密——跨租户的 trace 共享严格禁止。多租户 RAG 系统要保证 trace、回归集、训练数据不串租户。
  • GDPR 等法规下用户有被遗忘权——反馈数据存储要支持按 user_id 批量删除。设计 schema 时就加上 user_id 索引。

这些不是架构细节,是架构底线。缺失任何一条都可能导致重大合规事故。

4.5 三条链路的数据契约

三条链路之间通过稳定的数据结构耦合——任何一份契约改了,上下游都要跟着改。生产系统的技术债主要就是契约改动的成本。

四份核心契约:

契约为什么比架构图更重要

架构图画得再漂亮、具体代码到底怎么写才不坏,取决于数据契约。同一套架构图,三个工程师做出来可能是三套互不兼容的系统——就是因为没写下来的契约不同。生产 RAG 系统上线半年后,真正稳定的不是代码、也不是模型,而是这几份契约。每次契约破坏都会引发大规模 migration(索引重建、回归集失效、日志解析崩溃),成本远高于一次模型迭代。

这一节讨论的四份契约是经验值——实际项目可能要在此基础上增减。但核心原则不变:凡是跨链路共享的数据结构都必须在代码仓里以明确的 schema 声明(pydantic、protobuf、jsonschema 均可),任何变更都 PR review。不要靠文档约定——文档会过时,schema 不会。

chunk_id 方案

唯一稳定的 chunk 标识。推荐方案:{doc_id}#{chunk_index}#{content_hash}

  • doc_id:文档稳定 ID(UUID 或业务 ID)
  • chunk_index:文档内 chunk 序号
  • content_hash:chunk 文本的 SHA256 前 8 位

content_hash 让 "文档 ID 不变但内容改了" 的场景能被检测——旧 chunk_id 和新 chunk_id 不同,在线系统可以识别到"缓存失效"。

embedding 向量空间

embedding 的 model_version 必须在向量旁边存储。混用不同模型版本的向量会导致检索质量灾难性下降。查询端也必须用同一个 model_version 产生查询向量。版本切换要配合索引重建,不能只切其中一端。

metadata schema

每个 chunk 的 metadata 是一份半结构化字典。推荐字段:doc_id, chunk_id, source_type, publish_date, author, access_level, language, section_path。新字段必须向后兼容——增加字段不能破坏旧查询。

日志 schema

trace schema(第 3 章给过)是离线/在线/反馈三条链路共享的"对话语言"。所有链路读写 trace 都按同一 schema。schema 升级要做 major 版本号、双写、逐步迁移。

4.6 演进顺序:先跑通哪条链路

从零搭一个 RAG 系统,三条链路不要同时做。建议的演进顺序:

  1. MVP:在线链路跑通。用 langchain / llamaindex 快速串起来,离线用一次性脚本生成索引,反馈先人工看 badcase。目标:内部可用、拿到真实流量。耗时 1-2 周。
  2. 离线稳定化。把离线脚本改造成可重入、可回滚、带版本控制的 pipeline。目标:知识库能每天/每周安全更新。耗时 2-4 周。
  3. 反馈闭环建设。加入日志采集、回归集、LLM judge 监控。目标:线上质量可观测、可对比、可定位。耗时 4-8 周。
  4. 优化迭代。按第 3 章的归因工作流优化各阶段。每次迭代用反馈闭环验证。目标:答对率、延迟、成本同时改善。持续进行。

阶段 1 做完就可以内部试用、拿真实反馈;阶段 2 做完才能对外开放;阶段 3 做完才有能力长期维护;阶段 4 永远持续。跳过阶段 2 直接开放对外是常见陷阱——知识库更新一次出 bug 没法回滚。

每个阶段的"验收标准"

每个阶段声明可量化的 done 标准,避免"大概做完了"心态:

阶段 1 MVP 验收:内部 5-10 人连续使用一周,平均每人提 10 个问题以上。收集 50+ 真实 badcase 归入第 3 章的失败家族。此时答对率能做到 50-65% 就可以进入阶段 2。

阶段 2 离线稳定化验收:连续 4 周周度更新知识库零事故。索引重建 pipeline 能在工作时间完成、失败时有明确告警、回滚脚本演练过一次实战(从 v42 回到 v41 后在线服务正常)。

阶段 3 反馈闭环验收:12 个最小监控指标(第 3 章 §3.10 列出)全部上看板、每个都有 SLO 和告警阈值、每周 review 一次。回归集覆盖 20+ 问题类型、每次版本迭代自动跑回归。

阶段 4 持续优化:每月做一次归因分析、按最大失败家族投优化资源、每季度基准 review 一次整体架构是否还合适当前规模。

没有量化验收就没有客观的"完成"概念——团队会在各阶段之间反复回退,项目进度预估失真。

组织形态对架构的影响

Conway's Law 在 RAG 上同样适用。三条链路如果归属三个独立团队(数据团队做离线、应用团队做在线、算法团队做反馈),架构会自然演化成三个松耦合的服务。如果一个团队通吃,系统会更紧耦合但也更易出现"谁都能改核心表"的失控。推荐:

  • 小型项目(< 5 人):一个团队三条链路,但代码里严格按链路分层
  • 中型项目(5-15 人):一个核心团队 + 专门的数据工程角色负责离线 pipeline
  • 大型项目(15+ 人):三团队结构,但架构师角色跨团队守护数据契约

不管哪种组织形态,数据契约的所有权要 explicit——某一份契约必须明确"谁说了算",避免跨团队修改时无人负责。

4.7 三条链路各自的 SLA

三条链路的 SLA 指标完全不同——设计阶段不清晰,运维时会很痛苦。

离线索引链路

SLA 维度典型值说明
数据新鲜度知识库更新后 ≤ 24h 生效批模式目标;流模式可压到 5min
索引重建吞吐100 万 chunk / 30 分钟单 GPU worker 可达
版本一致性100%索引 + metadata + embedding 同版本
回滚 RTO< 5 分钟pointer 切回 + 缓存失效

在线检索链路

SLA 维度典型值说明
P99 端到端延迟< 2000ms对话场景
TTFT 首字延迟< 800ms流式体验关键
可用性≥ 99.5%月度;降级算可用
答对率≥ 85%按场景;回归集 + A/B 测量

反馈闭环

SLA 维度典型值说明
trace 落盘延迟< 1 分钟日志管道吞吐
看板指标更新5-15 分钟实时性 vs 聚合成本
回归集迭代每周新 badcase 入回归集
模型微调周期月度视训练数据量和业务

三组 SLA 共同定义了"生产可用 RAG"的含义——缺任何一组都意味着系统在某维度失控。

4.8 三链路协同的一次典型事故

真实场景比抽象架构图更说明问题:

  • 症状:周一早上业务反馈"系统全乱答"。答对率看板从 85% 跌到 40%
  • 初步定位:看第 3 章结构化 trace,发现 retrieval 阶段 score 分布完全变了,top-5 几乎全是历史悠久的旧文档
  • 根因追溯:周日夜里离线 pipeline 自动跑了全量索引重建。构建时用了新版 Embedding 模型(bge-m3-v1.6,上周更新的依赖),但在线服务的查询端还在用旧 bge-m3-v1.5 算查询向量
  • 修复:回滚索引到上一版(v20260421)、同时把在线服务的 embedding 客户端也回退到 v1.5。45 分钟后答对率恢复
  • 复盘:Embedding 模型版本没写进任何一份数据契约。加以下监控:索引 metadata 必须包含 embedding_model_version;在线查询编码器版本要与索引中的版本做启动时一致性检查;不一致直接拒绝服务

这类事故几乎所有 RAG 团队都会遇到一次。每次都是在提醒数据契约的重要性——§4.5 讨论的四份契约不是空头理论,是被这类事故反复验证出来的。

4.9 技术栈选型:不要造轮子

最后讨论一下三条链路的技术栈选型。这里给的是 2026 年初的主流组合——每个都有成熟替代品,但这份清单可以作为选型起点。

离线索引链路

  • 调度:Airflow(成熟)/ Prefect(现代 DX)/ Dagster(类型化 pipeline,类似 dbt 思路)。千级任务以下 Airflow 够用;复杂依赖图选 Prefect 或 Dagster
  • 解析:unstructured.io(通用 PDF/HTML/DOCX)/ marker(PDF 专精)/ pypdf / BeautifulSoup(HTML)
  • Embedding 推理:bge / text-embedding-3 / jina-embeddings / OpenAI / Voyage。自托管选 bge-m3,成本敏感选 OpenAI text-embedding-3-small
  • 向量库:Qdrant / Milvus / pgvector / Weaviate / Pinecone。自托管选 Qdrant(轻量、好用)或 Milvus(大规模);快速起步选 pgvector;云上选 Pinecone

在线检索链路

  • Web 框架:FastAPI(Python 主流)/ axum(Rust 选项,见同系列《Axum 设计与实现》)/ Gin / Actix
  • 检索编排:LlamaIndex / Haystack / 自写。快速起步用 LlamaIndex,生产化后多数团队自写
  • Rerank:Cohere Rerank API / BGE Reranker 自托管 / Jina Reranker / LLM-as-reranker
  • LLM:Claude / GPT / Gemini / Qwen / DeepSeek。企业场景注意数据合规要求

反馈闭环

  • trace:OpenTelemetry + Jaeger / Tempo
  • 日志:ELK / Loki / ClickHouse
  • 看板:Grafana + Prometheus
  • 回归集:自维护 JSONL + CI 集成
  • LLM-as-judge:ragas / trulens / 自写 prompt

选型原则:每个位置都用社区验证过的主流组件,不要在基础设施上追求差异化——差异化留给产品和数据。用 10 家以上公司在用的工具比用 1 家在用的工具故障率低一个数量级。

4.10 RAG 的部署模式:灰度、蓝绿、shadow 的具体实现

前面 9 节讲了 RAG 的架构——三条链路 + 数据契约 + SLA。但这些讨论都是"稳态运营"的视角。真实生产里系统每周都在变化:新 prompt 模板上线、新 embedding 模型试用、新 rerank 参数、新向量库版本、新 chunk 策略。发布策略决定每次变化的安全性。RAG 的部署模式和普通 Web 服务有关键差异——一条链路同时涉及有状态(索引、cache)和无状态(检索逻辑、LLM 调用)——发布策略要区别对待。

RAG 部署的独特挑战

普通 Web 服务滚动重启就能发布——RAG 里索引不能滚动重启(加载时间长、数据不能丢)、所以必须蓝绿。这是 RAG 部署的最大区别。

蓝绿索引的具体实现

第 8 章 §8.6 提过周期性全量重建 + 原子切换——那是蓝绿索引的一个场景。更一般化:

  • 蓝索引(当前服务):收全部流量、应用配置指向它
  • 绿索引(待发布):后台建起来、shadow 流量验证
  • 切换:改应用配置(通常一个 feature flag 或 config 值)、流量立即指向绿
  • 观察期:保留蓝索引 N 天(48-72 小时常见)、出问题立即切回
  • 清理:观察期过后下线蓝索引
python
# 应用端配置
ACTIVE_INDEX = os.environ.get("ACTIVE_INDEX", "blue")  # blue / green

def search(query):
    return vector_db.search(collection=f"chunks_{ACTIVE_INDEX}", ...)

切换只是改环境变量 + 重启 app / 调用 control plane API——秒级生效。

Canary 发布:模型和 prompt 的小流量上线

蓝绿适合整体切换、canary 适合小流量试水。主要用于:

  • 新 LLM 模型版本:Sonnet 4.6 → 4.7——先给 5% 流量
  • 新 prompt 模板:改过的 system prompt——先给 5%
  • 新 rerank 模型:替换 rerank——先给 10%
python
def generate(query, context, user_id):
    # 基于 user_id hash 决定走哪条路径、保证同一用户稳定
    bucket = hash(user_id) % 100
    if bucket < 5 and feature_flag("new_llm_canary"):
        return new_model.generate(query, context)
    return old_model.generate(query, context)

Canary 观察:

  • 先 5% × 1 小时——看错误率
  • 10% × 24 小时——看质量指标
  • 50% × 3 天——看业务指标
  • 100%——稳态

任一阶段指标异常 → 回滚到上一级流量。

Shadow 流量:无用户影响的真实验证

Canary 有用户流量风险——哪怕 5% 也可能误伤。更保守的 shadow:

  • 用户请求同时发给 生产路径shadow 路径
  • 生产路径的结果返回给用户
  • Shadow 路径的结果只记录、不返回
  • 后台对比两路的 recall / faithfulness / latency
python
async def retrieve(query):
    # 主路径(返回给用户)
    primary_result = await production_retrieve(query)
    
    # Shadow(异步、用户无感)
    asyncio.create_task(shadow_retrieve(query, primary_result))
    
    return primary_result

async def shadow_retrieve(query, primary_result):
    shadow_result = await new_retrieve_path(query)
    log_diff(query, primary_result, shadow_result)

Shadow 持续 1-2 周、积累几万条对比——确认新路径稳定后再升级到 canary。

Feature flag:发布的核心工具

现代 RAG 项目几乎每个变更都应该带 feature flag:

python
if feature_flag("use_new_rerank", user_id=user_id):
    rerank_model = new_rerank
else:
    rerank_model = old_rerank

Feature flag 让发布变得可回滚——出问题不用发代码、改 flag 秒级生效。工具:LaunchDarkly、Growthbook、自建。

组件粒度的 flag:

  • LLM 模型
  • Embedding 模型
  • Rerank 模型
  • Rewrite 策略
  • Prompt 模板
  • Chunk 策略

每一维度都能独立切换、便于精确归因("这次改动是不是 rerank 导致的")。

发布策略的组合

完整的 RAG 组件发布路径:

每一步都有明确进入条件

  • Shadow → Canary:shadow 指标稳定 1 周
  • Canary 5% → 10%:错误率不涨
  • Canary 10% → 50%:业务指标不降
  • 50% → 100%:观察 72 小时无异常

跳步的代价是事故风险——不要为"快"牺牲流程。

回滚演练

发布不是完整流程、回滚才是。每个发布都要伴随回滚预案:

  • 能不能一键回滚?(改 feature flag)
  • 回滚后数据一致吗?(索引有没有脏数据)
  • 回滚时长多少?(分钟级还是小时级)

每季度做一次回滚演练——故意触发一个假回滚、计时、看流程是否顺畅。没演练的回滚在真事故时永远不如预期。

发布的观测面板

每次发布时特别要盯的看板:

  • 流量分布:canary 流量比例符合预期吗
  • 错误率对比:canary vs 稳定版本的 error rate 差异
  • 延迟对比:p99 是否稳定
  • 业务指标:点击率 / 采纳率 / 满意度
  • Feature flag 状态:每个 flag 的开关实时显示

专门的 "release dashboard" 在发布当天集中看——比翻常规 Grafana 效率高。

发布的组织协调

复杂 RAG 项目、不是工程一个团队的事:

  • 产品:设定发布门槛(什么指标达标可以全量)
  • 工程:执行发布、写 runbook
  • 运维:监控、值班 on-call
  • 数据:分析业务指标变化、判断 A/B
  • 合规(如果涉及):check 权限 / 审计要求

每次大发布前开一次 kickoff、对齐节奏和责任——防止各做各的、谁都不知道整体状态。

发布的反模式

  • 没有 feature flag:每次改动要发代码才能切——回滚慢、心理负担大
  • 一次性 100% 上线:没有 canary、事故面积最大
  • 不做 shadow 验证:新路径没经过真实流量检验
  • 发布文档只写"怎么发"不写"怎么回滚":出事慌
  • shadow 跑太短就升级 canary:shadow 阶段的价值没充分利用
  • 回滚路径没演练:真需要回滚时发现脚本坏了

和事故响应的衔接

发布不顺 → 事故。ch22 §22.15 的事故响应 playbook 和发布策略紧密配合:

  • 发布过程中异常 → 立即触发 playbook
  • Playbook 的 "回滚步骤" 指向的就是 feature flag 或蓝绿切换
  • 发布变更进 audit log、事故 postmortem 能直接溯源

发布策略是事故预防、事故响应是发布补救——两者必须衔接。

4.11 三链路的资源隔离与独立扩缩容

前 10 节讲了三链路的职责和数据契约——但它们共享基础设施吗?生产 RAG 一个常被忽视的架构决策:离线索引、在线检索、反馈闭环三条链路应该资源隔离——否则一条链路的问题会拖垮另外两条。这节把资源隔离讲清楚、避免"离线重建时在线查询崩"、"反馈任务拖慢检索"这类典型事故。

为什么要资源隔离

共享资源的典型事故:

  • 离线全量重建时吃光 CPU / 内存、在线查询 P99 10×
  • 反馈 batch 处理大量 trace、网络打满、在线超时
  • 某个 ingest worker 泄漏、磁盘爆、向量库宕

隔离后每条链路独立边界——一条出问题不波及其他。

各链路的资源特征

链路CPU内存磁盘 IO网络特征
离线索引高(读写文件)吞吐型、可容忍延迟
在线检索高(索引缓存)高(QPS)延迟型、低吞吐
反馈 batch定时批处理

三者的资源画像完全不同——用一个 pool 跑不划算、各自用对应的资源池。

隔离的三种方案

方案 A:物理隔离(强)

每条链路独立 K8s 集群 / VM group。完全隔离、但成本高(重复的基础设施)。

适合:大规模生产、合规敏感

方案 B:逻辑隔离(中)

同一 K8s 集群、不同 namespace / node pool。node pool 指定 taint、各链路的 pod 只能调度到对应 pool。

yaml
# 离线 pool 的 node
taints:
  - key: workload
    value: offline
    effect: NoSchedule

# 离线 pod
tolerations:
  - key: workload
    value: offline

适合:多数中大型项目

方案 C:Quota 隔离(弱)

全部 pod 跑一起、用 K8s resource quota / LimitRange 限制每个链路的资源。最省基础设施、但邻居效应强。

适合:小项目、起步阶段

多数项目从 C 起步、规模起来后升到 B——物理隔离只在合规 / 超大规模才值。

独立扩缩容

各链路的扩缩容触发条件不同:

  • 离线索引:按 ingest 队列深度扩缩(队列满 → 加 worker)
  • 在线检索:按 QPS / CPU 利用率扩缩(QPS 涨 → 加 pod)
  • 反馈 batch:按时间调度(定时跑、不在线扩缩)

K8s HPA(Horizontal Pod Autoscaler)配置三套:

yaml
# 在线检索 HPA
online-rag-hpa:
  metrics:
    - queue_qps > 80% capacity
  min: 5
  max: 100

# 离线索引 HPA
ingest-worker-hpa:
  metrics:
    - kafka_lag > 10000
  min: 1
  max: 50

独立 HPA 让各链路各自优化成本——在线高峰时加检索 pod、离线不受影响。

共享资源的例外

不是所有东西都要隔离——以下应该共享

  • 向量库:数据只有一份、在线和离线读同一份(通过读写分离)
  • 元数据 DB:同上
  • 对象存储:原始文档源头、各链路都读
  • 监控栈:Prometheus / Grafana 覆盖全链路

共享的是数据和监控、隔离的是计算资源

多租户叠加资源隔离

§11.12 讲了向量库的多租户。三链路 + 多租户叠加更复杂:

text
租户 A 的离线 ingest pod
租户 A 的在线检索 pod
租户 B 的离线 ingest pod
租户 B 的在线检索 pod

这可能产生 N × 3 个 pool——维护成本爆炸。缓解:

  • 共享小租户:小租户(占总量 < 5%)共用一个 pool
  • 独立大租户:大租户(> 20%)独立 pool
  • 按优先级保证:付费 tier 高的租户优先资源

资源隔离的监控

隔离有效性的监控:

  • 跨链路干扰率:某链路出问题时、其他链路的指标是否变化?应接近 0
  • 资源池利用率:每个 pool 的 CPU / 内存 / 网络使用率、发现 hot spot
  • 队列深度:离线 ingest 队列、反馈队列——排得太长说明容量不够
  • SLA 达成率 by pool:每个链路的 SLA、不同 pool 各自看

资源分配的比例

典型的资源分配比例(按计算资源):

链路占比
在线检索50-60%
离线索引30-40%
反馈闭环5-10%
通用(监控 / 日志 / 网关)5-10%

比例大幅偏离这个分布说明有问题——比如离线占 70% 可能是数据量太大、或 ingest pipeline 效率低。

资源的动态转移

高级做法——跨链路资源动态转移

  • 非高峰时段:在线检索流量低、匀 20% 资源给离线、加速 ingest
  • 高峰时段:全量保在线、离线暂停 / 减速
  • 凌晨:反馈 batch 用全部资源跑一小时、白天几乎不占

K8s 的 Priority Class 能实现这类调度——高优先级 pod 抢占低优先级。

成本视角的资源分配

成本角度看:

  • 在线检索:延迟敏感、不能压——必须足够 buffer
  • 离线索引:吞吐敏感、可以用 spot / preemptible 实例(便宜 60-80%)
  • 反馈 batch:完全可以用 spot、夜间跑
  • 监控 / 网关:稳定中等配置就行

对每个 pool 按特性选实例类型——能省 30-50% 总成本。

实际事故的典型场景

真实事故——资源隔离失败的经典:

  • 场景 1:某租户上传百万文档、ingest worker 吃光集群 CPU、在线 RAG P99 从 2s 涨到 15s
  • 场景 2:反馈 trace 处理异常、对象存储写入风暴、向量库的日常查询被拖慢
  • 场景 3:embedding 服务用同一 GPU pool 处理离线和在线、离线大批量把在线延迟打到秒级

三个场景的共同根因:资源共享没有优先级和 quota。隔离方案能避免。

先做最重要的

完全隔离成本高——先做最关键的

  1. 在线检索独立 pool(必做):延迟 SLA 的保证
  2. 离线和反馈可共享(可选):两者都是延迟不敏感、共享 OK

这样的最小隔离已经能避免 80% 的资源干扰事故——小成本大收益

架构演进的 waterline

资源隔离的演进路径:

  • Week 1-3:所有东西跑一起、MVP 能用就行
  • Month 1-3:在线检索独立(quota 级)
  • Month 3-12:在线 + 离线 node pool 分开
  • Year 1+:完整三链路物理隔离(视规模)

这个节奏让复杂度和业务匹配、不过度设计也不过度简化。

4.12 架构文档化与 ADR:为什么这么决定的

RAG 系统运行几个月、团队扩充、新人加入——常见问题:"为什么选 Qdrant 不是 Milvus"、"为什么 chunk size 是 400"、"为什么 rerank 用 bge-reranker 不是 Cohere"。知道决策的人走了、文档没有、新人靠考古——决策靠谎言重新做一遍架构决策记录(ADR) 是解药——这节给 RAG 项目的 ADR 实践。

为什么要文档化

架构决策是最珍贵的组织知识——代码能看 git、决策靠 ADR。

ADR 的基本结构

ADR(Architecture Decision Record)是一个决策一个文档

markdown
# ADR-005: 选择 Qdrant 作为主向量库

## Status
Accepted (2026-01-15)

## Context
我们需要选一个向量库支撑 RAG 的检索层、规模预计 100-1000 万向量。

候选:
- Qdrant: Rust 原生、单机性能好、API 清晰
- Milvus: 云原生、亿级规模、生态活跃但重
- pgvector: 已有 Postgres 栈、简单但性能中等
- Pinecone: 全托管、省运维但贵 + 厂商锁定

## Decision
选 Qdrant。

## Consequences

### 正面
- 单机性能好、延迟 <10ms
- 运维简单、一个二进制
- 开源可控、无厂商锁定

### 负面
- 超过千万级时可能要换 Milvus
- 生态比 Milvus 弱

## Alternatives Considered
- Milvus:规模超我们当前需求、overhead 不值
- pgvector:性能不够
- Pinecone:厂商锁定 + 成本高

## References
- 压测报告:[link]
- 竞品对比:[link]
- 会议记录:[link]

每个 ADR 只有一页——关键信息齐备但不冗长。

关键 ADR 类型

RAG 项目的核心 ADR:

  • 选型类:向量库 / embedding / LLM / rerank / 分词器
  • 架构类:三链路划分 / 多租户架构 / 部署模式
  • 参数类:chunk size / top_k / rerank threshold(只对有争议的)
  • 流程类:发布流程 / 事故响应 / 数据治理
  • 安全类:权限模型 / PII 处理 / 加密策略

不是每个小决策都要 ADR——有分歧或重要决策才写。

写好 ADR 的几个要点

记 Context、不只记 Decision

  • 坏的:选 Qdrant
  • 好的:我们当时面临 X 约束、考虑 Y 候选、基于 Z 数据选了 Qdrant

没 context 的 ADR 是无法复盘的——未来想改决策时不知道为什么老决定这么选。

记 alternatives considered

  • 列几个认真考虑过的替代
  • 每个的优劣
  • 为什么没选

这让 ADR 有历史价值——未来考虑同样替代时不用重来。

记 consequences

  • 正面(为什么选)
  • 负面(接受了什么 trade-off)

诚实列出负面——ADR 不是 PR 文

ADR 的 review 和批准

不是写了就完事——走流程

  1. 起草:提议者写初稿、给相关团队
  2. Review:相关方提意见(通常 1 周)
  3. 讨论会(对重大决策):面对面讨论分歧
  4. 批准:team lead / 架构师签字
  5. 状态:Accepted

每次新的 ADR 发到团队、所有人知晓——不是藏在某个文件夹。

ADR 的状态流转

ADR 有生命周期:

  • Proposed:初稿、在讨论
  • Accepted:通过、执行中
  • Deprecated:不再执行、但保留历史
  • Superseded by ADR-XXX:被新 ADR 取代

老 ADR 不删——保留历史是 ADR 的核心价值。

如何组织 ADR

文件组织:

docs/adrs/
  README.md                # 索引
  0001-use-python.md
  0002-three-pipelines.md
  0003-use-qdrant.md
  0004-use-bge-m3.md
  0005-chunk-size-400.md
  ...

每个 ADR 带编号(0001, 0002...)——易引用("见 ADR-0003")。README 是索引 + 状态列表。

文档化的反模式

  • 只写 "我们选 X":没 context
  • 写了不 review:作者一言堂
  • 写完放一边:新人不知道有
  • 改决策不更新:老 ADR 和现实不符
  • 太细节:每个小参数都 ADR、噪声多

ADR 和代码的联动

ADR 引用代码、代码注释引用 ADR:

python
# See ADR-0005: chunk size 400 for bge-m3 sweet spot
CHUNK_SIZE = 400

这让代码行为可解释——不只是 magic number。

组织成熟度的 indicator

成熟团队的 ADR 实践:

  • 初创期:几份核心 ADR(选型)
  • 成长期:每季度 2-5 份新 ADR
  • 成熟期:有专门的"架构 review"会议、每月讨论提案

ADR 数量不是目标——质量和覆盖度是

ADR 和 postmortem 的对照

  • ADR:事前决策的文档
  • Postmortem:事后事故的文档

两者互补——ADR 防错、Postmortem 修错。成熟团队都有。

ADR 的外部价值

除内部文档、ADR 还可以:

  • 公开(经脱敏):社区贡献、招聘材料
  • 对外演讲:技术分享 / 会议
  • 客户交流(企业 SaaS):展示技术深度

公开的 ADR 让外部人了解团队的工程水平——招聘和影响力的无形资产。

快速起步

新团队如何开始 ADR:

  • Week 1:建 docs/adrs 目录、写第一份(任何已做的决定)
  • Month 1:补 5-10 份主要决策
  • Month 3:每个新决策自觉写 ADR
  • Month 6:ADR 成为团队习惯

从小步起——不要一开始就要求全部补齐

工具和模板

  • 模板:公开的 ADR 模板(Michael Nygard 风格)
  • 工具:adr-tools CLI、自动编号
  • 编辑:Markdown 编辑器、git PR 流程
  • 展示:VitePress / Docusaurus 渲染 ADR 集合

工具不重要——内容是核心。

和本书的关系

本书讨论的每个决策(chunk size、rerank、hybrid)都是某个 ADR 级别的决策。团队内部的 ADR 应该引用本书作为参考——再加上本团队的具体数据和 context。

本书给一般化的 decision framework、ADR 给你团队的具体实例——两者互补。

文档化文化的长期价值

十年老项目的痛点:为什么这样设计没人知道。所有改动变成"我不敢动、怕出事"。

有 ADR 文化的项目相反:

  • 新人一月内理解核心决策
  • 改动前 review 老 ADR、清楚影响
  • 团队迭代速度保持

这不是"多做一份文档"——是组织知识管理的底层基建。投入早、收益几十年。

为什么团队常不做 ADR

几个常见借口:

  • "没时间":每个 ADR 1-2 小时、长期收益大
  • "没必要、一眼看懂代码":代码看行为、不看意图
  • "写了没人看":建索引、在 onboarding 里明确指向
  • "总在变、写了也过时":ADR 就是为了记"当时为什么"、变了写新的

这些都是能解决的问题——不做 ADR 的真实原因是组织对文档化不重视。这要从文化层面改。

4.13 跨书关联:批 + 流 + 闭环是大数据架构的通用语言

RAG 的三链路架构并非 RAG 独有。

  • 大数据 Lambda 架构(Nathan Marz, 2011)把分析系统拆成 batch layer + speed layer + serving layer,是三链路的早期原型。
  • MLOps 里的"离线训练 + 在线推理 + 线上反馈"三循环是同构。MLFlow/Kubeflow 的 pipeline 抽象和 RAG 离线链路直接对应。
  • 推荐系统的召回 + 粗排 + 精排 + 反馈闭环和 RAG 的召回 + rerank + 反馈闭环同构到可以互相迁移。实际上做搜索推荐出身的工程师看 RAG 会觉得"换了套词汇讲老故事"。

理解这层同构的好处:搜索/推荐领域 20 年积累的工程经验(特征工程、A/B 测试、online learning、冷启动)都可以迁移到 RAG。第 10 章讨论向量索引时会回到这层关联——HNSW/IVF 等索引结构在搜索系统里早就成熟,RAG 只是把它们放进了 LLM 工作流。

4.14 本章小结

  1. 生产 RAG 由离线索引在线检索反馈闭环三条链路组成,三者时序解耦但数据耦合。
  2. 离线链路追求吞吐和一致性——分段可重入、原子发布、版本化索引。
  3. 在线链路追求延迟和可用性——并行召回、rerank 批处理、每阶段降级预案。
  4. 反馈闭环追求时效和信号质量——四种信号源互补、trace 转训练集是数据工程重点。
  5. 三条链路间通过 chunk_id / embedding 向量空间 / metadata schema / 日志 schema 四份数据契约耦合。
  6. 演进顺序:先跑通在线离线稳定化反馈闭环优化迭代。跳步必然踩坑。

下一章起进入第二部分"知识进入系统"——从文档解析开始,沿离线链路逐层拆解。第 5 章讲如何把 PDF、HTML、Markdown、代码仓库变成可索引的结构化文本。

基于 VitePress 构建