RAG(Retrieval-Augmented Generation) 把外部资料检索与模型生成连接起来。系统先找到与问题有关的文档,再把证据组织进上下文,让模型依据这些内容回答。它适合私有知识、频繁更新的信息和需要解释依据的任务。

RAG 的质量取决于整个数据链路。原文解析错误会影响切块,切块错误会影响召回,召回噪声会影响回答。因此,不能只通过更换大模型或增加 Top-K 来解决全部问题。

技术定位

预训练模型可能缺少企业私有知识,也可能不知道最新政策、代码或库存。RAG 通过更新外部索引提供资料,微调(Fine-tuning) 通过训练改变模型参数,常用于特定任务能力、格式或行为调整。

维度 RAG 微调
更新对象 文档、索引与检索规则 模型参数
信息更新 可以增量更新资料 通常需要训练和重新部署
依据追踪 可保留文档与位置 参数中的知识较难直接追踪
主要用途 外部知识、证据和权限过滤 行为、风格与任务能力
工程成本 摄取、检索、索引和生成 数据集、训练、部署和评测

两者可以组合。微调并非不能学习知识,RAG 也不保证回答一定正确。选型时要考虑知识变化频率、可审计性、延迟和运维成本。

参数微调

RAG 和 MCP 可以为一次请求补入外部知识,但通常没有更新基础模型参数。全参数微调通过训练修改所选模型参数,LoRA 则冻结基础权重,用较少的可训练参数表示权重增量。

设 W0∈Rdout×dinW_0\in\mathbb{R}^{d_{out}\times d_{in}},LoRA 可以写为:

W′=W0+ΔW,ΔW=αrBA,W'=W_0+\Delta W,\qquad \Delta W=\frac{\alpha}{r}BA,

其中 A∈Rr×dinA\in\mathbb{R}^{r\times d_{in}}、B∈Rdout×rB\in\mathbb{R}^{d_{out}\times r},rr 是较小的秩,α/r\alpha/r 是缩放系数。训练 A、B 后,可以在推理时使用这项增量或在合适条件下合并到权重中。

对一个 4096×40964096\times4096 矩阵,完整参数有 16,777,21616{,}777{,}216 个;取 r=5r=5,两个低秩矩阵共有 5(4096+4096)=40,9605(4096+4096)=40{,}960 个可训练参数。这说明某个目标权重的训练参数规模如何减小,不代表整套训练只需存储这两个矩阵,基础模型仍需参与计算。

低秩更新不是把“私有知识对应的一块权重”切出来,也不意味着所有知识都按领域存放在固定区域。微调改变参数中的行为与能力,RAG 保留可更新的外部证据;需要频繁更新、核对来源或删除的业务事实,仍可以与检索和工具结合。

系统架构

RAG 包含离线索引与在线查询两条管道。

RAG 双管道

离线管道读取文档,解析并清洗内容,生成分块,计算 Embedding,再写入向量库。在线管道处理问题,检索候选,排序并组装上下文,最后调用模型生成回答。

组件 职责
Parser / Loader 从 PDF、Markdown、HTML 等资料取得结构化内容
Splitter 控制分块长度、边界、重叠和来源位置
Embedder 将查询与文档映射到可比较的向量空间
Vector Store 存储向量、正文和元数据并执行检索
Router 选择数据源或判断是否需要检索
LLM Driver 装配请求、调用模型并解析结果

文档标识、分块标识、标题路径、版本、页码和权限应贯穿两条管道。只保存向量而丢失原文及来源,会使回答难以验证,也不便处理删除和更新。

最小实验可以先用内存保存 (原文, 向量):读取一组长短不同的自然段,为每段生成 Embedding;问题也经过兼容编码,计算相似度并降序取 Top-N,最后把入选正文与问题送给生成模型。

这条链路中有 两种模型职责:Embedding 模型生成定长向量,生成模型理解问题和证据并输出回答。它们可以由同一服务托管,但不是同一个接口,也不要求使用同一种模型。调试时分别检查正文、向量维数、召回片段和最终回答,才能判断问题出在检索还是生成。

