Agent 学会“偷懒”了?——TaskStateTracker 让每项子任务都有“勾选框”
Agent 学会“偷懒”了?——TaskStateTracker 让每项子任务都有“勾选框”
系列博客第八篇 · 2026-07-08
上次治好了“作弊”,今天治“偷懒”
一、上次治好了作弊,但它学会了另一种偷懒
如果你读过我上一篇博客,应该还记得那个“模型在正文里假装调用工具”的故事。我修复了那个问题——它不能再靠“嘴遁”蒙混过关了。
但是——
我以为问题解决了,结果它换了一种方式“偷懒”。
我让它生成一个 Excel 文件:
“帮我写一个 Excel,把工作区的文件都列出来,命名为
1.xlsx。”
它说:“好的,我先列出所有文件……”,然后真的调用了 list_files。
然后……它直接说“已完成”,结束了。
我一看,Excel 文件呢?没生成。1.xlsx 呢?不存在。
它只是做了“列出文件”这一步,就理直气壮地说“完成了”。
这感觉就像是:你让实习生“去把资料整理成一份报告交上来”,他跑去把资料翻了一遍,然后回来说“报告做好了”。你问报告在哪儿,他说“我已经看过资料了呀”。
它把“做了一步”当成了“做完了”。
今天这个功能,就是专门治这个的。
二、先解释一个概念:什么是“子任务追踪”?
概念解释:子任务追踪,就是把一个大任务拆成几个小步骤,然后一项一项地打勾,全部勾完了才算任务完成。
就像你列了一个 Todo 清单:
- 去超市买菜
- 回家洗菜
- 切菜
- 炒菜
- 装盘上桌
你不可能只做完“去超市买菜”,就说“饭做好了”。
同理,Agent 接到“生成 Excel”的任务,它的子任务可能是:
- 获取工作区文件列表
- 生成 Excel 文件
- 保存为
1.xlsx
只做第 1 步就说完成,这显然不对。
三、之前是如何判断任务完成的?
之前只有两个判断标准:
- 模型声明它已完成(
turn_done.reason = final_answer) - 轮次耗尽(
maxTurns用尽)
这两个标准都过于主观,容易被“糊弄”。
模型可能在仅完成 1/3 子任务时就声明“已完成”;也可以在任务明明没做完的时候,因为轮次到了而被迫结束。
所以,需要一个更客观的判断标准:子任务有没有全部完成。
四、解决方案:TaskStateTracker
4.1 核心思想
TaskStateTracker 把 IntentPlanner 拆出来的子任务(subtasks)变成一个可勾选的状态机:
IntentPlanner 生成的 subtasks
→ TaskStateTracker 初始化(全部标记为 pending)
→ 每执行一个工具,检查是否匹配某个子任务
→ 匹配上了 → 标记为 done
→ 全部 done → 才允许 final_answer
→ 没全部 done → 拦截结束请求,继续 loop
4.2 那它怎么知道“这个工具对应哪个子任务”?
这需要一个匹配规则。v1 版本用的是启发式匹配(说白了就是关键词 + 工具名组合):
| 子任务里包含这些词 | 完成条件 |
|---|---|
| 列出 / 目录 / list | list_files 执行成功 |
| Excel / xlsx / 导出 | create_excel 或 write_csv 执行成功 |
命名为 1.xlsx |
输出路径必须包含 1.xlsx(不仅仅是“生成一个 Excel 文件”) |
| 每个文件 / 逐个读取 | create_excel(from_workspace_files=true) 可一键完成 |
| 验证 / 确认 | read_file / list_files / run_script 执行成功 |
举个例子:
如果 IntentPlanner 拆出三个子任务:
- “列出工作区文件” → 匹配
list_files→ 勾掉 - “生成 Excel 文件” → 匹配
create_excel→ 勾掉 - “保存为 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:某个子任务被标记为 donetask_completion_blocked:模型试图结束但子任务未完成,被拦截
如果你看 Trace Report,会看到类似这样的信息:
Task State:
- [x] 列出工作区文件
- [ ] 生成 Excel 文件
- [ ] 保存为 1.xlsx
Status: 1/3 完成 → 任务完成受阻
对于调试和复盘,这非常直观。
六、如何与现有功能配合?
| 功能 | 作用 | 和 TaskStateTracker 的关系 |
|---|---|---|
| IntentPlanner | 拆子任务 | 负责“拆”,TaskStateTracker 负责“检查” |
| FakeToolCallGuard | 检测伪工具 | 确保“真正执行”,TaskStateTracker 确保“执行到位” |
| CompletionLoop | 未调用工具则不结束 | TaskStateTracker 是“调用了但未完成也不结束” |
| LoopController | 动态轮次预算 | 任务复杂 → 自动续期,为 TaskStateTracker 留足时间 |
这些功能组合在一起,形成了一个完整的“任务完成保障体系”:
- IntentPlanner:先想清楚要做哪几件事
- FakeToolCallGuard:不准假装做事
- TaskStateTracker:每件事必须做完才能交差
- CompletionLoop:没做就继续做
- LoopController:做不完就多给点时间
环环相扣,形成一个完整的“任务完成保障体系”。
七、关于代价和取舍
- API 成本:零增加(纯逻辑判断,不调用模型)
- 轮次消耗:可能会多几轮(因为要等所有子任务完成)
- 假阳性风险:匹配规则可能不够精准(比如子任务是“运行测试脚本”,但 Agent 先调用了
list_files也会被匹配?不会,因为关键词不匹配)
目前匹配规则是启发式的,可能会有漏匹配或误匹配,但 v1 版本我选择先保证**“宁拦不错”**——宁可多拦几次,也不要让未完成的任务溜过去。
后续可以用 LLM 来做更智能的匹配(比如判断工具调用结果是否真的满足子任务意图),但当前版本我选择先跑通。
八、一个顺带的思考:工具调用和任务完成之间的“缺口”
这次我最大的体会是:
工具调用成功 ≠ 任务完成。
list_files 成功了,但 Excel 文件没生成。run_script 成功了,但脚本输出是错的。write_file 成功了,但文件内容不对。
以前我觉得“调用了工具、返回 ok=true”就是任务完成了。但这是一种幻觉。
工具只是手段,任务是目标。
手段成功了,不等于目标达到了。
TaskStateTracker 就是用来填这个“缺口”的——它用子任务的完成度来验证目标是否真的达到了。
更多推荐



所有评论(0)