【强烈收藏】AI Agent全栈开发之路(15):RAG技术详解与向量模型实战
从零搭一个能跑起来的 Agent 不难,难的是让它不胡说。把知识库塞进“Prompt”里的做法,在一个两个文档时还能忍,真到了几万字、几十章产品手册面前,要么超出模型能接住的长度,要么塞进去的内容互相打架。我这一期直接切入 AI Agent 全栈开发中最关键的一块地基:RAG 检索增强生成,以及一切检索效果的地基——向量模型实战。
先说清楚这期到底讲什么。RAG 全称 Retrieval-Augmented Generation,翻译过来是“检索增强生成”。它解决的问题很明确:大模型只记得训练时见过的东西,而你的私有知识、最新动态、行业细节,它一概不知道,硬答就只能张冠李戴。RAG 的做法是在每次让模型回答前,先从知识库里把相关的片段捞出来,塞进提示词里作为参考资料,让模型“查了资料再说话”。这套东西对做客服机器人、企业知识库问答、合同尽调、故障诊断辅助这类 Agent 场景几乎是标配。
这个系列走到第 15 篇,前面聊过 Agent 的框架、工具调用、记忆设计,但一直没系统讲 RAG。原因很简单,RAG 看着简单,真正上线后影响效果的因素太多。切片粒度、向量模型选型、相似度阈值、召回重排,任何一个环节粗糙一点,回答质量就肉眼可见地往下掉。这期我会按“思路拆解 → 向量模型选型 → 分块与索引 → 检索与生成实战 → 踩坑排查”的顺序,把整条链路讲透,适合已经跑通基础 Agent、正准备接入私有知识的开发者,也适合那些被“看起来能答、细看全错”折磨过的运维同学。
1. 内容整体设计与思路拆解
RAG 不是某个算法,而是一套流程的组合。它包含三个固定环节:离线索引、在线检索、增强生成。我在做某企业内部知识助手时,最初以为只要把所有文档扔进向量库,再把 Top3 命中片段拼进提示词就能完工,结果测试时一顿狂翻车。后来把一条条链路拆开调,才明白问题大多出在前期设计。
1.1 先搞清楚 RAG 到底解决什么问题
大模型的能力边界,本质是被训练数据时间裁剪过的。比如你的系统里有一套最新的售后退款规则,2024 年年底才更新,模型的训练语料里根本没有。用户问“现在退款要传哪些凭证”,它只能根据旧知识或网络公开信息瞎编,甚至编得有模有样。RAG 的存在就是给模型外挂一个“可实时查询的知识仓库”。模型不直接记忆内容,而是根据语义相似度找候选段落,再由模型读取候选段落和问题来生成答案。
一个完整 RAG 系统的核心流程可以这样拆:
- 把原始文档切成若干文本块,每一块一般几百到上千字。
- 对每个文本块用向量模型编码,得到一个固定维度的向量,比如 768 维、1024 维。
- 把这些向量和原始文本一起存进向量数据库。
- 用户提问时,对问题也做一次向量编码。
- 在向量库里按余弦相似度或点积相似度检索出最相近的 K 个文本块。
- 把问题、检索出的文本块、系统提示词组装成完整 Prompt,送给大模型。
- 大模型基于这些内容生成回答,必要时给出引用出处。
这七步里最影响最终质量的,是第 2 步的向量模型和第 5 步的相似度阈值。向量模型负责“理解文本语义”,阈值负责“别把不相关的也塞进来”。我调试时发现,很多人把精力全花在提示词上,却忽略了这两项,结果无论怎么改 Prompt 都救不回检索错误带来的污染。
1.2 一个容易被忽略的设计:先确定场景再选方案
RAG 的落地要分场景讨论。不是所有知识都能直接切成块扔进向量库。比如企业内部有一份操作规范,章节之间引用关系强,这种文档光靠语义向量容易把上下文切碎;而大量用户提问的 FAQ 数据,问题答案一一对应,简直是 RAG 的天然适配对象。场景差异直接决定了要不要混合检索、要不要重排、用多大切片。
我把常见的 Agent 知识需求分成三类:
- 问答型知识:FAQ、产品说明、帮助手册。适合单纯 RAG,分块小一点,检索精度高。
- 推理型知识:故障排查、诊断树、复杂规则判断。必须配合规则过滤或思维链,RAG 只提供候选材料。
- 动态数据型知识:实时库存、订单状态、价格。这类不适合用文档 RAG,更适合工具调用和 API 查询。
做这套设计时有个经验:先画“问题会怎么被问”的树状图,再决定文档结构和检索策略。很多踩坑是拿着 PDF 就切,完全不考虑用户真实问法,最后检索召回的片段似是而非,模型答题自然跨掉。
1.3 为什么这期把向量模型单拎出来讲
RAG 里检索效果的 70% 由向量模型决定,剩下 30% 才是分块和数据库调参。我见过不少项目用同一个向量模型跑了半年,检索召回率只有不到 60%,用户问“我忘记密码怎么办”,召回出来的是“密码重置接口返回 401”。不是模型不行,是选型错了。不同向量模型对长文档、特定领域术语、多语言混排的敏感度完全不同,最忌讳“别人说好用就直接上”。
所以这期不只讲 RAG 流程,还要重点讲怎么给向量模型做评测、定维度、选相似度算法。后面第 3 节我会给出我常用的评测方法,那一套跑下来,你能根据自己的语料选出最合适的模型,而不是盲抄推荐。
2. 向量模型选型:三个核心维度与一套可用评测方法
向量模型也叫嵌入模型,它的任务是把一段文字转换成数字数组,让语义相近的文本在向量空间里距离也更近。可以把它粗暴理解成给每段话“拍照”,照片上保留的是这段话的主要含义,丢掉了标点和表面措辞。向量模型的优劣直接影响检索的准确度,而它的好坏又不能用肉眼直接看出。下面是我实测大半年后沉淀的选型框架。
2.1 维度一:语义契合度,尤其是领域术语和口语化表达
通用向量模型在新闻、百科、社交语料上表现不错,但一旦换成行业黑话,效果会直线下降。举个例子,某医疗 Agent 需要理解“患者主诉胸骨后压榨样疼痛”,一个基于通用语料训练的向量模型可能把“压榨样”当成无关词,而优秀领域微调模型会把它和“心绞痛”牢牢绑定。选型时你不要只看模型参数大小,要看它是否在你的语料领域上做过适配。
实操中我会准备一份约 300 条“问题-标准答案片段”配对语料,覆盖常用问法、同义改写、断句歧义、多实体混问四类场景。然后跑一轮评测,看召回率、准确率和延迟。评测样本不用特别多,但必须真实抓到用户会问的句子,千万别拿文档标题当问题来测。这样选出来的模型才更贴合真实场景。
2.2 维度二:输入长度上限与切片策略的匹配
向量模型存在最大输入长度限制,常见的有 512 token、2048 token、8192 token。如果你把 2000 字的文档整段喂进去,超出上限的部分可能被直接截断,向量表征也会变差。这里要特别提一个容易误导人的概念:模型支持长文本,不代表长文本的向量一定语义清晰。多个实验表明超过 512 token 后,部分模型的语义区分度反而明显下降。
因此选向量模型之前,先要确定切片长度。我的习惯是:FAQ 类用 200 字以下小块;操作手册用 300-500 字;复杂技术文档配合小节标题再切语义块。切片长度的上限必须低于向量模型输入上限的 70%,留出余量防止标点、特殊符号造成 token 计算偏差。
2.3 维度三:相似度计算方式与向量维度
常见的相似度计算是余弦相似度和向量内积。很多向量模型在训练时已经默认用余弦相似度,直接用内积可能出现反直觉结果。我见过不少开发者在代码里没有对向量做归一化,然后又用内积计算相似度,导致长文档天然得分偏高,短句命中率极低。解决办法很简单:向量入库前先做 L2 归一化,再统一用点积计算,这样效果等价于余弦相似度,且计算更快。
向量维度高低各有取舍。高维度(1536 维以上)通常语义表达能力更强,但存索引和计算相似度的开销更大;低维度(384 或 512 维)速度快、占空间小,但在复杂语义场景下召回率会吃亏。我的建议是:如果知识库在百万条以内,优先选 1024 维左右;如果数据量大且并发高,再考虑降维或量化的方案。
2.4 推荐评测框架:五组语料一次跑完
我把自己常用的评测设计成一个简单但有效的框架,你也可以拿来即用:
| 评测组 | 测试内容 | 通过标准 |
|---|---|---|
| 同义改写组 | 同一问题的 5 种问法 | 至少 4 种能召回目标片段 |
| 跨段落组 | 答案分散在两三个段落中 | Top10 命中率超过 80% |
| 易混淆组 | 相似但含义不同的两批文本 | 错误召回不超过 20% |
| 长文本组 | 单段超过 800 字的文本 | 中心语义不丢失 |
| 混合语言组 | 中英混杂、代码与自然语言混杂 | 关键词命中并能区分实体边界 |
每次换向量模型,跑完这五组,效果好坏立刻清晰。这个框架能帮你避免“凭感觉选模型”的陷阱。
3. 实操环节一:文档切块、向量化与索引入库
选好了向量模型,下一步就是把知识库内容真正变成可检索的索引。这一步藏着大量细节,很多人没到线上就失败了,因为切块切错了、元数据丢了、向量库连接方式选错了。我按实际操作顺序拆开讲。
3.1 切块策略:固定大小是一把双刃剑
最朴素的切块方法是按固定字符数切开,比如每 500 字一块,相邻重叠 50 字。这种策略最大优势是简单可控,毫秒级完成;但缺点是会把完整语义切断。比如一个段落讲“免税资质申请条件”,下一个段落马上是“申请流程及材料”,这两段本来是递进关系,固定切块很可能把“条件”和“流程”混在一个块里,检索时定位混乱。
更推荐的做法是“结构化切块 + 小段补充”。先把 Markdown 标题、列表、表格识别出来,按标题层级作为自然边界;每个章节内部若仍有超过 500 字的段落,再按句子切。这样做既保留了章节完整性,又让单块长度适中。实际操作时我会写一个解析脚本,先识别文档里的标题层级树,再把每个叶子节点对应的文本块打上“章节路径”作为元数据,例如“第三章 > 3.2 > 退款条件”。这些元数据后面可以用于检索过滤,比如用户问“退款条件”时优先只在“退款”目录下搜索,效果提升非常明显。
3.2 元数据设计与过滤:提高精度的隐藏利器
向量检索只能回答“哪个文本块和问题语义接近”,但用户还有一类常见约束:“只看某种类型”“只看最近三个月”。这些约束靠向量本身很难解决,必须靠元数据过滤。我建议每个知识块至少维护三个字段:
- source:原始文档名称、URL 或编号。
- doc_type:文档类型,如 FAQ、操作手册、政策法规。
- updated_at:最后更新时间或业务有效期。
建立好这些字段后,检索前可根据用户的限定条件倒排过滤,再走向量相似度。熟练后你会发现,带元数据过滤的检索效果往往优于单纯调大向量模型。比如某机构知识库里同时有 2023 年和 2025 年两份报销制度,如果不过滤时间,新版旧版内容会同时被召回,模型很可能把旧版条款当成正确答案。加上 updated_at 过滤之后,这类错误立刻消失。
3.3 库表结构设计与索引参数
向量数据库里存放的核心元素是“文本块 + 向量 + 元数据”。建索引时我会给向量字段设置兴趣度指标,包括度量方式、索引类型。度量方式通常选择余弦相似度,但如果前面做了归一化,可使用内积。索引类型在近几百万条数据规模下,推荐采用 HNSW 这类近似最近邻搜索结构。它能在毫秒级返回结果,代价是索引构建耗时略高。
入库时的批量大小也会影响效率。我实测过,每批次 64 条到 128 条相对稳定,太大容易内存增长异常,太小则吞吐量上不去。所有入库操作最好在服务初始化或后台任务里完成,不要和在线请求混在同一进程,否则容易互相挤占资源。
3.4 一个可用的完整入库伪代码参考
import hashlib import numpy as np # 假设已配置好 embedding_client 和 vector_db def encode_texts(texts, embed_client): vectors = [] for text in texts: vec = embed_client.embed(text) # 得到一维向量 vec = np.array(vec) vec = vec / np.linalg.norm(vec) # L2 归一化 vectors.append(vec.tolist()) return vectors def build_index(documents, embed_client, vector_db): chunks, sources = split_by_structure(documents) # 结构化切块 vectors = encode_texts(chunks, embed_client) batch = [] for i, (vec, chunk) in enumerate(zip(vectors, chunks)): record = { "id": hashlib.md5(chunk.encode()).hexdigest(), "vector": vec, "text": chunk, "source": sources[i], "doc_type": documents[i].type, "updated_at": documents[i].update_time, } batch.append(record) if len(batch) >= 128: vector_db.upsert(batch) batch.clear() if batch: vector_db.upsert(batch)这段代码是一个骨架思路。真实项目里,你还要加节点状态跟踪、失败重试、增量更新任务。我的经验是入库任务最终要和线上服务解耦,跑成独立作业,否则一次大批量导入就可能让接口响应从 200ms 涨到 3 秒。
4. 实操环节二:查询改写、向量检索与相似度阈值
在线检索这步,最容易出现两个问题:用户问题太口语化导致语义漂移;相似度阈值设太高导致召回太少。这一节讲我怎么处理这两类问题,以及最终如何把检索结果交给大模型。
4.1 先做查询改写,再去做向量编码
如果你把用户原话“密码找不着咋整”直接丢给向量模型,很多向量模型会把重心放在“找不着”“咋整”上,反而弱化了“密码”这个关键实体。这里我会引入一个轻量级查询改写步骤:让大模型先把用户问题改写成适合检索的疑问句候选,或者提取出核心实体和意图。
具体操作有两种。第一种是规则式改写,通过同义词映射、错别字纠正、停用词过滤把问题规整化。第二种是模型式改写,适合复杂问法。例如用户问“我之前那台设备坏了,开不了机,客服让我找售后记录,我现在怎么查?”,模型改写后变成两个候选检索问题:“查询设备售后记录”“开不了机故障排查”。两个候选分别进向量库,召回结果合回去重,能显著提高覆盖率。
不过改写会多一次模型调用,会增加约 200 到 500 毫秒延迟。对于客服场景一般可接受;但如果你的 Agent 要求极低延迟,可以把改写和主回答共用一个模型,同一个会话里先调改写,再调最终生成。
4.2 向量检索参数:Top-K、阈值与过滤条件
检索时我习惯设定 Top-K 从 5 到 10,再按相似度从高到低截断。Top-K 太小容易漏召回关键证据;太大则把无关内容混进上下文,大模型容易被噪声带偏。一个比较稳妥的组合是:Top-K 设 8,相似度阈值设 0.45 到 0.55,具体要根据不同类型文档单独调。
注意相似度阈值的基线不是固定的,它会随着向量模型变化而剧烈变化。同一个问题用模型 A 得到最高相似度 0.82,用模型 B 可能只有 0.68。最好在切换向量模型后重新跑一遍评测集,重新标定阈值。我见过一些人把“0.75 阈值”当成通用配置,在某个模型上让所有合法问题都被拦截,白白损失召回。
4.3 混合检索:向量检索和全文检索结合
纯向量检索有一个弱点:它对精确词条、代码片段、型号编号不够敏感。比如用户搜“A203 型号”,向量模型可能把它归类到“设备”概念里,错过了精确匹配。这时候建议增加一个传统的关键词/全文检索引擎,和向量检索做并行混合。两路召回结果按分数加权合并,最终得到更稳的候选集。
混合检索的权重配比我会从 7:3 起步,向量 7 成、全文 3 成,再根据效果调整。如果知识库专业术语多、型号编码多,全文检索权重可以提到 40%。最终没有统一答案,只能靠评测集说话。
4.4 把检索结果拼给大模型的正确姿势
当候选文本块确定后,别直接把 8 段文字全往 Prompt 里糊。我会按来源去重,再按相似度降序排列,然后为每段加上来源标识。大模型生成时如果引用了某段,可以直接让它输出“相关来源:[文档名]”。这样的输出可验证性很强,用户或者运维在排错时非常方便。
在这段上下文组装时,有些内容要裁剪:与问题语义重复的段落尽量只保留最相似的一段;相似度低于阈值的段落宁可舍弃也不要硬塞。不相关上下文一多,模型极易自行脑补,更糟的是它会把多个来源里的信息强行揉在一起,编造出文档里不存在的结论。我的原则是“上下文宁可少而准,不要多而杂”。
5. 工程落地:从单机脚本到稳定服务的五个改造点
前面讲的思路和代码都是单进程演示级别的。真正让 RAG 成为 Agent 的可靠组件,还需要考虑稳定性、性能、安全问题。这里分享五个我在实际项目中必须处理的关键改造点。
5.1 索引更新与增量同步
知识库永远不会静止。今天改了价格表,明天增加一段违规处理说明,如果索引不更新,Agent 永远用旧知识回答。最基础的做法是定时全量重建,但知识量大以后重建耗时长、成本高。更好的做法是设计增量更新机制:源文档变更后,根据文档 ID 找到旧版本关联的全部文本块,删除后重新切块编码入库。如果文档是数据库表,可以用更新时间字段定期轮询,把变化行转成文档候选题。
要注意的是删除旧文本块时,向量数据库里旧向量会被清除,但已经生成过的历史会话记录不受影响。如果想保留审计信息,建议在元数据里维护“version”字段,而不是物理删除。
5.2 缓存与并发控制
在线服务里,用户相似问题反复出现是常态。我加了一层用户级查询缓存,把规范化后的问题和检索结果做成键值缓存,缓存时间可以设几分钟到几小时。缓存命中能节省一次向量检索的开销,也能显著降低高热度问题的响应延迟。但也别缓存太久,知识库刚更新完,缓存里还是老答案,会让用户认为系统坏了。
并发控制上要注意两点:一是向量数据库连接池不要满负荷跑,二是大模型生成阶段要避免无休止重试。一次检索失败,重试更新索引即可;一次生成超时,不要让进程无限等待,否则用户请求堆积成雪崩。
5.3 可观测性与日志
RAG 出问题时的排查,最难的是定位“是检索错了还是生成错了”。我在项目里强制记录三类关键日志:查询改写前的问题、检索命中的 Top5 相似度与片段摘要、最终生成回答的完整输入。这三份日志能还原整条链路上的决策。每次效果变差,先用真实流量样本回放这三分日志,很快能锁定是召回不够还是模型误解了上下文。
5.4 成本控制与模型分层
如果你所有用户问题都走最贵的大模型,每月费用会非常吓人。RAG 落地时可以做简单的意图分流:简单问题、Hit 率高的高频问题,用规则或小模型直接回答;只有确实需要复杂推理的问题,才调用更大模型。向量模型可以固定用一个,不必每个场景都换大参数版本。这个分层方案我一直在用,成本能省三分之一以上,而且响应速度更快。
5.5 安全过滤与合规审核
知识库里有时会出现敏感的内部数据。RAG 上线前要建立过滤规则:哪些文档类型禁止被检索;哪些用户角色不能看到哪些来源。我通常把“角色”作为元数据写入每条知识块,检索阶段把角色条件直接塞进过滤表达式。生成阶段还要加一道输出合规检查,防止模型在引用不当时泄露信息。这一步虽然不像检索效果那样直接,但一旦出问题,后果很严重。
6. 常见问题与排查技巧实录
最后把我在项目里实际踩过的坑整理成一份速查表,按频率排序。碰到问题先从这几条开始查,基本上能覆盖八成故障。
6.1 向量检索结果“看着相关但答非所问”
这类问题的根因大多是相似度阈值过低或切片过大。一个文本块里塞了两个主题,它和问题的余弦相似度可能并不低,但答案真正的关键句只占小半部分,模型把另一个主题的内容当成了主答案。解决办法是把相似度阈值调高,同时把大切片改成结构化小切片。如果还是一样,再检查查询改写,是不是把“密码重置流程”写成了“密码错误原因”,导致检索方向跑偏。
6.2 检索召回为空但知识库明明有内容
先确认问题经过向量化后,和库里的向量是否真的在同一个语义空间。常见原因是向量模型版本不一致:入库时用了模型 A,在线检索时误用了模型 B,两套向量无法对齐。我一直会在向量字段里存一个“model_version”元数据,检索前校验版本一致。另一个原因是文本被切成空串或纯符号。这种情况通常发生在文档里全是图片、表格、特殊符号时,应该在做结构化解析时过滤掉空块。
6.3 同义词召回失败
比如用户说“工资条”,文档里写的是“薪酬明细”。通用语义模型可能把它们分得很远。我的处理方法有两个:一是扩充查询改写,把“工资条”改写为“工资条 OR 薪酬明细 OR 工资单”;二是在文档切块前做一次术语归一化,把常见的同义词统一替换成标准术语。注意术语归一化要谨慎,有些缩写有歧义,比如“AI”既可以指人工智能,也可以指人工授精,乱替换会引入噪声。
6.4 大模型引用来源时张冠李戴
生成模型会把几个来源的信息串成一句话,如果其中某个来源相似度高、事实无关,就容易出现“看似有理有据,实则蜀犬吠日”。我会在 Prompt 里要求模型“只依据给定文本回答,如果文本之间冲突,明确说明冲突并列出各自来源”。同时对文本块来自不同文档的 case,大模型输出需要附带序号引用,我在后处理后校验引用序号是否存在。这套方式不能做到 100% 防止串供,但明显降低了幻觉率。
6.5 表格与图片完全检索不到
这是一个容易被人忽略的大坑。知识库里大量内容其实都藏在表格和截图里,普通文本切块会把表格按压平文本处理,字段间的对应关系弄丢;截图则完全没有文本。解决表格的思路是转成 Markdown 表格后再做切片,让模型能读懂行列结构;解决截图的思路是用 OCR 或图像解析服务提取文字,再和原截图一起作为复合文档入库。这部分投入不小,但面对真实企业知识库,又绕不开。
6.6 性能突刺:检索变慢或超时
HNSW 索引在高并发写入时,查询性能容易出现锯齿抖动。如果发现点查延迟从几十毫秒涨到几百毫秒,先看后台是不是正在执行大批量 upsert。解决方案是把批量写入挪到低峰期,或者把写入路由到另一份索引副本,等同步完成后再切流量。另外,向量检索服务最好单独部署,和大模型推理服务隔离。两者都是高 CPU 或高内存需求的服务,堆在同一个实例上很容易互相拖垮。
6.7 一个容易被忽略的小问题:向量模型随机性
有些向量模型在重复编码相同文本时,会因加速卡推理模式或 batch 大小不同产生微小差异。如果你在离线批量入库时用了较大 batch,在线单个向量编码用了 batch=1,可能造成轻微偏移。大多数场景这个偏移影响可忽略,但如果你把相似度阈值设置得过于严苛,就可能出现同样的内容入库能搜到、线上实时更新后反而搜不到。解决方法是入库和上线前各做一次向量一致性核对,确保两套编码环境尽量一致。
一些小体会
在我迭代过的几个 RAG 项目里,最后阶段性能提升最大的,往往不是换更贵的模型,而是把查询改写、元数据过滤和阈值校准这三件事做扎实。向量模型实战不只是填一个 API key,它背后是反复的评测、切块和参数调优。今天这期把从索引到检索再到生成的经验都过了一遍,你可以直接拿我这份路线图去对照自己的项目。
如果你正在做 Agent,最后替给你提个建议:先把一条极小的知识链路完整跑通,只放二三十篇文档,再考虑扩充。不要一开始就追求海量文档、复杂重排。小链路能帮你快速暴露向量模型、切块、阈值这些基础选型里的问题,等基座稳了,扩量只是时间问题。
走到这里,这个系列应该能从“搭一个框架”过渡到“让它真正好用”了。下一期我会继续沿着这条路,分享 Agent 里工具调用和 RAG 结合时的状态管理问题,到时候见。