上一篇我主要做的是 Agent harness 的 Sandbox 和 Tool Permission。
当时的核心问题是:
模型能调用工具之后,哪些动作应该真的发生?
所以我加了 shell policy、项目目录边界、风险等级、approval 和安全 eval。今天没有继续堆安全规则,而是换了一个方向:
Agent 已经能安全地做事之后,它怎么在长任务里不迷路?如果中途失败或中断,它怎么知道之前做到哪一步?
这就是 Context Manager 和 Recovery Checkpoint 要解决的问题。
为什么 Context 不等于 Chat History?
最直接的做法当然是把所有历史消息都塞回模型。用户说了什么,模型想了什么,调用了什么工具,工具返回了什么,全部保留。这在短 demo 里能工作,但长任务里很快会出问题。token 成本爆炸。工具输出、文件内容、测试日志、目录列表都会迅速撑大上下文;上下文污染。旧的失败尝试、过期假设、重复工具结果和无关输出会混在一起,模型反而更难判断下一步;恢复困难。进程中断之后,如果系统只保存一坨聊天历史,恢复时还要重新从里面推断进度
所以今天的核心判断是:
Chat history 是原始过程,Context Pack 是当轮推理材料,TaskState 是当前任务状态。
这三者不能混成一个东西。
今天加了什么?
这次主要新增了两个模块:
agent/state.py
agent/context_manager.py
agent/state.py 里定义了 TaskState,类似于:
已阅读入口与核心循环:agent.py、agent/core.py
已阅读工具与沙箱模块:agent/tools.py、agent/permissions.py、agent/sandbox.py
已阅读上下文模块:agent/state.py、agent/context_manager.py、context_compressor.py
也就是说:
trace 记录事实,state 记录语义进度。
Context Pack 长什么样?
agent/context_manager.py 里实现了:
def build_context_pack(task_state, recent_trace, tool_summaries, max_chars=12000):
...
我的实现方式是一个 Markdown 文本,大概包含:
User Goal
Current Plan
Completed Steps
Open Questions
Important Facts
Files Touched
Commands Run
Error History
Resolved Errors
Recent Tool Calls
Recent Error
Next Action
恢复或下一轮模型调用时,模型不会看到完整历史,而是看到:
系统提示
用户原始目标
新的 Context Pack
最近 1-2 轮压缩后的 assistant / tool 消息
这让我对上下文管理的理解更具体了:
Context Manager 不是简单地“删掉旧消息”,而是把历史过程转成当前任务材料。
工具结果为什么要压缩?
今天还做了工具结果摘要。完整工具结果仍然保存进 trace,但进入模型上下文之前会压缩。规则大概是:
stdout / stderr 超过 4000 字符:保留 head + tail + summary
read_file 内容超过 8000 字符:保留路径、行号范围和片段
搜索结果超过 5 条:保留前 5 条和省略计数
这一步的边界很清楚:
trace 保存完整结果
context 只放压缩结果
这是我今天觉得很重要的设计分界。如果为了省 token,把 trace 里的完整工具结果也删掉,后面就没法复盘。如果为了复盘,把完整工具结果都塞回 context,长任务又会被噪声淹没。所以 trace 和 context 必须分离。
checkpoint / resume 是怎么做的?
现在每个任务会保存到:
runs/{task_id}/trace.jsonl
runs/{task_id}/state.json
runs/{task_id}/context_pack.md
恢复时执行:
python3 agent.py resume <task_id>
恢复流程大概是:
读取 state.json
读取 trace.jsonl
写入 resume_started 事件
从 trace 重建最近 1-2 轮压缩后的消息
重新构造 Context Pack
从旧 trace 最大 step 后继续执行
每次工具结果或最终回答后重新保存 checkpoint
这里有个关键点:
resume 不是从头重跑,也不是把完整 trace 全部塞给模型。
它是从 state.json 和 trace.jsonl 里恢复任务现场,再给模型一个短的、结构化的上下文包。
我用下面的命令测过:
python3 agent.py "读取 readme.md,总结一句。" --task-id demo_resume --max-steps 1
python3 agent.py resume demo_resume --max-steps 10
第一条命令故意让任务在 1 步后停下。第二条命令从 checkpoint 恢复,最终继续完成。trace 里能看到:
resume_started
task_state_updated
context_pack_built
checkpoint_saved
tool_result_compressed
这说明恢复路径确实跑到了。
为什么要加 error_history 和 resolved_errors?
一开始 TaskState 只有 last_error。如果任务最终完成,last_error = None 是合理的。但这会带来一个问题:中间发生过的错误会消失。比如一个任务可能经历过一些不熟悉环境导致的小错误,而这些错误很多时候llm可以自己纠正,例如使用了不允许的命令,shell编写错误等,最后llm自己恢复成功了。此时 last_error = None,但这不代表任务过程中没有错误。
所以我加了:
error_history
resolved_errors
这样最终状态可以表达:
{
"last_error": null,
"error_history": [
"max_steps: 达到最大循环次数 1,任务未完成。"
],
"resolved_errors": [
"max_steps: 达到最大循环次数 1,任务未完成。"
]
}
这对 eval 很重要。恢复能力不是“没有错误”,而是错误出现后,系统保留了状态,并能继续推进。而且这样也更有助于后续对harness修改后的效果评估,可能升级后中途出现的小错误明显变少了,但是如果只有last_error则不能直观的看到这个优化的效果。
Eval 结果和暴露的问题
今天跑过完整 13 条 eval。结果是:
total: 13
passed: 10
failed: 3
pass_rate: 76.9%
security: 4/4
Context Manager 相关统计里能看到:
state_updates: 51
context_packs_built: 106
tool_results_compressed: 10
resume_events: 1
checkpoint_saves: 51
tasks_with_compression: 13
tasks_with_resume: 1
Eval结果基本符合预期,三个预期失败的,不过有个明显问题:trace 现在非常密,一次较长任务里可能出现很多模型调用,function calling,上下文压缩等等。如果在这些节点都添加checkpoint的话,很明显会拖累整体的运行效率,后续考虑把 checkpoint 策略改成必须checkpoint和可选checkpoint,减少不必要的性能消耗。
核心收获
今天最大的收获不是“加了一个 resume 命令”,而是实现了最基本的Context Manager,给我的harness添加了类似于Codex中的session恢复能力;同时实现了state的。如果说前几篇是在让 Agent “能做事”和“安全做事”,那今天这一步是在让它开始具备长期任务系统的样子。