聊到AI Agent,很多开发者第一反应是推理循环、工具调用、规划算法。但真正把一个Agent从"演示品"推向"可用产品",最关键的反而是它怎么获取业务知识。RAG(检索增强生成)作为知识获取管道,是Agent感知外部世界的"触手"。这一篇我们专注RAG基础,把索引、检索、重排、生成的完整链路拆开讲清楚,并结合Agent架构说明RAG该放在哪里、怎么调参、什么才是真正的瓶颈。
1. 为什么AI Agent需要一条"知识获取管道"——从单模型到知识驱动的转变
很多人把大语言模型当成一个装满知识的数据库,问什么答什么。但在实际项目里,这种做法很快就会撞墙:模型参数里的知识有截止时间,无法覆盖你的私有文档、产品手册、内部规范,而且遇到不熟悉的话题时会一本正经地编造。于是我们在模型外面接一条知识获取管道——先检索相关资料,再把资料作为上下文交给模型,让它基于事实作答。这就是RAG的核心思路。
1.1 大模型的"知识存量天花板"与Agent的"外部认知"
可以这么理解:实习生脑子再聪明,也不可能知道你们公司所有的历史决策、接口规范和踩坑记录。模型也一样,它的"天然知识"是训练时从公开语料里学的,存在两个天花板:一是时效性,训练数据截止后发生的事它不知道;二是业务覆盖度,内部文档、非公开流程它根本没见过。
Agent要解决真实任务,比如"根据最近一个月的订单数据回答客户为什么发货延迟",模型必须临时去查知识库、数据库、工单系统。RAG就是这样一个外部认知通道,它让Agent不是靠记忆硬答,而是靠证据作答。这也是为什么我把RAG称为Agent的知识获取管道——它本质上是把"检索外部信息"这个动作系统化,并嵌入到Agent的每次决策之前。
1.2 知识获取管道在Agent动作循环中的位置
一个典型的Agent循环长这样:感知输入、理解意图、规划步骤、调用工具、生成回复。RAG主要作用于感知和理解阶段——在模型做推理之前,把相关的背景信息"喂"到上下文里。它不是工具调用,不是SQL查询,而是一个持续性的上下文装配器。
用一个简化伪代码表示:
def agent_cycle(query): # 1. 感知与理解 intent = detect_intent(query) # 2. 知识获取(RAG管道) context_chunks = rag_pipeline.retrieve(query) # 3. 推理与规划 plan = planner.generate(intent, context_chunks) # 4. 执行动作 result = executor.execute(plan) # 5. 生成回复(也可以再次注入检索结果) answer = responder.generate(query, context_chunks, result) return answer从这个角度看,RAG的质量直接影响Agent每一步决策的质量。如果检索回来的是一堆无关内容,规划器和生成器都会被带偏。所以理解RAG基础,不是学一个独立组件,而是在学Agent的地基。
2. RAG基础原理拆解:索引、检索、重排、生成的完整链路
RAG全流程分两条路:离线建索引和在线检索生成。离线阶段你需要把文档清洗、切分、向量化,存进向量数据库;在线阶段接收用户query,做向量召回、混合检索、重排,最后把筛选后的文本块注入提示词,让模型生成答案。
2.1 离线索引阶段:文档清洗、切分、向量化的关键点
离线索引的目标是让"知识变得可检索"。原始文档通常是PDF、Word、HTML、数据库导出的文本,甚至还有扫描件。第一步清洗非常重要:去掉页眉页脚、乱码、多余的换行和表格格式残留,否则切分和向量化都会被干扰。我见过不少团队直接拿PDF解析结果去做embedding,结果检索回来的片段里全是"第3页 共10页",这种噪声会显著稀释语义。
第二步是切分(chunking)。文本不能整篇丢进去,因为embedding模型有最大token限制,而且过长文本的向量表示会丢失细节。常见的策略是固定大小切分:比如512个token一个块,相邻块重叠64个token。这样能保留上下文连续。更进阶一点是按文档结构切:Markdown标题、段落、表格单元作为天然边界,语义更完整。中文环境下还要注意,512 token大概是几百字,按中文字符切可能会把一句话从中间截断,所以最好按句子或段落边界做二次修正。
第三步是向量化(embedding)。把一个文本块变成几百上千维的向量,维度越高不代表效果越好,关键是模型对语义的捕捉能力。生成向量后存进向量库,同时可以构建倒排索引(BM25)用于后续混合检索。索引阶段的产出是三样东西:原始文本块、向量索引、关键词索引。
2.2 在线检索阶段:query改写、召回、重排
用户query进来后,第一步通常是query改写。用户说"那个发不了货的单子怎么办",直接拿去检索,很可能因为缺乏实体信息导致召回不准。一个轻量做法是让LLM把query改写为更明确的检索表达式:
# 示例:query改写提示词 system_prompt = "将用户问题改写成适合搜索引擎检索的关键词序列,保留实体和时间信息。" rewritten = llm.rewrite(query, system_prompt)改写完之后,并行做向量检索和关键词检索。向量检索擅长语义匹配:用户问"手机屏幕碎了怎么修",能召回"碎屏更换流程"这种字面不重叠但语义相近的文档。关键词检索(BM25)擅长精确匹配:用户问"SN码在哪看",如果文档里就是"SN码"三个字,BM25能精确命中。两者各有优劣,所以主流做法是混合检索(Hybrid Search),再用RRF(Reciprocal Rank Fusion)合并排名。
重排(Rerank)是常常被忽略的一步。向量检索召回20条,其中前5条很相关,第6到20条可能一般。直接用top5也许还行,但如果要想更精准,就用一个专门的rerank模型对召回结果做精细打分,重新排序。重排模型的输入是"query+文本块",输出一个相关度分数。这一步计算量大一些,但能明显提升最终答案质量。
2.3 生成阶段:注入上下文的有效姿势
检索和重排完成后,最终选出的文本块要组装成提示词。组装不是简单把文本拼在后面,而是要让模型明确"以下内容来自知识库,请基于它们回答,不要随意发挥"。一个标准的RAG提示词会包含检索指令、上下文块、用户问题、回答约束。
你是一个客服助理。请严格基于【知识片段】回答用户问题。 如果知识片段中没有答案,请直接说"未找到相关信息",不要编造。 【知识片段】 <chunk_1> <chunk_2> ... 【用户问题】 <user_query>另外建议把每个片段也塞进一个结构化格式中,例如带上来源文档名和段落编号。这样模型在回答时能引用来源,后续做答案溯源和调试时也会轻松很多。注入的文本块数量不宜太多,通常3~5个足够。超过8个,模型注意力会被稀释,反而让核心信息被噪声淹没。
3. 在Agent架构里给RAG定位:记忆、工具、知识管道的边界
很多人在搭建Agent时会把RAG、长期记忆、工具调用混成一锅粥:既用RAG查知识,又把对话历史存进向量库,还让Agent直接调用一个"search"工具。实际上这三者的职责有清晰边界,搞混了后续维护成本会直线上升。
3.1 RAG与长期记忆的分工:该进库的进库,该进记忆的进记忆
知识库(RAG索引)存放的是稳定、权威、显式知识:产品文档、帮助手册、政策规范。长期记忆存放的是动态、个性化、从交互中产生的信息:用户的偏好、上次对话的结论、某个项目的最新进展。
| 维度 | RAG知识库 | 长期记忆 |
|---|---|---|
| 内容来源 | 文档、数据库、Wiki | 对话记录、用户反馈、业务日志 |
| 更新频率 | 低频(人工或定时同步) | 高频(每次交互后更新) |
| 用户 | 所有用户共享 | 按用户/会话隔离 |
| 回答方式 | 检索后注入上下文 | 作为背景信息或状态恢复 |
| 典型问题 | "退货流程是什么" | "这个用户之前催过单吗" |
如果把对话历史全部塞进RAG知识库,检索时很容易把"用户上周问过价格"和"官方价格表"混在一起,返回一堆对话片段,反而降低了答案可信度。正确做法是:知识查询走RAG,状态恢复走记忆模块,两者可以互相引用,但不要用同一套索引。
3.2 RAG与工具调用的边界:知识不是动作,检索也不是执行
RAG和工具调用另一个常见混淆点,是Agent误把所有查询都设计成"search工具"。工具调用意味着Agent决定执行一个动作、产生副作用(查数据库、调API、发消息);而RAG检索只是一个上下文补充动作,没有副作用,不应该被当作"Agent的一个可选工具"。
原因是:如果让Agent自己决定要不要调用search工具,它可以决定"不调用",但知识获取管道往往是刚需,不管用户问什么,Agent首先应该获得背景知识,再决定要不要查实时数据。举个例子,用户问"iPhone 15的保修政策变了没有",此时Agent应该先RAG检索保修手册,然后如果还需要了解最新公告,再调用工具请求实时网页。RAG是默认开启的"背景音",工具是"主动动作",顺序不能反。
3.3 Agentic RAG:把检索决策权交给Agent
基础RAG是固定流程:query进来,强制检索,拼装上下文。Agentic RAG则是把"是否检索、检索什么、检索几轮"的决策交给Agent。比如Agent先判断意图:如果用户问的是闲聊,就不检索;如果问题需要多跳推理("上周哪些客户被延迟发货且都催过单"),Agent会分解成多个子query,分多轮检索,每轮利用检索结果继续推理。
Agentic RAG能提升复杂场景的准确率,但也会引入更高延迟和更不可控的token消耗。我的建议是:先跑通基础RAG,再根据需求渐进式增加Agent决策逻辑。如果一个简单问答都要Agent拆成三步检索,体验会很糟糕。这个顺势承接了热门里的"agentic rag""RAG as service"等概念,但记住:基础是根本,不要为了追新而复杂化。
4. 搭建一条最小可用RAG知识管道:选型、参数与踩坑
这一节是实操重点。我自己搭过好几条RAG管道,也踩过不少坑。下面按照"选型→切分→向量化→调参"的顺序,给出一套能直接落地的方案。
4.1 技术栈选型:LangChain、Spring AI、还是自研管道
现在市面上的RAG框架很多,最纠结的是选谁。我的建议取决于团队背景和场景:
| 方案 | 适合场景 | 优点 | 缺点 |
|---|---|---|---|
| LangChain / LlamaIndex | Python团队、快速原型、社区资源丰富 | 组件齐全,文档多,示例多 | 抽象层多,调试困难 |
| Spring AI + LangChain4j | Java生态、Spring Cloud微服务团队 | 类型安全,集成Spring体系,支持RAG | 生态相对年轻 |
| 自研管道(基于Embedding API + 向量库) | 对链路有强控制需求,可定制 | 透明可调试,性能可控 | 需要自行处理切分、召回、重排 |
| RAG as Service(厂商托管) | 不想运维,快速上线 | 开箱即用,索引和检索托管 | 不够灵活,成本高 |
如果项目刚起步,我建议先用LangChain跑通流程,因为它的检索器、切分器、向量存储抽象能让你快速换组件、做对比实验。等跑通后,再逐步把核心链路替换成自研代码,减少对框架的隐式依赖。记住:框架是学习工具,生产级系统往往需要你理解框架内部到底做了什么。
4.2 数据切分与索引:从固定chunk到语义切分的实战参数
切分是RAG质量的第一大变量。固定大小切分最省事,比如:
from langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=512, # 按token计的近似值,中文场景要注意 chunk_overlap=64, # 重叠部分保留跨块上下文 separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] ) docs = splitter.split_text(text)这里有几个关键点:chunk_size=512不是字符数,而是token数。英文中大约对应500~800字符,中文则约600~800字。如果你用固定字符数切,不同语言的语义断裂情况差异很大。separators的顺序很关键,它表示优先按段落切,再按句子切,再按逗号切,最坏才按字符硬切。
实测下来,中文文档最佳实践是先把文档按Markdown标题或者PDF章节拆成"语义块",再把语义块内部做二次切分,保证每个块内部是一个完整小节,而不是从句子中间断裂。我自己经常用一个简单策略:超过512 token的章节才继续用递归切分器,小于512 token的整块保留。这样索引出来的chunk质量明显更好。
4.3 Embedding模型与向量库选型:影响召回率的两大件
Embedding模型直接决定"语义相近"怎么判断。中文场景推荐优先考虑开源中文优化模型(比如BGE系列、BCE系列),英文场景可以用OpenAI的text-embedding-3-small或Cohere。选模型时看两个指标:MTEB基准分数、推理延迟。Agent场景对延迟敏感,每查一次知识库要毫秒级,模型太大、太慢会拖垮整个Agent循环。
向量库的选择也重要。如果只是开发环境,FAISS足够轻量;生产环境建议用Qdrant或Milvus,它们支持过滤、持久化、混合检索能力更强。如果你的团队已经有Elasticsearch,也可以直接用它做向量检索,省去维护两套系统,只是ES的向量性能要专门调优。
我自己踩过一个坑:Embedding模型不区分大小写、标点敏感度等细节,导致检索时"iPhone"和"iphone"结果不同,后来在文档清洗阶段统一大小写就解决了。这类问题看似微小,却会直接影响召回率。
4.4 检索质量调试:topk、阈值、混合检索与rerank
检索参数不是固定值,需要根据你的语料和业务调。核心参数如下:
| 参数 | 常见范围 | 说明 |
|---|---|---|
| top_k | 4~10 | 召回数量,过少漏信息,过多噪声多 |
| 相似度阈值 | 0.2~0.6 | 低于阈值认为是无关,具体依赖embedding模型分布 |
| 混合检索权重 | 0.5/0.5 | 向量与关键词各半,靠RRF融合 |
| 重排模型top_n | 3~5 | 重排后进入上下文的最终块数 |
| 上下文最大token | 1500~2500 | 防止超过模型窗口或注意力稀释 |
调参的思路很简单:先跑一批测试问题,观察检索结果中相关文档是否排在前面。如果前排都是不相关内容,先提高向量检索召回数量,再加重排。如果相关文档出现了但被无关文档挤下去,需要降低chunk_size、增加chunk重叠、优化query改写。这里必须提一个关键指标:RAG hit rate(命中率),指检索结果中"包含正确答案"的比例。它的优先级高于最终答案的准确率——因为如果知识都没被检索到,生成模型再强也答不对。
5. 检索质量的真实瓶颈:从命中率到上下文污染
很多团队调了一段时间RAG,发现效果还是"时好时坏"。问题往往不在模型能力,而在检索链路本身。RAG系统的质量有两条线:检索质量(能不能找到对的内容)和生成质量(能不能用对的内容生成答案)。而前者是后者的前提。
5.1 为什么RAG hit rate是Agent知识管道的第一红线
RAG hit rate翻译过来就是"查得准不准"。没有这个指标,一切生成质量对比都是空中楼阁。实操中我会构建一个小的测试集:每个问答对包含用户问句和答案所在文档ID,跑一遍检索链路,计算top5中是否包含目标文档。这个测试集不需要太大规模,50~100条就能覆盖主要问题类型。
一旦hit rate低于80%,几乎可以断定最终答案会频繁出问题。优先优化检索,不要急着换大模型。大模型只能把"已经拿到的正确信息"整理得更好,但永远无法弥补"信息根本没被找到"的缺陷。这条经验在几乎所有RAG项目里都成立。
5.2 三个常见症状:召回不全、相关性差、上下文污染
第一个症状是召回不全,比如用户问"如何退换货",检索出来的却是"售后联系方式",没有"退换货流程"那一段。常见原因是chunk太大,导致目标信息在向量空间中被淹没;或者query改写后丢失了关键实体。
第二个症状是相关性差:top1返回的内容和问题字面上相关,但语义不在点子上。比如用户问"手机进水了怎么办",返回了"液体检测指南",而不是"干燥处理步骤"。这往往是embedding模型对领域术语理解不足,或者重排没有正确启用。
第三个症状是上下文污染:检索出的片段来源庞杂,比如同时包含"保修政策"和"新品发布会"的内容,模型会把无关的信息也写进答案里。解决方法是重排后只选top3,以及在提示词里强调"只能基于指定片段回答"。
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 召回不全 | chunk太大、query改写丢失实体 | 减小chunk、增加重叠、改进改写 |
| 相关性差 | embedding领域理解弱、未重排 | 换embedding、加rerank |
| 上下文污染 | topk太多、知识库含噪声 | 降低top_k、清洗知识库 |
5.3 快速定位问题的方法论:先测检索再测生成
定位RAG问题,我的四步法固定不变。
第一步,单独跑检索,不看最终回答。把用户query输入检索管道,打印返回的chunk标题与摘要,人工判断是不是想要的内容。如果这一步不对,直接调切分、embedding、重排,别浪费时间调生成提示词。
第二步,检查chunk边界。打印检索到的 chunk 原文,看它有没有从句子中间截断、有没有把两个不相关段落拼在一起。这个问题的出现频率极高,尤其在PDF解析后。
第三步,测试不同query表达。同一个问题换成不同表达,观察hit rate波动。如果波动很大,说明query改写策略不稳定,建议在改写时加入同义词扩展。
第四步,监控注入token量。把每次最终拼接的上下文token数记录下来,如果经常超过2000,模型注意力容易被拉偏。建议做个日志,观察长上下文的hit rate与答案相关性关系。
这一整套方法用下来,大多数RAG问题都能在索引和检索阶段就定位到根因,而不是在生成阶段反复试提示词。
关于RAG,我个人最深的体会是:它的难点从来不是"跑通demo",而是"让检索结果稳定可用"。很多教程会直接把LangChain几个API一调,看起来成功了,但一旦换真实业务文档,问题立刻现形。所以如果你准备把RAG用到Agent上,一定要先建立测试集、盯住hit rate、对检索结果做人工抽检。管道里的每一环都可以打磨,最重要的是形成自己的"质量抓手",知道什么时候该动chunk、什么时候该换embedding、什么时候该加重排。Agent的知识获取管道越扎实,后面的记忆、规划、工具调度才有发挥空间。