type
Post
status
Published
date
Jan 11, 2026
slug
agent02
summary
tags
Agent
category
icon
password
很多人第一次看 coding agent,最容易形成的印象是:模型想到什么,就去调什么工具。界面上也确实很像这么回事,读文件、改代码、跑测试,一个按钮接一个按钮,事情就往前走了。可我自己做一遍才发现,一次真实 repo 任务能不能稳稳做完,关键在于它有没有被收进一条受约束的执行链:begin_task -> approve_plan -> agent/step -> tool result -> 下一步决策 -> run_test -> finish。这篇文章真正要讲的正是任务怎么被一步步推进。
这篇只讲,TaskSession 怎么记状态,AgentRunner 怎么推进下一步,context bundle 怎么把刚好的上下文喂给模型。
先用一个真实任务把链路串起来
我这次用的是仓库里的 demo bug:slugify_title() 把单词用下划线拼起来了,测试想要的是连字符。这个任务够小,刚好能把运行时怎么推进任务讲清楚。
demo flow 里这一小段轨迹:
begin_task
approve_plan
read_file demo_app/string_tools.py
run_test -> exit_code 1
file_patch demo_app/string_tools.py -> approval_required
resolve_approval -> executed
run_test -> exit_code 0
finish can pass now
这条小轨迹把几个容易混在一起的判断拆开了:先读、再测、再改、再测;edit 不会直接落地,而是先停在 approval;第二次测试过了,任务才有资格 finish。更重要的是,每走一步,最新工具结果、最近文件上下文和时间线都会写回 session,模型再根据这份更新后的状态决定下一步。我想说的是,任务不是一口气做完的,它是被运行时沿着这条小轨迹一小步一小步往前推的。
plan mode 和 todo 先把任务收成一条线
为什么?因为如果任务入口就是散的,后面的 loop 再聪明也只是在放大混乱。
我一开始也想过先把工具铺多一点,先把 read、patch、test 这些动作都接齐,看起来会更像一个能工作的 agent。再往下做我才发现,如果任务一进来就能直接动 repo,后面很多修正都会变得很被动。所以我先保的第一个控制点就是 plan mode。begin_task() 以后,repo 还是只读的;只有 plan 被确认以后,变更型工具才有资格进入后续流程。我先保 plan mode,是因为我想先守住入口。
todo lifecycle 也是同一类取舍。我没有先去做更花的任务拆分,而是先把“当前到底在推进哪一步”这件事钉死。session 里同一时间只允许一个 in_progress,这样 agent/step 或 agent/loop 每成功执行一步,当前 todo 完成,下一个 pending 才会接上来。少了这个约束,loop 看起来还在跑,但系统很快就会说不清自己现在到底在修 bug、在补读文件,还是已经该进测试了。todo 在这里管的是顺序。
AgentRunner 在管什么
很多人听到 agent loop,会下意识把它理解成“模型自己跑起来了”。
我现在更倾向于把 AgentRunner 讲成一个有限步决策器。agent/plan 负责先把 plan 和初始 todos 草拟出来,agent/step 只推进当前一步,agent/loop 则是在有限步里连续推进,直到遇到明确的停机原因。这样设计最大的好处,是任务什么时候该继续、什么时候该停下,不再只靠模型的一句话。loop 在这里是受控推进器,不是自动驾驶。
finish 这个动作最能说明问题。按直觉看,模型说“我做完了”,系统似乎就该结束了。但仓库里我专门留了一条回归:finish_without_successful_test_is_repaired_into_run_test。它覆盖的是一个很真实的场景,模型先说任务已经完成,runtime 不直接相信这句话,而是把这次 finish 修正成一次 run_test,只有当前 repo 状态真的跑过成功测试,任务才算能收尾。finish 不是一句话,它得先过验证。
这还不只是“补跑一次测试”这么简单。session 会把最近一次成功测试和当前 repo state 绑在一起;只要 repo 再发生一次写入,系统就把这次成功测试视为过期。仓库里还有一条我很看重的回归:测试刚通过以后,再写一次 notes.txt,latest_successful_test 就会立刻失效。也正因为这样,我现在才敢把 finish -> run_test 讲得这么硬,因为系统记的不是“刚才测试过了”,而是“当前这份 repo 状态测试过了”。验证要绑当前状态,不然 finish 这件事站不住。
max_steps 也是同一类边界。它是开放式任务一定得有止损点。run_loop_stops_at_max_steps_when_work_continues 这条测试里,模型连续读了两个文件,任务还没真正完成,loop 就会以 max_steps_reached 停下来,同时把当前 todo 状态保留在正确的位置上。这样一来,系统至少知道自己为什么停,也知道下次该从哪接着走。有限步不是缩手缩脚,它是在给开放任务一个工程边界。
上下文够用就行,不要越多越好
为什么?因为任务推进到后面,最容易失控的是模型开始拿着一堆不合时宜的信息做判断。
我一开始也很容易往“多给一点上下文总没坏处”这个方向想。往下做我才发现,当前这一步最该看到的上下文本身就比“更多上下文”更重要。所以这一轮我把 agent 每步拿到的 prompt 收成了一个最小 context bundle,里面只放当前任务输入、plan、todos、最新工具结果、最新 diff、最近时间线、最近文件上下文和最近测试失败。它不求全面,只求下一步判断时够用。执行链里最有价值的上下文,永远是和当前动作最贴近的那一小撮。
这里有一条我很满意的回归:builder_prefers_source_file_as_primary_target_after_readme_read。它验证的是,哪怕模型刚读过 README,系统还是会尽量把目标源码文件放成 primary target。这个点对我很重要,因为如果不这么做,任务很容易出现一种偏差:明明已经进入修改阶段了,模型还在围着 README 打转,或者拿着旧说明去判断下一步该改哪里。这个判断让我后来越来越确认,按需看文件本身就是执行层能力,而不是模型自己的小聪明。context bundle 做的是帮下一步动作找准焦点。
如果把它压缩成一个更像真实 prompt 的样子,大概就是这样:
{
"plan": "1. Read the failing module and tests. 2. Apply the smallest safe fix. 3. Run the local unittest suite.",
"latest_tool_result": {"tool_name": "run_test", "exit_code": 1},
"read_focus": {
"primary_target_path": "demo_app/string_tools.py",
"preferred_next_action": "patch_or_test"
},
"recent_file_contexts": ["README.md", "demo_app/string_tools.py"],
"recent_test_failures": ["StringToolsTest.test_slugify_title_normalizes_whitespace"]
}
我很喜欢拿这种小 bundle 自己核对,因为它一眼就能看出下一步该关注什么,也能排掉一个很常见的误判:上下文越多越稳。对执行链来说,更重要的是下一步该看的信息有没有准时出现。所以我想说的是,给模型的上下文要够用,也要够准。
两次修正,最能说明这条执行链是活的
第一类修正,就是模型还没把目标文件看清楚,就想直接 patch。放回这个 demo 任务里,更容易理解这件事:如果模型还没读 demo_app/string_tools.py,就直接想去改它,runtime 不会让它往下走,而是会先把这一步判成无效请求,再把下一步拉回到 read_file demo_app/string_tools.py。从执行链视角看,这是系统在告诉模型:“你这一步走快了,先把文件读清楚。”我想说的是,一条好的执行链,能把任务从错误动作拉回到正确顺序。
第二类修正,就是模型想太早 finish。前面提到的 finish_without_successful_test_is_repaired_into_run_test 其实也是同样的模式。系统不接受一句没有验证支撑的“我做完了”,而是把它改写成下一步可执行、可核对的动作。这样处理以后,loop 的每次修正都不是在和模型对着干,而是在把任务重新收进一条能验证的路径里。repair / retry 最重要的价值,是把错误决策改写成下一步能落地的动作。
但我也没有把所有错误都做成自动修正。最直接的一类就是 approval_required:如果 edit 真的走到了审批边界,agent/loop 会直接停下来等人处理,不会自己再补一个“替代动作”绕过去。工具真的 failed 也是同样的处理,系统会给出明确 stop reason,而不是装作还能继续往前跑。我现在的原则很简单:如果错误还能被改写成下一步明确、低风险、可验证的动作,我就 repair;如果已经触到审批、执行失败或需要人判断,就直接 stop。 修正路径要有,但停机边界也得明确。
其实这也是我现在看 agent loop 的方式。它做的事情,就是持续把任务往一条更稳定的路径上拽。目录路径读错了、目标文件猜偏了、没先读文件就想改、没跑测试就想 finish,这些都可以被 runtime 识别成“这一步还不能这么走”。执行链的韧性,来自它愿意承认模型会走偏,并提前把修正路径准备好。
总览
对这篇文章来说,最关键的就是 4 条:没测过不能 finish、任务不能无限跑、没读文件不能直接 patch、读过 README 以后还得尽量把注意力收回源码文件。
这篇讲的能力 | 仓库里现在的证据 | 文章里的写法口径 |
任务怎么开始并推进 | begin_task、approve_plan、todo 单线推进、agent/step | 已实现 |
为什么任务不会乱 finish 或一直跑 | finish -> run_test、max_steps_reached、stop reason | 已实现 |
为什么任务能被拉回主线 | 最小 context bundle、primary target、read_file 修正 | 已实现 |
这篇把任务怎么走讲清楚,把执行链讲透。
我现在怎么理解这条执行链
答:任务先进入 TaskSession,plan 和 todo 先把顺序收住,AgentRunner 再在有限步里推动下一步动作,context bundle 只把当前最该看的信息给模型,任务走偏了就修正回正确路径,最后再用本地测试确认当前 repo 状态真的成立。这篇想说的,就是一条能讲清楚、也能被验证的执行路径。
一个 coding agent 能不能稳定干活,正取决于它有没有被收进一条有限步、可修正、可验证、也知道何时停下来的执行链。
分享