数据解析

PDF 解析需要处理双栏顺序、编码、扫描图像、页眉页脚和表格。HTML 解析需要区分正文与导航、广告或脚本,Markdown 则需要保留标题、列表、代码和表格边界。

清洗的目标是删除噪声并保留意义,而不是把全部内容转成一串无结构文本。过滤不可打印字符可以解决部分控制字符问题,但不能修复错误阅读顺序,也不能恢复被摊平的表格。

1
2
3
4
5
6
7
def clean_text(text: str) -> str:
"""保留常规可打印字符及有意义的换行、制表符。"""
cleaned = "".join(
char for char in text
if char.isprintable() or char in "\n\t"
)
return cleaned.strip()

这只是基础清洗步骤。工程中还需识别重复页眉、断词、乱码和空段落,并抽样与原文件对照。扫描文档需要 OCR,复杂版面则可能需要布局模型;解析失败应记录并进入修复流程,不能静默把错误文本写入知识库。

PDF 的视觉呈现与文本层可能不同:被遮盖、隐藏或排版异常的文字也可能进入抽取结果。解析应保留原页或原位置的核对能力,检查与任务无关的指令性内容,不能把文档自带的评价或操作要求当成系统规则。

窗口切块

切块控制检索粒度和上下文长度。固定窗口用 chunk_size 规定每块长度,用 overlap 保留相邻块共享内容,步长为:

s=chunk_size−overlap.s=\mathrm{chunk\_size}-\mathrm{overlap}.

要求 chunk_size > 0 且 0 ≤ overlap < chunk_size。重叠缓解跨块句子或实体被截断的问题,但会增加存储、重复召回和上下文冗余。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def split_windows(
text: str,
chunk_size: int = 500,
overlap: int = 100,
) -> list[str]:
"""按字符窗口切分,最后一块到达结尾后立即停止。"""
if chunk_size <= 0:
raise ValueError("chunk_size must be positive")
if not 0 <= overlap < chunk_size:
raise ValueError("overlap must be smaller than chunk_size")

chunks = []
start = 0
stride = chunk_size - overlap
while start < len(text):
end = min(start + chunk_size, len(text))
chunks.append(text[start:end])
if end == len(text):
break
start += stride
return chunks

字符数量与 Token 数量不同。实际模型预算应使用相应分词器计算。块过小可能缺少解释所需背景,块过大则容易把多个主题混在一起,降低匹配精度并增加生成输入成本。

向量输出长度固定,不表示返回给生成模型的证据也同样短。若一个块很长,Top-3 仍要拼入三个长正文,既增加预算,也可能让答案所需几句信息被大量无关文本掩盖。切块同时服务于编码、检索粒度和最终上下文。

重叠适合相邻段确实有依赖的资料;若每段本来就是独立事实,重叠可能只增加重复。readlines() 得到的是文本行,不自动等于完整语义段落,长段也仍需按模型输入上限再次划分。

语义切块

语义切块 依据文档结构划分内容。例如 Markdown 标题可以形成章节边界,代码块和表格应尽量完整保留。较长章节还需要二次切分。

一个分块对象可以保存以下字段:

1
2
3
4
5
6
7
8
9
{
"document_id": "handbook-v3",
"chunk_id": "handbook-v3-section-2-part-1",
"title_path": ["开发规范", "数据库访问"],
"text": "查询应使用参数绑定……",
"page": 8,
"version": 3,
"tenant_id": "team-a"
}

不能只用标题字符串作为字典键,因为同名标题可能出现在不同章节并被意外合并。应使用文档 ID、标题路径和分块位置构造稳定标识,并保留原始顺序。

标题、单位、脚注和相邻说明也可能影响理解。检索到某个表格行时,应同时提供其表头、适用条件与必要注释,避免只返回孤立数值。

文本向量

