Error Recovery - 从错误中恢复
让 agent loop 在面对 LLM 三类常见故障(输出截断 / context 太长 / 后端过载)时自我恢复,而不是把异常一路抛回出去。
对于一个 Java 程序员处理异常那就太熟悉不过了。给 LLM 的调用方法加上 try catch。
目前最常见的错误,输出太长,输入太长,调用太快,模型压力太大。
输出太长:
- 第一次 :把
max_tokens从默认 8000 → 升级到 64000(escalated),丢掉截断的输出,重新跑这一轮。模型重生成,通常这次能说完 - 第二次还截断 :已经 64k 还说不完 → 真的需要"分两次说"。append 截断输出 + 注入
Continue.user message,让模型续写 - 超过
maxRecoveryRetries=3次 continuation:放弃,Fatal
输入太长:
- 可以直接调用
reactive_compact压缩模式。使用比较激进的压缩模式,只保留最近的若干条消息。
调用太快、模型压力过大:
- 调用 LLM 的时候才用指数退避的方式重试。若干次后依然报错,则抛出异常。
Task System - 更强的规划能力
让 LLM 自己用文件级任务图把"大目标"拆成"有 DAG 依赖、可被 claim/complete 跟踪、跨 agent loop 持久化"的小任务,而不是把所有计划塞 todo_write 在内存里。
已经有 todo_write 了 ------ 内存里一个三态 todo 列表,LLM 用它做计划。但 todo_write 有 4 个致命缺陷,只要任务复杂一点就暴露:
| todo_write | task system |
|---|---|
| 只活在当前 session,重启 → 全没 | 文件级持久化 (.tasks/{id}.json) |
| 只有"线性列表",没法表达"B 必须等 A 完成才能做" | blockedBy DAG 依赖 , canStart 计算 |
| 只有 owner 隐式("当前 agent"),没 claim 概念 | claim_task 显式 owner + 状态机 |
| 整体替换语义(每次都重写整个列表) | 单条 update(完成 A 不动 B/C) |
更重要的是 task system 让子 agent 也能参与------ 父 agent 派子 agent 干活时,子可以读到 task 列表知道全局计划,而不是"接到 description 就盲跑"。
核心 API:
实际上 harness 只暴露了 4 个原语 ,claim/complete/block 都是 TaskUpdate 的不同用法 ------ 靠状态字段区分,而不是独立接口。
| API | 作用 | 关键参数 | 返回 |
|---|---|---|---|
TaskCreate |
建节点 | subject, description, activeForm |
新任务 id(初始 status=pending, owner="") |
TaskList |
看队列 | 无 | 所有任务的摘要(id/subject/status/owner/blockedBy) |
TaskGet |
读详情 | taskId |
单个任务的完整字段(含 description、blocks/blockedBy 双向边) |
TaskUpdate |
改一切 | taskId + 变更字段 |
更新后的任务 |
为什么没有独立的 claim / complete / block ? 因为它们都能用 TaskUpdate 的字段组合表达,合并成一个接口反而简化了状态机:
| 语义动作 | 对应的 TaskUpdate 调用 |
|---|---|
| claim(认领) | {taskId, owner: "self", status: "in_progress"} |
| complete(完成) | {taskId, status: "completed"} |
| 建依赖(我等 X) | {taskId, addBlockedBy: [X]} |
| 建依赖(我挡 Y) | {taskId, addBlocks: [Y]} |
| 让出(放弃 owner) | {taskId, owner: ""} |
| 删除 | {taskId, status: "deleted"} |
设计要点:
- TaskCreate 只建节点,不连边 ------ 依赖必须先拿到 ID 再用 TaskUpdate 补
- TaskList 是决策入口 ------ agent 每轮 tick 先 List 找可 claim 的(owner="" && blockedBy=\[\])
- TaskGet 是执行前的最后一步 ------ claim 后进入 in_progress,用 TaskGet 拿完整 description 再干活(因为 List 只返回摘要,为省 token)
- TaskUpdate 幂等 ------ 重复 mark completed 不会出错,方便 error recovery 场景重放
核心工作流程:
1、需要将一个复杂任务拆解。并确认 DAG (有向无环图)依赖。使用 DAG 意味着任务可并行, 可拓扑排序,无死锁。
为什么 DAG 而不是树/线性队列?真实开发依赖不是树形的:任务 D 可能同时依赖 B 和 C(菱形),线性队列表达不了并行,树形结构表达不了合并。DAG 是最小的能表达"部分序"的结构。
2、任务拆解以后,将任务持久化,方便 agent 认领提交。
3、Agent 会通过 tools,调用TaskList()获取 pending 和 无 blockedBy 的任务。
4、LLM 挑选一个 task 执行。
5、调用 TaskUpdate 更新 task 的状态。
scss
[Agent tick N]
TaskList() → 拿到 pending / owner="" / blockedBy=[] 的任务
↓
LLM 挑一个(通常最小 ID)
↓
TaskUpdate({taskId, owner: "self", status: "in_progress"})
↓
(可选)同一 tick 内立即开始第一步执行 ------ 发起 Read/Bash 等工具调用
↓
[后续多个 tick] 反复执行工具调用推进任务
↓
[某个 tick] TaskUpdate({taskId, status: "completed"})
↓
回到 TaskList 找下一个可 claim 的任务
注意:claim 和"开始执行"通常发生在同一个 tick 里,harness 不会在 TaskUpdate 之后强制切换 ------ LLM 一轮可以发多个 tool call。
Background Task - 后台执行耗时任务
**让 LLM 调慢工具(./mvnw test、docker build、pip install)时,Agent 立即返回 placeholder 让 LLM 继续工作,后台 daemon 线程跑完后通过 <task_notification> 文本块注入下一轮 --- 取消同步等待
为什么需要 Background Task
如果 agent loop 是完全同步的:
arduino
LLM tool_use(bash, "./mvnw test")
↓
agent loop 进程同步等子进程(直到默认 2 分钟超时)
↓
返回 ToolResult,继续 loop
LLM 这段时间里完全失去主动权 ------ 不能并行去做别的小任务,不能主动决定"要不要 abort 重新调",甚至连"我此刻在等什么"都看不到。当单线程 RPC 客户端用。
我们需要:
arduino
LLM tool_use(bash, "./mvnw test", run_in_background=true)
↓
agent 派 daemon thread → 立即返回 "[Background task bg_0001 started]"
↓
LLM 看到 placeholder,可以:
- 想"我先去看下其他文件"
- 调别的工具(读文件、跑快命令)
- 安排接下来的步骤
↓
后台 thread 完成,harness 在下一轮 tool_results 时
追加 <task_notification id="bg_0001">...</task_notification>
↓
LLM 看到通知,自然消费结果("噢 test 跑完了,结果是 X")
LLM 重新拿回主导权:它决定什么时候等、等多久、要不要先做别的事。
相关 API
| API | 作用 |
|---|---|
Bash(run_in_background=true) |
派后台任务,立即返回 task_id |
TaskOutput(task_id, block=true) |
阻塞等结果(等价于同步) |
TaskOutput(task_id, block=false) |
立即返回当前状态(异步 peek) |
TaskStop(task_id) |
主动取消跑飞的后台任务 |
<task_notification>不是特殊的 IPC 中断机制,而是 harness 在下一次 API 请求时拼在 user 消息里的一段文本。所以 LLM 是"在下一次思考中读到"通知,不是"被中断"------ 这就是它保持主导权的技术根源。
github 开源地址 github.com/diaozxin007...