修好 test_parse_date,最朴素的程序怎么写?
本课一问:我们已经知道 bug 在 dateparse.py 里,测试也已经告诉我们哪里不对。要修好 test_parse_date,最朴素的程序怎么写?它需要「agent」吗?
这一课先不碰:让模型自己跑命令、读文件、连续走很多步(A1);怎么只改一处而不是整份重写(A2);它要执行危险命令怎么办(A3)。
从已有出发
LLM 课的最后一课(C12 第 8 课)把模型训练到会写出一段「调用工具」的文字。可在给它造一整套执行工具的 harness 之前,先退一步:这件事真的需要模型自己去调工具吗?也许一段普通的代码、调一次模型就够了。
全课的贯穿任务:让本课项目任务仓里失败的 test_parse_date 通过,测试文件不许改。dateparse.py 埋了两处 bug——DD/MM/YYYY 分支把日和月解包反了;月份名分支用 MONTHS.index() 拿到的是从 0 起的下标,少了 + 1。测试在第一条失败的断言处就停:现在它只报 datetime.date(2024, 5, 3) != datetime.date(2024, 3, 5),第二处要等第一处修好才看得见。
先自己设计:一段代码,调一次模型
最容易想到的程序:测试失败了,就把测试输出和源文件一起发给模型,请它回一份修好的完整文件;程序把文件写回去,再跑一次测试。每一步做什么、下一步是什么,都写在代码里;模型只被调用一次,只负责「给出新文件」。
本课项目的 miniagent/workflow.py 就是这么写的,发给模型的 system prompt 只有 4 行:
WORKFLOW_SYSTEM = """You fix bugs in a small Python repository. You cannot run commands or read other files:
everything you get is in the user message. For every file you change, write a line
`FILE: <path>` followed by a fenced code block containing the COMPLETE new content of that file.
Do not change test files."""
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
04-实践项目/miniagent/workflow.py(快照 2026-09-26 17:46)数据:本课项目 T2 录制的 workflow 轨(src/data/project/agent-t2-workflow.json,2026-09-26;模型 Qwen/Qwen3-8B-MLX-4bit,本机 mlx_lm.server,seed 1,单次调用 max_tokens 8192)。token 逐段数由项目模型自带的 tokenizer 与 chat template 算出,相加与服务端 usage 一致(a00-workflow-agent.json);测试输出与模型回复约 900 字截取(保留头尾),本机路径已脱敏;耗时受本机其他任务影响,只能粗看。
失败分两层,性质完全不同。
第一层:信息要边做边找。parse_date 的两处 bug 其实都在发过去的文件里,模型「看得见」代码;可它只改了报错指着的那一处——指向第二处的报错,要等第一处修好、重跑之后才出现。一次调用只能用你事先喂给它的信息,而修 bug 需要的信息(下一条报错)是修的过程中才产生的。
第二层:写死的规则本身会出错。slugify 的模型答对了修法,错在程序:回复没按 FILE: 格式,代码的兜底规则(只有一个源文件、只有一个代码块,就当它是完整新文件)把函数片段当成整份文件写了回去。这不是「模型不够聪明」,是 workflow 自己的一条规则在一个没预料到的输入上走错了路。
两层各有自己的补法:第一层要「看了结果再来一次」,第二层要「格式不对就拒绝写回」或者干脆别整文件覆写(A2 的 edit)。
datetime.date(2024, 2, 5) != datetime.date(2024, 3, 5)——第二处 bug 的报错,在第一处修好之前根本不存在。slugify:模型的修法其实对,可它只回了一个函数片段、没写 FILE: 标记;代码的兜底规则把这 130 字符当成整份文件写回(原文件 365 字符),import re 没了,重跑 NameError: name 're' is not defined.。 试三件事:① 在 parse_date 上点「④ 整文件覆写」:为了改 1 行,模型写回了整份 1,302 字符,真实 diff 只有 2 行;② 点「⑤ 重跑测试」:报错换成了 datetime.date(2024, 2, 5) != datetime.date(2024, 3, 5),这就是第二处 bug;③ 切到 slugify 再点「④」:回复里没有 FILE: 标记,兜底规则把 130 字符的片段当成了整份文件,删掉的行里有 import re。
改进:先给流程加一步「看结果」
对第一层,最自然的补法是在流程末尾加一条回边:重跑没过,就把新的报错发回去,让模型再改一次,最多 3 次。对第二层,加一道检查:回复里没有 FILE: 标记就不写回,当作失败重来。
注意这两处改动都还是代码在决定:转不转由测试的退出码决定,写不写回由格式检查决定。模型的输出仍然只是「新文件的内容」,从来不决定下一步做什么。把它放到控制权光谱上看——
process_llm_output(llm_response)if llm_decision(): path_a() else: path_b()run_function(llm_chosen_tool, llm_chosen_args)while llm_should_continue(): execute_next_step()if llm_trigger(): execute_agent()def custom_tool(args): ... 六档与代码骨架逐字取自 smolagents 文档《Introduction to agentic systems》(Apache-2.0;2026-09-26 核对),原文写明 agency 是连续的光谱、不是 0 或 1。控制流图是本课把同一个任务画成六档的结构示意,不含任何数字;第 0 档与第 3 档对应本课项目真实录制的两条轨道。
试两件事:① 勾掉再勾上那条回边:图里的边多了一条、橙色边仍然是 0 条——加了循环,还在第 0 档;② 往右拖一档:模型第一次决定了一条边的去向。第 2 课会一路拖到 Multi-step Agent。
这条带回边的流程,正是 Anthropic 五种 workflow 模式里的 evaluator-optimizer,只是评估者换成了测试本身(第 2 课)。官方的原话是先找最简单的方案、只在需要时加复杂度,并且说很多应用里,把单次模型调用本身做好(加检索、加示例)通常就够了 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条官方建议,调研满票核实了原文存在;它是经验性的建议,原文没有给对照实验。 出处Anthropic · Building effective agents(2024-12-19) ↗需联网 。本课项目没有录这条带重试的流程在 4 个任务上的表现——它能不能修好 parse_date,现在没有数字;项目线的 M0 可以补录。
极小真数字:这一次调用花在哪
parse_date 那一次调用,服务端报告 prompt 782、completion 2,299 token。用项目模型自带的 tokenizer 和 chat template 逐段数,两边都与服务端一个不差:
month, day, year = (int(g) for g in m.groups()) → day, month, year = (int(g) for g in m.groups())。整份重写让这 17 token 的改动占用了 409 token 的代码块;而文件里任何一行被写错、漏写,都会跟着覆写进去——slugify 就是这样丢掉 import re 的。Anthropic 把两者都归为 agentic systems,只在「路径由预先写好的代码决定」还是「由模型动态决定」上做区分,并且建议先找最简单的方案,原话甚至说这可能意味着根本不建 agentic 系统 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条官方分野:深度调研 3-0 满票。「先找最简单的方案」是官方建议,不是实验结论。 出处Anthropic · Building effective agents ↗需联网 。这一课的真实数字也站在这边:workflow 修好的两个任务(word_freq、leap_year)各只用了 4,290 与 2,020 token、一次模型调用。workflow 是一种选择,不是低一级的 agent;它失败的地方要按失败的原因来补,不是一律换成 agent。
🔍 为什么不把测试文件也发过去?为什么只准写回源文件?
这是 workflow.py 里写死的两条规则:发给模型的是所有不在 tests/ 下、文件名不以 test_ 开头的 .py 文件;写回时,路径不在这个清单里的一律拒绝(项目测试 test_writes_outside_the_collected_sources_are_rejected 钉住了这一条)。第二条是为了守住「测试文件不许改」的任务约束——模型如果能改测试,「测试通过」就不再说明任何事。第一条是省事:对这个任务,测试输出里已经有失败断言的原文。规则写死在代码里的好处是可预期、好测;坏处就是 slugify 那种「规则没料到的输入」。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 用最少的机器(一次模型调用 + 几步写死的代码)修好一个已知位置的 bug;可预期、好测、便宜 |
| ② 为什么可能有效? | 当修复需要的信息全在「报错 + 源文件」里时,一次调用就拿到了全部输入 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条这是项目在录制前写下的判别标准 ①(MILESTONES.md · A0 · M0);是设计推理,不是被证明的结论。 出处M0 的判别标准(章首页 · 项目推进) ↗ |
| ③ 实验上呢? | T2 workflow 轨 2 / 4:需要的信息都在 docstring 里的两个任务一次修好;parse_date 缺的是运行时才出现的报错,slugify 败给了程序自己的写回规则 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测:单一模型(Qwen3-8B 4bit)、每个任务只跑 1 次(seed 1)。说明现象会发生,不估计比率。 出处本课项目 T2 录制(实验课) ↗ |
演进地图:你在这里
- 解决了只调一次模型,每一步都写在代码里:可预期、好测、便宜。卡在T2 workflow 轨 2 / 4 通过。parse_date 只改了报错指到的那一行,重跑才露出第二处(
datetime.date(2024, 2, 5) != datetime.date(2024, 3, 5));slugify 的回复只有函数片段,兜底规则把 130 字符的片段当整份文件写回(原文件 365 字符),丢了import re。 - 第一代 加步骤、加分支:五种 workflow 模式 prompt chaining · routing · parallelization · orchestrator-workers · evaluator-optimizer解决了每一类失败补一条预先写好的路径:测试没过就带着新报错再来一次(评估者是测试的退出码)、回复格式不对就拒绝写回。卡在分支要在设计时写出来,可真实路径是在运行时长出来的:T2 agent 轨 4 个任务走出 4 条不同的工具序列;同一配置重跑 4 次 parse_date(seed 1 / 2 / 3 / 1),走出 4 条。
- 解决了边在运行时长出来:T2 agent 轨 3 / 4 通过,包括 workflow 没修好的 parse_date 与 slugify。卡在代价:workflow 已经能解决的任务上 token word_freq ×5.3、leap_year ×4.5,其中 word_freq 的 agent 反而没修好;同配置、同 seed 重跑,2 / 4 个任务在中途分叉。
- 解决了录制前写下三条标准并逐个预测,录制后对照:预测对 3 / 4。卡在预测错的 slugify 不是任务本身需要 agent,而是 workflow 自己的写回规则出了错(回复没有
FILE:标记时的兜底)。每条轨道每个任务只跑 1 次:只能当例子,不能当比率。 - 解决了编排形状固定的部分(并行审查、逐条验证、断点续跑)写进脚本;模型只决定「要不要调这个 workflow、传什么参数」。卡在同一个仓库里两种说法并存:README
:11与:41说 agency 来自训练、不是编排;:239说编排形状固定就写进代码。界线画在哪,没有实验回答。 - 解决了同一个循环,识别出代码工作区后注入编码说明(约 577 token,Qwen3 tokenizer 计)与工作区快照;focus 模式才会收窄工具集与技能索引。卡在判定靠文件标记:本课任务仓单独拷出来(README.md、dateparse.py、test_dateparse.py,没有 .git)在默认 auto 下被判成 general;放在项目目录里原地跑,却因为往上 3 层有个
requirements.txt被判成 coding。姿态在会话开始解析一次,中途不变。
一句话
下一个缺口
那就多加几步、多加几个分支? → 第 2 课:多加几步、加分支,能覆盖所有情况吗?
上面那条「没过就再来」的回边补上了第一层。可每遇到一种新失败就加一条路,路要画到什么时候?下一课把 agent 轨真实走出来的路径摆出来,看看要预先画多少条。
你可能会问
- 「模型明明看得见第二处 bug 的代码,为什么不一起改?」 这次它的思考从报错出发,推出了第一处的修法就收尾了。一次运行说明不了它「总是」这样;能说明的是:只靠一次调用,修得全不全取决于模型这一次有没有顺带注意到,程序没有办法知道——直到重跑。
- 「让模型输出 diff,不是比整份文件省吗?」 省 token,但 diff 格式更难写对:Anthropic 在同一篇文章的附录里提到,写 diff 要先知道块头里改了几行。A2 讲「改哪里」的几种格式和各自的失败方式。
- 「单次调用什么时候就够了?」 T2 里的两个例子:word_freq 与 leap_year,需要的规格都写在 docstring 里,一次调用、一次覆写就过了。第 3 课把它写成判别标准。