Embedding 将文本映射成固定维数向量。查询和文档必须使用兼容的模型与版本,才能在同一向量空间中比较。向量维数由模型决定,不能把某个示例的 768 或 1536 维当作固定要求。

余弦相似度为:

cos⁡(A,B)=A⋅B∥A∥∥B∥.\operatorname{cos}(A,B)=\frac{A\cdot B}{\lVert A\rVert\lVert B\rVert}.

若向量已归一化,余弦相似度可以直接用点积计算;零向量没有有效的余弦方向,应拒绝或单独处理。文本相似并不意味着业务事实相同,例如两个不同版本号可能具有很接近的向量。

本地 Embedding 服务有助于控制数据处理位置,但仍有硬件、能耗和维护成本。BGE 等模型可以作为候选,选型需对目标语言、领域、长短文本和查询形式做评测。

Token 级表示与整段向量也不同。长度为 LL 的文本可能产生 L×hL\times h 的隐藏表示,再由相应模型形成 dd 维的段落向量;段落有三个词,不意味着原表示就是三维。模型的输入长度、隐藏维数和输出维数是不同参数。

两个向量同为 128 维,也不证明它们处于同一语义空间。查询与文档需要兼容的训练与编码配置;文本、图像等跨模态检索则需要经过对齐的模型,而不是随意把几个独立模型的输出放进同一个库。

距离度量

余弦相似度衡量方向接近程度:1 表示同向,0 表示正交,-1 表示反向。这是向量几何关系,不是“答案正确概率”;0 也不必被解释成两个业务对象完全无关。

常见距离还包括:

d1(x,y)=∑i∣xi−yi∣,d2(x,y)=∑i(xi−yi)2.d_1(x,y)=\sum_i|x_i-y_i|,\qquad d_2(x,y)=\sqrt{\sum_i(x_i-y_i)^2}.

它们分别是 Manhattan 距离和 Euclidean 距离。Hamming 距离适合比较位串等离散表示,Mahalanobis 距离还涉及协方差结构,不能认为所有向量库对任何表示都支持相同度量。

相似度与距离的换算要按具体定义。对于单位归一化向量,有 ∥x−y∥22=2(1−cos⁡θ)\lVert x-y\rVert_2^2=2(1-\cos\theta),因而两种排序在该条件下对应;一般情况下不能直接把相似度称为距离的倒数或导数。选择的度量应与 Embedding 模型和索引配置一致。

内存向量库

内存原型可以用 NumPy 保存归一化向量,并通过矩阵运算扫描全部文档。它能清楚展示向量库的基础行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import numpy as np


class TinyVectorDB:
"""保存归一化文档向量,以余弦相似度执行精确检索。"""

def __init__(self, vectors, texts: list[str]):
matrix = np.asarray(vectors, dtype=float)
if matrix.ndim != 2 or len(matrix) != len(texts):
raise ValueError("Vectors and texts must have matching rows")
norms = np.linalg.norm(matrix, axis=1, keepdims=True)
if np.any(norms == 0):
raise ValueError("Document vectors must be non-zero")
self.vectors = matrix / norms
self.texts = texts

def search(self, query_vector, top_k: int = 3) -> list[dict]:
"""验证查询维数,再返回按相似度降序排列的候选。"""
query = np.asarray(query_vector, dtype=float)
if query.shape != (self.vectors.shape[1],):
raise ValueError("Query dimension does not match")
if top_k <= 0:
raise ValueError("top_k must be positive")
norm = np.linalg.norm(query)
if norm == 0:
raise ValueError("Query vector must be non-zero")

scores = self.vectors @ (query / norm)
indices = np.argsort(-scores)[:top_k]
return [
{
"text": self.texts[index],
"score": float(scores[index]),
}
for index in indices
]

若文档数为 NN、向量维数为 dd,扫描约需要 O(Nd)O(Nd) 运算,排序还产生额外成本。对于小数据,它是有效基线;大规模高并发服务则需要索引、过滤和存储治理。

向量索引

