ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【 长任务 Agent 为什么总会跑偏?我用“三平面分治”重构了一个 200 张发票审核 Agent】

【 长任务 Agent 为什么总会跑偏?我用“三平面分治”重构了一个 200 张发票审核 Agent】 如果你做过长任务 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_packetgoal: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 真正需要的“进度追踪”。
返回列表