做 Agent 的朋友应该都有过这种体验:一个 Demo 跑通了,能回答问题、能调用工具,看起来挺像那么回事。但一放到真实场景里,立马露馅——聊到第三轮就忘了自己说过什么,让它办一件事办到一半就断了,更不用说连续执行多步骤任务时经常“捡了芝麻丢西瓜”。说白了,就是缺了三样东西:记忆、目标、执行力。这三个词听着玄乎,其实是很具体的工程问题。这篇就以实战经验为主,聊聊怎么把一个只会“一问一答”的原型,改造成一个能记住上下文、能拆解目标、能真正把事办完的 Agent。适合正在从 Demo 往生产环境推进的开发者,也适合想系统理解 Agent 核心能力的同学。
1. 先把“记忆、目标、执行力”拆成三个可落地的模块
很多人刚开始搞 Agent,习惯把 prompt 写得越长越好,恨不得把所有逻辑都塞进一段提示词里。结果就是上下文越滚越长,模型越跑越乱,最后连简单任务都做不好。我的经验是:别把这三件事混在一堆提示词里,要拆成独立的模块,各自管好自己的一摊事。
1.1 这仨能力为什么得分开做
用一个生活化的类比来解释:一个人要完成一件复杂的差事,脑子里得记得来龙去脉(记忆),心里得清楚要达到什么结果(目标),手上得能操作工具、迈开步子(执行力)。这三件事听起来是一体的,但真出问题的时候,你得分得清是哪个环节出了问题。
如果是“忘了”,那是记忆模块的问题,跟规划没关系。如果是“跑偏了”,那是目标分解没做好。如果是“调工具报错”,那是执行链路没通。三者混在一起,排查问题就是灾难——模型输出一段莫名其妙的话,你根本不知道它是因为上下文丢了,还是因为任务没想清楚,还是因为工具参数写错了。
分开做还有一个现实好处:可以单独迭代、单独测试。我自己的做法是给每个模块写独立的测试用例,记忆模块测存取和召回,规划模块测目标分解的合理度,执行模块测工具调用的成功率。哪个不好就单独优化哪个,不用每次改动都全链路回归。
1.2 三个模块的边界与数据流
先说一下整体数据流,这样后面每个章节的定位就清楚了:
用户输入进来,先交给记忆模块,把跟当前话题相关的历史信息检索出来。然后这些历史信息会和当前输入拼到一起,交给规划模块。规划模块负责理解用户到底想要什么结果,把大目标拆成小步骤。拆完的步骤交给执行模块,执行模块去调各种工具、API、代码,拿到结果。这个结果一方面会作为回复内容返回给用户,另一方面会写回记忆模块,成为下一轮对话的上下文。
三个模块的边界很清晰:
- 记忆层:管数据的存、取、更新、过期,不关心任务是什么。
- 规划层:管状态和决策,根据当前状态决定下一步做什么,不关心具体怎么调用工具。
- 执行层:管动作和工具,把规划层给的目标翻译成具体的 API 调用、代码执行、文件操作。
这个设计有一个好处:任何一层出问题,都可以单独降级。记忆挂了,Agent 就退化成无状态对话,但还能用。规划挂了,至少还能做单轮问答。执行挂了,还能把规划好的步骤当成建议输出给用户,不至于完全瘫痪。
2. 记忆:从“聊过就忘”到“真的记住”
记忆这块,通常分成短期记忆和长期记忆两套体系。短期记忆解决“当前对话别断片”,长期记忆解决“下次来还能想起来”。很多 Agent 做得不好,往往是把这两件事当成了一件事,用一个大而全的上下文窗口硬扛。
2.1 短期记忆:上下文窗口的取舍
模型有 token 上限,这是硬约束。早期做法简单粗暴,把所有历史消息都塞进上下文,塞不下了就从最早的开始丢。这种做法到 20 轮以内没问题,但超过 30 轮,前面的重要信息就会丢失,而且 token 成本直线上升。
我的做法是维护一个滑动窗口,窗口大小按 token 数控制,比如 8000 token 以内的对话历史完整保留,超过就触发截断策略。截断不是直接丢,而是把中间部分做摘要压缩。摘要生成的时机要把握好,我实测下来,每 6 到 8 轮做一次增量摘要比较合适,太频繁成本高,太稀疏会漏掉关键信息。
具体流程是:每当对话轮数或者 token 数超过阈值,就把当前窗口里的内容喂给模型,让它用固定模板生成一段摘要,然后只保留摘要加最近几轮完整对话。关键细节是,摘要模板里一定要要求模型保留“用户明确表达过的偏好”“已经确认的事实”“未完成的事项”,这三类是后续对话最需要的信息。
我踩过一个坑:一开始摘要做全量重写,每次触发都把从第一轮到现在的所有历史全部重新摘要一遍。由于频繁调用模型重写整段摘要,响应变慢,成本也高。后来改成增量式——旧摘要不动,只把新增的几轮对话糅进旧摘要里,效果和成本都有明显改善。
2.2 长期记忆:向量化存储与召回
长期记忆的目标是:用户下次来,或者聊了几十轮后,还能把之前说过的重要信息捞回来。如果只靠上下文窗口,信息量撑不住。所以要做外部存储。
存储之前先要解决一个问题:什么值得存?我的经验是设一道关卡,只提取三类信息存长期记忆:用户偏好(比如“我比较喜欢简洁的报告风格”)、项目状态(比如“上周那个数据分析需求已经完成了初版”)、关键决策(比如“最终确认选用方案 B,理由是成本更低”)。普通寒暄、临时性信息、已经过时的事实,不值得入库。存多了反而会污染召回结果。
存的时候把信息转成向量,方案一般是接一个 embedding 模型,把文本转成向量,写入向量库。召回的时候,把当前用户输入也转成向量,做相似度搜索,取 TopK 条返回。
这里面有很多坑。最典型的是,同一个信息重复写入多次,导致召回结果全是同一个内容的多个变体,浪费上下文又不提供新信息。需要在写入前做一次相似度检索,如果库里已有高相似度记忆,就做合并更新而不是新增一条。
召回策略也别只用 TopK。我建议加两道过滤:相似度阈值(低于 0.7 的不要)和时间衰减(太久远的记忆降权)。另外追加强调:如果用户当前输入里有明确的实体词,比如某个项目名、某个文件名,优先按实体精确匹配为主,向量相似度为辅,精确匹配的准确率远高于语义匹配。
2.3 记忆如何为“目标”和“执行”服务
记忆不只是给模型当背景资料,它还需要给规划模块提供状态信息。举个例子,用户说“上次那脚本改得怎么样了”,如果只是把对话历史一股脑堆给模型,模型很可能雾里看花。更好的方案是,记忆模块维护一个状态字段,记录当前任务的阶段,比如“脚本初版已完成,待用户验证”。
规划模块拿到这个状态后,它就知道用户问的是“验证后的反馈”,而不是“从零开始改脚本”,行动路径完全不同。所以记忆库里设计一个专门的 state 区域,每次执行完一个关键步骤后,由执行模块把结果摘要和当前阶段回写到 state 里。这样整个 Agent 就有了“当前在干什么”的意识和“下一步该干什么”的依据。
2.4 记忆模块的工程化要点
存储选型,坦白说没有一步到位的方案,都是跟着阶段走:
- 原型阶段:直接内存里放一个 Map,key 是会话 ID,value 是对话列表和相关记忆,简单直观,跑通逻辑为主。
- 单机应用:上 SQLite 或者文件存储,解决重启丢数据的问题。
- 需要并发和分布式:上 Redis 做会话级缓存,Postgres 做持久化,向量库做召回。
整套记忆能力最好封装成独立服务,对外暴露三个接口:put(写入记忆)、get(按场景和实体读取)、search(按向量召回)。这样上层 Agent 代码根本不需要关心记忆是存在哪里、怎么检索的,只管调用接口。
还有一个常被忽略的点:记忆过期和清理。很多记忆是有时效性的,比如“正在调试的接口报错信息”,三天后就没意义了。我的做法是对每类记忆设置 TTL,短期工作记忆 24 小时过期,长期偏好记忆设 90 天,定期跑清理任务。
3. 目标:从一句话到可执行的任务树
“目标”这个模块,核心是把用户的模糊需求变成一组清晰、有序、可执行的任务。难点不在“拆”,而在“拆得准”和“拆得可执行”。拆得太大,每一步都像个黑盒;拆得太细,步骤列表长到模型自己都跑不完。
3.1 意图识别与目标澄清
用户通常不会用结构化语言下达指令。“帮我看看这个数据不对劲”到底是要分析数据、修数据、还是生成可视化报告?先把话里的目标要素提取出来:要做什么、输入是什么、输出是什么、约束条件是什么。如果信息不齐,不要硬猜,多问一句澄清。
这一步我建议单独设计一个“澄清轮数上限”。最多问三轮,如果三轮后用户还是给不出明确信息,就按默认约定执行。比如用户说“随便看看”,那就按默认的分析流程跑一遍,输出标准报告。这是为了防死循环——Agent 一直问“您能具体说一下吗”,用户直接失去耐心。
3.2 目标分解:任务树的生成与修正
解析完目标之后,生成任务树。树的根节点是目标,往下逐级分解到最小执行单元。比如“整理一份行业竞品分析”这个目标:
- 根目标:竞品分析报告
- 子任务一:确定分析维度(产品功能、定价策略、市场表现)
- 子任务二:收集信息(搜索、抓取、调 API)
- 子任务三:信息结构化(按维度整理成表格)
- 子任务四:生成报告(按预置模板输出)
- 子任务五:自查与校验(检查数据完整性和格式规范)
任务树生成有两种路径:让模型自由发挥,或者从任务模板库匹配。我的经验是混合使用——先匹配模板,模板命中不了再让模型自由分解。模板的好处是稳定可控,自由分解的好处是覆盖面广。
任务树的关键是处理好依赖关系。哪些任务可以并行,哪些必须等待前置任务完成。我建议在任务节点上加 status 字段:pending、running、done、failed。执行模块只负责把 pending 且依赖已满足的任务拿出来跑。这样即使某个任务失败,也只需要重新规划它的下游,而不是整棵任务树推倒重来。
3.3 两种执行模式怎么选
在“目标规划”和“执行”的配合方式上,存在两种主要路径:
- 边想边做(ReAct 模式):每执行一步,就把结果反馈给模型,模型看到结果后再决定下一步。适合任务路径不确定的场景,像调试代码、查资料这种方向随时会变的活。
- 先想好再做(Plan-and-Execute 模式):拿到目标后先把任务树完整展开,然后按顺序执行,中途不打断。适合步骤明确、流程固定的场景,像批量处理数据、生成标准报告。
我的建议是看任务的不确定性:路径越不确定,越应该用 ReAct。流程越固定,越应该用 Plan 模式。还有一种折中方案:高频复用的任务先规划成模板,走 Plan 模式;新任务、异常任务走 ReAct 模式。另外不论用哪种模式,都要额外加一个最大步骤数的终点条件,防止模型陷入“呼吸循环”出不来。
4. 执行力:把“想得到”变成“做得到”
执行模块是三块里最累的活。规划做得再好,工具调用一步出错,前功尽弃。我在这里踩过的坑最多,这一节多写点细节。
4.1 工具调用的核心:让模型知道“有什么工具”和“怎么用”
很多模型支持函数调用功能,这里给模型提供工具列表和描述即可。核心不在于这个机制本身,而在于工具描述怎么写。很多开发者描述写得极其敷衍,比如“查询天气”,模型根本不知道接受什么参数、返回什么结构,自然经常调错。
我写工具描述的经验是,除了说明功能,必须写四件事:
- 适用场景:什么时候该用这个工具,什么时候不该用。比如“当用户询问天气时使用,仅用于获取天气信息,不用于其他查询”。
- 参数说明:每个参数的字段名、类型、取值范围、是否必填。
- 返回结构:返回值长什么样,可靠字段有哪些。
- 失败行为:什么情况下会失败,失败后返回什么错误码。
比如一个工具描述的示例可以这样写:
{ "name": "get_trend_data", "description": "获取指定关键词的热度趋势数据。仅当用户询问趋势、热词、搜索量变化时使用。", "parameters": { "type": "object", "properties": { "keyword": { "type": "string", "description": "要查询的关键词" }, "start_date": { "type": "string", "description": "起始日期,格式 YYYY-MM-DD" }, "end_date": { "type": "string", "description": "结束日期,格式 YYYY-MM-DD" } }, "required": ["keyword"] } }描述写清楚之后,模型误调用的概率大幅下降。还有一个细节:工具列表不要全量塞给模型。如果 Agent 有二十个工具,模型一次性拿到二十份描述,注意力会被稀释。建议做工具路由——先根据用户意图筛出候选工具,只把 Top 3 到 Top 5 的描述交给模型选择。
4.2 多工具编排与结果回填
真实任务很少只用一个工具。做一份报告,可能要先用搜索工具收集资料,再用文档工具生成初稿,最后用质检工具检查格式。多工具编排的需求是:让上一个工具的输出,变成下一个工具的输入。
这个环节技术上不复杂,但有个工程问题:中间结果可能非常大。搜索工具返回 10 条链接加正文摘要,整个文本可能几千 token,如果直接全部塞回上下文,几轮下来又爆了。我的做法是给每个工具的输出做一次摘要或字段提取,只保留下一步需要的字段。比如搜索工具的输出,对规划模块而言,有用的只是每个结果的标题、URL、相关性评分,正文不需要回填进上下文,需要的时候再单独拉取。
工具是 Agent 和真实世界之间的桥梁,所以要在工具层加容错。模型给出调用意图后,工具层要做参数校验,不要无条件执行。比如模型传了一个日期格式不对、或者输入超出合理范围,工具层可以直接拦截并返回错误提示,让模型重新给参数。
4.3 执行安全与容错
这是最容易被忽视的部分,也是生产环境里最容易出事故的部分。
第一是权限边界。给 Agent 的工具必须有明确的白名单,能读的目录、能调用的 API、能执行的命令都要限制。涉及删除、覆盖、外部发送消息这类危险操作,必须二次确认。我见过不止一次 Agent 按照指示执行了一个破坏性命令,事后复盘发现是目标理解有偏差,但工具已经执行了。
第二是超时和重试。每个工具调用都要设置超时时间,超时后重试一次,两次失败就标记失败原因,交给规划模块重新决策,而不是无限重试。
第三是幂等性。设计工具时尽量让操作可重复执行且结果一致。如果做不到(比如“发送邮件”这类会造成重复副作用),就要在执行前加确认步骤,并且记录执行状态,避免重复发送。
5. 常见问题与排查技巧实录
这章是实战记录总结。以下是排查频率最高的一些情况,把它们整理成了一张速查表,方便对照处理。
5.1 记忆相关:检索不到、召回错误、上下文污染
| 问题现象 | 常见原因 | 排查方法 |
|---|---|---|
| 用户提了之前说过的事,Agent 毫无印象 | 关键信息没有写入长期记忆,只留在了过期短窗里 | 查记忆库中该会话的写入记录,确认是否做了实体提取和时间衰减过滤 |
| 检索出的记忆跟当前话题不相关 | 向量召回阈值过低,相似度不相关也被放行 | 提高相似度阈值,改用实体精确匹配优先召回 |
| 同一信息重复写入,上下文重复 | 没有前置查重,同一个事实被存成多条变体 | 在写入前做向量相似度检查,高相似度时选择合并更新 |
| 记忆里混入过期无用信息 | TTL 设置不合理,或者未设置过期清理任务 | 检查每类记忆的过期策略,补清清理任务 |
一个比较独特的排查技巧:把记忆模块的检索结果直接打印出来,看“原始输入、召回条件、召回结果”三者的对应关系。很多时候你发现不是它没记住,而是它记错了重点——存的多少与话题无关的旁支信息。
5.2 目标规划相关:方向跑偏、步骤错乱、任务卡死
| 问题现象 | 常见原因 | 排查方法 |
|---|---|---|
| Agent 做的事跟用户要求的不一致 | 目标解析阶段漏掉了用户话里的约束条件,规划模块也没澄清 | 拉起规划模块的“目标解析结果”,检查提取出的目标要素是否完整 |
| 任务步骤顺序错乱 | 任务节点遗漏了依赖关系标注,导致下游先于上游执行 | 检查任务树中每个节点的依赖字段,确认拓扑排序逻辑 |
| 任务中途卡死,反复执行同一个动作 | 没有设置最大步骤数或终止条件 | 加上步骤数上限,循环检测逻辑一旦发现重复动作就强制退出 |
| 复杂任务分解太粗,执行模块不知道怎么做 | 任务模板库命中了个过于概括的模板 | 调整模板的分解粒度,细化到“最小执行单元”级 |
5.3 工具执行相关:参数报错、超时、副作用
| 问题现象 | 常见原因 | 排查方法 |
|---|---|---|
| 模型输出的工具参数经常报错 | 工具描述里的参数说明不清晰或缺少枚举值 | 重写工具描述的 parameters 部分,写明类型、必填、取值范围 |
| 工具调用超时 | 没有设置超时控制,外部 API 响应慢就挂起 | 给所有工具加超时中断,超时后返回超时错误码 |
| 同一个工具被重复执行,产生副作用 | 缺少幂等控制或状态记录 | 为会产生副作用的工具保存调用标记,重复调用前先检查 |
| 工具返回了结果,但 Agent 不会用 | 返回结构没写清楚,模型无法定位关键字段 | 工具返回值用统一 JSON 结构包装,加字段说明 |
最后聊两句体感层面的东西。我刚做这套体系的时候,优先级安排得不对,先把精力花在了规划模块上,试图让 Agent 把任务拆得又细又准。后来实测下来发现,大部分失败其实是记忆没兜住信息、工具执行错了没人拦,而不是规划拆得不够好。这三者的优先级排序应该是:先让工具层稳,保证每步执行都能可靠完成;再让记忆层全,保证执行所需的上下文不缺失;最后做规划层优化,让执行顺序更合理高效。按这个顺序推进,返工最少,见效最快。
还有一个很实用的小技巧:Agent 的日志不能只记状态,要把每个模块的关键输入和输出单独打日志。排查问题的时候,按“记忆检索了啥→规划定了啥→执行干了啥”这条链路看,基本一眼就能判断问题出在哪个环节。这套方法论不止适用于个人项目,拿来当团队协作的技术方案红线也好使,跟成员对齐“模块自治、边界清晰、日志可追溯”的原则,Agent 的质量就能在一个可控的水平线上稳定住。