工业向量库提供持久化、元数据过滤、近似最近邻搜索和服务管理。Qdrant、Milvus 等系统可用于构建这些能力,但具体选择需考虑数据规模、过滤需求、吞吐、运维和一致性。

HNSW 用多层图组织向量,在较高层快速接近目标,再在低层细化搜索。它用近似结果换取较低延迟,不能保证对任意数据都具有固定最坏复杂度。

参数 主要影响
m 图连接规模、内存和索引质量
ef_construct 建图搜索范围、构建时间和召回
查询 ef 在线搜索范围、延迟和召回
精确搜索阈值 小集合是否直接扫描

增大参数通常会增加资源消耗。应对照精确检索计算 Recall@k,并同时观察 P95 延迟和内存,而不是只测一条查询的速度。

HNSW 与跳表的类比主要在于“较高层快速移动,较低层细化”。它是分层近邻图,不是保证把空间递归分成互不重叠的簇。高层通常较稀疏,低层包含更多点;搜索依据邻接关系逐步靠近查询。

m 主要控制每个节点连接的规模,不表示每层有 m 个“小世界”。ef_construct 控制构建时搜索范围,查询 ef 控制在线候选范围。ANN 有召回与延迟权衡,不提供对任意数据都固定为 O(log⁡N)O(\log N) 的最坏保证。

基础检索

最小 RAG 流程可以表达为以下五步:

1
2
3
4
5
将问题转换成查询向量。
从向量库取得 Top-K 文档。
按长度预算组织正文和文档标识。
告诉生成器仅依据给定证据回答,不足时说明无法判断。
返回回答与所使用的文档标识。

真正服务还需要空召回处理、权限过滤、超时、长度限制和引用核对。把无关文本拼进上下文,会使模型围绕错误材料生成看似合理的回答。

检索结果也不是最终答案本身。制度查询可能同时需要适用对象、条件和截止时间,生成器要把多段资料与用户情况组合起来。若知识不足,不能把模型推导出的新内容伪装成文档明确规定。

在“猫的知识”一类小实验中,即使 Top-N 的正文已经正确进入上下文,小型生成模型也可能错误理解或忽略它们。应同时展示候选片段、分数和回答,分别验证检索与证据使用。

Naïve RAG 常见问题包括检索不到、候选太杂和长上下文中关键证据被忽略。诊断时应先确认正确答案是否在库中,再检查切块、召回、排序和生成,不能直接把全部失败归因于模型。

查询改写

用户问题往往包含省略、错字、上下文指代或非专业用语。Query Rewriting 将其改写为适合检索的明确问题,可能展开缩写、补充已确认上下文或拆成多个子查询。

例如“这个版本怎么升级?”缺少产品和当前版本。系统可以从已有对话中取得明确产品;无法确定时应澄清,不能自行选择一个版本并写入检索问题。

也可以用多个角度生成检索候选,例如把“怎么连不上”分别展开为网络、认证或服务可用性相关查询。它们只是调查方向,不能未经确认就认定用户正在遭遇某一种故障。多角度结果需要去重,并保留原始问题。

改写应保留原始查询以及新增信息的依据。它增加一次推理成本,所以可以只对短问题、歧义问题或召回失败启用,并比较改写前后的证据召回。

假设文档

HyDE 先生成一段可能回答问题的假设文档,再用其向量检索真实资料。这利用较完整的文档语义缓解短查询与长答案之间的表达差异。

1
2
3
4
5
问题:分布式事务失败后怎样恢复?
假设文档:描述事务日志、补偿操作和恢复状态。
向量化:把假设文档转换成查询向量。
检索:寻找知识库中真实的恢复方案。
回答:依据真实方案生成结果。

假设文档可能含有错误细节,不能作为最终证据。它只辅助定位真实文档。额外生成会提高延迟,适合准确性要求较高且能接受该成本的查询。

可以把假设文档理解成检索的“中间表达”,但不是任意插入一个文本就一定更靠近答案。生成内容应尽量与问题领域相符,并以召回的真实资料验证效果;错误实体或虚构术语可能把检索带向其他方向。

