先说一个我观察到的现象:现在聊 AI Agent,很多人的第一反应是“这不就是给大模型加个工具调用嘛”,或者“RAG 不就是检索文档喂给模型让它回答嘛”。这种认知不能说错,但如果你真的拿这套理解去设计一个 Agent 系统,大概率会踩坑踩到怀疑人生。尤其是把 LLM 和 RAG 放进同一个项目里时,它们的关系绝不只是“一个负责思考、一个负责找资料”那么简单。
这篇东西我不打算写成教科书式的概念堆砌,而是基于我自己实际搭建过的几个模拟项目(什么客服问答、资料整理、自动报告生成都试过),拆解一下 AI Agent 的底层逻辑,重点讲清楚 LLM 作为推理核心到底在干什么、RAG 在什么环节介入、两者之间是怎么协作的。同时会把一些“文档里不会写”的取舍和教训拿出来晒一晒。如果你正在设计自己的 Agent,或者准备学习这方面的架构,这篇文章应该能帮你省掉不少瞎折腾的时间。
1. 先拆清楚:Agent 到底“智能”在哪一步
很多人把 Agent 想成“一个聪明的东西”,实际上 Agent 不是什么黑盒,它是一套循环系统:感知任务 → 规划步骤 → 调用工具 → 观察结果 → 再规划 → 直到完成。这套循环里的“大脑”是 LLM,但 LLM 不是每时每刻都在“想”,它只是在关键节点做决策——就是你要它从一堆选项里选一个下一步动作的时候。
举个我做过的最简客服 Agent:用户问“我的订单怎么还没发货”,如果只是套一个 LLM 摆在前面直接让它回答,它大概率会从自己的训练记忆里编一个物流状态,这在生产环境里是不能接受的。所以我在它的循环里加入了工具集:查订单接口、查物流接口、查售后政策,Agent 的“智能”体现在它决定调哪个接口、怎么处理接口返回的数据、以及拿到数据后怎么组织语言回复用户。
这就是 Agent 和普通 chat 接口的分水岭:普通的 LLM 对话是一次性生成,Agent 是多轮递进生成,它会根据每一轮的结果调整下一步行为。理解这个以后,再看 RAG 在其中的角色就清晰了——RAG 负责的是“系统在决定下一步动作前,需要补充哪些知识”这件事。
1.1 为什么说 LLM 是“推理器”而不是“知识库”
说个最容易被误解的点:很多人觉得大模型训练了那么多数据,它应该什么都知道。实际用下来,它“知道”的东西是它的训练语料里出现过的模式,而不是随时更新的、具体到你业务场景里的事实。你让它总结一段文档内容,它做得很好;你让它回答“咱们公司今天库存还有多少”,如果你没有接入任何实时数据源,它要么编一个,要么告诉你不知道。
所以在 Agent 架构里,我通常把 LLM 的作用限定为:基于给定的信息,进行拆解、判断、选择、生成。它更像一个拿着工具的工人,工具给得好不好、工作流程顺不顺,决定产出质量。RAG 就是给这个工人递资料的人——但它比一般的人靠谱一些,能在递资料之前先帮工人筛选掉不相关的东西。
1.2 RAG 在 Agent 循环里到底补了什么
RAG,检索增强生成,全称是 Retrieval-Augmented Generation。它的核心动作是“先查再答”:把用户的问题先去知识库检索相关片段,把检索到的片段拼进 prompt,再由 LLM 基于这些片段组织答案。这样做有两个直接的好处:一是答案的准确性大幅提高,因为模型不需要凭空推测事实;二是回答可以带上知识来源的“痕迹”,方便后续追溯。
放到 Agent 的循环里看,RAG 往往不是每轮都要触发。Agent 先判断当前这一步需不需要外部知识,如果需要就走检索通道,如果不需要就直接让 LLM 推理。这个判断本身的准确度,决定了整个系统是不是高效。
2. 构建 Agent 第一步:决策循环的设计
我在项目里最常用一个极简的四步循环:
- Step 1:接收任务,做意图识别和初步拆解
- Step 2:检查工具清单,判断当前需要哪些工具或外部知识
- Step 3:调用对应工具 / 进行检索,观察返回结果
- Step 4:根据结果判断是继续下一步还是输出最终答案
这个循环看起来简单,真正麻烦的是 Step 2 和 Step 4 的“判断”。LLM 在这里扮演的是路由器的角色,但路由器也会有犯迷糊的时候。
2.1 用大模型做“路由判断”的实战设计
我最初做的 Agent 里,路由逻辑写得很粗暴:每条用户输入都先用一个二分类标签(需要检索 / 不需要检索)过一遍。分类标签本身也有人让 LLM 直接做,效果不稳定,最后选择了给 LLM 一个“工具选择列表”和“当前任务状态”让它自己选,实测效果好很多。
具体怎么设计这个 prompt?我习惯把所有工具描述成一个 JSON 列表,每个工具包含名称、用途、适合场景、参数要求。这里不能偷懒去写“调用 xxx 工具”,而是要把工具的白话场景说明写清楚,模型才能准确匹配。例如你有一个“查天气”工具,如果只写“天气查询,参数是城市名”,模型在一个复杂的 Agent 任务里就很容易调用错地方,因为描述缺少“适合什么时候用”的语义信息。
2.2 一次真实的“多步骤任务”拆解演算
有个实际例子,我让 Agent 执行一个“整理目标客户公司近期动态并做摘要”的任务。Agent 收到这个任务后的内部序列长这样:
- 第一次调用 LLM,它判断需要两个能力:公司信息检索 + 新闻文章检索。它列出了一个计划。
- 执行公司信息检索,拿到了公司简介和基础资料。
- 再执行新闻检索,发现没有找到近期新闻,返回的结果为空。
- 它没有就此停住,而是判断“这个公司可能有其他别名或者媒体报道里用的是简称”,于是改了一个关键词重新检索。
- 这次拿到了新闻列表,接着它从新闻里提取出和“融资、团队变动、产品发布”相关的段落,再调用 LLM 做摘要。
- 最后输出一份包含多条结论的汇总报告,每条后面都标注了信息来源。
整个序列里,最让我惊喜的其实是第三步那个“发现检索结果为空 → 换一个关键词再试”的行为。这不是我显式写进规则里的,而是 LLM 在“当前状态 + 可用工具描述”的组合下自主决策的结果。这也说明了设计 Agent 时到底应该把多少逻辑交给 LLM、多少逻辑交给规则。
我的经验是:稳定可控的部分用规则,需要灵活判断的部分用 LLM。比如工具之间的依赖顺序、必须校验的参数格式,这类我全用代码约束;至于“检索结果为空时下一步怎么办”,交给模型判断反而更灵活。
3. RAG 链路的关键环节:检索不是把文档塞进 prompt 那么简单
RAG 看似是个简单的三步曲——索引、检索、生成,实操起来每一步都有不少坑。我在早期项目里直接把 PDF 文字塞进向量库,检索效果惨不忍睹,后来拆开才发现问题出在“分块策略”上。
3.1 文档拆块的粒度与策略选择
分块大小直接影响检索质量。块太长,向量表示不够聚焦,检索出来的一段内容里会包含大量无关信息;块太短,语义可能不完整,模型拿到手后的生成质量很差。
我做过一组对比实验,同一份技术文档分别按 256、512、1024 的 token 长度分块,配合 50 token 的滑窗重叠,测试不同的问答效果。结论是:512 token 左右的块在这份文档上综合表现最好,既保住了语义完整性,又没有引入太多噪声。但这个数值不是万能参数,如果你的文档本身是问答对形式,可能几百字一块就够了;如果是长段落论述,可能要更大的块加重叠窗口。
对于分块,我的个人建议是:不是简单地按字数硬切,而是结合文档的标题层级、自然段落来切。一种比较稳的方法是把 Markdown 文档按段落先拆散,然后基于 token 上限做“块合并”,保证每一块的语义相对独立。
3.2 检索效果不好时的排查套路
我把检索质量的排查套路大致整理成了一个清单,每次 RAG 效果不对劲就走一遍这个流程:
| 检查项 | 具体做法 | 预期标准 |
|---|---|---|
| 文档本身是否已被正确解析为纯文本 | 直接打开索引前的内容看一眼 | 没有乱码、没有乱序 |
| 分块是否合理 | 抽样看 10 个块的起始和结束位置 | 每块语义相对完整 |
| 检索结果与问题相关性 | 用同一个问题手动执行一次 embedding 搜索 | 看 TopK 结果里有没有正确答案 |
| 重排序有没有做 | 对比重排序前后结果排序差异 | 重排序能修正向量相似度的偏差 |
| prompt 里有没有给模型足够的上下文指引 | 检查生成 prompt 的指令区域 | 明确要求“仅基于材料回答” |
这个清单陪我排查过好几个“模型回答文不对题”的问题,多数时候问题出在前端检索而不在生成链路。
4. 把 LLM 和 RAG 接起来的落地细节
理论讲再多,落到代码层面才是最真实的挑战。我这边用一个 Python 伪代码把核心逻辑展示一下,方便你在此基础上搭建自己的结构。
class Agent: def __init__(self, llm, retriever, tools): self.llm = llm self.retriever = retriever self.tools = tools def run(self, task): messages = [{"role": "user", "content": task}] for step in range(self.max_steps): response = self.llm.chat(messages) if response.is_final_answer(): return response.content action = response.parsed_action() # 假设返回一个 JSON 动作 if action.is_retrieval(): docs = self.retriever.retrieve(action.query) messages.append({ "role": "user", "content": f"检索结果:{docs}" }) else: tool_result = self.tools.execute(action.tool_name, action.args) messages.append({ "role": "user", "content": f"工具返回:{tool_result}" }) return None # 超过最大步数则失败核心思路:让模型每一轮返回一个结构化动作,是回答就输出内容,是检索就给检索词,是调工具就给工具名和参数。Agent 执行完这个动作后,把结果作为新的上下文继续喂给模型。
4.1 给 Agent 配好“工具描述”的分寸感
工具描述写不好,整个 agent 的水平能跌一半。我见过太多次 agent 把“查天气”工具当成“问时间”工具来用,原因就是描述里缺少边界条件。我现在的写法会包含:工具名称、工具用途一句话、适合触发的情况、参数说明、以及不适合触发的情况。
举个例子:
{ "name": "search_news", "description": "检索指定主题的新闻文章列表,适合用户询问某公司、某行业或某事件的最新动态时使用。如果用户只是询问常识而不需要最新消息,无需使用此工具。", "parameters": { "keyword": "string, 必填,搜索关键词", "time_range": "string, 可选,时间范围,例如 7d、30d" } }工具描述写得详细以后,模型选错的概率明显下降。这不是什么高深的技巧,就是你能不能让模型理解你的“设计意图”。
4.2 引入 RAG 时的 Prompt 组装顺序
我发现一个影响很大的细节:RAG 检索到的资料放在 prompt 里的位置和格式,会直接影响生成质量。我把检索结果放在一个独立的“参考资料”字段,而不是当成普通对话历史的一部分,并且在指令里明确声明“仅供参考,不包含答案时要主动说明不知道”。
如果检索结果格式是杂乱的网页文本,我会先做一个简单的清洗步骤,把段落拼接成一个带编号的列表。这样模型在回答时能够引用编号,方便后续追踪它到底用了哪些材料。
5. 工程化落地的几个“非功能”问题
除了核心逻辑,生产环境的 Agent 系统还有很多边角问题,不处理好一样会难产。
5.1 上下文窗口管理
RAG 和工具调用都会显著增加上下文长度,尤其是多轮工具调用之后,对话历史里塞满了中间过程。直接用原始对话历史喂给模型,很快会顶到上下文上限。
我处理的方法是把 Agent 的“长期记忆”和“短期记忆”分开。多轮工具调用的中间结果只保留摘要,不保留全文;用户对话历史则做降采样。这样上下文窗口的占用可以大幅下降,同时不丢失关键信息。
5.2 检索结果的去重和重排序
向量检索出来的 TopK 结果经常有重复内容,尤其当文档分块有重叠窗口时。我在 RAG 链路里加了一个简单去重:先按文档来源 + 块内容哈希去重,然后再用独立的 rerank 模型对剩余结果重新排序。
重排序这一步实测收益不小,它能修正纯向量相似度带来的相关性偏差。如果你已经用上了 rerank,检索结果的质量会再上一个台阶。
5.3 可观测性:给 Agent 装上日志
Agent 系统调试靠什么?靠日志。没有一次完整的推理过程记录,出了问题你根本不知道是模型选错动作、工具返回异常,还是检索没找到东西。我在每次循环的每个关键节点都打了一条结构化日志,包含:当前步骤、模型输出动作、工具调用结果、以及本次调用的 token 数。这样复盘时就像重放录像,定位问题很快。
日志字段主要包含以下几个:
| 字段 | 说明 |
|---|---|
| step_id | 当前循环步数 |
| action_name | 模型选择的动作名 |
| action_input | 模型给出的动作参数 |
| tool_status | 工具/检索返回的状态码 |
| latency_ms | 单步耗时 |
| context_tokens | 当前已消耗的上下文 token 数 |
6. 踩过的三个经典坑,以及我怎么爬出来的
干货不踩坑等于没干货。分享三个真实项目里让我印象极深的坑。
6.1 依赖 LLM 判断所有事,结果它乱来
早期我把动作判断和内容生成全丢给 LLM,没有做任何规则兜底。结果有一次 Agent 在检索结果为空时,没有重试也没有放弃,而是编造了一篇“不存在的新闻摘要”输出,用户差点投诉。加了一段校验逻辑之后,再也没出过这种情况。
校验逻辑的核心是:如果最终答案里有从外部获取的事实性内容,必须能从检索或工具返回中找到对应来源,否则强制拒答或输出“未知”。这个“来源验证”约束,是我现在所有 Agent 项目的标配。
6.2 RAG 检索到了内容,但生成时“跑偏”
有一次我调好的 Agent 在回答专业问题时,明明检索结果正确,模型回答却加了大量自己训练记忆里的泛化内容,带着错误信息。后来发现问题出在 prompt 指令的措辞上——我给出的要求是“请根据以下资料回答问题”,这太软了,模型把它理解为“资料仅供参考,你可以自由发挥”。改了几个版本下来,“你只能使用以下资料中的信息回答,不要加入资料以外的知识”效果最稳定,但同时也建议加上“如果资料不足,直接说无法回答”。
6.3 纠结于“一步到位”的复杂设计,反而拖垮了效果
我不止一次见到上来就想做多 Agent 协作、动态规划、自我反思的团队,最后卡在调试地狱里几个月出不来。我自己早期也有这个倾向。实际上,单体 Agent + 简单 RAG 能解决 80% 的问题,复杂架构只在规模、效率、准确率确实撑不住的时候才值得上。
我的原则是:先把一个简单闭环跑通,再逐步增加复杂度。每一步增加都要有明确的目的和可量化的效果提升,而不是“为了架构而架构”。
7. 最后分享一点心得
我自己把 Agent 从“玩具”调到“勉强能用的生产工具”的经历里,最大的教训是:别迷信模型能力,要把你的系统当作一个工程系统来设计。LLM 是组件,RAG 是组件,工具调用是组件,中间的数据流、约束、兜底、监控才是让整个系统立住的骨架。
还有个实用技巧,如果你刚开始做 Agent,与其用复杂的框架,不如先用几十行代码把循环逻辑自己写一遍,你能对每个环节有更直觉的理解。等清楚了瓶颈在哪,再引入框架解决也不迟。
包括 RAG 的分块参数、检索 TopK、重排序、prompt 指令设计,这些看起来不起眼的细节,才是真正决定 Agent 好用不好用的那部分。别让概念停留在“我知道”的层面,亲自动手逛一圈,你会发现那些坑都长得一模一样。