RAG与Agent实战:从零构建知识库问答与智能体应用
1. 先搞懂Agent和RAG在解决什么问题——为什么这两个词总是绑在一起我见过不少人刚接触大模型应用开发时一看到Agent和RAG这两个词就发怵。尤其是一些写了几年业务代码、刚转过来想搞清楚对话模型怎么落地的程序员经常会问这俩是不是同一件事是不是必须同时学甚至有人把RAG直接理解成模型联网搜索把Agent理解成写一大堆if-else的机器人这种偏差在实战里会走很多弯路。先说一个最朴素的观察现在的对话大模型本质上是一个海量知识压缩器。它把训练时见过的语料压缩成参数回答问题靠的是记忆联想而不是检索查阅。这意味着两件事——第一模型只会说它学过的内容你的私有文档、最新资料、公司内部规范它一概不知道第二就算它知道也不能保证说的每个数字、每项细节都准确。RAGRetrieval-Augmented Generation检索增强生成解决的就是这个知识边界问题。它的思路很直白模型不知道那就先从一个外部知识库里找出相关的资料片段把资料拼进提示词里让模型开卷考试。这样一来模型不需要背下你的文档只需要学会照着资料回答。说白了RAG就是把模型的记忆能力外包给一个可检索的数据库。Agent解决的则是另一个问题——光会说不够还得会做。模型再聪明回答完问题之后没法帮你查天气、没法帮你提交表单、没法帮你调第三方接口那它就是一个高级聊天框。Agent的本质是让大模型作为大脑去编排和执行一系列动作它先理解你的目标再把目标拆成步骤每一步决定调用哪个工具拿到结果后继续判断下一步做什么直到任务完成。这里面的关键是工具调用和任务规划而不是一堆硬编码的规则。为什么这两个概念总是被放在一起说因为在真实应用里它们的配合非常紧密。一个纯粹的RAG应用做的是搜出来、读进去、答出来流程是固定的一条线而一个Agent应用如果完全靠模型自己发挥遇到需要私域知识的问题就会开始编造答案。所以最常见的做法是用RAG帮Agent提供事实底座用Agent帮RAG承接后续动作。一个负责知识一个负责执行这才是现阶段大模型落地的两个主要抓手。这篇文章面向的读者是刚入门或者还没入门的小白程序员。我会尽量用能直接抄作业的方式把这两个方向拆开讲明白RAG怎么一步步搭起来、Agent怎么一步步写出来、两者怎么结合、以及我实际开发中踩过哪些坑。文中的代码都是简化示例核心逻辑可以直接迁移。2. RAG实战链路从零搭一个知识库问答要过的四道关口很多人第一次搭RAG以为就是装个向量数据库把文档塞进去然后检索一下就完了。等真正跑起来才发现效果差得离谱——问什么都能答但答得都不对。问题几乎都出在四个环节文档加载与分块、向量化、检索、生成。每个环节都有独立的坑一个环节没做好后面全白搭。2.1 选型和环境准备别一上来就追求全家桶先说说选型。RAG的核心组件有三个Embedding模型把文本变成向量、向量存储存向量并做相似度检索、对话模型根据检索结果生成回答。我的建议是刚上手阶段不要引入太多中间件能用Python脚本跑通最小链路就别急着上分布式检索集群。Embedding模型我建议先用开源的国产或通用嵌入模型这类模型通常有几档尺寸一个几百MB的版本就够起步用了。选型的标准主要看两点一是中文效果好不好二是向量维度是否适中。维度太高会导致存储和检索变慢起步阶段没必要追求极致的语义精度。向量存储的选择比较多样。有一种是内存式的简易方案适合几百份文档以内的场景进程重启就丢了但用来跑通流程完全够。另一种是本地文件持久化的方案数据可以落盘比内存方案稍微正式一点。等到文档量级上了百万、需要分布式部署才需要考虑专门的向量数据库服务。我见过不少团队一上来就部署一整套向量数据库集群结果一个月后检索效果该差还是差问题根本不在存储层。对话模型的选择我建议直接用你平时最顺手的那个商用模型API。起步阶段用它的reasoning能力做最终生成比用一个落后的开源小模型效果好得多。等你的RAG管道稳定了再考虑用开源模型替换做私有化部署那个阶段才是真正抠成本的阶段。2.2 文档分块最容易被忽视、却最影响效果的环节RAG的检索单元不是整篇文档而是一个个文本块。分块的好坏直接决定检索召回的内容是否精准。如果一块太大里面可能混合了三四个主题检索时明明是问A问题结果连同B、C的信息一起塞给了模型模型很容易被干扰。如果一块太小又可能截断上下文让模型看不懂前因后果。我常用的一个分块策略是按标题层级切分。很多文档是Markdown或Word格式本身带有章节结构。先用标题把文档切成节然后对每一节判断长度如果这一节很短就并到相邻节里如果这一节很长就再做二次切分用换行符或句号做边界尽量保持段落完整。这个策略的代码看起来复杂一点但效果立竿见影。还有个容易踩的坑是切分时吞掉上下文。比如一个表格从中间切开后上表头信息就丢失了检索出来的片段可能只是一堆没有表头的数字模型根本看不懂。我的习惯是遇到表格单独处理把表头信息拼到每个单元格内容前面遇到代码块尽量不要硬切除非代码块特别长否则整块保留。这里给出一个基础分块函数的思路你可以根据自己文档类型调整def split_document(document_text, max_chunk_size500, overlap50): # 如果设置了重叠保证相邻块之间有共同片段避免关键信息恰好落在边界上 paragraphs document_text.split(\n\n) chunks [] current_chunk for para in paragraphs: if len(current_chunk) len(para) max_chunk_size: current_chunk \n\n para else: chunks.append(current_chunk.strip()) # 保留尾部overlap长度的文字作为下个块的前缀 current_chunk current_chunk[-overlap:] \n\n para if current_chunk.strip(): chunks.append(current_chunk.strip()) return chunks分块重叠能减少关键句刚好被切断的概率但重叠也不是越大越好重叠过大会让检索结果出现大量重复片段既浪费空间也让模型生成时更啰嗦。2.3 向量化与检索为什么有时候相似度最高的片段反而是错的向量化的核心工作是把文本块通过Embedding模型变成一个固定长度的向量。这个向量的语义含义是这句话大概在说什么。检索时把用户问题也转成向量然后在库里找距离最近的几个向量对应的文本块就是召回结果。这一步最反直觉的地方在于语义相似的句子向量不一定近字面相似的句子向量反而可能非常近。举个例子用户问工资几号发你库里有一条每月十五日发工资两者字面差异很大但语义很近好的Embedding模型能把它们拉近。反过来用户问蛋糕好吃吗库里有一条蛋糕店怎么走字面很像但一问口味一问路线向量距离大概率并不近。所以检索的效果上限很大程度上取决于Embedding模型本身的质量。检索到候选块之后我不建议直接拼进提示词。很多场景下Top3里可能有两条相关、一条无关无关片段混进去会把回答带偏。这时候可以加一个重排环节用一个额外的模型对召回的候选块重新打分把真正相关的排到最前。轻量做法是直接用对话模型逐个判断这段内容和问题是否相关但代价是多几次模型调用延迟会增加。进阶做法是用专门的重排序模型。对于小白阶段我的建议是至少做到召回到5个块生成时只取前3个用数量控制来规避噪声。2.4 生成提示词里的两种坏写法最后一步是把召回的文本块和用户问题拼进提示词交给对话模型。这里有两个坏写法最常见。第一种是不管来什么资料都直接扔给模型。有些资料检索出来可能是无关的模型会为了显得有用硬把无关内容也组织进回答。正确的做法是在提示词里明确告诉模型只能依据给定资料回答如果资料中没有相关内容就明确说知识库中没有找到相关信息不要自行发挥。这个约束可以大幅减少胡说八道的概率。第二种是不给引用来源。在内部工具或企业应用里回答附带上来源文档的编号或标题至关重要。做法很简单在把文本块拼进提示词时每个块前加一个标识比如【文档1】【文档2】然后要求模型在回答中引用时用【文档1】这样的标记。这样你就能跟踪每条回答的依据来自哪里方便排查召回问题。一个参考的生成提示词模板是你是企业知识库助手。请根据以下资料回答问题。 规则 1. 只能依据资料内容回答不得使用模型自身知识补充。 2. 如果资料中没有相关内容直接说明知识库中未找到相关信息。 3. 可在回答末尾标注引用来源如【文档1】。 资料 【文档1】xxx 【文档2】xxx 问题xxx到这里一个最小可用的RAG系统已经跑通了。从我带新人的经验看完整走通这个流程大概需要两到三天。很多人卡住的不是代码而是不知道效果不好是哪个环节造成的——这个排查能力只能靠把每个环节拆开单独测试来练。比如你怀疑是分块问题就把同一个文档用不同的分块策略检索同一个问题对比召回片段怀疑是Embedding问题就干脆用两个不同的模型对比检索结果。3. Agent实战拆解工具调用与任务编排是核心别把Agent想得太玄如果说RAG是让模型知道更多那Agent就是让模型做更多。Agent并不是一个神秘的系统它只是一个循环。这个循环大致是接收任务 → 模型判断下一步动作 → 调用工具 → 把工具结果返回给模型 → 模型再判断下一步……直到模型认为任务已完成输出最终答复。3.1 从ReAct到工具调用Agent最基础的运行结构Agent的运行模式学术上常叫ReActReason and Act推理与行动交替进行。翻译成大白话就是模型先思考当前该做什么然后做做完看结果再思考下一步该做什么。在实际开发中一个最小的Agent循环模型要能够在每一步输出两样东西一个是我的推理解释为什么这么做另一个是我要调用的工具名和参数。这个输出会被程序解析然后由代码去真实执行对应工具再把工具的返回结果作为新的消息扔回给模型。这里解释一下为什么工具调用是Agent的关键。模型的上下文窗口始终有限它不可能在脑子里运行一个函数必须借助外部执行环境。工具调用就是给模型开了一扇触手查天气预报、发HTTP请求、执行一段本地代码、查询数据库这些能力通过注册成工具让模型自己决定何时使用。工具注册通常遵循一个固定格式工具名称、工具描述、参数结构。工具描述尤其重要模型是靠描述来匹配当前任务该用哪个工具的。描述越清楚模型调错工具的概率越低。比如你有一个查询订单状态的工具描述里最好写明当用户询问订单配送、物流、签收状态时使用而不是笼统写查询订单工具。3.2 一个最小Agent循环的示例代码我建议小白先自己写一个最简单的Agent循环不要一上来就套现成的Agent框架。自己写一遍你才能理解框架内部发生了什么。以下是一个概念性示例def run_agent(user_task, tools, model_client): messages [{role: user, content: user_task}] max_steps 10 for step in range(max_steps): response model_client.chat_completion( messagesmessages, tools[tool.schema for tool in tools] ) if response.finish_reason stop: # 模型认为任务已完成不再调用工具 return response.content elif response.finish_reason tool_call: # 模型请求调用某个工具 tool_call response.tool_call matched_tool next(t for t in tools if t.name tool_call.name) result matched_tool.execute(tool_call.arguments) # 把工具执行结果作为一条新消息加入对话 messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) else: raise RuntimeError(出现未知的结束原因) raise RuntimeError(超过最大步数任务未完成)这个循环里有几个参数值得琢磨。第一个是max_steps也就是最大循环步数。这一步是安全底线防止模型陷入反复调用同一个工具的死循环。我实际遇到过Agent在天气查询场景里连续调用八次同一个城市的天气接口原因就是模型对工具返回结果不满意总想再查一次。没有步数上限API费用会持续累积。第二个是工具执行结果如何呈现给模型。工具返回的可能是一个很大的JSON结构直接塞进消息里会占掉大量上下文。建议在工具返回层面做精简只保留关键字段。比如查询订单状态原始响应可能有几十个字段传给模型的只保留订单号、状态、更新时间三个字段剩下的对模型决策没有帮助只会浪费窗口。第三个是异常处理。工具执行不一定成功比如第三方接口超时。如果你的Agent遇到工具报错就直接崩溃那它就不是一个合格的产品。比较好的做法是把异常信息也作为工具结果返回给模型让模型知道这个工具调用失败了然后由它决定是换一个工具还是换一种参数重试或者直接向用户说明失败原因。3.3 任务编排别让模型什么都自己来很多Agent教程会强调全自动规划但我个人建议在真实业务里不要把整条流程完全交给模型自由发挥。原因很简单模型的规划能力还不稳定步骤越多出错概率越大。更稳妥的做法叫半自动编排你把任务的骨架定好模型只在关键节点做选择。举个例子一个订单售后处理Agent骨架可以是第一步查订单是否存在第二步判断订单是否在售后期内第三步根据用户诉求决定退款还是换货还是维修。这个骨架是代码写死的模型只需要做第三步中选哪个这个决策。这种做法既保留了Agent的灵活性又限制了模型的自由度出错率会大幅下降。在任务编排设计上还有一个容易被忽略的经验工具粒度要适中。粒度太粗每个工具都是一大坨业务逻辑模型难以复用Agent的灵活性就没了粒度太细比如把解析用户地址都单独做一个工具模型每一步要做的选择太多就特别容易搭错。我的习惯是一个工具完成一个不可再拆的业务动作比如查订单算运费发退款申请这样粒度正好。4. Agent与RAG结合的真实场景让问答升级成能办事的问答现在把两条线合起来看一个我最常拿来举例的真实场景企业内部工单系统助手。这个场景是典型的知识行动复合需求非常适合展示Agent和RAG怎么配合。用户可能问两类问题。第一类是公司报销流程是什么——这是一个纯知识型问题答案在制度文档里需要用RAG去检索。第二类是我要把一张发票提交报销但系统一直报错——这就不只是知识问题了用户需要别人帮他查一下单据状态甚至帮他重试提交。Agent需要先判断这属于哪类问题再决定是检索文档回答还是调用工单系统的工具去操作。这个判断本身也是模型来做的。做法是给Agent注册两个工具一个是知识库检索工具底层就是RAG管道一个是查工单状态工具底层调用工单系统API。模型拿到用户问题后如果发现需要查制度文档就调用检索工具如果需要查单据就调用查询工具。甚至在复杂场景下两个工具会先后被调用先查单据状态发现审批被退回再从知识库里检索审批被退回应该怎么办。这个结合模式里有几个设计要点值得单独说。第一RAG检索结果返回给模型之后模型的任务并没有结束它可以继续行动。比如检索到报销单需要补充电子发票PDFAgent可以接着调用发送补充材料提醒工具给用户发一条通知。很多纯RAG系统做不到这一步因为它们的流程在生成回答后就停了Agent则让回答成为行动的起点。第二要让模型知道什么情况下用RAG、什么情况下用工具。这需要在工具描述里写清楚使用边界。比如知识库检索工具的描述可以写当用户询问流程、制度、规范、操作方法时使用优先于其他工具。而查工单状态的描述写当用户询问具体工单的处理进度、审批状态时使用。模型读到这些描述调用准确性会明显提升。第三注意记忆问题。Agent在多个步骤之间需要记住用户最初问的是什么还要记住中间步骤的结果。这个在实现上其实很简单就是让对话上下文一直保留每一步的推理、工具调用和结果都作为历史消息继续传入模型。但要注意控制上下文膨胀步骤多了以后早期的消息会被逐渐挤出窗口导致模型忘记最初的目标。一个常见优化是每轮循环后对历史消息做摘要把之前的思考过程压成几句话而不是保留完整日志。组合场景的示意代码如下def handle_ticket_request(user_input): # 先让模型判断意图 intent model_client.judge_intent( user_input, candidates[knowledge_query, ticket_operation, mixed] ) if intent knowledge_query: # 走RAG检索资料生成回答 chunks retrieve_from_knowledge_base(user_input) return generate_answer_with_chunks(user_input, chunks) elif intent ticket_operation: # 走Agent工具查单、操作 return run_agent(user_input, tools[ticket_api_tool]) elif intent mixed: # 先查单再看知识库 ticket_status ticket_api_tool.execute(user_input) policy retrieve_from_knowledge_base(f{ticket_status} 应该如何处理) return generate_answer_with_chunks(user_input, [policy], extraticket_status)别小看这个intent判断的步骤。它相当于给Agent加了一道非常轻量的路由闸门先分流再处理比直接让模型在庞大的工具列表里自己选更稳当也能减少工具调用错误的次数。5. 实测最容易翻车的三件事上下文窗口、幻觉、效果评估这个部分是我想特别强调的实战经验。前面讲的都是怎么做这里讲怎么不翻车。RAG和Agent的Demo阶段通常都很顺利真正的问题在接入真实数据和复杂任务后集中爆发而且翻车的点非常类似。5.1 上下文窗口不是越多越好而是够用就好我见过最典型的问题是有人为了不遗漏信息把检索到的所有候选块全部塞进提示词结果对话模型因为上下文过长出现两种糟糕表现一种是迷失在中间模型忽略掉塞在长文本中间的关键信息转而依赖开头和结尾的内容另一种是回答变得啰嗦把所有资料都复述一遍。更麻烦的是在Agent场景里工具返回结果、历史步骤、RAG检索块都会不断累积占窗口迟早会顶到模型的最大长度限制。设计系统时一定要给上下文做预算RAG检索结果控制在不超过800到1500字Agent历史消息控制在最近几轮超出部分压成摘要。这个预算要在代码层面强制不能指望模型自己节约用词。实测下来给RAG的回答质量做一个简单统计就会发现当拼接的上下文超过2000字后回答的准确率不升反降。原因不是模型能力不够而是信号淹没在噪声里。你给的资料里要是有两条信息是矛盾的模型很容易选错那一条。所以真正有效的做法是精选少量高相关块而不是堆砌一堆可能相关的块。5.2 幻觉管理RAG解决不了所有幻觉很多人的幻想是加了RAG模型就不会胡说八道了实际不然。RAG只能缓解知识型幻觉——就是模型不知道答案时瞎编的那种但对于推理型幻觉——比如模型明明看到了正确资料却在归纳时得出错误结论——RAG无能为力。还有一种情况是资料本身是矛盾的模型没有能力判断哪个可信于是自己编了一个折中答案。管理幻觉有三个实际手段。第一在提示词里强制不知道就说不知道这能挡住一部分硬答行为。第二对回答做来源标注让每条关键结论能对应到具体的文档块至少在事后能追查这句话是哪份资料里的。第三对高风险的回答加一道人工复核闸门比如涉及报销金额、合同条款等敏感问题不要直接给出最终结论提示用户联系审核人员。这种设计看着不够AI但在企业内部应用里非常管用用户不会因为你回答得保守而投诉你但会因为一个错误金额产生极大的信任损失。5.3 效果评估凭感觉调参是最大的坑RAG和Agent系统有一个共同特点看起来都能跑但效果好坏很难量化。如果不做评估你根本无法知道改了一个分块参数是变好了还是变差了于是所有优化都变成盲调。我的建议是哪怕再小的项目也要建一个评估集。评估集至少包含30到50条典型问题每条问题标注标准答案或包含答案的文档块编号。每次改动后跑一遍评估集用几个指标来看效果。召回阶段看两个指标召回率标准答案的文档块有没有被检索到和命中位置标准答案排在候选的第几名。生成阶段主要看回答的准确率和完整性。Agent场景还要额外看工具调用成功率和任务完成率。下面是一个简单评估结果表的示例指标改动前改动后结论检索召回率72%86%分块策略调整有效Top3命中标准答案比例40%65%需要进一步优化Embedding回答准确率62%78%提示词约束生效工具调用成功率88%92%工具描述改写有效有了这张表你就能清晰地知道该往哪个方向使劲。我现在每接到一个RAG或Agent项目第一周肯定是在搭评估集而不是在写主流程代码。这看起来慢实际上是最快的路。6. 小白两周上手清单按这个顺序练比看十篇教程有用最后给一个可执行的学习路径。我知道很多刚入门的人最大的困惑是东西太多不知道从哪里下手。这里我给一个按天拆分的实战路线每个阶段都对应一个可以自己敲出来的动手项目。别急着啃大部头理论先把东西跑起来再回头看理论理解会深很多。第一阶段第1到3天搞定一个最小RAG。选一个你熟悉的领域文档比如一份开源项目的说明文档用开源的Embedding模型和轻量向量存储搭出提问-检索-回答的最小链路。每天换一种分块策略对比检索效果。这个阶段的目标是理解分块、向量、检索、生成四个环节各自的作用。第二阶段第4到6天搭一个最小Agent。不要用任何框架从3.2节的那个循环开始注册两个简单工具比如一个算数工具、一个查时间工具让模型学会根据任务选择工具。重点是观察模型输出tool_call和普通回复时有什么区别。这个阶段的目标不是做出成品而是彻底理解模型怎么决定调用工具。第三阶段第7到10天把RAG和Agent合并。做一个内部知识问答工单处理的组合场景至少把4节的示例代码跑通并补充一个查完知识库后自动生成待办通知的动作。这个阶段的目标是理解两种能力的边界和结合点。第四阶段第11到14天建立评估集迭代一轮。拿你前两个阶段的问题累计成评估集改一次分块或改一次提示词记录指标变化。这个阶段的目标是培养用数据判断效果的习惯这比任何具体工具都重要。在上手过程中有两点提醒想专门说一下。一是不要囤教程不要收藏了几十个链接就觉得学会了真正上手敲代码之后那些收藏里能用的可能只有两三个。二是如果某个概念卡住了回头去看官方文档或源码不要被二手解读带着走。我见过太多人在某个框架的某个高级特性上钻牛角尖其实那个特性在他的场景里根本用不上。先把最小链路跑通高级功能等有真实需求再去折腾。从我个人的实践体会来说Agent和RAG最吸引人的地方正是那种不确定性——同一个问题调整一个参数、换一种提示词表达系统表现可能截然不同。这种不确定性让人又爱又恨但学会了怎么控制它你就算真正入门了。希望这份笔记里踩过的坑和验证过的做法能帮你少走几段弯路。