混合检索

稠密向量擅长语义匹配,BM25 依据词项统计捕捉字面匹配。版本号、产品型号、股票代码、序列号等内容可能需要精确词项,因此两路召回可以互补。

BM25 的常见形式为:

BM25⁡(q,d)=∑t∈qIDF⁡(t)f(t,d)(k1+1)f(t,d)+k1(1−b+b∣d∣/avgdl⁡).\operatorname{BM25}(q,d)=\sum_{t\in q}\operatorname{IDF}(t) \frac{f(t,d)(k_1+1)}{f(t,d)+k_1(1-b+b|d|/\operatorname{avgdl})}.

f(t,d)f(t,d) 是词频,∣d∣|d| 是文档长度,avgdl 是平均长度,k1k_1 与 bb 控制词频饱和和长度归一化。实际系统还需合适的分词器和字段设计。

向量相似度与 BM25 分数不处于相同标尺,不能直接相加。可以对分数校准,也可以使用基于排名的融合方法。

例如查询软件版本 2.4.1 时,向量检索可能同时召回 2.4.0、2.4.2 的材料;关键词或版本元数据可以保留这一精确条件。融合不会自动把所有不满足版本的候选删除,硬条件仍需明确过滤。

Dense 与 Sparse 描述表示方式,不描述召回数量:Dense Embedding 通常多数坐标非零,词项稀疏表示只有少数词项有权重。两路各返回多少候选由检索参数决定,两者都可以返回多或少的结果。

排名融合

倒数排名融合(RRF) 只利用各路候选中的排名:

RRF⁡(d)=∑m∈M1k+rm(d).\operatorname{RRF}(d)=\sum_{m\in M}\frac{1}{k+r_m(d)}.

rm(d)r_m(d) 从 1 开始,kk 是平滑常数。未出现在某路结果中的文档不贡献该路分数。常见的 k=60k=60 会减少第一名对第三名等相邻位置的过度优势,具体值仍应根据任务验证。

若同一文档在两路分别排名 2、3,k=60k=60 时贡献为 1/62+1/631/62+1/63。两路原分数即使分别是余弦值和较大的 BM25 分数,也可以在排名层面相加。BM25 包含词频、逆文档频率和长度处理,不只是统计匹配次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from collections import defaultdict


def reciprocal_rank_fusion(
ranked_lists: list[list[str]],
k: int = 60,
) -> list[tuple[str, float]]:
"""合并不同检索路径的排名,避免同一路重复文档被累计。"""
if k < 0:
raise ValueError("k must be non-negative")

scores = defaultdict(float)
for ranked_docs in ranked_lists:
seen = set()
unique_rank = 0
for doc_id in ranked_docs:
if doc_id in seen:
continue
seen.add(doc_id)
unique_rank += 1
scores[doc_id] += 1.0 / (k + unique_rank)

return sorted(scores.items(), key=lambda item: (-item[1], item[0]))

融合前要统一文档标识,并处理同文档重复分块。相同证据反复出现会占用上下文,也可能让结果被重复计分。

候选重排

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
2
3
4
5
def place_evidence_at_ends(ranked_items: list) -> list:
"""将高相关候选交替放在两端,保留所有入选项。"""
left = ranked_items[::2]
right = ranked_items[1::2]
return left + list(reversed(right))

这种布局是可以评测的策略,不是所有模型必然受益的规则。需要先过滤噪声,保留来源与时间;具有明确先后语义的条款和步骤不宜因重排而改变含义。

图谱检索

普通 Top-K 检索适合局部事实,对跨文档全局问题可能缺乏整体关系。GraphRAG 从资料中抽取实体和关系,形成社区结构与摘要,再结合图和文本回答问题。

实体关系与社区结构

例如总结两家公司三年的合作,需要连接项目、公司、时间和成果,而不是只找到一个最相似段落。社区摘要可以帮助全局归纳,原文片段则用于核对具体事实。

