Agent 学会“偷懒”了?——TaskStateTracker 让每项子任务都有“勾选框”

系列博客第八篇 · 2026-07-08
上次治好了“作弊”,今天治“偷懒”


一、上次治好了作弊,但它学会了另一种偷懒

如果你读过我上一篇博客,应该还记得那个“模型在正文里假装调用工具”的故事。我修复了那个问题——它不能再靠“嘴遁”蒙混过关了。

但是——

我以为问题解决了,结果它换了一种方式“偷懒”。

我让它生成一个 Excel 文件:

“帮我写一个 Excel,把工作区的文件都列出来,命名为 1.xlsx。”

它说:“好的,我先列出所有文件……”,然后真的调用了 list_files

然后……它直接说“已完成”,结束了。

我一看,Excel 文件呢?没生成。1.xlsx 呢?不存在。

它只是做了“列出文件”这一步,就理直气壮地说“完成了”。

这感觉就像是:你让实习生“去把资料整理成一份报告交上来”,他跑去把资料翻了一遍,然后回来说“报告做好了”。你问报告在哪儿,他说“我已经看过资料了呀”。

它把“做了一步”当成了“做完了”。

今天这个功能,就是专门治这个的。


二、先解释一个概念:什么是“子任务追踪”?

概念解释:子任务追踪,就是把一个大任务拆成几个小步骤,然后一项一项地打勾,全部勾完了才算任务完成。

就像你列了一个 Todo 清单:

  • 去超市买菜
  • 回家洗菜
  • 切菜
  • 炒菜
  • 装盘上桌

你不可能只做完“去超市买菜”,就说“饭做好了”。

同理,Agent 接到“生成 Excel”的任务,它的子任务可能是:

  1. 获取工作区文件列表
  2. 生成 Excel 文件
  3. 保存为 1.xlsx

只做第 1 步就说完成,这显然不对。


三、之前是如何判断任务完成的?

之前只有两个判断标准:

  • 模型声明它已完成(turn_done.reason = final_answer
  • 轮次耗尽(maxTurns 用尽)

这两个标准都过于主观,容易被“糊弄”。

模型可能在仅完成 1/3 子任务时就声明“已完成”;也可以在任务明明没做完的时候,因为轮次到了而被迫结束。

所以,需要一个更客观的判断标准:子任务有没有全部完成。


四、解决方案:TaskStateTracker

4.1 核心思想

TaskStateTrackerIntentPlanner 拆出来的子任务(subtasks)变成一个可勾选的状态机

IntentPlanner 生成的 subtasks
  → TaskStateTracker 初始化(全部标记为 pending)
  → 每执行一个工具,检查是否匹配某个子任务
  → 匹配上了 → 标记为 done
  → 全部 done → 才允许 final_answer
  → 没全部 done → 拦截结束请求,继续 loop

4.2 那它怎么知道“这个工具对应哪个子任务”?

这需要一个匹配规则。v1 版本用的是启发式匹配(说白了就是关键词 + 工具名组合):

子任务里包含这些词 完成条件
列出 / 目录 / list list_files 执行成功
Excel / xlsx / 导出 create_excelwrite_csv 执行成功
命名为 1.xlsx 输出路径必须包含 1.xlsx(不仅仅是“生成一个 Excel 文件”)
每个文件 / 逐个读取 create_excel(from_workspace_files=true) 可一键完成
验证 / 确认 read_file / list_files / run_script 执行成功

举个例子:

如果 IntentPlanner 拆出三个子任务:

  1. “列出工作区文件” → 匹配 list_files → 勾掉
  2. “生成 Excel 文件” → 匹配 create_excel → 勾掉
  3. “保存为 1.xlsx” → 匹配 create_excel 的路径包含 1.xlsx → 勾掉

全部勾掉 → ✅ 允许结束。

如果只做了第 1 步 → ❌ 拦截,“任务完成受阻,继续执行”。

4.3 实际效果

还是那个“生成 Excel”的例子:

修复前

Agent: 调用了 list_files
Agent: “已完成!”(结束)
用户: ???Excel 呢?

修复后

Agent: 调用了 list_files
Agent: “已完成!”(尝试结束)
[TaskStateTracker] 拦截!子任务状态:1/3 完成。继续执行。
Agent: 调用了 create_excel
Agent: “Excel 已生成!”(尝试结束)
[TaskStateTracker] 再拦截!子任务状态:2/3 完成(还差命名验证)
Agent: 调用了 create_excel(path="exports/1.xlsx")
[TaskStateTracker] 3/3 完成 ✅ 允许结束
Agent: “Excel 已保存为 exports/1.xlsx”

虽然多花了几轮,但保证了任务真正完成


五、Trace 里能看到什么?

新增了三个 Trace 事件:

  • task_state_init:初始状态,列出所有子任务
  • task_state_updated:某个子任务被标记为 done
  • task_completion_blocked:模型试图结束但子任务未完成,被拦截

如果你看 Trace Report,会看到类似这样的信息:

Task State:
  - [x] 列出工作区文件
  - [ ] 生成 Excel 文件
  - [ ] 保存为 1.xlsx

Status: 1/3 完成 → 任务完成受阻

对于调试和复盘,这非常直观。


六、如何与现有功能配合?

功能 作用 和 TaskStateTracker 的关系
IntentPlanner 拆子任务 负责“拆”,TaskStateTracker 负责“检查”
FakeToolCallGuard 检测伪工具 确保“真正执行”,TaskStateTracker 确保“执行到位”
CompletionLoop 未调用工具则不结束 TaskStateTracker 是“调用了但未完成也不结束”
LoopController 动态轮次预算 任务复杂 → 自动续期,为 TaskStateTracker 留足时间

这些功能组合在一起,形成了一个完整的“任务完成保障体系”:

  1. IntentPlanner:先想清楚要做哪几件事
  2. FakeToolCallGuard:不准假装做事
  3. TaskStateTracker:每件事必须做完才能交差
  4. CompletionLoop:没做就继续做
  5. LoopController:做不完就多给点时间

环环相扣,形成一个完整的“任务完成保障体系”。


七、关于代价和取舍

  • API 成本:零增加(纯逻辑判断,不调用模型)
  • 轮次消耗:可能会多几轮(因为要等所有子任务完成)
  • 假阳性风险:匹配规则可能不够精准(比如子任务是“运行测试脚本”,但 Agent 先调用了 list_files 也会被匹配?不会,因为关键词不匹配)

目前匹配规则是启发式的,可能会有漏匹配或误匹配,但 v1 版本我选择先保证**“宁拦不错”**——宁可多拦几次,也不要让未完成的任务溜过去。

后续可以用 LLM 来做更智能的匹配(比如判断工具调用结果是否真的满足子任务意图),但当前版本我选择先跑通。


八、一个顺带的思考:工具调用和任务完成之间的“缺口”

这次我最大的体会是:

工具调用成功 ≠ 任务完成。

list_files 成功了,但 Excel 文件没生成。
run_script 成功了,但脚本输出是错的。
write_file 成功了,但文件内容不对。

以前我觉得“调用了工具、返回 ok=true”就是任务完成了。但这是一种幻觉

工具只是手段,任务是目标。
手段成功了,不等于目标达到了。

TaskStateTracker 就是用来填这个“缺口”的——它用子任务的完成度来验证目标是否真的达到了。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