大多数 RAG 架构图只有三个方框:一个用户、一个向量数据库和一个 LLM。这张图足以在会议上解释思路,却帮不了任何运维这个系统的人,因为决定回答质量和安全性的决策都不在图上:文档如何分块,用哪个嵌入模型,谁能看到哪个分块,结果如何排序,以及大家怎么知道回答是好的。
本文把一个检索增强生成应用的完整图景画成围绕共享模型和存储的两条路径,然后介绍应该标注什么,以及最常见的错误。
不是一条路径,而是两条
这个术语来自 Lewis 等人(2020),他们把一个语言模型与维基百科的稠密向量索引结合起来,由检索器在生成时进行搜索。生产环境中的大多数 RAG 应用保留了这个思路,但不会把各部分放在一起训练:检索相关段落,放进提示词,再让模型据此作答。
因此,每个 RAG 应用都有两条路径:
- **摄取(ingestion)**在后台持续运行。它把文档变成分块、向量和元数据。
- 查询每个问题运行一次。它把一个问题变成带来源的回答。
两条路径在共享部分汇合:两条路径上版本必须一致的嵌入模型、向量索引和元数据存储。只画查询路径的图,等于藏起了半个系统和大部分 bug。
摄取路径
- 数据源与变更检测。 连接器、Webhook 或爬虫发现新增、修改和删除的文档,并把任务放进队列。删除和权限变更也是任务。
- 解析。 在保留结构的前提下提取文本:标题、列表、表格,以及标题、URL、负责人、最后更新时间和访问控制列表等元数据。
- 分块。 先按文档结构切分,再按大小切分。分块大小和重叠是需要用你自己的问题来调整的参数,而不是通用的数字。在每个分块中保留标题路径,让一个段落单独拿出来也能读懂。
- 嵌入。 只调用一个嵌入模型,并固定它的版本。把模型名称和版本与每个向量一起保存。不同模型产生的向量不可比较,所以更换模型就意味着要为整个语料库重新生成嵌入。
- 建立索引。 对每个分块执行 upsert,带上向量、文档 ID、分块编号、版本和访问标签。如果计划使用混合检索,就在向量索引旁边再维护一个关键词索引。
- 刷新与删除。 让 upsert 具备幂等性,删除已移除文档的分块,并跟踪新鲜度:数据源中的一次变更要多久才能进入索引。
查询路径
- 识别用户。 在检索开始之前解析出用户所属的组。
- 改写问题(可选)。 像“那外包人员呢?”这样的追问,需要借助对话上下文改写成一个独立的查询。
- 检索。 向量检索找到语义相近的段落;关键词检索找到错误码、产品名这类精确的词。混合检索同时运行两者并合并结果列表,通常使用倒数排名融合(reciprocal rank fusion),按各列表中 1/(k + 排名) 的总和给每个文档打分。RRF 的原始论文把 k 固定为 60。
- 在查询内部按权限过滤。 把用户所属的组作为访问标签上的过滤条件传入,确保用户无权查看的分块永远不会进入提示词。检查你的存储如何应用过滤条件。pgvector 的文档指出,使用近似索引时,过滤是在扫描索引之后进行的:在 HNSW 默认的
ef_search为 40 时,一个匹配 10% 行的过滤条件平均只返回约 4 行。迭代扫描、部分索引或分区可以解决这个问题。 - 重排序。 多检索一些,例如 50 个候选,让重排序模型根据问题为它们打分。只保留最好的几个,例如 8 个。
- 组装提示词。 在令牌预算内放入指令、选中的分块(带 ID 和标题)以及问题。要求按分块 ID 标注引用。
- 生成并引用。 调用 LLM,以流式方式返回回答,并把分块 ID 转换成用户可以核对的链接。
两条路径之外
- 把检索到的文本当作不可信输入。 文档中可能包含针对模型的指令。OWASP 将其列为间接提示注入。让模型能接触的工具和数据,不超过用户本人的访问权限。
- 追踪每一个请求。 记录改写后的查询、检索和重排序后的分块 ID 及其分数、提示词版本、模型版本,以及每一步的延迟。
- 用固定的问题集做评估。 每次修改分块、模型、检索或提示词时都运行一遍。RAGAS 论文提出了三个有用的维度:对检索上下文的忠实度、回答的相关性和上下文的相关性。还要检查预期的来源是否出现在前几个结果中。
- 谨慎使用缓存。 缓存查询的嵌入通常是安全的。缓存回答则只有在按权限集分别缓存时才安全。
图
图表内容
- Ingestion path · continuous
- Source documents
- Ingestion jobs
- Ingestion worker
- Embedding model (pinned)
- Vector index
- Metadata + ACLs
- LLM
- Query path · per request
- User
- RAG API
- Reranker
- change events
- jobs
- embed chunks
- upsert chunks + ACL tags
- document metadata
- question · answer + sources
- embed question
- hybrid search + ACL filter
- user's groups
- top 50
- prompt with top 8
这张图的读法如下:
- 左侧,摄取。 数据源的变更变成队列中的任务。摄取 worker 解析每个文档并分块,调用嵌入模型,把分块和向量连同访问标签 upsert 到向量索引,再把文档元数据写入 Postgres。
- 中间,共享的模型和存储。 版本固定的同一个嵌入模型同时服务两条路径。LLM 也在这里,只有查询路径会调用它。
- 右侧,查询。 RAG API 查出用户所属的组,为问题生成嵌入,带着权限过滤运行混合检索,把 50 个候选发给重排序器,再用最好的 8 个分块调用 LLM。
- 左下角。 一个按步骤划分的延迟预算示例,这样一次慢回答就能追溯到超出预算的那一步。
应该标注什么
- 每个模型节点上的模型版本,包括嵌入模型。
- 分块参数,写在摄取 worker 上或注释里。
- 权限在哪里生效,写在检索连线上,而不是脚注里。
- 步骤之间流转的结果数量: 检索 50 个,进入提示词 8 个。
- 新鲜度和延迟目标: 数据源的变更多久后可被检索,以及查询每一步的预算。
常见错误
- 只有一个叫“AI”的方框。 检索、重排序和生成的失败方式各不相同,需要分别画成方框。
- 没有摄取路径。 过时或缺失的回答往往从这里开始。
- 没有权限。 如果索引中包含面向不同受众的文档,一张没有访问标签的图描述的就是一次数据泄露。
- 没有评估。 没有问题集,对提示词或分块大小的每一次修改都只是猜测。
- 嵌入版本混杂。 用新模型只为半个语料库重新生成嵌入,会在不知不觉中破坏检索。
什么时候 RAG 不是答案
- 答案在结构化数据里。 查询数据库或 API,而不是为表格的行生成嵌入。
- 任务需要整篇文档。 总结一份合同时,只要放得下,把整份合同放进提示词效果更好。
- 语料库非常小。 几页内容可以直接放进提示词。
- 你需要的是不同的行为,而不是更多的知识。 那是提示词或微调的问题。
关于使用队列的事件驱动摄取,请参阅事件驱动架构图指南。
在 Archboard 中构建
- 使用 Google 或 GitHub 登录,并新建一张图。
- 添加两个基础节点包中的 框架:左边是“Ingestion path”,右边是“Query path”。
- 在摄取框架中,为源文档添加一个数据节点包中的 对象存储库,为摄取任务添加一个数据节点包中的 队列,为摄取 worker 添加一个 C4 模型节点包中的 容器。
- 在中间,为嵌入模型添加一个 ML 节点包中的 模型,再添加一个 ML 节点包中的 向量索引、一个存放元数据和权限的数据节点包中的 Postgres,以及表示 LLM 的第二个 模型。
- 在查询框架中,为用户添加一个 C4 模型节点包中的 人员,为 RAG API 添加一个 容器,为重排序器添加一个 ML 节点包中的 模型。
- 给每条连线标注流经的内容,包括“top 50”和“top 8”,并把延迟预算写进一个基础节点包中的 Markdown 块。
这个示例共用 13 个图表节点,在 Free 套餐的限制之内。如果你使用 AI 智能体,它可以根据你对两条路径的描述,通过 MCP 为你画出第一版;请参阅如何用 AI 智能体绘制架构图。通用方法请参阅如何绘制软件架构图。
常见问题
做 RAG 需要专门的向量数据库吗? 不需要。在你已经在运行的数据库中使用向量扩展(例如 PostgreSQL 的 pgvector),或者使用支持向量的搜索引擎,对很多应用来说就足够了。根据语料库规模、过滤需求以及团队擅长运维的技术来选择。
分块大小应该设多少? 没有通用的数字。先按标题和段落切分,再用你自己的问题集比较几种大小和重叠设置。
权限检查应该放在哪里? 在摄取时,把每个文档的访问列表复制到它的分块中;在检索时,按这个列表进行过滤。永远不要依靠提示词来隐藏内容。
如何评估一个 RAG 应用? 维护一组固定的真实问题,并为每个问题标出预期来源。衡量检索能否找到这些来源,以及回答是否忠实于检索到的上下文、是否与问题相关。
LLM 应该调用工具而不是使用 RAG 吗? 对于实时数据或结构化数据,是的:直接查询权威系统的工具,胜过一份副本构成的索引。很多应用两者都会使用。