外观
第 6 章 检索与知识库
员工问“跨区支援的住宿报销上限是多少”,答案写在本公司的员工手册里,既不在任何模型的参数里,公开互联网上也查不到,模型硬要回答,就只能编造,也就是产生幻觉。模型不知道的知识有两类,一类是训练数据有截止日期,截止之后发生的事模型没见过,另一类是企业内部的知识,从来没有进入过训练集;补上这两类空白,常规做法是把知识放在模型外部,用到哪一段就取哪一段,这个取用的通道叫检索,本章讲怎么把检索做成知识库的一项可靠能力,它在上下文工程里是五大策略之一,与上下文预算怎么挂钩,第 7 章换一个角度再讲。6.1 完整走一遍检索管道,也就是 RAG 的索引、检索、生成三个阶段;6.2 拆开管道里最不透明的一环,也就是嵌入与向量库,中文项目的检索质量在很大程度上由这一步的选型决定;6.3 讲 Agentic RAG 如何把检索的决策权从固定管道交给模型,靠路由、查询规划和迭代检索这三件事;6.4 讲质量工程,包括切分、增量更新和检索评测,demo 和生产环境的差距主要就在这几件事上;6.5 讲知识图谱:向量检索答不了的关系类问题和全局类问题,交给节点和边来答;6.6 是并行的另一条路,数据本来就存在业务数据库里,用 text-to-SQL 直接在库上查询,不必绕道向量。主线案例仍然是第 4 章那家连锁品牌,这一章给它增加一个制度问答助手,知识库由员工手册、考勤制度、报销制度和门店运营规范四类文档组成,服务全国门店的员工和店长,六节共用同一条故事线。
6.1 RAG 三阶段
如果把几十页制度文档整份放进系统提示,token 费用首先就承受不了。第 1 章说过,中文一个汉字大约折合 1.5 到 2 个 token,这几份文档每次请求都要按全文长度重新付费。而且制度还会改版,改一处就要重新部署一次提示词,这条路行不通。
改用函数调用呢?第 4 章的工具调用适用于“有接口的数据”,一段用自然语言写成的条款,无法套进 get_daily_revenue 那样的函数签名。真正缺少的是另一种查找能力:按问题的含义,从文档中找出相关的几段,只把这几段送给模型。
关键词过滤看起来更简单,按词匹配,不需要把文字转成向量,速度快,但准确度不可靠。这一点有人实际算过。有一项分析要在 5,304 家 Y Combinator 公司的公开描述里筛出做智能体的,按关键词匹配的办法漏掉相当一部分,最后靠 LLM 逐条读描述做结构化分析才筛齐,筛出 726 家真做智能体的。按字面匹配拿来做粗筛还行,要按含义取材料,还是得用语义检索。
检索增强生成(Retrieval-Augmented Generation,RAG)的做法是给模型外挂一个知识库,回答之前先把相关材料检索出来,让模型只依据材料回答。一套 RAG 应用通常分为创建索引、实现检索和生成结果三个阶段,三个阶段各自负责一部分,哪个阶段出了问题,可以直接定位。下面这张表里出现了“嵌入”这个词,它的作用是把文字转成向量,6.2 详细讲。
| 阶段 | 运行时机 | 输入 | 输出 |
|---|---|---|---|
| 创建索引 | 离线为主,由定时任务或文档变更触发 | 原始文档 | 向量库里的片段、嵌入与元数据 |
| 实现检索 | 在线,随用户请求发生 | 用户查询 | 按相关度排好序的片段列表 |
| 生成结果 | 在线,紧跟检索之后 | 查询加相关片段 | 模型作答,可带后处理 |
现在的模型窗口动不动就上百万 token,因此容易想到一个看起来更省事的方案:把整篇文档直接交给模型。这个方案要先排除,更合适的做法是把文档拆开,只使用相关的片段。
为什么排除?可以从两个方面算。先算费用:全文进入窗口,每次提问都要按全文长度付费;检索只送相关的几段,实测把一次 37,000 多个 token 的上下文压缩到 304 个,节省 99.2%。再看效果:无关内容进入窗口,不仅费钱,还会成为噪声干扰回答。输入越长,模型对中间部分信息的把握也越差,实证和机理放到第 7 章 7.1 展开。
微调这条路也要排除:微调修改的是模型参数,每改一次就要重新训练,适合把稳定的技能和风格固化下来。制度库这类经常改版的知识,写进参数里根本跟不上更新节奏。放在外部知识库就不同,文档改完、索引重建,新内容立刻生效。什么时候值得微调,14.4 会给出完整的判断依据。
三条路还可以再比较一点:以后想撤回,能不能撤?检索把知识放在外部库,撤一条就删一条向量;上下文注入把知识写进提示词,每次调用时带上,提示词一改,下一轮就不带了;微调把知识融进权重,想单独删掉其中某一条几乎做不到,只能默认一旦放进去就退不回来。制度这类修改频繁、发错了要马上撤回的知识该走哪条路,用这个标准一问,答案往往就出来了。
这条管道分为上下两个流程。上面是离线流程:把文档清理干净、切成片段、计算嵌入,再写入向量库;下面是在线流程:每次提问时,从库里取出最相关的几段,拼给模型。
索引阶段要做四件事:清理文档并补充元数据,把文档切成更小的片段,逐个计算嵌入,再把结果存入向量数据库。整条管道用伪代码表示如下,每一行的输入输出都与上面的表格对应。
python
# 伪代码:RAG 三阶段的管道骨架
# 离线:创建索引
for doc in load_documents(): # ① 清理文档,补元数据(部门、版本、生效日期)
for chunk in split(doc.text): # ② 切分成片段
vec = embed(chunk.text) # ③ 每个片段单独计算嵌入
vector_store.add(chunk.id, vec, # ④ 片段、向量、元数据一起入库
chunk.text, chunk.meta)
# 在线:检索 + 生成
q_vec = embed(user_query) # 查询用同一个嵌入模型转向量
hits = vector_store.search(q_vec, k=4) # 相似度搜索,取最相关的几段
answer = llm(prompt(question=user_query, materials=hits)) # 拼提示词作答讲 RAG 的资料常常把注释里的第①步一笔带过。其实清理文档要做的事不少:PDF 转出来的文字夹着页眉页脚、断行的表格,不清理干净,切出的片段就带着杂质进入库。元数据设计需要更多判断。文档编号、版本、生效日期和适用岗位这些字段没有现成的组件代劳,要根据业务自己定义。这些字段之后处处要用:检索靠它们过滤,回答用它们作为出处,增量更新用它们比对新旧。切分决定片段的颗粒度,嵌入把片段变成向量,这两件事分别在 6.4 和 6.2 讲。
解析这一步要按文档格式分别处理。HTML 这类带标记的文档要交给解析器按结构处理;图省事直接用正则去掉标签,正文常常被一起破坏;Word 有现成的库,可以按段落和表格读取;PDF 最麻烦,文字层规整的可以直接抽取文本,扫描件要先做 OCR;Excel 这类表格数据不要整表拍平成文本,按 6.4 要讲的表格索引方式处理。清洗必须在切分之前完成:杂质要是进了片段,片段就废了,后面切得再细也补救不了。图像类材料怎么交给模型,第 16 章讲多模态时另有安排。
索引和在线服务的运行时机不同,这是 RAG 工程里第一个容易混淆的地方。索引属于离线工作,用户不需要等它运行完,典型做法是挂一个定时任务,每隔一段时间把公司内部文档整体重新索引一遍。负责索引的代码也可以独立成一个应用,专门负责建库。唯一的例外是用户自己上传自定义文档,比如员工往知识库里补一份新门店的开业流程,这时创建索引必须在线完成,作为应用的一部分运行。
检索与生成则始终在线,用户每问一句,管道就运行一次。离线和在线的界线划清楚,后面做增量更新、算成本时,才能知道改动的究竟是哪一边。
两边的成本也分开计算:离线部分花费在嵌入和存储上,在线部分每次请求要付一次查询嵌入的费用,外加一次生成调用。混在一起算,容易看错单次问答的真实成本。
检索阶段有三个关键动作。第一个没有商量余地:查询必须先用入库时使用的同一个嵌入模型转成向量,原因 6.2 接着说明。
接着向量库按相似度给片段排序,返回最靠前的几条。取几条本身就需要权衡:取少了,本该命中的条款排在第五位就被丢掉;取多了,不相关的片段进入上下文,既浪费 token 又造成干扰。常见的 k 值有取 5 的,也有取 3 到 4 的。起步先取个位数,再根据评测结果调整,伪代码里的 k=4 就是按这个起点随手取的,评测在 6.4 讲。
检索返回的是片段和出处,不是现成的答案。这一段管道从头到尾都没有去理解问题,只是按语义远近取回材料,理解发生在生成阶段。
生成阶段决定了整条链路的可信度。模型手里是问题和几段材料,指令要说清两件事:只依据材料回答,材料里没有的就如实说没有。材料按检索给出的相关度排序放进模板,出处随片段一起带上。这个出处是索引阶段写入元数据的字段,不是模型从片段正文里自己识别出来的,答案末尾的条款编号就是从这里来的。以后材料之间互相矛盾、两条制度冲突时,该以哪一条为准,同样靠元数据里的版本和生效日期判断。制度问答的生成模板如下。
text
你是公司制度问答助手。只依据【材料】里的内容回答【问题】,材料里没有的,
如实回答"制度库中没有找到相关规定",不得编造。答案末尾注明依据的条款编号。
### 示例
【问题】跨区支援的住宿报销上限是多少?
【材料】第二条 报销。跨区支援产生的交通与住宿费用,须在返回后 7 个工作日内
提交发票报销;住宿单晚超过 600 元的部分需店长审批。
示例输出
制度库中没有找到住宿报销上限的规定。材料里仅有一条相关规则,住宿单晚超过 600 元的部分需店长审批,注意这是审批门槛,不是报销上限。
依据:员工手册第二条。
### 输入数据
【问题】{question}
【材料】{chunks}这份模板首先约束一种幻觉。示例输出演示的正是这条纪律:问的是上限,材料里只有审批门槛,那就直接说没有找到规定,把唯一相关的规则如实列出,并说明它不是上限。答不出就是答不出,不允许自由发挥。
另一种幻觉来自检索质量。材料取错了,模型依据错误的材料自信地回答,比不回答的危害更大。因此调试 RAG 有一条主线:先看检索取回了什么,再看模型说了什么。材料正确而答案错误,问题出在生成;材料本身就错,就要回到索引和检索中找原因。开头提到的按阶段定位问题,在这里正好用上。
生成结果之后还可以做后处理,语法检查、事实验证都属于这一类。也有人通过微调生成模型来适应具体场景,但制度问答这类格式稳定的场景,微调的收益有限,先把提示词和检索做扎实更划算。
“只依据材料”这条规则是针对问答场景设计的。到了归因、分析这类需要说明来龙去脉的场景怎么调整,留到 20.1 再讲,那里的周报系统会用一次真实的改造继续说明。
三个阶段前后衔接,全部写死在代码里。用户问一句,管道就从头跑到尾,中间没有任何决策点。问题简单时,这反而是优点:行为可预期,成本也容易预算。问法一复杂,这条固定的管道就开始出问题。
6.2 嵌入与向量库
6.1 伪代码里的 embed 是一个黑盒:文本进去,一串数字出来,中间的过程没有交代。这一节把它打开。中文项目的检索质量,有一大半在这一步已经决定了。
嵌入做的事,是把一段文本编码成固定长度的数字序列,长度从几百到几千不等,每个数字对应一个维度。这串数字单独看没有意义,意义全在相互关系里。嵌入模型训练时见过海量文本,从中学到一个规律:含义越接近的文本,输出向量的夹角越小;含义无关的,夹角就大。
sofa 和 couch 指的是同一种家具。关键词搜索只认字面,不认含义,搜 sofa 永远找不到只写了 couch 的文档,除非人工维护一张同义词表。向量搜索不需要这张表,因为嵌入模型在训练中已经见过这两个词反复出现在相近的上下文里,两者的向量自然靠得近。
制度问答里同样如此。知识库里那条原文是“连续工作满 12 个月的员工享有年假”,员工问的却是“带薪休假的时长”,两边字面几乎不重叠,语义检索照样能命中。远近用什么衡量?余弦相似度(cosine similarity),即两个向量夹角的余弦值,越接近 1 越相似,向量库默认的相似度排序基本都用它或它的变体。
嵌入模型的结构和生成模型不同。生成模型走自回归(autoregressive)路线,从左到右逐词生成,每个位置只能看到前文;经典的嵌入模型走另一条路,叫仅编码器(encoder-only):用双向注意力把整段文字前后都看全,再压缩成一个向量。把文本压成向量的任务,正适合这种看全文的结构,这也是两类结构很少共用同一个底座的原因。后来也有用自回归底座改造成的嵌入模型,通过池化把生成结构改造成能做表示的,属于另一条路线。选型时看到 encoder-only、bidirectional 这类字样,指的就是看全文的结构。
除了余弦,还有一个工程细节要补充:点积。不少系统直接用点积当相似度。点积等于余弦乘上两边模长的乘积,模长不一致时,模长大的向量占优势,方向差一点也能得高分。惯例是先做 L2 归一化,把向量统一压到单位长度,这样相似度完全由角度决定,与模长无关。但这要与 6.4 的检索器分数归一化区分开:那边归一化的是几路检索器打出的分数,这边归一化的是向量自身的模长,两件事经常被混着讲。
还有一条工程上的原因:整篇文档不能算成一个嵌入。第一,文档可能超出嵌入模型的输入上限;第二,一篇文档往往混着多个主题,一个向量把它们平均掉,检索精度就会下降,一本同时写考勤、报销、消防规范的手册压成单个向量,问年假未必能把它排到前面;第三,即使前两条都不成立,把整篇文档返回给上下文也是浪费。综合这三点,6.1 伪代码里的切分不是可选步骤,而是嵌入的前置条件。切多细、怎么切,6.4 讲,这里只需要知道要切成片段、逐段嵌入。
几十页制度切下来,不过几百个片段。几百个向量放在 Python 列表里,每次查询把余弦相似度逐个算一遍,也就几百次乘加,完全够用。
但数据量一大,这种做法就不行了。每次查询都要与库里全部向量逐一比较,计算量随片段数线性增长,百万级片段的库,查一次就是一百万次比较。向量数据库就是为这种规模设计的:把向量连同原文和元数据一起持久化,再用近似最近邻(Approximate Nearest Neighbor,ANN)索引结构加速,代价是牺牲一点精度。
大致思路是把向量空间划分成许多小区域。查询到达时,先确定它在哪个区域,只与本区域和相邻几个区域的向量比较,远处的区域直接跳过。速度就是这样换来的,代价是可能漏掉个别真正的近邻。向量库还支持按元数据过滤。
市面上的选择大致分为几类。Chroma 走轻量路线,可以在本地运行,适合原型和小规模场景;Pinecone 是全托管的云服务;Weaviate 开源,带企业级特性;Qdrant 用 Rust 编写,过滤功能是强项;Milvus 面向十亿级向量场景。本书示范使用 Chroma,6.2 末尾的 pgvector 是另一条路线。
这里经常有人问:FAISS 这类库也能计算近邻,为什么还需要向量数据库?区别在“算”与“管”。FAISS 是向量函数库(vector library):近邻搜索很快,但不负责元数据过滤、权限和多应用隔离这些数据库该做的事。数据量小、查询逻辑简单时,直接用它没问题;要做成知识库,还是要用带管理能力的向量库。
选嵌入模型之前,有一条必须遵守的规则:库里存的向量和查询用的向量,必须出自同一个嵌入模型。每个模型的语义空间都是各自训练出来的,A 模型里两个向量的距离,放到 B 模型的空间里没有可比性,维度甚至对不上。所以更换嵌入模型等于全库重建,所有片段要重新计算一遍嵌入,这笔费用在 6.4 的增量更新里还会再出现。顺便提一句:Chroma 自带的默认嵌入模型是 all-MiniLM-L6-v2 的 ONNX 版本,384 维,以英文语料为主。中文项目不要图省事,要自己计算好嵌入再入库。
对中文项目来说,嵌入模型的选择很关键,先看有哪些可靠的选项。用 API 的话,阿里云百炼的 text-embedding-v4 适合作为默认起点。它默认输出 1024 维向量,通过 dimensions 参数可以在 64 到 2048 维之间的八个档位里选择,可以通过 OpenAI 兼容端点直接调用。价格在 API 嵌入模型里属于低价档,批处理接口另有折扣,截至 2026 年 8 月以阿里云官方文档为准。
中文场景需要本地部署时,开源模型里绕不开北京智源人工智能研究院的 BGE 系列。系列里 bge-m3 覆盖面更广:输出 1024 维,输入上限扩展到 8192 token,支持 100 多种语言,稠密检索、稀疏检索与多向量三种方式由同一个模型提供;bge-large-zh-v1.5 是 1024 维、单条输入上限 512 token,定位是短条款。稠密检索就是前面一直在讲的做法,把语义压缩成向量,按相似度比较远近。稀疏检索换了思路,按关键词做字面匹配,向量里绝大多数位置是零。两种检索怎么配合,6.4 讲。
OpenAI 一侧也有两个模型,常被拿来对照:text-embedding-3-large 默认 3072 维,text-embedding-3-small 默认 1536 维。维度高低本身不能说明优劣,它影响的是向量占用的存储和比对开销。更应该关注的是模型对中文语义的区分能力、输入长度上限、部署方式和价格。
选型再往上还有两个进阶方向。存储紧张时可以做压缩,常用的方法是乘积量化(product quantization):把高维向量切成几段,每段只记录最近的中心点编号,占用空间能降低一个数量级;代价是精度下降,所以压缩后的库多用于粗排,先放大取几十条候选,再用原始向量或重排器精排。重排是什么,6.4 详细讲。如果嵌入模型在专业术语上区分不开,比如医疗、法律和代码这类语料,通用模型就不够用,换成领域专用的嵌入模型是正路。选择时不要只听厂商宣传,MTEB(Massive Text Embedding Benchmark)这类公开榜单把各家模型放在同一批任务上打分,可以先按榜单圈出候选,再用自己的语料实测一遍。本节开头说检索质量有一大半在这一步决定,原因就在这里。
代码演示按建库、入库和检索三段来写,运行环境是 chromadb 1.5.9,已经实测通过(pip install chromadb)。嵌入调用使用百炼的 OpenAI 兼容端点,端点和业务空间的域名见第 1 章,密钥照 1.3 的做法放进环境变量,变量名 DASHSCOPE_API_KEY。
python
import os
from openai import OpenAI
import chromadb
embed_client = OpenAI(
api_key=os.environ["DASHSCOPE_API_KEY"],
base_url="<百炼兼容端点,见官方文档>",
)
def embed(texts: list[str], model: str = "text-embedding-v4") -> list[list[float]]:
"""批量计算嵌入。text-embedding-v4 同步接口单次最多 10 条,按 10 条分批。"""
vectors = []
for i in range(0, len(texts), 10):
resp = embed_client.embeddings.create(
model=model, input=texts[i:i + 10],
dimensions=1024, encoding_format="float")
vectors.extend(item.embedding for item in resp.data)
return vectors
# 本地持久化模式:数据落在一个目录里,重启不丢
# 集合名只能用字母数字和 . _ -,取 3 到 512 个字符
chroma = chromadb.PersistentClient(path="./policy_chroma")
rules = chroma.get_or_create_collection(
"policy_chunks", metadata={"hnsw:space": "cosine"}) # 指定余弦度量
def index_chunks(chunks: list[dict]) -> None:
"""入库。chunks 元素形如 {"id", "text", "meta"},id 同时写进元数据备查。"""
for i in range(0, len(chunks), 10):
part = chunks[i:i + 10]
rules.add(
ids=[c["id"] for c in part],
documents=[c["text"] for c in part],
metadatas=[{**c["meta"], "id": c["id"]} for c in part],
embeddings=embed([c["text"] for c in part]),
)
def search(query: str, top_k: int = 4, where: dict | None = None) -> list[dict]:
"""检索。where 按元数据过滤,如 {"doc": "报销制度"},先缩小范围再比向量。"""
res = rules.query(
query_embeddings=[embed([query])[0]],
n_results=top_k, where=where,
include=["documents", "metadatas", "distances"],
)
return [
{"text": doc, "meta": meta, "distance": dist}
for doc, meta, dist in zip(res["documents"][0],
res["metadatas"][0], res["distances"][0])
]读这段代码要注意三处细节。hnsw:space 指定距离度量,缺省时 Chroma 按欧氏距离计算,而语义检索的惯例是用余弦。where 参数负责元数据过滤:问报销的问题,先把范围限定在报销制度这份文档上,再在缩小后的集合里比较向量,又准又省,6.4 会讲元数据怎么设计。检索结果里的 distance 是距离,越小越相关,方向与相似度相反,排序逻辑不要写反。
元数据过滤背后还有一个集合设计的权衡。把全部数据放进一个大集合,查询时靠过滤缩小范围,运维最省事,但这样的结构里 ANN 检索与精确 KNN 的匹配率只有约 50%。匹配率衡量的是同一个查询下两种检索各自取回的前几条有多少重合。精确 KNN 就是前面那种逐个计算相似度的算法,ANN 走索引,跳过大部分比较,速度快但会漏掉部分结果。
ANN 的做法不止划分区域这一种,另一种不划分区域,而是把向量连成图,沿着边跳转来找近邻,这是近似近邻的两种常见思路。Chroma 配置里的 HNSW 走的就是建图这一路,也是这类索引里最常用的一种。它用一张分层的图查找近邻,从最上层粗略地往下跳,每一层比上一层更细,前面 hnsw:space 配置的距离度量,正是这张图使用的。
单一的大集合里,不同主题的向量全部混在同一张 HNSW 图上。在这张图上找近邻靠沿边跳转,每一步只能到达相邻的向量。查询点附近如果挤着其他主题的向量,跳转路径会被它们带偏,真正的近邻在够不着的位置,取回的前几条常混着其他主题的内容。元数据过滤只在查询那一刻缩小范围,缩小后向量依然是混放的,该漏的还是会漏。
如果按日期这类维度把数据拆到多个集合,每个集合各自配一套局部 HNSW 索引,向量分布会均匀得多,比较范围也缩小到集合内部,同一口径下匹配率最高能到 98%。代价是集合一多,索引成本和运维复杂度也随之增加。要省事还是要精度,取决于查询模式和数据增长速度。
pgvector 提供的是另一条路线。它不是独立的向量数据库,而是 PostgreSQL 的一个扩展,当前版本是 0.8.6。安装后,业务表里可以直接增加向量列,vector 类型最长 2000 维,余弦距离的操作符写成 <=>,大规模检索交给 HNSW 索引。建表和查询的 SQL 如下。
sql
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE policy_chunks (
chunk_id text PRIMARY KEY,
doc_id text NOT NULL,
clause text NOT NULL, -- 条款原文
meta jsonb NOT NULL,
embedding vector(1024) -- 维度与嵌入模型保持一致
);
-- 余弦距离的 HNSW 索引
CREATE INDEX ON policy_chunks USING hnsw (embedding vector_cosine_ops);
-- 查询向量由嵌入模型现算后填入,距离越小越相关
SELECT chunk_id, clause, embedding <=> '[0.12, -0.45, ...]'::vector AS distance
FROM policy_chunks
ORDER BY distance
LIMIT 4;两条路线怎么取舍,先看数据主要存放在哪个系统,再看团队的需求。制度问答是独立的新场景,数据就是知识库本身,Chroma 用本地一个目录就能起步,零运维,原型到小规模场景都没有问题。代价是它自成一体,权限、备份、事务都要另外考虑。
反过来,条款如果本来就在 PostgreSQL 里,检索结果还要与订单表、会员表联查,公司也想复用现成的 Postgres 运维体系,pgvector 自然合适。它把向量列直接建在业务库的表里,一个数据库管理两类数据。同时要接受 ANN 索引调优、维度上限这些约束。
哪条路线都不是万能的,真正要做的判断只有一个:知识库是建成一个独立的服务,还是做成数据库里的一张表。
选型归选型,两条路线有一条共同的底线:向量库本质上就是数据库。访问控制、加密和多应用共用时的数据隔离一样不能少,认证和授权的标准,与任何数据库都是一样的。对嵌入还有一个常见误解,把它当成一层加密,以为文本变成了数字就还原不出原文。但 2020 年的研究已经证明,不需要访问嵌入模型,也能从向量还原出原始文本。嵌入不能替代加密,敏感数据该不该进库,要按明文的标准来判断。
真实系统里向量怎么使用,Airbnb 的房源搜索是一个现成的例子。数百万房源的嵌入离线计算好入库,查询时只现场编码那一句话,房源侧不用重算,规模靠这一点支撑。训练信号也不依赖人工标注,用户点击过哪个房源,就是一条“查询-房源”相关的隐式证据。但点击本身带有位置偏差,排在前面的天然更容易被点击,这层偏差不剥离,嵌入模型学到的就是位置而不是语义。
新房源没有任何交互数据,嵌入从哪里来?先用描述和属性计算一个基于内容的嵌入顶上,等点击、预订的信号积累够了,再频繁重新计算。评测上还有一条教训:离线排序指标好看,线上效果不一定好。真正应该看的是预订转化率这类业务指标,技术分数只是过程中的参考。
线路和集合定下来之后,检索这一环还有几档常见的升级,库都不用动,只需更换检索器。第一档要解决块尺寸的两难:块太大,上下文是全了,检索却不够准;块太小,检索准了,上下文又不够全。父子块的做法是切成两层,父块负责进入上下文,子块负责计算嵌入,用子块的向量去命中,返回时交出子块所属的父块,问得宽或问得窄,都能查到合适的材料。多向量表示是同一思路的推广,一个片段对应多个向量,摘要向量、假设问题向量都可以。
再上一档,是把元数据过滤自动化。用户提问不会自带过滤条件,“上个月生效的报销制度”这句话里就包含两个过滤项,一个是时间,一个是文档类型。自查询检索(self-querying)让模型先从问句里抽取这些条件,拼成一条带元数据过滤器的增强查询,先经过过滤器,再在筛选后的集合里做语义搜索。
相似度评分还有一种做法,是把时间也变成分数的一部分。每个片段在元数据里保存一个最近访问时间戳,命中一次就刷新一次。评分使用 adjusted_similarity_score = similarity_score + (1.0 - decay_rate) ** hours_passed 这个公式,以相似度为基础,再按距离上次命中的小时数补一笔新鲜度,decay_rate 越大,补分消退得越快。这样,最近使用过又相关的片段就排到前面,每天都有人问的常用条款,在一轮轮检索中一直占据靠前的位置。
与父子块相近的还有一种更简单的方法,叫块扩展:命中哪个细粒度块,就把它在原文里的前一块和后一块一起取回,被切断的上下文当场补回来。它与父子块解决的是同一个问题,实现成本更低,只是多带的两块不一定相关,token 消耗略多一些。后处理中另有一道质量控制,与 top_k 搭配使用:相似度分数阈值。top_k 只控制取几条,不管取回的这几条是否够格;设一道分数下限,低于它的直接丢弃,宁可少给材料,也不让勉强沾边的片段混进上下文。一个管数量,一个管质量,两道限制一起使用。
嵌入不只能用于文字。最有名的是 CLIP(Contrastive Language-Image Pretraining):通过对比训练把文本和图像编进同一个嵌入空间,巡检照片、验收现场图和制度条款从此可以在同一个空间里比较远近,以文搜图、以图搜文都做得到。对比训练在比什么?训练数据里图和文成对出现,一张图配上它自己的描述算正例,随机抽一句别的描述来配这张图算负例,训练的目标就是把正例对的距离拉近、把负例对的距离推远。训练完成后,一张图和描述它的那句话就会在空间里落到相近的位置,拿一句话当查询,搜出来的就是语义上最接近的那几张图。问“消防通道堆物的门店是什么样”,取回的除了条款,还可以有其他门店同类问题的现场照片,跨模态这条线的完整展开在第 16 章。跨模态嵌入与重排模型怎么来?有两条现成的路:自己部署模型,不想维护 GPU 就调用商用 API,Cohere Rerank、Jina Reranker 这类服务把交叉编码器包装成了按次付费的接口。6.4 讲的两段式检索在这里照样成立,只是精排这一段从自托管换成了 API 调用。
这条管道从索引到生成全部写死,用户问什么,它都按同一套流程运行。问题稍微复杂一点,比如需要拆解问题、改写查询、先查数据再检索,这些动作一个都不会发生。
6.3 Agentic RAG
固定管道的根本问题在决策:何时检索、检索什么、检回的内容够不够用、不够怎么办,这些判断全部写死在代码里,而写代码的人不可能预见所有问法。两个真实的问法就能看出问题。员工一句“我是店长,上个月跨区支援了一周,住宿费怎么报,调休怎么算”里实际包含两个子问题,固定管道拿整句话去检索,报销和调休两个主题在向量里互相稀释,哪一个都可能进不了前四;“上个月营业额最低的三家门店,店长按新制度要不要参加复训”要分两步才能回答,先用第 4 章的工具取营业数据,再进入制度库查复训规定,固定管道里根本排不出这样一条路径。
Agentic RAG 的对策,是把这些判断交给模型。检索不再是管道里固定的一步,而是回路中的一个工具。查营业额是一个工具,查制度库是另一个工具,调用哪个、按什么顺序调用、调用完还缺什么,模型根据上下文自己决定。固定管道与 Agentic RAG 最主要的几处差别,列在下面。
| 对照点 | 固定管道 RAG | Agentic RAG |
|---|---|---|
| 检索决策 | 静态规则控制,每次都检 | 模型自己决定何时检索、检索什么 |
| 查询来源 | 只用用户最初的查询 | 能结合对话历史改写、分解 |
| 检索过程 | 除检索外不做别的动作 | 检索前或检索中能调用其他工具收集信息 |
| 结果处理 | 检索到什么就用什么 | 模型评估结果质量,可以再检索 |
| 代价 | 路径短,成本稳定 | 每次决策都要一次模型调用 |
落实到工程上,RAG Agent 大致分为三类,自主程度依次提高。最简单的是路由 Agent:模型分析输入查询,从几个下游检索管道中选择一个。制度问题进入制度库,商品问题进入商品库,工单问题进入工单库,这是让模型做选择的最简单形式。
再上一级是查询规划 Agent,模型把复杂查询拆成可以并行的子查询,各子查询可以走不同的数据源,最后把几路结果合并成一份。“住宿报销加调休”这类一句话两问的问题,拆开后各自检索,两边都能进入前四。工具使用 Agent 的层级更高,检索工具与 SQL 查询、内部 API 处于同等地位,模型按任务自由编排,先查数据再查制度这条路径就此打通。
Agent 与 RAG 的融合贯穿整条链路,从查询的理解与分解、检索策略的选择、结果合成、迭代查询与反馈,一直到知识库的管理与更新。生产中最常实现的就是前面三类能力。最常见的起步做法,是把现有的 RAG 管道包装成一个工具。
这类决策除了靠提示词,也可以直接训练出来。R3-RAG 用强化学习训练检索策略,奖励分两种:结果奖励按最终答案的对错给正负一,过程奖励对每一步检索的相关性与格式打分。模型借此学会三件事:何时检索、检索什么、怎么分解问题。训练时给模型定义的动作清单里还有一项 skip_retrieval,有了这一项,模型也学会了不检索:问题简单时,直接回答。论文自报在多跳问答上平均准确率 52.6%,比强基线高 15 个百分点。制度问答值不值得专门训练一套,要看问法分布是否稳定,但“查不查”本身是一个可以学出来的决策位,这是这项工作的贡献。
智能体与管道最直观的区别,就是迭代检索。第一轮结果不理想时,固定管道只能照原样往下生成;智能体会先看一眼结果,发现调休没有覆盖,就改写查询再检索一轮,直到材料足够才进入生成。这“先看后改”的过程,把检索质量问题从架构问题变成了模型推理问题。改写得好不好,取决于模型能力和提示词引导,出错后调试也更绕,这是要付出的代价。
改写又分出两个变体,思路各不相同。假设文档嵌入(Hypothetical Document Embeddings,HyDE)针对的是问句与文档天生形式不同:用户的问题又短又口语化,库里的文档又长又书面化,拿问句的向量直接找文档,检索质量会下降。HyDE 让模型先针对问题写一段假设性的答案,这段假答案的形式更接近文档,再用它的向量去检索,离真文档反而更近。代价是多一次模型调用,假答案如果编偏了方向,检索也会跟着偏。
退一步提问(step-back prompting)要解决的是另一类问题。问题抠得太细,检索拉回来的全是零碎的细节,真正的通用原理反而进不来。它的做法是先往抽象的层面退一步,提一个更笼统的问题,把讲原理的那部分材料取回来,再回到原问题作答。以理想气体为例:先不问温度升高后压强怎么变,而是问压强究竟由哪些因素决定,有了原理,具体问题就好回答。两种方法的出处见本章参考资料。
改写还有第三种做法:多查询生成(multi-query)。模型先把同一个问题改写成几个问法不同的变体,“跨区支援的住宿费怎么报”可以同时改出“出差住宿标准与审批门槛”“支援期间住宿报销流程”,各自检索,最后用倒数排名融合把几张榜单合并成一张,RRF 这个名称,6.4 讲混合检索时再正式介绍。如果一个问法与文档措辞不匹配,检索就会一无所获,几个变体同时检索,匹配上的机会就大得多,召回范围会扩大不少。代价是每次多几次检索调用和查询嵌入,对问法五花八门的知识库,这笔开销几乎总是划算的。
这种“先检索、再评估、不行就重查”的思路,文献中有几个命名的变体,读资料时对得上号就行。给检索结果先做一道质量评估,判定为无关就改写查询或换数据源再检索的,叫纠错式 RAG(Corrective RAG,CRAG)。让模型边生成边反思、自己判断该不该检索、答案有没有依据的,叫自反思 RAG(Self-RAG)。按问题复杂度分流,简单问法走一次检索的直路,复杂问法才升级为多轮检索的,叫自适应 RAG(Adaptive RAG)。三套机制拆开看,对应的就是这一节讲过的迭代检索、生成端自评与路由,名称各不相同,骨架没有新东西。
search 不用从零重写,把 6.2 的实现包一层,就能挂进第 4 章的回路。工具定义也按第 4 章的 JSON Schema 来写,get_daily_revenue 直接复用那边写好的,这里只列出新增的那个。
python
POLICY_SEARCH_TOOL = {
"type": "function",
"function": {
"name": "search_policy",
"description": "检索公司制度知识库,返回与查询最相关的制度条款片段,"
"片段附条款出处。适用于考勤、年假、报销、门店运营规范等"
"制度类问题;查营业额等业务数据请改用 get_daily_revenue。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "检索词,用一句具体的制度问题,"
"如\"店长岗位的年假天数\"",
}
},
"required": ["query"],
},
}
}
def search_policy(query: str) -> list:
"""6.2 的 search 包成工具,返回带出处的片段,序列化交给执行器。"""
hits = search(query, top_k=4)
if not hits:
return [{"result": "没有检索到相关条款"}]
return [{"条款": h["text"], "出处": h["meta"].get("doc", "")} for h in hits]路由的关键就在描述里那句“查营业额等业务数据请改用 get_daily_revenue”。两个工具各管一类数据,边界写清楚了,模型的选择就不会含糊,这正是第 4 章 4.5 工具设计法中“用描述划定边界”的应用。回路本身一行不用改,run_agent 的 tools 清单换成这两个,其余照旧。
search_policy 返回的是列表而不是字符串,序列化由执行器那一行 json.dumps 统一处理,和 get_daily_revenue 使用同一个约定,谁也不需要自己多包一层。拿店长的那个复合问题运行一遍,执行轨迹大致如下(示意,只为展示决策链,不是真实输出)。
text
用户:我是店长,上个月跨区支援了一周,住宿费怎么报?调休怎么算?
第 1 轮 assistant 发起 search_policy(query="跨区支援 住宿费 报销 标准")
第 2 轮 tool 返回:第二条 报销……(覆盖报销,未提调休)
assistant 评估材料不全,发起 search_policy(query="跨区支援 调休 规定")
第 3 轮 tool 返回:第五条 调休。跨区支援期间的双休日计入调休……
assistant 汇总作答:住宿费凭发票在返回后 7 个工作日内报销,单晚超过
600 元需店长审批;支援期间的双休日计入调休,回店后 30 天内补休。
(依据:员工手册第二条、第五条)这三步里,每一步都要做决策。先是把复合问题拆成两个检索词,这是查询规划;再是发现材料没有覆盖调休、主动补检索一轮,这是迭代检索;换成“营业额最低的三家店要不要复训”,模型会先调用 get_daily_revenue 再调用 search_policy,这是路由。固定管道要处理这些情况,工程师得预先写成 if-else,问法一变就漏;决策权交给模型后,没见过的问法也能拆出一条可用的路径。
把上述零件组合起来,就是各家模型厂商都在做的深度研究模式。它的结构是两级编排:研究规划者负责拆分目标、安排步骤,网页搜索、抽取和分析这类无状态的工人只负责执行,结果交给规划者,自己不保留状态。材料够不够不由规划者一个人判断,中间设了一道批评者的审核关卡:判定不够,退回重新查;判定够了,才放行到写报告这一环。
审核关卡之外还有两条规则。其一是引用规则:没有来源的断言不允许进入答案,报告里每条关键主张都要标出处,答案与检索材料能对应上。其二是预算控制:每次运行设置 token 上限并配置告警,提前防止循环失控把费用滚大。本节开头那个店长的复合问题,还用不到这么大的开销;真到问题要综合几十个来源的时候,才值得使用。
关卡的思想还可以用在检索回路的细节中,常见的做法是两道评分加一道判决。生成前判断相关性:材料还没拼进提示词,先给检索回来的片段打一遍分,不相关的当场拦下,宁可少给。生成后判断依据和质量:答案是不是每句话都有材料支撑,行文是否合格,再打一轮分。两道判决连同理由按固定的 JSON 格式返回,放行、重新检索还是打回,由代码根据字段决定,用的正是第 3 章的结构化输出。两道评分各自要多花一次模型调用,值不值得要看答错的代价:制度答错要追责的场合,多花两三次调用买一道保险,成本上划得来。
还有另一条路线:检索不靠向量也能做。数据组织得规整时,结构化探索往往更直接,仓库的目录、文档的层级、函数名和文件名,都是摆在面上的线索。智能体带上 list_files、read_file 这几个轻量工具,先浏览目录结构,再挑选相关文件打开阅读,同样能找到答案,Claude Code 这类编码助手在代码库上走的就是这条路。向量检索擅长的是海量弱结构文本,结构化探索擅长的是组织良好的数据,两者在智能体手里都可以当作工具使用,并不互斥。
代码库这条线还要把边界说得更明确:代码检索默认用向量,十有八九会出错。语义相近造成的假阳性相当高。问“鉴权中间件怎么改”,按含义检索出来的可能是密码重置邮件的模板,两边都高频出现“用户”“校验”“安全”这类词,语义距离确实近,但相近是一回事,相关是另一回事。取错的片段还会占用上下文预算,把真正该进入窗口的代码挤出去。
Claude Code 自己的取舍就是现成的参考。2025 年 5 月之前,它用向量索引对代码库做检索;之后撤掉了整套方案,改为让模型亲自运行 glob 和 grep 搜索代码。创建者 Boris Cherny 公开讲过,搜索的本质就是 glob 加 grep,由模型自己驱动,比 RAG 那套更准,而且自己搜出来的结果,能说明是在哪里找到的。反对的一方也有道理:grep 每查一次都要实打实读文件,token 消耗比查索引高,仓库一大,开销不小。两边各有道理,结论却指向同一个方向:代码检索的默认做法从向量索引换成了结构化探索。
分界线很清楚:向量检索只留给智能体工具够不到的内容,比如库外的官方文档、厂商 API 的说明、不在仓库里的事故复盘。仓库里的代码,模型用搜索工具自己就能找完。判断标准只有一句话:智能体现有的工具能不能接触到这份材料?能接触到,就不必绕道向量。
结构化探索要做得顺畅,还有几个现成的小技巧。文本文件带行号读取,先编号、后按区间取,长文件只把需要的段落读进窗口;CSV 和 Excel 先转成 markdown 表格再交给模型,列的对应关系能保留,直接放入原始格式常常读错;PDF 走双通道,文字层抽取文本用于快速查找,每一页再转成一张图片,图表和版面看图才明白,两条通道按需使用。
等第 7 章讲上下文策略时,还会回到这个例子,到时侧重点是它怎么节省 token。
每一次决策都是一次模型调用,延迟和 token 成本随跳数增加。固定管道一次请求就能答完的问题,Agentic 可能要用掉三四次。决策一旦出错,错误路径就混在模型的推理里,比管道里的 bug 难查得多,只能靠轨迹复盘,整套方法第 12 章专门讲。
所以不要把 Agentic 当成默认选项。高频而简单的制度问答,一问一答一次检索,固定管道又便宜又稳定。问题分布复杂、需要多步推理、还要跨数据源时,把决策权交给模型才划算。架构什么时候该升级、简单方案什么时候够用,第 9 章展开。
练习:把前面那个复合问题各输入五遍,一遍给固定管道(整句只检索一次),一遍给挂了 search_policy 的回路。比较两边的回答,看报销和调休是不是都讲全了,再统计回路平均多用几次模型调用。
6.4 知识库的质量工程
知识库在 demo 阶段建完就没人再管,进入生产之后,它就变成一个持续变化的数据系统。制度会改版,年假从 5 天涨到 6 天,旧片段如果还留在库里,就是错误答案的来源;新文档不断进入,格式五花八门;检索质量也会随语料变化慢慢下降,上个月命中率不错的查询,这个月可能就掉出了前排。这一节做三件事:切分,把入库颗粒度做对;增量更新,让库跟上文档变化;评测,给质量一个数字作依据。
讲这三件事之前,先说一道更靠前的关卡:什么数据可以进库。切分和评测管的是入库以后的内容,可是往库里放什么,本身就是生产问题。密码、密钥、薪资数据一旦进入知识库,一句看似普通的提问就可能把它们带出去。
数据一入库,就要按密级贴上标签,公开、内部和机密至少三个档位,过滤和授权都以这套分级为依据。敏感项在入库前先扫描一遍,密码、密钥这类内容清理干净才能放行。也不要认为数据越多越好,无关信息一多,答案质量会下降,攻击面也随之扩大。用户群按最敏感的那一档划定,内外部都要使用时就拆成两个应用,对外的那个只放公开内容。库里的敏感材料越多,越要收窄与外部系统连通的接口。数据、不可信内容和对外通道都凑齐会出什么事,13.2 的致命三要素讲这个。检索进来的内容同样可能是污染源,文档里埋一句恶意指令,会顺着检索通道送到模型面前,第 13 章讲注入时再详细说明。
扫描有现成的工具可用。密钥这类通用的敏感项,TruffleHog、gitleaks 这类工具开箱就能扫描,命令行运行一次,按发现的顺序列出清单,还能输出 JSON 接入自动化流程;工号、手机号这类自家格式,用自定义正则表达式扫一轮也能找出来。误报要接受,某些模式在正常文件里高频出现,组织用不到的检测项直接排除。两道扫描跑完再抽检复核,入库这道关卡才算真正把住。
该进哪些数据,有一个直接的问法:一个新员工要上手这项工作,需要读哪几份正式文档?这份清单就是候选清单,wiki 里的闲聊内容不算。要建立一套完整流程,也有现成的做法:先盘点组织里的敏感数据类型,再明确需求、人工抽检、自动扫描,应用上线后用专门设计的提示词试着提取敏感内容,最后加一道输出过滤作为兜底。
6.2 只说明了要切分,没有讲怎么切。通用的切法是定长加重叠:按固定长度切分,相邻片段共享一段重叠文字,重叠通常取片段长度的 10% 到 20%,常见的片段大小在 200 到 1000 token。重叠的那一段是为了保留切断处的上下文,前一段的结尾和后一段的开头共享几个句子,代词和被切断的半句话就不会因为失去前文而无法理解。
LangChain 的递归分割器思路相同,但更细致:优先按段落切分,段落太大就降到句子,句子还大才落到字符层,常用参数是片段 800、重叠 80。
表格在切分中是另一类麻烦。对照表按行列存储信息,定长切分一刀下去,行与列的关系就被破坏了,嵌入出来的向量与哪一部分都不相关。半结构化的做法分两步:先让模型给每张表写一段摘要,写明这张表的用途、有哪些列、能回答哪类问题,摘要用来计算嵌入并入库;检索命中摘要,取回的却是整张表。摘要负责找得到,整表负责答得全,员工查调休对照表上的某一行,进入上下文的是完整的表,不是被切碎的几格。
切块还有一类通病,表格之外也避免不了:片段一从原文里切下来,所在的语境就丢了。一句写增长率的话,单看不知道是哪家公司哪个季度;一条写审批门槛的规定,单看不知道属于哪份制度哪一章。用这样的片段计算嵌入,向量也带着这种含混。有一种做法专门解决这个问题,叫上下文感知检索(contextual retrieval):入库之前,先让模型给每个片段写一小段说明,交代它出自哪份文档、讲的是哪一节,说明放在片段前面,合并后的整体一起计算嵌入、进入索引;检索命中时取回的仍是原文,定位依靠的却是补足了语境的新向量。这种做法配合关键词检索和重排,可以显著减少检索失败,代价是入库时每个片段多一次模型调用,好在这是一次性的离线开销,改版重新嵌入时才需要再付一次。
制度文档本身带有结构,可以切得更好。员工手册一条条写着“第X条”,每条条款都是一个自包含的知识单元,含义完整、长度适中,还有天然的编号可以作为出处。按条款切分,一条对应一个片段,比定长切分干净得多。定长切分如果恰好从条款中间切开,前半条和后半条各自丢失上下文;两条不同的规定被放进同一个片段,又会互相污染。
python
import re
def split_by_clause(doc_text: str, doc_id: str, max_chars: int = 300) -> list[dict]:
"""按"第X条"切分制度文档;超长条款再按句号二次切分。"""
pattern = re.compile(r"第[一二三四五六七八九十百\d]+条")
parts = pattern.split(doc_text)
heads = pattern.findall(doc_text)
chunks, idx = [], 0
for i, body in enumerate(parts):
# i=0 是第一条之前的开头段,原样入库,不往文本里添字
text = (heads[i - 1] + body).strip() if i else body.strip()
if not text:
continue
while len(text) > max_chars: # 条款太长,按句号再切
cut = text.rfind("。", 0, max_chars) + 1 or max_chars
chunks.append({"id": f"{doc_id}:{idx}", "text": text[:cut],
"meta": {"doc": doc_id, "seq": idx}})
text = text[cut:]
idx += 1
chunks.append({"id": f"{doc_id}:{idx}", "text": text,
"meta": {"doc": doc_id, "seq": idx}})
idx += 1
return chunks片段 ID 按 文档:序号 拼出,唯一、稳定、可读,出处可以直接印进答案。增量更新依靠的正是唯一 ID,用它作为比对的依据。ID 里带着来源、片段序号这类定位信息,新数据先和库里已有的比对一遍,重复的滤掉,只把新片段加进去。
再进一步就是生产版,只看 ID 不够用。条款内容变了而序号没变时,ID 还是原来那个,文本却已经是新文本了。因此要给每个片段再计算一个内容哈希,存入元数据,同步时按三种情况处理:库里没有的 ID 就新增,哈希变了的重新嵌入,库里多出来的 ID 删除。
python
import hashlib
def content_hash(text: str, meta: dict) -> str:
return hashlib.sha1(
(text + str(sorted(meta.items()))).encode("utf-8")).hexdigest()[:16]
def sync_chunks(chunks: list[dict]) -> None:
"""增量同步:只重新嵌入有变化的片段,删除已消失的片段。"""
# rules 和 embed 沿用 6.2 的建库代码
incoming = {}
for c in chunks:
meta = {**c["meta"], "id": c["id"], # id 进元数据,检索结果才报得出出处
"hash": content_hash(c["text"], c["meta"])}
incoming[c["id"]] = {**c, "meta": meta}
stored = rules.get(include=["metadatas"])
old_hash = {cid: (m or {}).get("hash")
for cid, m in zip(stored["ids"], stored["metadatas"])}
changed = [c for cid, c in incoming.items()
if old_hash.get(cid) != c["meta"]["hash"]]
stale = set(old_hash) - set(incoming)
if stale:
rules.delete(ids=sorted(stale)) # 条款删了,旧向量跟着删
for i in range(0, len(changed), 10):
part = changed[i:i + 10]
rules.upsert( # upsert 按 ID 覆盖,天然幂等
ids=[c["id"] for c in part],
documents=[c["text"] for c in part],
metadatas=[c["meta"] for c in part],
embeddings=embed([c["text"] for c in part]))
print(f"共 {len(incoming)} 段,新增或更新 {len(changed)} 段,删除 {len(stale)} 段")年假天数从 5 天改成 6 天,手册只需要重新运行一遍切分和同步。几百个片段里哈希变化的只有那一条,需要重新嵌入的也只有它。嵌入按 token 计费,全量重建和增量同步的费用,算下来相差两个数量级。同步挂在定时任务上,这就是 6.1 说的那条离线索引管道。
定时任务的频率决定了库的新鲜度上限。要求更及时的话,全量重建和隔夜批处理之间还有一档微批刷新(microbatch refresh):先确定目标新鲜度,比如数据最多滞后 60 秒,再每隔几秒到几分钟处理一小批新数据,只处理上次运行之后新增的部分。一批成功就把水位线向前推进,失败则退回上一次成功的水位线,按指数退避重试,靠幂等合并保证既不重复也不损坏。延迟超出目标窗口时,给数据打上陈旧标志,智能体做决策时就知道,手里拿的是最近一次确认过的快照。结论可能因此降级,但行动仍然连贯。
同步的触发不止定时任务一种方式。文档和数据如果本来就在业务库里,还有更及时的同步来源,叫变更数据捕获(Change Data Capture,CDC):源表一有新增或修改就推送给索引管道,切分、嵌入、入库一整条链路自动完成,不必等下一轮全量扫描。工单表新增一条记录,隔几分钟就能查到,新鲜度从隔夜提升到接近实时。
真要规划同步与重建,要按吞吐量计算,单位是每秒嵌入数:一段文本从计算嵌入到写入库算一条。测量方法很朴素,拿一千条片段运行一遍同步,总耗时除以条数。有了这个数,重建窗口就能估算出来:假如测得每秒一百条,十万片段的库单线程大约需要一千秒,不到二十分钟;窗口如果只有一小时,并行度开多大、要不要分片,心里就有数了。
吞吐量的上限常常不在本机,而在嵌入 API 的限速。6.2 那段代码一次最多发送十条,这还只是单次上限,账号每分钟能发送多少次请求另有配额,超了就会被限流。规划时用“批大小乘每秒允许的请求数”作为理论峰值,再留出两三成余量;真的遇到限流,就按指数退避降低速度,硬顶着重试只会越来越糟。本地部署 BGE 这类模型时,瓶颈变成 GPU 显存和批量大小,测量方法不变,仍然是每秒嵌入数。
哈希比对之外,元数据也要专门设计。doc、生效日期和适用岗位这类字段都要写进元数据,检索时用 where 过滤。店长一发问,先把范围限定在适用岗位含店长的条款上,其余条款根本进不了候选集。
适用岗位这道过滤同时也是一道安全关卡,检索阶段的权限控制走的就是同一套机制。每个身份能看哪些文档,入库时定义成权限标签写进元数据,检索那一刻先按权限收窄范围,无权查看的内容根本不进入候选集。这道关卡放在检索之前还是检索之后,两边各有代价。检索后过滤实现简单,先按全库检索再按权限筛选,但 top_k 的位置可能全部被无权查看的片段占据,筛完剩不下几条,只能加大 k 重新查询,甚至一无所获;检索前过滤先把范围收紧,安全上不留缺口,代价是召回可能受影响,过滤条件与 ANN 索引结合时,候选一少,近似检索的漏检就更明显,6.2 说过单一大集合配合查询期过滤把匹配率拉低到五成,就是这种代价的极端情况。
通行的做法仍是检索前预过滤。召回不足可以用其他方法弥补,按 6.2 的思路拆分多个集合、调整索引都可以,唯独不能把无权限的内容放进候选再指望事后筛选。这道关卡还必须设在检索系统里,与 13.2 的原则一致:权限判断在数据层,不指望提示词里的模型替安全把关。
分类这项元数据不必等人一条条标注。拿库里的片段做一轮聚类,看看自然聚成几组,每组抽几条样本阅读并命名,一套分类体系就有了雏形,再人工整理一遍就可以作为过滤项使用,问消防类的问题先按类别缩小范围。企业里这类体系多半已经零散存在,商品目录、合规文档的分类和客服工单的话题层级都是现成的素材,聚类起到的作用,是把它们跟检索这一侧对上。
生效日期还能避免一个常见的问题。制度改版时,常常新旧两份文件同时存在,旧版手册没有删除,库里就同时有两套条款,向量检索分不清哪一套已经作废,不做过滤的话,回答给员工的可能就是已作废的 5 天年假条款。只有按生效日期或版本把旧片段排除在检索范围之外,这个问题才算解决。
第三件事是评测。没有数字作依据,切分和同步做得再勤,也只是凭感觉。检索质量评测就是第 12 章评测方法论的一次具体应用,做法完全一样:确定一套固定的任务集和指标,改动前后各运行一遍做对比。
任务集就是一批问题,每个问题标注应命中的片段 ID,指标只看两个。命中率(Hit Rate@k)统计的是,有多大比例的问题,应命中的片段进了前 k 条;平均倒数排名(Mean Reciprocal Rank,MRR)把每个问题第一条命中的排名取倒数再求平均。MRR 比命中率多看一层:即使命中了,排到第四位与排第一位的实际效果相差很大。
python
def evaluate_retrieval(cases: list[dict], k: int = 4) -> dict:
"""cases 元素形如 {"question", "expected_ids"},返回命中率与 MRR。"""
# rules 和 embed 同样沿用 6.2 的建库代码
hit_count, rr_sum = 0, 0.0
for case in cases:
res = rules.query(
query_embeddings=embed([case["question"]]),
n_results=k, include=["metadatas"])
top_ids = [m["id"] for m in res["metadatas"][0]]
rank = next((i + 1 for i, cid in enumerate(top_ids)
if cid in case["expected_ids"]), None)
if rank:
hit_count += 1
rr_sum += 1 / rank
n = len(cases)
return {"hit_rate": hit_count / n, "mrr": rr_sum / n}任务集不用为来源发愁。第一批直接取真实问法:客服记录、员工群里的高频问题,十几条就能起步,标注好应命中的条款编号,按第 12 章案例卡的方法慢慢积累。
什么时候运行评测,触发点也很明确。更换嵌入模型之前必须运行一遍,同时验证全库重建后的效果;调整切分参数、修改 top_k 和增加元数据过滤,每次改动前后各运行一遍,数字下降就回滚。知识库在不断新增内容,任务集也要跟着补充,新上线的文档配上几条新问题,任务集才不会与库脱节。
评测再往下深入,先要把幻觉分成两类。答案与检索回来的材料矛盾,叫内在幻觉(intrinsic hallucination),问题多出在检索侧:材料取错或根本没取到,就回去修索引和检索。答案与客观事实矛盾,叫外在幻觉(extrinsic hallucination),问题出在生成侧:是模型自己编造的,就修提示词和生成。同样是答错,归到哪一类,下手的方向完全不同。
命中率和 MRR 衡量的是“该取回的内容有没有进入前排”,另一类失败它们发现不了:材料明明检索回来了,答案仍然不行。评价“检索到的材料够不够回答问题”,要看三个方面。回答这道题需要的每条材料是否都在,是完备性;调休那一半没有检索回来,材料再相关也不完备。几条材料之间是否互相矛盾,是矛盾性;新旧两版年假同时出现,材料再齐全也无法给出干净的回答。材料是否含糊其辞,是歧义性;“原则上从简安排”这种措辞能否支撑一个确定的答案,本身就要先判断一轮。
判断这三个方面,只能请模型当裁判:把问题连同检索回来的材料交给一个独立的评审模型,按三个方面各给出一个判决,输出固定的 JSON。判决宁可取二元,不取打分,充分或不充分、矛盾或不矛盾,迫使评审说清依据,也比数值分数在多个评审之间更可比。端到端的答案质量另有 LLM 裁判负责,这一层评价的是材料本身的质量,两层叠加,检索侧的评价才完整。
三方面的判决写成代码,就是一次结构化输出的评审调用,用的正是 3.1 的 JSON mode。
python
import json
import os
from openai import OpenAI
judge_client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="<DeepSeek 兼容端点,见官方文档>",
)
def check_materials(question: str, chunks: list) -> dict:
"""判断检索回来的材料够不够答题,完备、矛盾、歧义三个二元判决。"""
materials = "\n\n".join(
f"【片段{i}】{c['text']}" for i, c in enumerate(chunks, 1))
resp = judge_client.chat.completions.create(
model="deepseek-v4-flash",
temperature=0,
response_format={"type": "json_object"},
messages=[
{"role": "system",
"content": "你是检索材料的质检员。对照【问题】检查【材料】,只输出 JSON,"
"格式:{\"complete\": true 或 false, \"contradictory\": true 或 false, "
"\"ambiguous\": true 或 false, \"reason\": \"一句话依据\"}。"
"complete 指材料是否已包含答题所需的全部内容;"
"contradictory 指材料之间是否互相矛盾;"
"ambiguous 指材料是否含糊到无法支撑一个确定的回答。"},
{"role": "user",
"content": f"【问题】{question}\n\n【材料】\n{materials}"},
],
)
return json.loads(resp.choices[0].message.content)chunks 直接传 6.2 search 的返回值,判决是 true 还是 false 由代码按字段接住,不靠解析自然语言。
三个方面的判决还能直接指明改进方向。判为不完备,回去修召回和 top_k;判为矛盾,回去修生效日期过滤和版本管理;判为歧义,生成端就要把不确定性如实说出来,不要硬答。把材料缺陷分好类、导回对应的环节,比一句笼统的“检索质量差”更容易落实。
整条链路能达到的上限,取决于检索质量。材料没有检索回来,后面的生成模型再强也发挥不出来。所以这一层先用确定性的指标把关,端到端的答案质量留给 LLM 裁判评价,那是第 12 章的内容。
调参还有一个顺序问题,乱调会陷入循环。可调的参数分布在三层:摄取层管切分和嵌入模型,检索层管 top_k、过滤和重排,生成层管提示词和温度。层与层之间有依赖,这个循环我也走过一次:检索分数下降就去调整切分,切分一动全库重新嵌入,重新嵌入之后检索分数又变了,绕了一圈回到原点,越调越糊涂。要避免陷入循环,就要固定调整顺序。摄取层一动就要全库重来,成本最高,起步阶段就要确定下来,之后要改,先经过评测再全量重建;日常调优的主要对象是检索层,改动只影响在线一侧,评测也运行得快;生成层最后收尾。从摄取到检索再到生成,每一步的取舍都用本节的评测数字作依据。
检索这一层还有三个常见的调优手段,接下来几段可以当作备查清单,每种说明是什么、解决什么问题,不必逐一记住,调优时哪种用得上再回来细看。混合检索把关键词匹配和向量检索的分数归一化后加权,精确的编号与模糊的语义两头兼顾;MMR 在相关性和多样性之间取得平衡,避免前四条全是同一条规定的重复;重排把检索分成粗排和精排两段。
MMR 的全称是最大边际相关性(Maximal Marginal Relevance),解决的是结果扎堆的问题。按相关度一路排下来,前几条常常是同一条规定的近似内容,一个问法取回四段几乎一样的话,占着 top_k 的位置却没有增加新信息。MMR 挑选片段时两方面都看,既看与查询的相关度,也看与已选中的片段是否相似,每一步都倾向于既相关、又不重复的候选。一次要取多条、又要求条条不重复的场景,最适合使用它。
关键词这一路也有自己的经典算法,使用最久的一类是 BM25(Okapi BM25)。一个词在这篇文档里出现得越多,两者越可能相关,但这种相关性会饱和,同一个词出现第十次,并不比出现第九次更能说明问题;打分还要按文档长度做归一化,同样的词频,出现在短文档里比出现在长文档里更有意义。它的前身 TF-IDF 只计算了词频和词的稀有程度,BM25 在这之上补充了饱和与长度两项修正,排序更准,一直沿用至今。这一路在混合方案里始终占有一席之地,依靠的正是字面精确匹配:问句里带着专有名词、条款编号这类固定标记时,向量按语义比较远近常常分不出高低,关键词一检索就能直接定位到原文。
召回与精排是两段不同的工作。前一段就是向量召回,问句和片段各自编码后比较相似度,速度非常快,但两段文字从头到尾没有任何交互。精排这一段换成交叉编码器(cross-encoder),把问句和候选片段拼接起来整体送入模型,让两边逐词对照,判断谁更相关自然更准确;只是每一对候选都要单独运行一遍模型,整库根本运行不了,只能先靠召回把候选缩小到几十条,再用它排出最终的名次。粗排追求速度,精排保证精度,分工很明确。
几路召回的结果要合并成一份榜单,直接的写法是加权相加,但相加之前要先归一化,问题在于两路分数的量纲不一致。余弦相似度在 0 到 1 之间,BM25 的分数没有上界,两路不归一化就直接加权,量纲大的那一路会完全压过另一路,加权就形同虚设。通行的做法是 min-max 归一化,按各路自己的最大最小值把分数压进同一个 0 到 1 的区间,再按权重合成;想连归一化这一步都省掉,还有一种只看名次的方法,叫倒数排名融合(Reciprocal Rank Fusion,RRF)。它不看分数大小,只看排名先后,把每个检索器给片段的名次取倒数再加总,量纲不同的几路检索结果也可以直接合并。
粗排与精排之间还有一种过渡形态,叫迟交互(late interaction)。它不做单向量压缩,而是给文本保留一组 token 级的向量。查询端和文档端各自编码,计算分数时让每个查询 token 逐一与文档的全部 token 向量比较,取最大余弦相似度(MaxSim),再把逐 token 的最大值汇总成相关度分数。
文档侧的向量离线计算好保存,在线只需要计算查询这一端,比交叉编码器便宜得多,又比单向量更精细。代价同样存在:多向量放不进常规的单向量 ANN 索引,需要支持多向量比对的引擎。ColPali 把这个思路推广到文档页,整页截图直接编码入库,OCR 和版面解析都省掉了。
重排同样有代价,并非稳赚。它把看起来更相关的内容往前提,但相关不等于答案正确。语义相近但不含答案的干扰段落,重排一把这类段落提前,它们反而显得更有说服力,这个风险 6.5 再详细说明。各种手段谁合适,要靠分阶段的失败监控来判断:重排器是否稳定,看同一查询多次运行的分数方差,方差大就是不稳定;提示构建要关注截断率,也就是材料拼进模板时被截掉的比例;问题按索引、检索、重排、提示、生成分阶段归因,哪一段出了问题就使用哪一段的指标。
切分还有一个与引用相关的隐患。制度条款经常互相引用,“调休按第五条执行”这句话写在第二条里,切成独立片段之后,引用指针还在,被指向的内容却不在手边:检索命中第二条,第五条进不了上下文,答案就缺了一半。解决办法有两档,轻的一档在切分时处理,把引用双方切进同一个块,或者两块互相挂指针、取回时一起带上;重的一档是换表示方式,把引用关系建成图上的边,向量负责找起点、沿边连接关系,这正是 6.5 要讲的解法。混合约束的问法同理,条件一半在表格里、一半在条文里,“华东区哪些门店的消防检查频次低于制度要求”这样的问题,表格和条文两边都要查到,靠的也是这种找起点再连关系的办法。
切分决定检索颗粒度的下限,增量更新保证知识的新鲜度,评测让每一次调整都有依据。
6.5 图谱检索,当相似度不够用时
向量库的质量保住了,仍有一类问题回答不了。“结算服务升级会波及哪些下游”就是例子:制度里一条写结算依赖财务,另一条写财务依赖消息队列,从头到尾没有哪个片段同时提到结算和消息队列。向量检索按语义远近取片段,取回来的全是带“依赖”二字的段落,中间那一跳始终接不上。
再问“过去一年制度里哪类事项调整最频繁”,答案同样不在任何单一片段里,而是分散在整个库中,要逐条统计才能回答。
这两种问法答不好,不是库没有调好,而是向量这种表示方式本身有上限。它的缺陷可以归纳为五条:上下文失忆、关系盲视、时序盲视、推理瘫痪、工具混乱。第一条指每次查询都从零开始,跨次的理解积累不下来;最后一条指工具之间的关系看不出来,编排只能靠猜测。中间三条都与表示方式有关,讲的其实是同一件事:文本相似度给不出事实之间的联系。
向量检索的底层动作,是把查询和片段比较语义远近。它最擅长“答案与问题相似”的本地问法:问句和答案段落用词接近,这样的段落自然排在前面。
向量检索对付不了的是全局问法。微软研究院把这两类区分开来:本地查询的答案在少数几个文本区域里;全局查询则要求对数据集的大部分甚至整体做综合,答案不会完整出现在任何一个片段中。
这组对照在 BenchmarkQED 基准上进行:一边是带一百万 token 上下文窗口的向量 RAG,一边是图谱路线的 LazyGraphRAG。窗口大到几乎能装下整个数据集,但向量 RAG 遇到全局查询,仍然全面失败。
把数字拆开更直观。向量 RAG 处理 DataLocal 这类“检索具体事实”的查询,性能超过 90%,答案在某个来源里,语义相似度天然找得到;但换到 ActivityGlobal 这类需要整体分析的查询,只剩 20% 到 30%。同一份对照里,LazyGraphRAG 在多跳推理类查询上领先向量 RAG 50% 到 60%,数字按微软论文口径。图检索方向的厂商也有自己的研究:十万页规模的语料下,向量 RAG 准确率下降约 12%,图路线只下降约 2%。这组数字只作参考口径,方向与那组对照是一致的。
这个结果彻底否定了“窗口够大就不必检索”的想法。信息都在窗口里,并不等于模型能把散落各处的信息组织成综合答案。第 7 章 7.1 要讲的注意力稀释,在这里换了个更大的窗口,又重演了一遍。
还有一种更隐蔽的失效叫干扰效应(distracting effect):与查询无关、语义却相近的段落混进上下文,模型不会拒答,反而自信地给出错误答案。有研究专门量化过这种效应,结论很反直觉:重排不仅没有压制干扰,反而在放大干扰。检索器越精巧,送回来的段落与查询语义越接近,即使不含答案,也等于把说服力更强的干扰内容推到了前排。
三档健康计划在“急诊室就诊”“住院”“外科手术”这些条目上措辞几乎一样。问银计划的紧急住院保障,向量相似度可能把金计划那条 90% 报销的条款检索回来,智能体用错误的费率计算赔付,多付几千美元都有可能。6.4 刚说用重排提升精度,这里又说它会放大干扰,两件事并不矛盾:答案确实在库里,只是没进前排,重排有用;答案根本不在库里,只有相似段落顶替,重排就成了放大错误的那一个。图这条路对这类干扰天然免疫。PathRAG 只沿真实存在的边走,这位投保人与金计划条款之间没有边,错误条款根本进不了上下文。
解决办法是换一种存储方式。知识图谱(knowledge graph)用节点和边存储知识,节点代表实体,边代表实体之间的关系,检索从比较相似度变成沿边遍历。结算依赖财务、财务依赖消息队列,两条边就摆在那里。问波及范围,就从结算节点沿依赖边走两跳,答案直接从图上读出来,不用从文本里拼凑。
实体在图里有稳定的身份。CRM、邮件和工单三个系统里的同一位客户,在图上就是同一个节点,问跨系统的问题,三个系统措辞的差异就不再造成干扰。这条路上最有名的实践是 Google 搜索,2012 年提出“从字符串到事物”的转变。搜 Jaguar 能区分车企、猛兽和球队,靠的是实体和关系,不是关键词匹配。
时间维度在图里也能表示。年假从 5 天改成 6 天时,新旧条款在向量空间里几乎重合,措辞九成一致,相似度区分不开。6.4 给片段挂生效日期,靠元数据过滤排除旧版;有效归有效,时间仍然藏在元数据里,不进入向量。图则把时间直接编入结构:边可以带生效区间,新旧两版各占一段,问“上个月此刻适用哪一版”,按区间一查就知道。第 8 章记忆库的 graphiti 就采用这种记法,8.3 会把它展开成两个时间轴。
建图要先经过抽取这一关:让模型读一遍员工手册,输出一组组三元组,比如调休规则引用支援定义,支援定义适用出差办法,报销标准依赖出差办法。每条三元组都是两个节点加一条带名称的边。
抽取实现起来就是第 3 章的结构化输出:给模型一份提取提示词,让它从一段制度文本里识别出节点和边,按固定的 schema 返回。示意如下:
json
{"triples": [
{"source": "调休规则", "edge": "引用", "target": "支援定义"},
{"source": "报销标准", "edge": "依赖", "target": "出差办法"}
]}建图也不必做成一次性的大工程。智能体每读一份新文档,顺便把里面的实体和关系抽取出来加入图中,图随着使用一点点增长。第 8 章的记忆系统走的就是这条路:对话和执行历史里每出现一条值得记录的事实,当场抽取成结构化条目入库,知识图谱与记忆在机制上合流,差别只在一个存放公共文档,一个存放私有经历。
抽取也会出错,错误的形式很具体。把“调休规则适用于全体员工”抽成两个节点一条边,语法上没有问题,但“全体员工”并不是值得单独建节点的实体。这类噪声一旦进入,图会越积越乱。兜底的机制,就是下面这套分层存储方式。
抽取出来的内容怎么存放,生产级的答案是分成三张图。领域图存放经过治理校验的可信数据,是唯一的事实来源,规范的产品清单、权威的组织层级都在里面;词汇图保留原始文本,只进不改,溯源完整;主题图存放 LLM 抽取的实体和事实,附带置信度、模型版本这类抽取元数据。三张图先各自隔离,等实体解析(把不同系统里指向同一实体的记录认定为同一个)建立起可信链接之后才连通,抽取错误就不会污染可信层。这套分层还有一个好处:更换抽取模型时,只需要从词汇图重新抽取主题图,领域图不用改动。它在生产环境里处理过数十亿实体量级的数据,量级只作参考。
沿着主题图的置信度设计再往前一步,是多视角存储:同一个属性允许挂多个值,各自带有来源和置信度,不强行合并成一个。员工手册写报销在返回后 7 个工作日内提交,某区域的实施细则写 5 个工作日,两份都有效,图上就让这条属性同时挂两个值。采信哪个值,在读取时按权威顺序决定:总部制度优先于区域细则,新版本优先于旧版本,裁决规则本身也写成图上的关系。强行合并的坏处是把冲突隐藏在一个数值里,等审计要还原当时依据的是哪一版,已经无法追溯;多挂几个值多占用一点存储,换来的是证据链完整。
数据分成三张图,约束则交给本体。本体不只是节点和边的清单,还可以声明定义域、值域这类语义约束:哪类节点连接哪类边、一条保障规则能作用于哪类理赔,都固化在 schema 里。它拦截的正是那种语法成立、领域却荒谬的边。把牙科处置放进视力保障里去批准,就是一个现成的例子,两条都是带额度的保障项,结构上对得上,语义上对不上。针对 ESG 知识图谱有一套本体引导框架,报告过一组对照:不做本体校验时系统错误率 83.3%,加上校验后降到 19.44%,论文自报口径。
用户说“上个月跨区支援了一周,调休和住宿怎么算”,向量检索先用这句话把调休规则节点筛选出来,它语义最接近;随后从该节点沿引用边走到支援定义,再走到出差办法。就这两条边,6.3 里要模型反复跳两轮才能连上的链路,一趟就走完了。最后把沿途节点的条文连同关系合成上下文,模型据此回答。
实体身份在这里也发挥作用。同一位店长,工单系统里叫张先生,CRM 里挂在注册手机号下,邮件署名却是英文名。三处叫法不同,向量检索只能各查各的。在图中,三个账号合并到同一个实体节点,问“这位用户上季度提过几类诉求”,就从节点出发把三个系统的边遍历一遍,一次完成。
把三个账号合并成一个节点,术语上叫实体解析,做法里有不少讲究。直接让 LLM 判断两条记录是不是同一个人,天生有四个短处:同一个输入,运行两次可能给出不同结论;它给出的解释是事后补充的;非西方姓名的变体识别不全,阿拉伯语、汉语和俄语的取名习惯都是例子;置信度分数也反映不了真实的准确率。
证据式解析的做法不同:它不直接给出“是或不是”,而是把结论展开成一条可以核对的证据链。看一条匹配就能理解,总体置信度 89% 由三处构成,姓名 87%、地址 100%、电话 95%,哪一环较弱,一眼就能看出来。图上的解析结果按三种边分类,RESOLVED 指强匹配、记录属于同一实体,POSSIBLY_RELATED 指较弱的相关证据,DISCLOSED 指数据里本来就写明的关系。抓欺诈时,智能体沿 RESOLVED 边追踪化名,再顺着 POSSIBLY_RELATED 边查找关联人员。
工程上的实体链接常分三段进行。先用属性键粗筛,电话、证件号这类强键先把明显不同的记录排除一大批;再用字符串相似度细比,Jaro-Winkler 这类算法给姓名地址打分,超过阈值的进入下一轮;最后让模型结合语境校验,看两条记录的上下文是否吻合。成本高的判断只用在真正模糊的少数记录上,人工审核也只看最后拿不准的那一小部分。
全局查询在图上有专门的解法。微软 GraphRAG 在索引阶段让模型把图聚合成一些社区,也就是紧密相连的实体群,并为每个社区预先写好摘要;查询阶段让每份摘要各自产出一份局部答案,最后归约得到全局答案。面对分散在整个库中的综合问题,不需要现场翻遍全部片段,摘要已经把该综合的材料提前准备好了。
处理全局问法不止建图一种做法,树也是一种。RAPTOR(Recursive Abstractive Processing for Tree-Organized Retrieval),走的是纯文本的层次索引:先把语料里含义相近的片段聚成组,让模型给每组写一份摘要;再把摘要当作新的材料重新聚组、重新摘要,一层层递归向上,整棵树就从底层的原文片段长到顶层的总览。查询时树的每一层都可以取用,问细节就落到低层片段,问全局就落到高层摘要,也可以把几层混合检索。它解决的是与图谱同一类的问题,即散落在整个库中、没有一个片段能单独回答的问法,但省去了实体与关系抽取这一大块工程;要付出的代价是中间层的摘要会磨掉细节,问题恰好抠在细节上时,还要回到底层去取原文。结构化索引由此有了两条路:图谱靠关系把事实连起来,摘要树靠层次把全局信息准备好。第 8 章 8.3 压缩记忆库用的聚类加摘要,与这里是同一种方法,那边为的是节省检索位置,这边构建的是检索树。
图检索的查询分三段进行:先把问题转成向量,筛出起点节点,再沿着带名称的边走两跳,最后把沿途节点的条文连同关系合成上下文交给模型。
工程上不需要在向量和图之间二选一。主流做法,是把两者放进同一条三段式流水线。
第一段仍然交给向量检索,用问题筛选出语义相关的候选节点,这一步用向量最合适。第二段从候选节点出发做图遍历,把关联的实体和关系一并扩展进来:从调休规则节点沿引用边走到支援定义,再走到出差办法,传递链就接上了。第三段把两路材料合成上下文交给模型。向量负责找起点,图负责连关系。
第二段的图遍历是什么样子?用 networkx 演示一遍最直接(pip install networkx):
python
import networkx as nx
kg = nx.DiGraph()
kg.add_edges_from([
("调休规则", "支援定义", {"rel": "引用"}),
("支援定义", "出差办法", {"rel": "适用"}),
("报销标准", "出差办法", {"rel": "依赖"}),
])
# 从向量筛出的起点出发,沿边走两跳
reached = nx.single_source_shortest_path_length(kg, "调休规则", cutoff=2)
for node, hop in sorted(reached.items(), key=lambda kv: kv[1]):
if hop > 0:
print(f"{hop} 跳:{node}")
# 1 跳:支援定义
# 2 跳:出差办法建图那几行就是三元组的直接翻译:两个节点,加一条带名称的边。遍历只需一行,把起点两跳内可以到达的节点连同跳数一起返回,前面“跨区支援一周”那条链路就这样走完了。6.3 的迭代检索里,一跳就是一次模型调用,问句越长跳数越多;而这里的一跳只是图查询的一步,跳得再多也还是这一次查询的成本,稳定得多。
图上做推理,还有两条基于学习的路线,当作备查内容记住即可。一条把图输入图神经网络,让模型在训练中学会哪类节点与哪类查询相关,查询来了单次前向就能给出相关子图,速度快,但推理模式固定在权重里;另一条用强化学习训练智能体在图上迭代,每走一跳观察一次、再决定下一跳,速度慢但灵活,训练好的策略还能迁移到别的图上使用。生产中的主流仍然是前面的规则遍历加模型合成,这两条路先放着,等图和查询量都大了再回头看。
建图本身有工程成本:文档里的实体和关系通常靠模型批量抽取,有费用也有噪声;schema 随业务演进,维护是持续的投入;知识面太宽或文档结构松散的库,硬建图得不偿失。后面几段是备查清单,每段管一件事,每段第一句就是主题,看一眼有个印象,真要使用图时再回来逐条对照。
维护也有方法。schema 要演进,迁移就要遵守规则:每次变更都必须与上一个应用版本保持向后兼容。重命名关系类型这类操作要分两个阶段发布,先在旧结构旁边加上新的,等新代码上线后再删除旧的,中间不留一段谁都无法运行的窗口。
图数据的体量会自己增长。一个团队在 Neo4j 上维护时态基础设施图,200 项资源每 5 分钟采样一次,仅一个小集群一天就要多出 57,600 个节点。200 项资源乘以一天 288 个采样点,算术上完全吻合。不做生命周期管理,几个月后查询就无法运行。快照按 TTL 分级:前七天保留全粒度,接着降为小时级,再往后降为天级。账号、区域这类充当查询锚点的枢纽节点,则永久保留。
边的过期也有讲究。新信息与既有事实冲突时,旧边打上 t_invalid 时间戳而不是删除,历史查询仍然按有效期窗口过滤。增量更新借用了 LightRAG(香港大学团队的开源图谱检索框架)的 MERGE upsert:没见过的对象就创建,已有的就补全,清单里消失的依赖标记为失效而不是删除。剪枝要把握分寸:运营快照可以清得激进一些,事故记录则要保留得长远得多。剪得太狠会把历史模式一并剪掉,而智能体预测将来哪里会出问题,依靠的正是过去事故的样本。
图建起来之后,质量也需要数字作依据,向量那边的命中率和 MRR 套用不了,图要看另外四项。精确率靠抽样:随机取一批查询,人工对照取回的边和节点是不是真的支撑了答案,算一个比例。穷尽度靠核对:拿一组已知应该走得通的路径,检查图里是不是真有这些边,缺失的就是遗漏。新鲜度看告警:源文档改版之后图没有跟上,两边一比就是陈旧度,超过阈值就报警。覆盖度看比例:本体里声明的几类关系,图里实际出现了几类,比如本体定义了十种关系、图里只长出六种,支撑推理的连接就缺了一部分。
图里还能存放工具。每个工具连同它的输入类型和输出类型一起注册成节点和边,get_daily_revenue 输入门店 ID、输出营业额数字,search_policy 输入自然语言、输出条款,类型对得上,链路就可以直接从图上读出来。这解决的是嵌套调用的长期难题:让模型一层一层猜下一步该调用谁、上一个输出怎么接下一个输入,最强的模型也会在第二层第三层上出错;把类型匹配交给图,猜测就变成了寻路,“营业额最低的三家门店要不要复训”这类问题,先取数、再查制度的链条不用猜,沿着边走就行。
图对“查不到”的解释方式,还关系到一个安全问题。数据库默认封闭世界假设,查不到就当作不存在;知识图谱天生是开放世界假设,一条边缺失只说明还没有录入,不说明关系不存在。智能体把“查不到”当作“不存在”去行动,事故就发生了:员工问某类门店有没有消防演练的规定,图里没有这条边,回答一句“没有规定”,门店从此不做演练,可这个“没有”其实只是“不知道”。解决办法是使用 SHACL(Shapes Constraint Language):这个 W3C 的图校验标准把哪些关系必须存在、哪些禁止存在、哪些可有可无声明成形状约束,检索前、行动前、写入后各设一道校验。通过校验的查询,“没有”才可以当真;没有通过的,答案只能是未知,而未知不能作为行动的依据。
要决定用什么存储图,先看最省事的路径:图不一定要放进图数据库。PostgreSQL 加 Apache AGE 扩展就能存图,pgvector 存嵌入,源文档、向量和关系都放在同一个库里,事务支持是原生的,多库同步的麻烦也一并省掉。已经有企业 AI 公司整体从图数据库迁移到了这个方案。真要使用专门的图数据库,自建还是托管,六条判断依据列在下面:
- 论控制力,自建把 schema 语义和推理逻辑全部掌握在自己手上,托管则必须服从厂商的 API 和模型类型。
- 性能和规模上,自建可以贴合自己的负载调优,工程投入要自己承担;托管则遍历、推理开箱即用。
- 运维复杂度,自建要自己承担索引、存储、扩容,托管则连运维带支持一并提供。
- 集成方面,自建的图层可以直接嵌入现有的关系库或数据湖,托管则可能受限于厂商生态的适配器。
- 成本构成上,自建前期投入高而经常性开销低,托管正相反,前期省事,许可与使用费却持续产生。
- 落到场景,图如果是自研 AI 平台的一部分,自建更合适;分析、推荐、给知识库补充数据这类负载,交给托管更省心。
还有一点要明确:买图数据库,买到的只是存储和查询引擎,Customer→Order→Product 这类领域模型不在其中,schema 怎么设计、每条关系是什么含义,仍然需要自己完成。
值不值得使用图,要看问法的形式。问法经常要跨好几条事实接力才答得上来、实体关系密集又稳定,就值得使用图,制度体系、代码依赖、供应链都是典型场景。如果问法以“这一条是什么”为主,属于 FAQ 型的库,向量配合 6.2 的元数据过滤就足够了。
6.6 结构化数据的另一条路,text-to-SQL
前五节反复处理的都是非结构化文本,做的主要是切块、转向量和比较相似度。但连锁品牌另有一类数据天生不在文档里:营业额、考勤和报销流水都存放在业务库的表里。店长问“上个月营业额最低的三家门店是哪几家”,6.3 给出的办法是挂载 get_daily_revenue 工具,函数要人工编写。问法稍有变化,想按区域汇总、按品类算环比,就要再增加一个函数。更通用的做法是让模型把自然语言直接翻译成 SQL,在库上执行,这条路叫 text-to-SQL。数据在哪套系统里,查询就在哪套系统上执行,不需要导出成文本、切块入库再返回。
第一道难点是模式链接(schema linking):把问句里的词逐个对应到库里的表和列。“上季度成交了多少单”里的“成交”,库里究竟是 total_sales 还是 revenue?“区域”是门店表上的一个列,还是独立的一张区域表?自然语言本身带有歧义,要靠语境分辨;SQL 又要求精确,必须贴合库的模式。这是 text-to-SQL 的核心瓶颈,库越大、命名越混乱,这道差距就越大。
在大库上解决模式链接,有一种常用的设施叫语义层(semantic layer):在异构数据源之上建立一层统一的业务术语,“成交”在这一层里只有一个定义,指向哪张表哪一列都写清楚,各系统自己的叫法在底层各自映射。它解决的是同名异义的问题:门店系统按城市划分“区域”,财务系统按大区划分“区域”,不先统一口径,两条 SQL 各算各的,报表对不上又查不出原因。text-to-SQL 的提示词里带上的就是这套统一术语,连锁算错的根源就此消除;语义层同时也是数据治理的一部分,口径定在一处,人和智能体使用的是同一套词。
生成的 SQL 会以四种方式失败,每一种都配有对应的回退办法。
- 遇到歧义,问句自己就没有说清,比如“成交”没有定义。回退的办法是反问澄清,把两种可能的口径列出来让用户选择。
- 模式对不上时,表列明明叫 total_sales,SQL 却写成 revenue。回退是带上模式提示重新生成一次,把相关的表结构重新放进提示词,用户这边没有任何感知。
- 出现幻觉,SQL 引用了库中不存在的表或列。回退是执行前先核对,用库的真实 schema 校验引用,对不上就打回重写。
- 执行错误:语法错误、超时、权限不足。回退是降级到简化查询,把复杂问法改写成一个更基础的版本,仍然能给出有用的结果。
主回路落实到代码就是三步:表结构放进提示词,模型负责翻译成 SQL,执行前再用真实表名核对一遍引用到的表:
python
import os
import re
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="<DeepSeek 兼容端点,见官方文档>",
)
SCHEMA = """CREATE TABLE stores (store_id TEXT PRIMARY KEY, region TEXT, city TEXT);
CREATE TABLE daily_revenue (date TEXT, store_id TEXT, revenue_yuan REAL);"""
REAL_TABLES = {"stores", "daily_revenue"} # 生产里从 information_schema 读
def text_to_sql(question: str) -> str:
resp = client.chat.completions.create(
model="deepseek-v4-flash",
temperature=0, # 翻译要的是稳定,不是发散
messages=[
{"role": "system",
"content": "把问题翻成一条 SQLite 查询,只输出 SQL,不要解释。表结构如下:\n"
+ SCHEMA},
{"role": "user", "content": question},
],
)
sql = resp.choices[0].message.content.strip()
# 幻觉的回退:执行前抽出 SQL 里引用的表,对一遍真实表名
used = {t.lower() for t in re.findall(r"(?:from|join)\s+(\w+)", sql, flags=re.I)}
if not used <= REAL_TABLES:
raise ValueError(f"引用了不存在的表:{used},打回重新生成")
return sql代码里的第一道防线就是模式链接:列名全部写进提示词,模型不必猜“成交”对应 total_sales 还是别的什么。最后那道校验对应幻觉回退:SQL 引用了库里没有的表,执行前就先拦下来。问“上个月营业额最低的三家门店”也好,问“哪个区域本季度总额最高”也好,入口都是同一个函数,问法变了也不用新写工具。相比 6.3 逐个挂载工具,这里省下的就是这一块。
模式链接还有两种最省事的改进方法,都是在提示词上下功夫。一种是给列配上说明性的注释,比如在 revenue_yuan 旁边注明存的是当日营收,模型对口径就不用猜,注释最好写进表定义里,业务含义跟着表结构一起进入提示词,不必另外抄写一份;另一种是在提示里带上两三条示范 SQL,每条示范配上对应的问法,模型照着样子翻译,比只给一张裸表结构稳定得多。上面的示意代码为了简短只放了裸表结构,真正用于生产时,先把这两点补齐。
SQL 要在真实的库上执行,环境纪律比准确率更靠前,先确定四条底线:查询先在测试沙箱里运行通过,再接触生产;只连接只读副本,从源头杜绝写入和更新;设置查询超时和资源上限,不让一条失控的连接拖垮整个库;全量查询日志保持开启,审计和排错才有依据。这四条与 13.2 的沙箱、最小权限出于同一原则,那边有完整的展开。
评测要区分一对指标。精确匹配(exact match)把生成的 SQL 与标准答案逐字比对,写法差一点就算错;执行准确率(execution accuracy)只看执行结果,结果集相同就算对。SQL 的正确写法常常不止一种,连接顺序对调、别名换掉,结果不变,硬套精确匹配会把正确的 SQL 判成错的,生产评测因此多以执行准确率为准。出错后还要能够定位:把 SQL 拆成选表、选列、条件、聚合等组件分别计分,选表出错与只错一处聚合条件,要改进的方向差别很大。
判断两条写法不同的 SQL 算不算同一个查询,也有一个省事的顺序:先把别名去掉、替换回真实的表名列名,再把等价写法归一,连接顺序排序、无关紧要的括号去掉,然后比较结构;结构对上了,再比较执行结果。评测里那些“写法不同、结果相同”的误判,按这个流程就能纠正,不要把字面差异当作语义差异。
向量检索和 text-to-SQL 各有分工:前者处理非结构化的制度文本,后者处理结构化的业务数据。在 6.3 的工具箱里,两者本来就是平等的工具,谁的数据归谁处理。制度问答助手最后的形态是:员工手册放在向量库,营业数据放在业务库,一个问题需要哪种材料,由模型自己路由。
反过来说,SQL 库里也能做语义搜索。工单表的备注列、巡检记录的问题描述列,增加一个向量列就能按含义查询,pgvector 在 6.2 已经演示过这条路线的 SQL 写法,结构化条件和语义条件可以在同一条查询里合并使用。多语言混合库的场景另有现成的解法:多语言嵌入模型把不同语言的文本编码到同一个语义空间,中文问句天然与英文文档靠得近,bge-m3 覆盖一百多种语言就是这种做法,中英文混合的库不必分建两套。
本章小结
- 6.1 RAG 三阶段:索引、检索、生成各管一段,哪个阶段出问题按阶段定位;检索只送相关片段,实测能把一次 37,000 多 token 的上下文压到 304 个;生成端只依据材料回答,材料里没有的就如实说没有。
- 6.2 嵌入与向量库:库里存的向量和查询用的向量必须出自同一个嵌入模型,换模型等于全库重建;中文项目 API 起点用 text-embedding-v4,本地部署用 BGE 系列;数据本来在 Postgres 就用 pgvector,要独立服务就用 Chroma;跨模态靠 CLIP 做对比训练,成对图文拉近、随机错配推远,一张图和描述它的那句话落到相近位置,以文搜图就是这么来的。
- 6.3 Agentic RAG:路由、查询规划和迭代检索三件事把检索决策交给模型,复合问题拆得开、查得全;固定管道一次请求答完的问题它要三四次调用,高频简单的制度问答不要默认用它。
- 6.4 质量工程:入库前按密级分级、敏感项先扫描;按条款切分让片段自包含;增量更新按内容哈希分三种情况处理;检索质量用命中率加 MRR 量化,每次改动前后各跑一遍评测。
- 6.5 图谱检索:向量答不了关系类与全局类问题,图用节点和边接上那一跳;三段式是向量找起点、图连关系、合成上下文;开放世界假设下“查不到”不等于“不存在”,SHACL 校验负责把关。
- 6.6 text-to-SQL:难点集中在模式链接,歧义、模式错配、幻觉和执行错误四类失败各有回退办法;执行环境四条底线,测试沙箱、只读副本、超时上限和全量日志;评测看执行准确率,不看字面是否一致。
第 4 章讲过,智能体的行动能力来自函数调用;这一章补上另一半,知识能力靠检索。检索本身就是一种工具,挂进回路后,模型按需取用。
参考资料
- 阿里云百炼 text-embedding-v4 模型信息(默认 1024 维、64 到 2048 维可选;单价以模型页为准,不录)https://help.aliyun.com/zh/model-studio/text-embedding-v4 核实于 2026-08
- 阿里云百炼 向量化模型 OpenAI 兼容接口(OpenAI 兼容的调用形态、dimensions 参数,端点地址见该页)https://help.aliyun.com/zh/model-studio/embedding-interfaces-compatible-with-openai 核实于 2026-08
- 阿里云百炼 文本向量同步接口(text-embedding-v4 最大行数 10、单行最大 8192 token;Batch 批量调用有折扣,单价不录)https://help.aliyun.com/zh/model-studio/text-embedding-synchronous-api 核实于 2026-08
- BAAI bge-large-zh-v1.5 模型卡(1024 维、最大输入 512 token)https://huggingface.co/BAAI/bge-large-zh-v1.5 核实于 2026-08
- BAAI bge-m3 模型卡(1024 维、最大输入 8192 token、100 余种语言、稠密/稀疏/多向量三种检索)https://huggingface.co/BAAI/bge-m3 核实于 2026-08
- OpenAI 文档 Vector embeddings(text-embedding-3-small 默认 1536 维、text-embedding-3-large 默认 3072 维)https://developers.openai.com/api/docs/guides/embeddings 核实于 2026-08
- sentence-transformers all-MiniLM-L6-v2 模型卡(384 维、英文语料,Chroma 默认嵌入函数的模型)https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2 核实于 2026-08
- Carbonell & Goldstein, The Use of MMR, Diversity-Based Reranking for Reordering Documents and Producing Summaries(SIGIR 1998,MMR 的原始论文)https://dl.acm.org/doi/10.1145/290941.291025 核实于 2026-08-28
- Chroma 官方文档(PersistentClient、集合命名规则与元数据过滤、向量与全文检索、Apache 2.0,集合名长度 3 到 512 字符见 manage-collections 页)https://docs.trychroma.com/ 核实于 2026-08,命名规则复核于 2026-08-29;本章代码在 chromadb 1.5.9 实测
- pgvector 仓库 README(v0.8.6、vector 类型 2000 维上限、余弦距离操作符 <=>、HNSW 索引)https://github.com/pgvector/pgvector 核实于 2026-08
- 《AI Agent 开发实战》第 4 章 知识型 Agent(RAG 三阶段、索引四步、Agentic RAG 对比与三类 RAG Agent、八种融合方式、Chroma 集成与基于唯一 ID 的增量更新、递归分割与 MMR),见 txt-ref/ai-agent开发实战/
- 《Build an AI Agent (From Scratch)》第 5 章 Building knowledge bases with RAG(内部数据的检索动机、四种搜索方法、嵌入与切分参数、O(n) 与 ANN、向量数据库盘点、37312 到 304 token 的压缩实验、结构化探索),见 txt-ref-zh/build-ai-agent-from-scratch/
- 微软研究院博客 BenchmarkQED: Automated benchmarking of RAG systems(本地/全局查询划分、一百万 token 上下文窗口的向量 RAG 与 LazyGraphRAG 对照结论),https://www.microsoft.com/en-us/research/blog/benchmarkqed-automated-benchmarking-of-rag-systems/ ,核实于 2026-08-28
- 微软论文 From Local to Global: A GraphRAG Approach to Query-Focused Summarization(社区划分与摘要、映射归约答全局查询),https://arxiv.org/abs/2404.16130 ,核实于 2026-08-28
- Gao, Ma, Lin & Callan, Precise Zero-Shot Dense Retrieval without Relevance Labels(HyDE 假设文档嵌入的原始论文:先生成假设性文档、再以它的嵌入检索,ACL 2023)https://arxiv.org/abs/2212.10496 核实于 2026-08-31
- Zheng, Mishra, Chen, Cheng, Chi, Le & Zhou, Take a Step Back: Evoking Reasoning via Abstraction in Large Language Models(退一步提问:先问抽象问题取回原理再答原题,Google DeepMind,理想气体压强为例)https://arxiv.org/abs/2310.06117 核实于 2026-08-31
- 《AI Agents and Applications with LangChain, LangGraph and MCP》第 8 章 高级索引(多向量检索、父子块双层结构:子块生成嵌入、父块进上下文)、第 9 章 问题变换(改写-检索-阅读、多查询生成、退一步提问、HyDE、单步与多步分解)、第 10 章(自查询:从问句推断元数据过滤器;TimeWeightedVectorStoreRetriever 时间衰减公式),Roberto Infante,Manning 2026,译文见 txt-ref-zh/agents-langchain-langgraph-mcp/
- 《Agentic GraphRAG》第 1 章(向量检索五缺陷、strings to things 与 Singhal 2012 出处、混合三段式架构、DataLocal 与 ActivityGlobal 性能数字、EyeLevel 十万页对照)、第 3 章(三图架构:领域图/词汇图/主题图分层与实体解析建链;证据式解析与 RESOLVED/POSSIBLY_RELATED/DISCLOSED 三种边;自建与托管六判据、PostgreSQL 存图路线)、第 5 章(干扰效应与重排放大、PathRAG 结构性防护、R3-RAG 双奖励与 skip_retrieval、本体定义域值域校验 OntoMetric 83.3%→19.44%)、第 8 章(schema 迁移 N-1 兼容与两阶段发布、Fischer 时态图 57,600 节点/天与 TTL 分级、LightRAG MERGE 增量更新、事故记录长于运营快照),Anthony Alcaraz 与 Sam Julien,O'Reilly 2026,译文见 txt-ref-zh/agentic-graphrag/
- 《Building Multimodal Generative AI and Agentic Applications》第 2 章(单集合载荷过滤与多集合局部索引的 50%/98% 匹配率对照)、第 6 章(迟交互与 MaxSim、ColPali/ColQwen 多模态变体)、第 9、10 章(重排与交叉编码器两阶段检索、混合检索与分数融合、倒数排名融合 RRF、嵌入 L2 归一化)、第 14 章(text-to-SQL 模式链接、歧义/错配/幻觉/执行错误四类失效与回退策略、测试沙箱/只读副本/超时上限/全量日志、精确匹配与执行准确率、组件级准确率)、第 18 章(按管道阶段的 RAGOps 失败追踪:重排器分数方差、提示截断率),Indrajit Kar,BPB Publications 2026,译文见 txt-ref-zh/building-multimodal-agentic/
- 《GenAI Security》第 6 章(RAG/上下文注入/微调三路供数的可撤性判据、数据选用五原则与六步流程、新员工判据)、第 8 章(向量与嵌入薄弱点:向量库须同数据库设防、嵌入还原文本研究),Paul Zenker,CRC Press 2026,见 txt-ref-zh/genai-security/(全书译文,数字保真核对通过)
- Victor Dibia《Designing Multi-Agent Systems》(2025,Manning)第 14 章(5,622 家 YC 公司两阶段过滤:关键词过滤器对实际智能体公司 53.1% 召回 406/765、LLM 结构化分析补齐漏网),见 txt-ref-zh/designing-multi-agent-systems/
- Micheal Lanham《AI Agents in Action》第 2 版(Manning Publications 2026)第 9 章(深度研究智能体的外部状态与计划对象、五类终止闸门)、第 11 章(两级编排:规划者大脑加无状态工人、批评者充分性闸门、引用纪律与 token 预算),见 txt-ref-zh/ai-agents-in-action-2e/
- 《Building Reliable AI Systems》(构建可靠 AI 系统,Manning 2026,MEAP 版)第 1 章(内在幻觉与外在幻觉二分)、第 4 章(Airbnb 嵌入案例:双编码器离线预计算加实时查询编码、点击隐式信号与位置偏差去偏、冷启动内容嵌入、技术指标让位业务指标),见 txt-ref-zh/building-reliable-ai-systems/
- Blaize Stewart 与 Ed Huang《Agentic AI Data Architectures》第 3 章(微批刷新:目标新鲜度 60 秒口径、水位线推进、失败回退与陈旧数据标志、降级但连贯的决策),O'Reilly 2026,见 txt-ref-zh/agentic-ai-data-architectures/
- Song & Shmatikov, Information Leakage in Embedding Models(从嵌入向量还原输入文本、属性推断与成员推断三类攻击,CCS 2020)https://arxiv.org/abs/2004.00053 核实于 2026-09-01
- Amiraz, Cuconasu, Filice & Karnin, The Distracting Effect: Understanding Irrelevant Passages in RAG(干扰效应的量化:无关但语义相近的段落诱发自信错答、精巧检索与重排放大干扰,ACL 2025)https://arxiv.org/abs/2505.06914 核实于 2026-09-01
- Li 等, R3-RAG: Learning Step-by-Step Reasoning and Retrieval for LLMs via Reinforcement Learning(结果奖励加过程奖励的双奖励训练、何时检索/检索什么/如何分解、多跳问答 52.6% 高出基线 15 个百分点,EMNLP 2025 Findings,论文自报口径)https://arxiv.org/abs/2505.23794 核实于 2026-09-01
- Faysse 等, ColPali: Efficient Document Retrieval with Vision Language Models(整页截图的多向量嵌入、迟交互匹配免 OCR 管道)https://arxiv.org/abs/2407.01449 核实于 2026-09-01
- Yu 等, OntoMetric: An Ontology-Driven LLM-Assisted Framework for Automated ESG Metric Knowledge Graph Construction(本体校验前后错误率 83.3% 与 19.44%,论文自报口径)https://arxiv.org/abs/2512.01289 核实于 2026-09-01
- Addy Osmani《Agentic Engineering》(O'Reilly,Early Release 2026)第 4 章(Over-RAG 反模式:代码场景语义相似度假阳性高、检索块挤占已知相关的预算、检索该留给工具够不到的知识,模型越强判断越成立),见 txt/agentic-engineering.txt(英文原文,第 4 章,行 1891-1927)
- The Pragmatic Engineer 访谈《Building Claude Code with Boris Cherny》(Gergely Orosz 主持;Claude Code 创建者谈代码检索:模型驱动的 glob 与 grep 胜过向量索引,自己搜的说得出在哪找到)https://newsletter.pragmaticengineer.com/p/building-claude-code-with-boris-cherny ,核实于 2026-09-01(2025-05 弃向量检索换 grep/glob 的口径与裁决核对一致)
- LightRAG 代码仓库(香港大学 HKUDS 团队的开源图谱增强 RAG 框架,6.5 增量更新 MERGE upsert 的出处)https://github.com/HKUDS/LightRAG ,核实于 2026-09
- 《AI Agents and Applications with LangChain, LangGraph and MCP》第 6 章(FAISS 一类向量函数库的能力边界与向量数据库的分界)、第 8 章(表格等结构化内容先做摘要再嵌入、命中摘要取回整表;块扩展把前后相邻块并入命中的细粒度块)、第 10 章(相似度分数阈值检索器、语义 SQL 搜索),Roberto Infante,Manning 2026,译文见 txt-ref-zh/agents-langchain-langgraph-mcp/
- 《Agentic GraphRAG》第 1 章(学习式图推理两条路线:图神经网络单次前向找相关子图、强化学习智能体图上迭代检索)、第 3 章(多视角模式:同条语句挂多来源值、读取时按权威裁决;开放世界对封闭世界假设与智能体安全、SHACL 检索前/行动前/写入后三种使用模式;图谱质量四维:精确率/穷尽度/新鲜度/覆盖度;三阶段实体链接与 Jaro-Winkler 相似度)、第 6 章(工具连同输入输出类型注册为图节点、类型匹配替代嵌套调用猜测)、第 7 章(充分上下文框架:完备性/矛盾性/歧义性三维与二元裁决),Anthony Alcaraz 与 Sam Julien,O'Reilly 2026,译文见 txt-ref-zh/agentic-graphrag/
- 《Building Multimodal Generative AI and Agentic Applications》第 2 章(CLIP 双塔把文本与图像编进共享嵌入空间)、第 6 章(检索相关性评分器与生成式响应评分器的回路整装、纠错式 RAG/Self-RAG/自适应 RAG 命名谱系)、第 9 章(Cohere Rerank 一类商用重排 API),Indrajit Kar,BPB Publications 2026,译文见 txt-ref-zh/building-multimodal-agentic/
- 《Building Complex Multi-Agent Systems Using Pattern Prompting》第 3 章(RAG 参数依赖图与防打环的调参次序)、第 5 章(摄取吞吐按每秒嵌入数规划、变更数据捕获 CDC、企业数据中的分类体系发现),Tim O'Brien,Packt 2026,见 txt/pattern-prompting-mas.txt(英文原文)
- 《GenAI on Google Cloud》第 2 章(语义层:异构数据源之上统一业务术语、防同名异义连锁算错),Ayo Adedeji 等,O'Reilly 2026,见 txt/genai-google-cloud.txt(英文原文)
- 《Building Reliable AI Systems》第 4 章(乘积量化等向量压缩、FAISS 索引类型、MTEB 榜单与嵌入模型选型),Rush Shahani,Manning 2026,译文见 txt-ref-zh/building-reliable-ai-systems/
- 《Build an AI Agent (From Scratch)》第 5 章(结构化读取技巧:文本带行号按区间读、CSV 与 Excel 转 markdown 表格、PDF 文本层加页面转图双通道),Jungjun Hur 与 Younghee Song,Manning 2026,译文见 txt-ref-zh/build-ai-agent-from-scratch/
- 《GenAI Security》第 6 章(敏感数据扫描实操:Trufflehog 命令行扫描、JSON 输出接自动化、误报与检测项排除),Paul Zenker,CRC Press 2026,见 txt-ref-zh/genai-security/
- 《构建 Agentic AI 系统》(原书 Building Agentic AI Systems,Packt 2025)第 1 章(生成模型的类型分野:自回归、仅编码器、编码器-解码器,嵌入模型出身的来历),Anjanava Biswas 与 Wrick Talukdar,清华大学出版社 2025,见 txt/构建AgenticAI系统.txt
- 《AI Agents with Java》第 8 章(RAG 作为世界模型:按格式选文档解析器、解析后清洗先于切分),Alex Soto Bueno、Markus Eisele、Mario Fusco,O'Reilly 2026 Early Release,见 txt/ai-agents-with-java.txt(英文原文)
- 《Building Agentic Applications with CrewAI and MCP》第 3 章(多语言嵌入模型把不同语言编进同一语义空间),Max Gfeller,Manning 2026,见 txt/crewai-mcp.txt(英文原文)
- 《Designing AI Agents》第 4 章(切分不顾交叉引用结构的失败与同块、带指针对策),Jia Huang,Manning 2026,见 txt/designing-ai-agents.txt(英文原文)
- 《Building Agent-Powered Applications》第 4 章(实体、关系与事件抽取喂给知识图谱构建的建图侧视角),Vasyl Zvarydchuk,Packt 2026,见 txt/building-agent-powered-apps.txt(英文原文)
- 《Build an AI Agent (From Scratch)》第 5 章(关键词检索的老牌算法:TF-IDF 拿词频乘逆文档频率估相关,BM25 在其上按文档长度调整、计入词频饱和),Jungjun Hur 与 Younghee Song,Manning 2026,译文见 txt-ref-zh/build-ai-agent-from-scratch/
- Anthropic 官方博客《Contextual Retrieval in AI Systems》(2024-09-19 发表;入库前用模型给每个片段生成所在文档的上下文说明、原文连说明一起嵌入并建关键词索引,配上重排后检索失败率大幅下降),https://www.anthropic.com/news/contextual-retrieval ,核实于 2026-09-16
- Sarthi 等, RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval(树状层次索引:对片段递归做嵌入、聚类与摘要,自下而上构起不同抽象层的树,查询时各层均可取材),https://arxiv.org/abs/2401.18059 核实于 2026-09-16