如果你做过长任务 Agent,大概率见过这种情况:
任务跑到一半,Agent 突然“钻牛角尖”——揪着一个无关紧要的细节死磕几十轮,预算烧掉一半,主线任务却纹丝不动。
还有一种更隐蔽:
Agent 最后告诉你“任务完成”,但回头一查,还有三分之一根本没处理,只是 Agent 自己没有意识到。
很多时候,问题不是模型不够聪明,而是任务状态没有被正确管理。
这篇文章用一个具体场景——批量审核 200 张供应商发票——看看长任务 Agent 为什么会跑偏,以及我怎么用“三平面分治”解决它。
本文的理论框架参考了黄佳《AI Agent 设计模式》专栏中的“进度追踪”一讲,在此基础上结合具体业务场景做了案例化拆解。理论部分建议阅读原文。
先说结论:Todo List 为什么不够?
很多人第一反应,是给 Agent 加一个todo.md:
- [x] 审核发票 1-50 - [x] 审核发票 51-100 - [ ] 审核发票 101-150 - [ ] 审核发票 151-200短任务没什么问题,但任务一长,很快会暴露三个问题:
目标漂移:Agent 越来越沉浸在局部问题里,忘了最初到底要完成什么。
事实污染:ID、金额、账号等关键数据,被 Agent 在自然语言上下文中反复复制、转述,容易串错。
完成幻觉:Todo 勾完了,不代表任务真的完成了。
所以我更倾向于把 Agent 的状态拆成三个平面:
| 平面 | 负责什么 | 一句话理解 |
|---|---|---|
| 目标 | 要做什么、不能做什么、什么算完成 | 我要干什么? |
| 事实 | ID、金额、账号、工具返回值等真实数据 | 真实情况是什么? |
| 进度 | 做到哪了、哪些完成、哪些阻塞 | 我干到哪了? |
这里的“三平面”不是说一定要拆成三个数据库,而是把三种不同性质的信息分开管理。
核心原则其实只有一句话:
LLM 负责判断,程序负责记真值,编排器负责推进任务。
下面用 200 张发票跑一遍。
一、先把目标冻成契约,而不是直接开干
用户说:
“帮我把上个月的供应商发票核对一遍,没问题的生成付款批次。”
如果直接把这句话扔给 Agent,它很容易自行扩大任务边界。
所以第一步不是调用工具,而是先把目标明确下来:
success_criteria:-只处理 2026 年 7 月开票、状态为“待审核”的发票-金额与采购订单(PO)匹配才能放行-生成付款批次,但不实际发起转账non_goals:-不处理历史遗留的争议发票-不修改供应商主数据-不直接调用银行接口打款这里最重要的其实是non_goals。
比如某张发票发现供应商银行账号疑似发生变化,Agent 很可能会想:
“既然发现问题,那我顺便去供应商系统确认一下,再更新账号。”
看起来很合理。
但这已经超出了当前任务。
所以:
目标不仅要定义“做什么”,还要定义“不做什么”。
二、第 60 张发票:Agent 开始钻牛角尖
处理到第 60 张时,遇到一个问题:
发票上的 PO 号是:
PO-2026-0088系统里的 PO 号却是:
PO2026088Agent 开始认真研究:
查历史记录
猜测编码规则
分析是不是系统升级导致的
尝试重建匹配算法
然后一轮又一轮地调用工具。
没有进度追踪
Agent 可能花 40 轮把这个问题解决了。
但此时:
已完成:60 / 200 剩余:140预算却已经烧掉了一大截。
有进度追踪
这时候可以增加一个简单的漂移检测机制。
例如每 10 步检查一次:
最近 8 个动作: 都围绕同一张发票 里程碑: 60 / 200 进度: 没有推进于是系统判断:
WATCH然后触发一次复诵:
当前里程碑是完成 200 张发票的匹配核验,不是解决单张发票的编码问题。PO 号格式异常的发票记入待人审清单,继续下一张。
Agent 于是把这张发票标记为:
needs_review记录下来,继续处理下一张。
这里的关键不是“阻止 Agent 思考”。
而是:
允许它解决问题,但不允许一个局部问题吞掉整个任务。
三、第 95 张:Ledger 不是 Todo,而是决策账本
第 95 张发票又出现了问题:
发票金额比 PO 金额高了 3%。
Agent 判断:
合同允许一定范围的金额浮动,因此可以放行。
如果只记录:
INV-20260795 ✓以后几乎没有追溯价值。
所以进度账本记录的应该不是简单的“做没做”,而是:
做了什么判断,为什么这么判断,依据是什么。
例如:
event:发票 INV-20260795 金额超出 PO 3%decision:判定为合同允许浮动范围内,放行reason:采购合同条款 §4.2 允许 ±5% 浮动evidence_refs:-tool:fetch_po#0795-contract/vendor-acme-2026.md#section-4.2state_delta:write:-STATE.batch_approved_invoices这样一周之后,如果财务发现这批发票整体超支,可以直接追溯:
哪些发票被放行? ↓ 为什么放行? ↓ 依据了哪条合同? ↓ 这个判断是不是出了问题?这就是 Ledger 和普通 Todo 最大的区别:
Todo 记录“做没做”,Ledger 记录“为什么这么做”。
四、金额、ID、账号:不要让 LLM 当数据库
还有一个非常容易被忽略的问题。
Agent 在上下文里可能会说:
“发票 INV-20260795 已核验,关联 PO-0795,金额 128,000 元。”
但这些自然语言并不应该成为下一步工具调用的真实参数来源。
真正调用“生成付款批次”时:
供应商 ID PO ID 发票 ID 金额 银行账号应该由程序从SessionState等结构化状态中读取。
也就是说:
LLM ↓ 做判断 / 提出 Tool Call ↓ 程序 ↓ 从结构化状态读取真实参数 ↓ 调用 API而不是:
LLM ↓ 生成一段自然语言 ↓ 再从自然语言里解析金额、ID、账号 ↓ 调用 API为什么要这么较真?
因为长任务里,Agent 很可能会把两个名字相近的供应商搞混。
如果关键参数一直存在于自然语言上下文中,这种错误就可能一路传播。
而如果真实参数始终由程序维护:
LLM 可以犯语言错误,但不能直接污染系统真值。
这就是“事实平面”的意义。
五、200 张全部跑完:Agent 说完成,不代表真的完成
终于,Agent 汇报:
“200 张发票已经全部审核完成。”
如果系统直接相信它:
Agent:完成 ↓ 生成付款批次那么“完成幻觉”就产生了。
所以最后需要一个验证闸门。
例如:
已处理发票数 = 200? ✓ 金额总和是否超过预算阈值? ✓ 所有 needs_review 是否进入人工审核清单? ✗ 发现 3 张漏标于是系统拒绝进入下一阶段:
completed ↓ Verifier ↓ 失败 ↓ needs_rework只有验证通过,才允许生成付款批次。
所以:
“完成”不是 Agent 自己宣布出来的,而是系统验证出来的。
这可能是长任务 Agent 最重要的一道保险。
六、如果中途崩了,怎么接着跑?
假设处理到第 130 张时,容器突然崩了。
最差的做法,是让新 Agent 把之前 130 张的完整聊天记录重新读一遍,然后自己猜:
“我上次做到哪里了?”
更合理的方式,是保存一个轻量的resume_packet:
goal:success_criteria:...non_goals:...milestone:name:invoice_reviewprogress:130/200recent_ledger:-...-...-...-...-...needs_review:-INV-20260088-...state_keys:-STATE.batch_approved_invoices-STATE.pending_review新一轮 Agent 只需要恢复:
目标 ↓ 当前进度 130 / 200 ↓ 最近决策 ↓ 待人工审核 ↓ 读取需要的结构化状态 ↓ 继续第 131 张而不是重新阅读整个历史上下文。
这就是为什么长任务 Agent 最终需要的,不只是“记忆”,而是:
可恢复的执行状态。
七、把整个设计放在一起
现在回头看,整个系统其实没有那么复杂。
长任务 Agent │ ┌──────────────┼──────────────┐ ↓ ↓ ↓ 目标 事实 进度 要干什么? 真实是什么? 干到哪了? │ │ │ Goal Contract SessionState Ledger │ │ │ └──────────────┼──────────────┘ ↓ 编排器 ↓ Tool 执行 ↓ 验证闸门 ↓ 完成 / 继续 / 重做再把前面的几个机制放进去:
目标 └── Goal Contract └── 复诵:防止目标漂移 事实 └── SessionState / Provenance └── 关键参数不经过自然语言传递 进度 └── Ledger / Milestone └── 漂移检测:防止局部死磕 └── Verifier:防止完成幻觉 └── resume_packet:支持断点恢复这样看下来,“三平面”其实解决的是三个不同的问题:
| 问题 | 对应机制 |
|---|---|
| Agent 忘了自己为什么做 | 目标 |
| Agent 把数据搞串了 | 事实 |
| Agent 不知道做到哪了 | 进度 |
八、最后总结:长任务 Agent 缺的可能不是更强的模型
如果你的 Agent 经常出现:
在一个细节上死磕几十轮
参数在多轮对话中逐渐串错
最后说“完成了”,实际上漏了一堆
中途崩溃后只能重新开始
出问题之后很难追溯到底是哪一步判断出了错
那么问题未必是模型能力不够。
更可能是:
你把目标、事实、进度,全都塞进了 LLM 的上下文里。
而这三种信息,其实应该由不同的机制负责:
目标负责约束方向。
事实负责保证真实。
进度负责推动任务。
最终形成一个很简单的原则:
让模型负责“想”,让程序负责“记”,让编排器负责“推”。
这可能才是长任务 Agent 真正需要的“进度追踪”。