应用系统体系架构-03:RAG
RAG(Retrieval-Augmented Generation) 把外部资料检索与模型生成连接起来。系统先找到与问题有关的文档,再把证据组织进上下文,让模型依据这些内容回答。它适合私有知识、频繁更新的信息和需要解释依据的任务。
RAG 的质量取决于整个数据链路。原文解析错误会影响切块,切块错误会影响召回,召回噪声会影响回答。因此,不能只通过更换大模型或增加 Top-K 来解决全部问题。
技术定位
预训练模型可能缺少企业私有知识,也可能不知道最新政策、代码或库存。RAG 通过更新外部索引提供资料,微调(Fine-tuning) 通过训练改变模型参数,常用于特定任务能力、格式或行为调整。
| 维度 | RAG | 微调 |
|---|---|---|
| 更新对象 | 文档、索引与检索规则 | 模型参数 |
| 信息更新 | 可以增量更新资料 | 通常需要训练和重新部署 |
| 依据追踪 | 可保留文档与位置 | 参数中的知识较难直接追踪 |
| 主要用途 | 外部知识、证据和权限过滤 | 行为、风格与任务能力 |
| 工程成本 | 摄取、检索、索引和生成 | 数据集、训练、部署和评测 |
两者可以组合。微调并非不能学习知识,RAG 也不保证回答一定正确。选型时要考虑知识变化频率、可审计性、延迟和运维成本。
参数微调
RAG 和 MCP 可以为一次请求补入外部知识,但通常没有更新基础模型参数。全参数微调通过训练修改所选模型参数,LoRA 则冻结基础权重,用较少的可训练参数表示权重增量。
设 ,LoRA 可以写为:
其中 、, 是较小的秩, 是缩放系数。训练 A、B 后,可以在推理时使用这项增量或在合适条件下合并到权重中。
对一个 矩阵,完整参数有 个;取 ,两个低秩矩阵共有 个可训练参数。这说明某个目标权重的训练参数规模如何减小,不代表整套训练只需存储这两个矩阵,基础模型仍需参与计算。
低秩更新不是把“私有知识对应的一块权重”切出来,也不意味着所有知识都按领域存放在固定区域。微调改变参数中的行为与能力,RAG 保留可更新的外部证据;需要频繁更新、核对来源或删除的业务事实,仍可以与检索和工具结合。
系统架构
RAG 包含离线索引与在线查询两条管道。

