遇到“同一个任务跑两次做法不一样、中途自己换路”,通常不是先改提示词,而是先把计划变更发生的那一步找出来,再用两个变量分别做单变量对照:任务拆解粒度、工具返回是否真正写回上下文。判断顺序建议是:先定位变更点(第几步、变更前后目标差在哪),再只改拆解粒度重跑,最后检查返回体字段与下一步可见信息是否一致。这两个原因的表现不同——粒度太粗时变更点往往出现在同一类子任务上、比较稳定;返回没回填时变更点跟着某次工具调用走、偶发居多。边界在于:单次轨迹只能说明现象,结论需要多跑几次并用显式日志固定下来,不能靠肉眼比对下判断。
处理这类问题,先不要动模型参数。第一步在任务轨迹里标出计划变更的步骤号和触发前的那次工具调用;第二步固定工具与提示词,只调整拆解粒度,同一任务各跑三次比对变更点是否前移或消失;第三步把工具返回体字段与下一步实际可见的上下文逐项对照,找出被截断或丢弃的部分。若变更点在细粒度下仍稳定出现在同一位置,更可能是返回未回填或子任务本身信息不足;若变更点随粒度变化而漂移,才优先怀疑拆解问题。所有判断以日志和配置为准,单次运行不下结论。
在任务轨迹里找出计划发生变更的那一步
先把“跑两次不一样”落到具体步骤上。做法是把一次完整运行按步骤切成片段,每步只记四件事:这一步的目标、调用的工具与入参摘要、返回的关键字段、与上一步相比的目标差异。差异第一次出现的那一步,就是计划变更点。
步骤 目标 调用工具 返回摘要 与上一步的差异
03 确认订单状态 query_order status=paid -
04 查询可用库存 query_stock items=[] (空) -
05 改为先取消订单再下单 cancel_order - 目标从“查库存”跳成“取消并重建”上例里变更点在步骤 05,触发输入是步骤 04 返回了空列表。记录下来的是“哪一步、因为什么返回、目标怎么变”,先不解释模型为什么这么想。执行位置就是你的 agent 运行日志或轨迹可视化页面;如果目前没有逐步日志,先在循环里加一行打印步骤号和工具名,再复现一次。
把拆解粒度当作唯一变量做对照
定位到变更点后,保持模型、提示词、工具集、输入任务完全不变,只改拆解粒度。粗粒度版本把任务拆成三五个大步骤,细粒度版本把每一步收窄到一个明确动作加一个可判定的产出条件。两版对同一任务各跑三次,记录变更点落在第几步。
| 版本 | 拆解方式 | 第 1 次变更点 | 第 2 次变更点 | 第 3 次变更点 |
|---|---|---|---|---|
| 粗粒度 | 查订单→处理库存→提交 | 第 4 步 | 第 4 步 | 第 5 步 |
| 细粒度 | 查订单→逐条校验库存→记录缺口→决定分支 | 无变更 | 无变更 | 第 6 步 |
读表方式要克制:变更点从第 4 步前移或消失,说明粗粒度下模型在一步里同时承担了判断和决策,容易自行改路;变更点仍在同一位置、只是序号变了,说明拆解不是主因。表格只记位置和次数,不写成功率和耗时差异,除非你真的从日志里统计过并说明统计口径。
检查工具返回是否被写回上下文
这一步专门排除“工具返回了,但没进下一步”的情况。做法是拿一次运行里某个工具的原始返回体,和下一步模型实际收到的上下文做逐字段对照,标出被截断、被摘要替换或被丢弃的字段。
| 返回体字段 | 下一步是否可见 | 处理方式 |
|---|---|---|
| items | 可见(空数组) | 原样写入 |
| items[].warehouse | 不可见 | 被截断,仅保留前若干条 |
| error_message | 不可见 | 空结果时未拼接进上下文 |
常见原因有三类:序列化长度上限把长返回截断;空数组或空对象被当成“无内容”直接跳过;工具报错走了异常分支,错误信息只进日志没进上下文。对应的检查项是:字段是否完整保留、空值是否显式表达、错误是否显式表达。可以先在拼接上下文的那一层加一条断言,长度或字段缺失就打印告警,再复现一次看是否命中。
把计划变更写成显式日志字段
靠肉眼比对轨迹不可持续。建议在 agent 循环里固定输出结构化日志,每步一行,字段保持稳定,方便事后过滤。
{"run_id":"r-001","step":5,"goal":"取消订单后重建","prev_goal":"查询可用库存","plan_changed":true,"change_reason":"empty_tool_result","trigger_tool":"query_stock","tool_ok":true}骨架里三个字段是关键:step 定位变更点,change_reason 记录变更由什么引起(可用固定枚举,如 empty_tool_result、tool_error、goal_conflict),trigger_tool 指向触发变更的那次调用。写入时机放在“决定下一步动作之前”,而不是执行之后,否则会漏掉被放弃的计划。过滤命令示例:
grep '"plan_changed":true' agent_trace.jsonl | jq -r '[.run_id,.step,.trigger_tool,.change_reason]|@tsv'依赖 jq 的机器上可直接跑,没有 jq 时用 grep 加 awk 取字段即可。验证方式是:重跑一次,确认变更记录数量和轨迹里肉眼看到的变更次数一致。
重跑三次观察变更点是否稳定
单次变更不足以归因。用固定的输入、固定的工具集、固定的提示词,重跑三次,只用显式日志统计变更位置的出现次数,不推断模型内部原因。
| 变更位置 | 出现次数 | 触发工具 |
|---|---|---|
| 第 4 步 | 2 | query_stock |
| 第 5 步 | 1 | query_stock |
| 无变更 | 0 | - |
判读方式:变更集中在同一触发工具附近且多次出现,优先检查该工具返回是否被完整回填,再看这一步的子任务是否包含多个可判定条件;变更位置分散、无固定触发工具,再回头看拆解粒度,把包含多个判断的步骤拆开。三次只是抓现象的起点,若迹象明显,可以把次数加到五次或十次,但结论口径仍然是“出现次数”,不是“修复效果”。每次只改一个变量,改完立即重跑同一组输入,这样下一次定位才有可比性。