如果你手上的Agent还停留在“十个简单任务八个能跑通”的阶段,别急着庆祝,那只是问题的开始。过去大半年,我把一个基于大语言模型的多步Agent从纯Demo状态推到仿真业务环境,前前后后处理了不下二十类故障,最后沉淀下来,真正反复出现、改了又犯的“Agent经典问题”,其实集中在六块:目标拆解、上下文管理、工具调用、循环控制、权限边界、效果评测。这篇文章就把这些问题连同我的排查路径一起整理出来,给正在被Agent折磨的同行一份参考。
无论你是刚跑通一个带工具调用的Demo,还是已经做了一段时间的多Agent协作开发,应该都能在里面找到自己踩过的坑。文中所有案例都来自我自己维护的一个模拟项目X,不涉及任何特定平台或厂商,还原的是Agent开发中跨框架、跨模型的共性问题。
1. 目标拆解跑偏:Agent不是真的在规划,而是在猜
1.1 最典型的翻车现场
我的模拟业务场景里有一个高频任务:让Agent从收件箱中找出某位客户最近一次提交的发票文件,提取金额和日期,生成汇总表并发送到指定位置。
任务看起来不复杂。Agent在第一轮也给出了漂亮的计划:“先搜索收件箱,定位该客户的邮件,下载附件,提取字段,做汇总,输出报告。”但实际执行时,它可能在搜索一封邮件后就自行跳到汇总阶段,然后脑补出一个“看起来合理”的金额。更常见的是,它只做了三步里的第一步,就宣布“任务已完成”,附上一段煞有介事的总结。你打开结果一看,和实际数据对不上。
这种问题的可怕之处在于:最终输出非常流畅、自信,如果不是人工验证,根本发现不了它在编造。
1.2 为什么会这样:规划是模型对“规划”的模仿
很多开发者把“规划能力”当成Agent的固有属性,这其实是个误解。大语言模型生成计划时,本质是在预测“一个看起来合理的计划应该长什么样”,而不是在运行一个有状态、有约束、步步可验证的真实计划。
单步推理时模型还能保持专注。一旦任务超过三到五步,早期生成的计划在后续执行中逐渐被遗忘,模型开始“权宜变通”——它并不是故意撒谎,而是token预测的自然结果是倾向于给出一个连贯、完整的回答,哪怕这个连贯性建立在幻觉之上。
我在排查时最痛苦的一点是:从日志看,每一步执行好像都有依据,实际上模型在尾部的每一步都在修正自己前面的“记忆偏差”。计划丢失得越早,后面的编造就越大胆。
1.3 我的修正方案:计划与执行分离,加验证回填
后来我把“规划”和“执行”彻底分成了两个模块,而不是让同一个推理循环既负责想又负责做。
第一步,规划器只负责输出结构化任务清单,每项包含目标、输入、预期产物和完成条件。第二步,执行器按清单逐项执行,每完成一步都要把结果回填到计划中,并让一个独立的校验节点判断“这一步的产出是否真的符合预期”。第三步,如果校验失败,才允许重规划,但重规划次数硬性限制在两次以内,避免模型无限推翻自己。
我在代码里大致是这样做的:
def agent_run(goal): plan = planner.create_plan(goal) # 显式任务清单 for step in plan.steps: result = executor.run(step) # 执行单个步骤 ok = verifier.check(step, result) # 独立校验 if not ok: reason = verifier.failure_reason() if plan.retry_count < 2: plan = planner.revise(goal, plan, reason) continue raise TaskAborted(reason) plan.record(step, result) # 结果回填计划 return plan.summary()这个改动最大的收益不是成功率提升多少,而是让每个错误的定位变得清晰:到底是规划错了、执行错了,还是校验条件不对,日志里一目了然。
2. 上下文越长越糊涂:信息被注意力淹没之后的连锁故障
2.1 长对话里的信息锈蚀
Agent一旦跑多轮,第二个经典问题马上浮现:上下文窗口明明很大,但它就是“记不住”早期的关键约束。
我的测试任务有一道硬性条件:汇总结果只允许输出PDF格式。前几轮模型执行得很好。大概跑到第15轮的时候,用户随口问了一句“能不能顺便出一份Excel明细”,Agent居然直接采纳了新格式,把原始硬约束完全抛在脑后,最后输出的不是PDF而是Excel。
这类问题我称之为信息锈蚀:不是模型不知道约束存在,而是约束在对话历史里被大量无关内容稀释,注意力权重被后期信息占据,早期指令慢慢失去效力。
2.2 根因不在“记不住”,而在“找不到”
分析下来,注意力机制天然更关注近期内容,长上下文里稍早的信息即使还在窗口内,有效权重也会逐渐下降。再加上Agent系统本身会往上下文里塞大量工具返回值、系统提示、中间日志,真正的关键指令占比可能只有百分之几,又被压在最容易被忽略的位置,模型不跑偏才怪。
这里还有一个容易被忽略的工程因素:Agent框架在拼接消息时,常常把系统指令放在最前面,把多轮历史和工具结果放在后面。当历史很长时,模型尾部生成阶段主要受最近几轮影响,早期系统指令几乎相当于背景噪声。
2.3 我的记忆分层实践
我后来没有继续依赖“加大窗口”,而是把记忆拆成了三层。
短时工作记忆只保存最近两轮对话和当前正在执行步骤的状态。任务记忆保存的是本次任务的目标清单、不可变约束、中间产出物摘要。长期记忆则通过向量检索方式存储历史项目中的相似案例和关键结论,只在需要时按相关性召回注入。
每次把内容拼给模型之前,我会用一个上下文管理器做统一打包:先固定放置“不可变约束”锚点区,再放当前任务状态,最后才放最新的对话片段。关键信息每次都在最前面出现,而不是让模型从头到尾再翻一遍。
以PDF约束为例,我在锚点区写入:
【不可变约束】 - 输出格式:仅PDF - 所有金额字段必须保留两位小数 - 涉及客户敏感信息时,禁止写入日志这样无论对话进行到第几轮,约束区永远在上下文开头,模型每次生成时都能先看到它。实测下来,约束类信息的保持能力提升明显,像“改格式”这类隐性跑偏基本消失。
3. 工具调用的三大坑:参数幻觉、契约失配、失败后无路可退
3.1 模型会自己发明参数
Tool调用是Agent最核心的能力,也是问题最密集的雷区。第一个坑是参数幻觉:工具定义里根本没有的字段,模型会一本正经地生成出来。
我的模拟项目里有一个发送邮件工具,参数要求是收件人、主题、正文。某次测试中,模型把收件人直接填进了“抄送”字段,还在参数里增加了一个工具定义中完全没见过的“优先级”字段,值为“high”。结果解析器直接把这次调用判为非法,Agent却没有意识到是自己的参数写错了,而是重复提交了三遍一模一样的错误请求。
这类问题在参数多、字段名相近时尤其严重,比如“user_id”和“user_name”放在一起,模型非常容易把两个字段的值搞混,或者把数字类型的ID写成字符串。
3.2 契约失配:工程问题,别全赖模型
第二个坑往往被误判成模型智商问题,实际上是工程上的契约管理失效。
工具提供方更新了Schema,把某个字段从必填改成了可选,调用方Prompt里的描述还是旧的。或者同一个工具被两个模块复用,模块A期望它返回JSON格式,模块B期望它返回纯文本,但工具只做了一种实现。这类问题在Agent的调试日志里表现得很像“模型调用格式错误”,追到底却发现是接口文档和实际Schema不一致。
我在一次排查中就遇到这种情况:工具的workspace_id字段长度从20位扩展到了32位,但另一个服务的入参校验还限制在20位,Agent每次调用都在这个隐蔽的边界上被切断,排了半天才发现不是模型问题,而是上下游字段长度契约不同步。
3.3 失败后没有退路
第三个坑是失败恢复路径缺失。很多Agent框架在工具报错后,只是简单地把错误消息拼接进对话历史,系统提示里也没有告诉模型遇到错误应该怎么办。于是模型采取的最常见策略就是:用相同参数重试,或者假装调用成功并继续编故事。
我在日志里看到过一次非常典型的记录:文件下载工具返回“403 Forbidden”,Agent在接下来的五次循环里每次都调用同一个工具、传同一组参数,只是换着措辞自我鼓励,“尝试再次下载”“再试一次”“这次应该可以”。它完全没意识到需要去检查权限申请状态,更没想过换一个备用下载路径。
3.4 我的校验与恢复设计
我后来给所有工具调用加了一层强校验中间件,任何参数在发出前都要经过JSON Schema校验。校验失败时返回给模型的不只是一段错误文本,而是一份结构化的“纠错指南”,明确告诉模型哪个字段错了、正确的取值是什么。
同时我给每个工具规定了失败恢复动作:第一次失败后返回完整错误上下文,第二次失败后附加备用方案提示,第三次失败则直接中断当前调用链,转人工或切换替代工具。这里最关键的是,不要给模型无限制重试的自由,否则它会用低质量的反复尝试污染整个上下文,甚至拖垮成本预算。
错误返回的消息模板大致是这样的:
{ "tool": "send_email", "status": "validation_error", "errors": [ { "field": "priority", "message": "该字段不存在,可接受字段列表: receiver, subject, body" } ], "suggestion": "请移除priority字段,并检查收件人是否填写完整" }这样改完之后,工具调用的第一次成功率有肉眼可见的提升,更重要的是,失败之后的路径清晰了,Agent不会再原地转圈。
4. 死循环、重复动作与成本黑洞:失控Agent怎么止损
4.1 “原地打转”比报错更可怕
Agent开发到中后期,我碰到的最高频且最耗时的故障就是死循环。
报错毕竟还有提示,循环却可以无声无息地运行很久。最典型的表现是Agent反复执行同一个工具,每次都返回同样的结果,但它还是会继续尝试,因为下一个模型token预测觉得“任务还没完成”。有时循环表现为同一动作换着参数反复执行,但这个参数根本没有朝目标方向收敛,只是在有限空间里来回跳。
我的模拟项目里出现过一次最离谱的循环:一个数据库查询工具被连续调用了37次,每次都查同一张表、条件只改了一个等号方向,结果当然全部为空,Agent依然不结束。直到我设置了单次任务最大步骤数为60才被硬性拦停,否则这个任务能把一天的预算额度烧掉大半。
4.2 循环控制不能靠模型自觉
这个教训让我明白:循环控制必须放在外部执行框架里,不能指望模型“意识到自己在循环”。模型没有运行时的自我监控能力,它只知道生成下一个文本片段,无法感知自己的行为轨迹已经重复。
我在执行框架里加了几个硬护栏。
第一个是最大步骤数。任何任务最多执行50步,超过直接终止并输出当前进度摘要。
第二个是结果去重。维护一个最近结果的哈希集合,如果连续多次调用返回的结果完全相同,或者同一工具同一参数的调用出现超过三次,系统自动触发中断,不再等待模型生成“下一次尝试”。
第三个是时间与预算熔断。按任务的预估复杂程度设置token消耗上限,实际消耗达到上限的120%时强制结束。
我用一个监控器统一管理这些规则:
def execute_with_guardrails(agent, task): seen = {} while agent.step_count < MAX_STEPS: action = agent.next_action() key = action.signature() # 工具名+参数哈希 if seen.get(key, 0) >= 3: raise LoopDetected(action) seen[key] = seen.get(key, 0) + 1 result = executor.call(action) agent.observe(result) if token_usage.ratio() > 1.2: raise BudgetExceeded(token_usage) raise MaxStepsExceeded(agent.step_count)有了这套护栏之后,我再也没有因为死循环而半夜爬起来终止任务。诚实地说,把任务跑失败的次数依然存在,但“让它自己跑个没完”这件事彻底消失了。
4.3 成本视角下的分阶段预算
除了防止死循环,成本控制也需要区分阶段。我的做法是给规划阶段和执行阶段分别设置预算:规划阶段只占总预算的20%,执行阶段占80%。如果规划器反复重规划、长时间不进入执行,它很可能在无意义地消耗预算,这时候就强制切换到人工接管。
另一个实用技巧是阶段性上下文压缩:当执行接近预算上限时,不是简单粗暴地停止,而是先把历史中的工具调用细节折叠成摘要,释放上下文空间后再让Agent继续。很多任务在上下文中多余信息被清掉之后,反而能更快收敛。
5. 权限边界的经典失衡:既怕它乱动,又怕它不动
5.1 放权太少,Agent就成了高级搜索框
权限设计是Agent落地时绕不开的坎。一开始,我在模拟项目中采取了最保守的策略:Agent的每一个外部动作都需要人工确认。结果是任务确实安全了,但Agent变得极其笨重,用户每操作一步都要弹窗确认,体验和效率还不如直接用表单工具。
当时我的一个用户就直说,如果每个环节都要人点头,那这个Agent本质上只是一个“高级搜索框”,毫无价值。这句话点醒了我,权限设计的关键是分级,而不是一刀切。
5.2 放权太多,一次事故教会我做人
吃过保守的苦,我又走向了另一个极端:把文件删除和对外发送的权限全部放开,想让任务执行更流畅。结果没几天就出事了。
某个清理任务本来应该删除临时缓存目录下的过期文件,模型在路径拼接时出现了偏差,把同级的另一个目录当成了目标,一口气删了几十个测试数据文件。复盘时发现,模型在权限充足的情况下,面对不确定时会倾向于“完成任务优先”,而不是“保守不做”。它宁可执行一个有风险的操作,也不愿意停下来向用户确认。
那次事故之后我彻底明白:Agent的权限风险不是模型能力问题,而是执行环境缺少分层约束。
5.3 我的分级授权方案
现在我把权限分成三个层级。
只读类操作,如查询、检索、读取文件,默认允许执行,不需要确认。可恢复类操作,如创建草稿、修改非关键配置、发送可撤回的通知,可以自动执行,但在执行记录里打标记,事后可追溯。高风险类操作,如删除文件、覆盖已有数据、对外发送不可撤回的消息、触发扣费,必须二次确认。
除了动作级别,我还加了一道“场景护栏”:在系统提示里明确告诉模型,只有当前上下文确实出现了相关业务意图时,才允许调用高风险工具。推荐引擎做一次预判,比如文本里没有出现“删除”“清理”“覆盖”等明确意图,即使模型主动请求调用删除工具,中间件也会直接拦截。
这套方案执行以后,任务成功率没有下降太多,但高风险动作的误触发率基本降为零。权限放到了合适的位置,Agent才开始真正像一个可以交代干活的助手,而不是一个每一秒都要盯着的熊孩子。
6. 评测与回归:为什么改一版Prompt别的任务全崩
6.1 Agent评测的难处在哪里
单轮模型评测已经有比较成熟的指标,但Agent是多步决策系统,最终结果正确不代表过程正确,过程正确也不代表没有隐藏的幻觉。
我早期只关心最终成功率,结果被狠狠坑了一次。某个版本的任务成功率看起来是80%,但其中大量成功案例都是模型“绕路”达成的,调用了错误工具却带来了侥幸正确的结果。这些隐性问题在下一次Prompt改动时集中爆发,因为模型的能力分布一变,以前靠绕路掩盖的问题全部暴露出来,成功率直接从80%崩到30%。
Agent评测必须有过程指标。我现在跑每一个基准任务时都会记录五类数据:最终成功率、平均步数、首次失败点、工具误调次数、人工介入次数。任何一个指标异常回升或异常下降,都说明某个环节正在出问题。
6.2 回归任务集的搭建思路
我维护了一份相对固定的回归任务集,规模不大,但覆盖很全面,一共10个主场景,每个主场景再配2到3个变体,总共约25个用例左右。
任务类型包括:长链条信息提取与汇总、多轮约束保持、工具选择与参数对照、模拟失败后的恢复路径、面对不安全请求时的拒绝行为、跨会话记忆继承。每次升级模型版本或调整Prompt,我先跑这个子集,确认全部场景没有问题之后,再扩展到更大范围的数据集。
以下是一个简化版的评测记录表:
| 任务场景 | 成功率 | 平均步数 | 工具误调次数 | 人工介入次数 | 主要问题 |
|---|---|---|---|---|---|
| 收件箱发票汇总 | 92% | 24 | 1 | 2 | 第8步格式跑偏 |
| 多轮格式约束保持 | 96% | 18 | 0 | 1 | 无 |
| 高权限误触发拒绝 | 100% | 9 | 0 | 0 | 无 |
| 工具失败恢复路径 | 78% | 31 | 4 | 3 | 恢复策略混乱 |
这个表一旦建起来,每次迭代的对比就非常直观。改完Prompt后如果某个场景的指标明显变化,我就能快速定位是哪一类能力受到影响,而不是笼统地靠感受判断。
6.3 控制回归风险的几个实操细节
评测还要注意控制随机性。Agent调用大模型时如果温度设得太高,同一份任务跑两次可能结果差异巨大,这种情况下你很难区分指标变化是代码改动引起的还是随机波动。我在回归测试时会固定温度参数,并固定随机种子,让模型解码路径尽量可复现。
另外一个容易被忽略的细节是:尽量不要同时改多个变量。很多人有个坏习惯,这次升级同时改了系统Prompt、工具描述、上下文压缩策略,结果效果变差了,却根本说不清是哪一项导致的。我现在一次只改一个变量,跑完回归确认后再动下一个,虽然过程慢一点,但每一步都踏实。
最后,基线版本必须长期保留。我在自己的项目里专门保存了一份旧版本配置,当前版本出现问题的时候,我会直接跑一次基线版本做对比。绝大多数情况下,问题都出在新旧版本对某类提示词处理路径上发生了偏移,而不是业务逻辑变化。
最后说一点我的个人体会
Agent项目最容易翻车的地方,往往不在单个环节,而在环节之间的接缝:规划到执行的接缝、上下文到记忆的接缝、权限到恢复的接缝。我每次踩坑后第一时间做的不是改Prompt,而是先给系统加一个可观测点,把当时的状态、完整的上下文和模型输出全部抓出来,等真正看清了故障全貌,才敢动逻辑代码。
如果你也在做Agent开发,建议把上面六个问题当成一份排查清单,提前给系统加上步骤数限制、参数强校验、权限分级和回归评测。这些护栏看起来枯燥,却能在你真正调试到深夜的时候,替你挡掉一半以上的灾难。后面我还会继续整理Agent可观测性建设方面的实战记录,如果这篇文章对你有用,欢迎持续关注。