离线管道读取文档,解析并清洗内容,生成分块,计算 Embedding,再写入向量库。在线管道处理问题,检索候选,排序并组装上下文,最后调用模型生成回答。
| 组件 | 职责 |
|---|---|
| Parser / Loader | 从 PDF、Markdown、HTML 等资料取得结构化内容 |
| Splitter | 控制分块长度、边界、重叠和来源位置 |
| Embedder | 将查询与文档映射到可比较的向量空间 |
| Vector Store | 存储向量、正文和元数据并执行检索 |
| Router | 选择数据源或判断是否需要检索 |
| LLM Driver | 装配请求、调用模型并解析结果 |
文档标识、分块标识、标题路径、版本、页码和权限应贯穿两条管道。只保存向量而丢失原文及来源,会使回答难以验证,也不便处理删除和更新。
最小实验可以先用内存保存 (原文, 向量):读取一组长短不同的自然段,为每段生成 Embedding;问题也经过兼容编码,计算相似度并降序取 Top-N,最后把入选正文与问题送给生成模型。
这条链路中有 两种模型职责:Embedding 模型生成定长向量,生成模型理解问题和证据并输出回答。它们可以由同一服务托管,但不是同一个接口,也不要求使用同一种模型。调试时分别检查正文、向量维数、召回片段和最终回答,才能判断问题出在检索还是生成。
数据解析
PDF 解析需要处理双栏顺序、编码、扫描图像、页眉页脚和表格。HTML 解析需要区分正文与导航、广告或脚本,Markdown 则需要保留标题、列表、代码和表格边界。
清洗的目标是删除噪声并保留意义,而不是把全部内容转成一串无结构文本。过滤不可打印字符可以解决部分控制字符问题,但不能修复错误阅读顺序,也不能恢复被摊平的表格。
1 | def clean_text(text: str) -> str: |
这只是基础清洗步骤。工程中还需识别重复页眉、断词、乱码和空段落,并抽样与原文件对照。扫描文档需要 OCR,复杂版面则可能需要布局模型;解析失败应记录并进入修复流程,不能静默把错误文本写入知识库。
PDF 的视觉呈现与文本层可能不同:被遮盖、隐藏或排版异常的文字也可能进入抽取结果。解析应保留原页或原位置的核对能力,检查与任务无关的指令性内容,不能把文档自带的评价或操作要求当成系统规则。
窗口切块
切块控制检索粒度和上下文长度。固定窗口用 chunk_size 规定每块长度,用 overlap 保留相邻块共享内容,步长为:
要求 chunk_size > 0 且 0 ≤ overlap < chunk_size。重叠缓解跨块句子或实体被截断的问题,但会增加存储、重复召回和上下文冗余。
1 | def split_windows( |
字符数量与 Token 数量不同。实际模型预算应使用相应分词器计算。块过小可能缺少解释所需背景,块过大则容易把多个主题混在一起,降低匹配精度并增加生成输入成本。
向量输出长度固定,不表示返回给生成模型的证据也同样短。若一个块很长,Top-3 仍要拼入三个长正文,既增加预算,也可能让答案所需几句信息被大量无关文本掩盖。切块同时服务于编码、检索粒度和最终上下文。
重叠适合相邻段确实有依赖的资料;若每段本来就是独立事实,重叠可能只增加重复。readlines() 得到的是文本行,不自动等于完整语义段落,长段也仍需按模型输入上限再次划分。
语义切块
语义切块 依据文档结构划分内容。例如 Markdown 标题可以形成章节边界,代码块和表格应尽量完整保留。较长章节还需要二次切分。
一个分块对象可以保存以下字段:
1 | { |
不能只用标题字符串作为字典键,因为同名标题可能出现在不同章节并被意外合并。应使用文档 ID、标题路径和分块位置构造稳定标识,并保留原始顺序。
标题、单位、脚注和相邻说明也可能影响理解。检索到某个表格行时,应同时提供其表头、适用条件与必要注释,避免只返回孤立数值。
文本向量
Embedding 将文本映射成固定维数向量。查询和文档必须使用兼容的模型与版本,才能在同一向量空间中比较。向量维数由模型决定,不能把某个示例的 768 或 1536 维当作固定要求。
余弦相似度为:
若向量已归一化,余弦相似度可以直接用点积计算;零向量没有有效的余弦方向,应拒绝或单独处理。文本相似并不意味着业务事实相同,例如两个不同版本号可能具有很接近的向量。
本地 Embedding 服务有助于控制数据处理位置,但仍有硬件、能耗和维护成本。BGE 等模型可以作为候选,选型需对目标语言、领域、长短文本和查询形式做评测。
Token 级表示与整段向量也不同。长度为 的文本可能产生 的隐藏表示,再由相应模型形成 维的段落向量;段落有三个词,不意味着原表示就是三维。模型的输入长度、隐藏维数和输出维数是不同参数。
两个向量同为 128 维,也不证明它们处于同一语义空间。查询与文档需要兼容的训练与编码配置;文本、图像等跨模态检索则需要经过对齐的模型,而不是随意把几个独立模型的输出放进同一个库。
距离度量
余弦相似度衡量方向接近程度:1 表示同向,0 表示正交,-1 表示反向。这是向量几何关系,不是“答案正确概率”;0 也不必被解释成两个业务对象完全无关。
常见距离还包括:
它们分别是 Manhattan 距离和 Euclidean 距离。Hamming 距离适合比较位串等离散表示,Mahalanobis 距离还涉及协方差结构,不能认为所有向量库对任何表示都支持相同度量。
相似度与距离的换算要按具体定义。对于单位归一化向量,有 ,因而两种排序在该条件下对应;一般情况下不能直接把相似度称为距离的倒数或导数。选择的度量应与 Embedding 模型和索引配置一致。
内存向量库
内存原型可以用 NumPy 保存归一化向量,并通过矩阵运算扫描全部文档。它能清楚展示向量库的基础行为。
1 | import numpy as np |
若文档数为 、向量维数为 ,扫描约需要 运算,排序还产生额外成本。对于小数据,它是有效基线;大规模高并发服务则需要索引、过滤和存储治理。
向量索引
工业向量库提供持久化、元数据过滤、近似最近邻搜索和服务管理。Qdrant、Milvus 等系统可用于构建这些能力,但具体选择需考虑数据规模、过滤需求、吞吐、运维和一致性。
HNSW 用多层图组织向量,在较高层快速接近目标,再在低层细化搜索。它用近似结果换取较低延迟,不能保证对任意数据都具有固定最坏复杂度。
| 参数 | 主要影响 |
|---|---|
m |
图连接规模、内存和索引质量 |
ef_construct |
建图搜索范围、构建时间和召回 |
查询 ef |
在线搜索范围、延迟和召回 |
| 精确搜索阈值 | 小集合是否直接扫描 |
增大参数通常会增加资源消耗。应对照精确检索计算 Recall@k,并同时观察 P95 延迟和内存,而不是只测一条查询的速度。
HNSW 与跳表的类比主要在于“较高层快速移动,较低层细化”。它是分层近邻图,不是保证把空间递归分成互不重叠的簇。高层通常较稀疏,低层包含更多点;搜索依据邻接关系逐步靠近查询。
m 主要控制每个节点连接的规模,不表示每层有 m 个“小世界”。ef_construct 控制构建时搜索范围,查询 ef 控制在线候选范围。ANN 有召回与延迟权衡,不提供对任意数据都固定为 的最坏保证。
基础检索
最小 RAG 流程可以表达为以下五步:
1 | 将问题转换成查询向量。 |
真正服务还需要空召回处理、权限过滤、超时、长度限制和引用核对。把无关文本拼进上下文,会使模型围绕错误材料生成看似合理的回答。
检索结果也不是最终答案本身。制度查询可能同时需要适用对象、条件和截止时间,生成器要把多段资料与用户情况组合起来。若知识不足,不能把模型推导出的新内容伪装成文档明确规定。
在“猫的知识”一类小实验中,即使 Top-N 的正文已经正确进入上下文,小型生成模型也可能错误理解或忽略它们。应同时展示候选片段、分数和回答,分别验证检索与证据使用。
Naïve RAG 常见问题包括检索不到、候选太杂和长上下文中关键证据被忽略。诊断时应先确认正确答案是否在库中,再检查切块、召回、排序和生成,不能直接把全部失败归因于模型。
查询改写
用户问题往往包含省略、错字、上下文指代或非专业用语。Query Rewriting 将其改写为适合检索的明确问题,可能展开缩写、补充已确认上下文或拆成多个子查询。
例如“这个版本怎么升级?”缺少产品和当前版本。系统可以从已有对话中取得明确产品;无法确定时应澄清,不能自行选择一个版本并写入检索问题。
也可以用多个角度生成检索候选,例如把“怎么连不上”分别展开为网络、认证或服务可用性相关查询。它们只是调查方向,不能未经确认就认定用户正在遭遇某一种故障。多角度结果需要去重,并保留原始问题。
改写应保留原始查询以及新增信息的依据。它增加一次推理成本,所以可以只对短问题、歧义问题或召回失败启用,并比较改写前后的证据召回。
假设文档
HyDE 先生成一段可能回答问题的假设文档,再用其向量检索真实资料。这利用较完整的文档语义缓解短查询与长答案之间的表达差异。
1 | 问题:分布式事务失败后怎样恢复? |
假设文档可能含有错误细节,不能作为最终证据。它只辅助定位真实文档。额外生成会提高延迟,适合准确性要求较高且能接受该成本的查询。
可以把假设文档理解成检索的“中间表达”,但不是任意插入一个文本就一定更靠近答案。生成内容应尽量与问题领域相符,并以召回的真实资料验证效果;错误实体或虚构术语可能把检索带向其他方向。
混合检索
稠密向量擅长语义匹配,BM25 依据词项统计捕捉字面匹配。版本号、产品型号、股票代码、序列号等内容可能需要精确词项,因此两路召回可以互补。
BM25 的常见形式为:
是词频, 是文档长度,avgdl 是平均长度, 与 控制词频饱和和长度归一化。实际系统还需合适的分词器和字段设计。
向量相似度与 BM25 分数不处于相同标尺,不能直接相加。可以对分数校准,也可以使用基于排名的融合方法。
例如查询软件版本 2.4.1 时,向量检索可能同时召回 2.4.0、2.4.2 的材料;关键词或版本元数据可以保留这一精确条件。融合不会自动把所有不满足版本的候选删除,硬条件仍需明确过滤。
Dense 与 Sparse 描述表示方式,不描述召回数量:Dense Embedding 通常多数坐标非零,词项稀疏表示只有少数词项有权重。两路各返回多少候选由检索参数决定,两者都可以返回多或少的结果。
排名融合
倒数排名融合(RRF) 只利用各路候选中的排名:
从 1 开始, 是平滑常数。未出现在某路结果中的文档不贡献该路分数。常见的 会减少第一名对第三名等相邻位置的过度优势,具体值仍应根据任务验证。
若同一文档在两路分别排名 2、3, 时贡献为 。两路原分数即使分别是余弦值和较大的 BM25 分数,也可以在排名层面相加。BM25 包含词频、逆文档频率和长度处理,不只是统计匹配次数。
1 | from collections import defaultdict |
融合前要统一文档标识,并处理同文档重复分块。相同证据反复出现会占用上下文,也可能让结果被重复计分。
候选重排
Bi-Encoder 分别编码查询与文档,文档向量可以预计算,适合大规模召回。Cross-Encoder 把查询与候选联合输入模型,让两者在同一次编码中交互,适合对有限候选精排。

双编码器本身也可以使用注意力,区别在于查询和文档是否联合编码。常见流程是先用低成本检索得到较大候选集,再用 Cross-Encoder 选出少量高相关证据。
精排成本随候选数量和长度增长。分类模型输出的 logits 或概率需要按模型定义解释,不能假设所有模型分数都在同一范围。图中的数值表示示意匹配,并非适用于任意问题的置信度。
可以先召回 50 个候选,再对查询与每个候选联合编码,选出 5 个作为证据。这些数量是示例:先扩大候选避免漏掉,再把较昂贵的联合计算限制在有限集合内。
Cross-Encoder 通常由训练得到的评分头输出相关性分数,并不是简单取某个 Attention 权重作为最终得分。若直接对全库每份文档联合推理,延迟和算力成本会明显增加。
上下文压缩
检索后的优化包括去重、重排、过滤和压缩。应先删除无关候选,再控制重复证据,最后在预算内保留能够支持回答的内容。
LLMLingua 等压缩方法可以减少 Token,但压缩后必须保留否定词、单位、数字、适用条件和文档标识。仅用压缩比评价效果,可能鼓励删除必要信息。
相关证据可以放到更容易利用的位置,避免长上下文中间包含大量噪声。生成器还需要知道哪些内容属于同一文档或版本,不能把相互矛盾的材料混成一个结论。
可以对已按相关性降序选出的证据采用首尾交替安排。例如原排名为 [1, 2, 3, 4, 5],可组织为 [1, 3, 5, 4, 2],让第一、第二名分别位于两端。
1 | def place_evidence_at_ends(ranked_items: list) -> list: |
这种布局是可以评测的策略,不是所有模型必然受益的规则。需要先过滤噪声,保留来源与时间;具有明确先后语义的条款和步骤不宜因重排而改变含义。
图谱检索
普通 Top-K 检索适合局部事实,对跨文档全局问题可能缺乏整体关系。GraphRAG 从资料中抽取实体和关系,形成社区结构与摘要,再结合图和文本回答问题。

例如总结两家公司三年的合作,需要连接项目、公司、时间和成果,而不是只找到一个最相似段落。社区摘要可以帮助全局归纳,原文片段则用于核对具体事实。
图抽取和摘要同样可能出错,还带来构建与更新成本。实体合并、关系来源和时间变化必须可追踪。图检索与向量检索可以结合,是否值得使用应由问题类型和评测结果决定。
跨段落关系可以用一个两跳例子说明:
1 | 张三 --father_of--> 李四 |
第一条明确表示张三是李四的父亲。沿两跳可说明“张三是该公司员工李四的父亲”,不能推出张三也在该公司工作,更不能倒置父子方向。关系抽取和答案生成都要核对边的方向与证据。
补考、重修、退课等条款也可能在业务上有关联,虽然文本向量未必很接近。保留章节、适用条件和关系有助于补充关联资料;不能仅因出现了这些词就推定所有学校都允许同一处理路线。
k-hop 邻域与社区是不同概念。前者按边数扩展,后者依据图中的连接结构分组;社区层级参数不一定等于一跳、两跳。社区报告可以摘要相关实体与关系,仍需保留原始证据。
同一个人可能以姓名、职位或其他名称出现,但相同职位也可能在不同公司、时期对应不同人。实体归一化需结合主体、时间和证据,不能只靠模型把类似字符串合并。普通检索若取得相同事实,也可能回答两跳问题,GraphRAG 的收益需要实际比较。
多模态资料
财报中的表格和统计图包含空间关系。把表格按文本直接摊平,可能丢失列名、单位、行列对应和合并单元格,导致模型错误比较数值。
表格应保留标题、表头、单位、脚注和页码。简单表格可以转换为 Markdown;复杂表格还需单独记录合并关系。统计图可以生成文字说明,但描述必须保留可核对数值,不能仅保存“增长明显”这样的概括。
LayoutLMv3 等文档理解模型可以辅助版面和内容处理。选用时要考虑文本是否可直接提取、是否扫描、布局复杂度及任务类型,不能用一个布局模型替代全部解析与验证。
多模态处理可以走两条路线:先用 OCR 或视觉模型把表格、图像转成可核对的文本,再使用文本 Embedding;或者直接用经过对齐的多模态编码器做跨模态检索。前者的 Caption 与表格转换可能丢失细节,后者也需要能向生成器返回原资源。
因此应把转换文本、原图或原表格位置关联起来,尤其保留单位、表头、脚注和关键数值。生成了一段流畅描述,不表示原模态的全部信息都已保存。
自动评估
RAG 评估需要区分 检索质量 和 生成质量。Ragas 等框架提供不同维度的指标,具体定义应与所用版本一致。
| 维度 | 需要检查的内容 |
|---|---|
| 上下文召回 | 应有证据是否进入候选 |
| 上下文精度 | 候选是否集中于相关内容 |
| 忠实性 | 回答是否受给定证据支持 |
| 回答相关性 | 是否回答用户真正的问题 |
| 答案正确性 | 结论是否符合标注或业务事实 |
| 服务指标 | 延迟、成本、错误率和更新时效 |
黄金集应包含正常问题、无答案问题、跨文档问题、权限边界和文档更新。自动裁判可能存在偏差,需要人工抽样与硬断言校准。
改变切块、Embedding、Top-K、重排或模型后,应运行回归。检索分数提高不一定意味着回答更好,端到端指标和关键类别都需要比较。
增量更新
生产知识库需要处理新增、修改和删除。文档 ID、内容哈希与版本可用于识别变化;更新后必须删除或替换旧分块,并同步索引、元数据和缓存。
1 | 读取文档标识与内容哈希。 |
只向库中追加新内容会留下过时证据。更新流程还需要明确一致性窗口,让在线查询知道自己使用的是哪个版本。
哈希可以定位内容变化,但需要选择合适算法。把字符数值相加再取模,会让 ab 与 ba 等不同内容得到相同值;这种弱指纹不适合作为唯一更新依据。SHA-256 等内容指纹降低碰撞风险,关键数据仍需配合标识、版本和完整性策略。
Merkle Tree 将子节点摘要逐层汇总,某个文件或分块变化会影响其祖先路径,便于定位局部更新。文本不变也不一定可以跳过重建:解析器、切块规则或 Embedding 模型版本变化,都可能使旧索引失效。更新键应同时包含内容与生成配置,并同步替换旧分块和索引。
链路观测
一次查询应能还原问题、改写、候选、分数、精排结果、最终上下文、模型配置和回答。Trace 帮助判断延迟来自 Embedding、检索、精排还是生成,也帮助定位错误证据从哪里进入回答。
观测工具可以使用 OpenTelemetry、Phoenix 等实现。日志应脱敏并控制访问,尤其不能把所有私有文档和用户内容直接写入公共监控平台。
长上下文与 RAG 需要依据任务比较。小文档集合可能适合直接输入,较大或频繁更新的库需要检索。比较应包括总 Token、索引维护、证据质量、权限过滤和端到端延迟,而不是只比较模型窗口上限。
安全与缓存
多租户系统应在检索阶段实施权限过滤,并核对最终证据集合。仅在 Prompt 中要求“不要泄露其他用户的数据”无法完成隔离。缓存、日志和引用链接也必须遵守相同权限。
检索文档可能包含间接 Prompt Injection,应作为资料处理,不能让其中的命令获得执行权。回答生成与工具执行需要独立的控制边界。
语义缓存 利用问题相似度复用结果,但相似问题未必答案相同。实体、时间、租户、授权和知识版本都可能改变结果。缓存键应包含这些因素,并设置有效期与失效机制;相似度阈值需要根据误命中代价评测。
自主检索
Agentic RAG 根据问题决定是否检索、选择哪个库,以及是否继续查找证据。问候和简单常识不必总是检索,复杂业务问题则可能需要多数据源或多轮查询。
Self-RAG 与 Corrective RAG 分别研究生成与检索反馈,不能仅用同一个重试循环代替全部算法。工程上可以先检查证据相关性和充分性,再选择改写、补充检索、外部查询或拒答。
路由结果应限定在已注册的数据源中,未知意图允许澄清或降级。循环必须有最大轮次和成本预算,模型自评“证据足够”也应尽量结合来源、内容与可验证条件。
MCP 可以统一外部资料与检索工具的接入,但不自动完成解析、切块、向量化和索引更新。把协议、算法与业务权限分开,有利于独立测试。
工程实践
Cursor、Windsurf、Bolt.new 等工具可以辅助实现检索模块。使用这些工具时,应明确数据接口、BM25 与向量召回格式、RRF、去重、精排和失败处理,并提供可验收的固定测试数据。开发流程仍包括需求、生成、审核、验证和迭代。
Unstructured、LlamaParse 等负责解析,LangChain、LlamaIndex 提供编排,Ollama 提供本地推理,Qdrant、Milvus 承担向量检索,Ragas 与 Phoenix 支持评估和观测。各组件需要稳定接口,不能任意拼接并假定数据含义一致。
综合实验应保存文档、参数、评测集与结果,检查数据预处理、多路召回、重排序和自动评估。工程师需要理解算法和业务约束,才能判断生成代码是否正确。
RAG 的核心是让证据从原文到回答的每一步转换都可检查。数据质量、检索策略、生成约束和持续评测需要共同维护。