图抽取和摘要同样可能出错,还带来构建与更新成本。实体合并、关系来源和时间变化必须可追踪。图检索与向量检索可以结合,是否值得使用应由问题类型和评测结果决定。

跨段落关系可以用一个两跳例子说明:

1
2
3
张三 --father_of--> 李四
李四 --works_at--> 某公司
张三 --occupation--> 出租车司机

第一条明确表示张三是李四的父亲。沿两跳可说明“张三是该公司员工李四的父亲”,不能推出张三也在该公司工作,更不能倒置父子方向。关系抽取和答案生成都要核对边的方向与证据。

补考、重修、退课等条款也可能在业务上有关联,虽然文本向量未必很接近。保留章节、适用条件和关系有助于补充关联资料;不能仅因出现了这些词就推定所有学校都允许同一处理路线。

k-hop 邻域与社区是不同概念。前者按边数扩展,后者依据图中的连接结构分组;社区层级参数不一定等于一跳、两跳。社区报告可以摘要相关实体与关系,仍需保留原始证据。

同一个人可能以姓名、职位或其他名称出现,但相同职位也可能在不同公司、时期对应不同人。实体归一化需结合主体、时间和证据,不能只靠模型把类似字符串合并。普通检索若取得相同事实,也可能回答两跳问题,GraphRAG 的收益需要实际比较。

多模态资料

财报中的表格和统计图包含空间关系。把表格按文本直接摊平,可能丢失列名、单位、行列对应和合并单元格,导致模型错误比较数值。

表格应保留标题、表头、单位、脚注和页码。简单表格可以转换为 Markdown;复杂表格还需单独记录合并关系。统计图可以生成文字说明,但描述必须保留可核对数值,不能仅保存“增长明显”这样的概括。

LayoutLMv3 等文档理解模型可以辅助版面和内容处理。选用时要考虑文本是否可直接提取、是否扫描、布局复杂度及任务类型,不能用一个布局模型替代全部解析与验证。

多模态处理可以走两条路线:先用 OCR 或视觉模型把表格、图像转成可核对的文本,再使用文本 Embedding;或者直接用经过对齐的多模态编码器做跨模态检索。前者的 Caption 与表格转换可能丢失细节,后者也需要能向生成器返回原资源。

因此应把转换文本、原图或原表格位置关联起来,尤其保留单位、表头、脚注和关键数值。生成了一段流畅描述,不表示原模态的全部信息都已保存。

自动评估

RAG 评估需要区分 检索质量 和 生成质量。Ragas 等框架提供不同维度的指标,具体定义应与所用版本一致。

维度 需要检查的内容
上下文召回 应有证据是否进入候选
上下文精度 候选是否集中于相关内容
忠实性 回答是否受给定证据支持
回答相关性 是否回答用户真正的问题
答案正确性 结论是否符合标注或业务事实
服务指标 延迟、成本、错误率和更新时效

黄金集应包含正常问题、无答案问题、跨文档问题、权限边界和文档更新。自动裁判可能存在偏差,需要人工抽样与硬断言校准。

改变切块、Embedding、Top-K、重排或模型后,应运行回归。检索分数提高不一定意味着回答更好,端到端指标和关键类别都需要比较。

增量更新

生产知识库需要处理新增、修改和删除。文档 ID、内容哈希与版本可用于识别变化;更新后必须删除或替换旧分块,并同步索引、元数据和缓存。

1
2
3
4
5
6
读取文档标识与内容哈希。
若未变化:跳过重新向量化。
若变化:解析新内容,生成新分块和向量。
写入新版本,确认成功后使旧版本失效。
若文档删除:删除相关分块并使缓存失效。
记录流水线状态,失败任务可重试和恢复。

只向库中追加新内容会留下过时证据。更新流程还需要明确一致性窗口,让在线查询知道自己使用的是哪个版本。

哈希可以定位内容变化,但需要选择合适算法。把字符数值相加再取模,会让 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 的核心是让证据从原文到回答的每一步转换都可检查。数据质量、检索策略、生成约束和持续评测需要共同维护。