How AI Coding Works II: Scenarios & Self-Correction
用户只说了一句”用 C++ 写个 hello world 并运行”。第一次 API 调用,发送的 messages 包含三类内容:
tools 参数):声明 write_file、bash、read_file 等可用工具的 schema(见工具调用)。user 消息):{"role": "user", "content": "用 C++ 写个 hello world 并运行"}
模型读完这些,决定第一步先写源码文件。
模型不直接写文件,它输出一个 write_file 工具调用(见工具调用),让外部程序代为执行。返回大致长这样:
{
"role": "assistant",
"tool_calls": [{
"id": "call_001",
"type": "function",
"function": {
"name": "write_file",
"arguments": "{\"path\":\"hello.cpp\",\"content\":\"#include <iostream>\\nint main(){\\n std::cout << \\\"Hello World\\\";\\n}\\n\"}"
}
}]
}
要点:
arguments 是 JSON 字符串,里面的 content 又包含 C++ 代码(含 \n、\" 等转义)。嵌套两层字符串,是读这类日志最容易眼花的地方。write_file 执行成功后,宿主程序把结果作为一条 tool 消息追加进数组(见消息数组),再发起第二次调用。此刻 messages 里有 3 条:
1. system "你是 Claude Code……"
2. user "用 C++ 写个 hello world 并运行"
3. assistant tool_calls: write_file(hello.cpp, ...)
4. tool "已写入 hello.cpp(XX 字节)" ← 上一步的执行结果
模型读到第 4 条”文件已写好”,于是决定下一步:编译它。
用户从没提过编译器名字。模型选出 g++ hello.cpp,靠的是三个来源叠加:
g++ 或 clang++。g++);如果工具返回”command not found”,它读到错误后会换 clang++ 或 c++ 再试——这正是 Agent 循环(见 Agent 循环)的自我纠错能力。这次模型输出了一个 bash 工具调用:
{"name": "bash", "arguments": "{\"command\":\"g++ hello.cpp -o hello\"}"}
每完成一次工具调用,结果就追加为一条 tool 消息,下一轮调用整段重发(见消息数组)。到第 4 次调用前,messages 已经累积到 7 条:
1. system "你是 Claude Code……"
2. user "用 C++ 写个 hello world 并运行"
3. assistant write_file(hello.cpp, ...)
4. tool "已写入 hello.cpp"
5. assistant bash(g++ hello.cpp -o hello)
6. tool ""(编译成功,无报错输出)
7. assistant bash(./hello)
8. tool "Hello World" ← 运行结果
旁白里提到的”第三次调用前已有 7 条消息”,指的就是这个累积过程:第 N 次 LLM 调用发送时,messages 里已经包含了此前 N−1 步产生的所有 assistant / tool 消息。
第 4 次调用时,模型读到 ./hello 的输出是 Hello World,判断任务已完成。这一次它不再输出任何 tool_calls,只生成一段普通文字总结给用户:
{"role": "assistant", "content": "已完成。hello.cpp 编译并运行成功,输出:Hello World"}
没有工具调用,Agent 循环(见 Agent 循环)的终止条件命中,循环跳出。最终 messages 增长到 8 条。
tool_call_id 必须和发起时的 id 对上,否则模型在重读历史时无法把”结果”和”调用”对应起来,可能重复执行或困惑。把这次”写 hello world”的完整会话按顺序铺开,8 条消息 + 1 个 system prompt 就构成全部”记忆”:
| # | role | 内容 |
|---|---|---|
| 0 | system | Agent 行为说明书(独立参数,每次都附带) |
| 1 | user | “用 C++ 写个 hello world 并运行” |
| 2 | assistant | tool_call: write_file |
| 3 | tool | “已写入 hello.cpp” |
| 4 | assistant | tool_call: bash(g++ ...) |
| 5 | tool | ""(编译成功) |
| 6 | assistant | tool_call: bash(./hello) |
| 7 | tool | “Hello World” |
| 8 | assistant | 文字总结(任务完成) |
旁白里用颜色区分:绿色=用户输入,蓝色=助手输出,紫色=工具结果。System Prompt 则作为独立参数附在每次调用上,不混在这 8 条里。
这是最关键的一点。第 4 次调用时,模型之所以”知道任务已经完成”,不是因为它”记得”,而是因为这次发送的 messages 里完整包含了第 1~7 条全部历史——它每次都重新读一遍所有上下文(见聊天记忆的真相)。
这就回答了”AI 的记忆到底存在哪里”:不在模型权重里,在每次发送的 messages 数组里。所谓黑魔法并不存在。
8 条消息的例子很短。真实工程里,一次复杂任务可能累积几十甚至上百条消息,每条 tool 结果(日志、源码片段)又可能很长。因为每次调用都重发整个数组:
这正是后文”上下文压缩”要解决的问题。
让 Claude Code 分析一个它从没见过的开源项目(如 nanochat),它本地没有任何上下文。和人类工程师拿到陌生 repo 一样,它要先用工具建立认知,每一步都是一次工具调用,每次结果都追加进 messages(见消息数组)。
典型探索顺序:
Glob(或 LS)看有哪些文件夹和文件。Grep 搜索关键类名 / 函数名 / 装饰器,找到实现位置。Read 精读入口文件和关键模块。十几轮下来,messages 里塞满了读过的代码片段。
| 工具 | 作用 | 何时用 |
|---|---|---|
| Glob | 按文件名模式列出文件(如 **/*.py) | 先看全局结构 |
| Grep | 在文件内容里搜索文本/正则 | 定位某个类、函数、字符串出现在哪 |
| Read | 读取指定文件的指定行范围 | 精读关键实现 |
这三件套是几乎所有代码 Agent 的标配。它们对应人类工程师”翻目录 → 全局搜索 → 打开文件”的工作流。
所以 Agent 的策略永远是按需检索:先 Glob/Grep 粗筛出可能相关的少数文件,再 Read 其中关键片段。这和人类”带着问题查代码”是一回事。
经过十几轮 Glob/Grep/Read(见探索阶段),messages 里已经积累了大量代码片段。最后一轮调用,模型不再调用任何工具,而是把上下文里这些零散的代码证据综合成一份连贯的架构文档:模块划分、调用关系、数据流。
终止条件和写 hello world 那次一样——模型不再输出 tool_calls,循环结束(见 Agent 循环)。
模型对项目的全部”理解”,都来自当前 messages 数组里实际放进去的代码。它没有本地长期记忆,也没有”心智模型”在后台持久化。换句话说:
这也是为什么检索策略(Glob/Grep/Read 选什么)是 Agent 质量的核心:喂什么,决定能产出什么。
当上下文不足时,模型不会主动说”我没读到这部分”,而是倾向用预训练知识补全——尤其是它见过的知名开源项目。结果可能是:文档读起来很顺,但具体到这个 repo 的某个内部函数,描述其实是模型”猜”的,和真实代码对不上。
辨别方法:让 Agent 在文档里标注每条结论来自哪个文件哪几行(引用回链)。能给出精确引用的,可信度高;含糊带过的,需要人工复核。
用户说”编译我的项目”,模型发起 bash(make)。make 报错了——这个报错对模型来说不是”失败”,而是一条普通的 tool 结果消息,照常追加进 messages(见消息数组):
{"role": "tool", "tool_call_id": "call_007",
"content": "main.cpp:14:5: error: use of undeclared identifier 'fooo'\nmake: *** [Makefile:9: main] Error 1"}
模型在下一次调用时读到这条报错,于是改变策略:不再重试 make,而是去读 main.cpp 第 14 行附近、修掉拼写错误、再编译。整个流程仍然是同一个 Agent 循环(见 Agent 循环),没有任何特殊机制。
... assistant: bash(make)
tool: error: undeclared identifier 'fooo' ← 报错
assistant: read_file(main.cpp) ← 改去查源码
tool: <源码片段>
assistant: edit_file(main.cpp, fooo→foo) ← 修复
tool: "已修改"
assistant: bash(make) ← 重新编译
tool: ""(成功)
assistant: 文字总结(完成)
注意模型是读了报错后自己推断出该看哪个文件——main.cpp:14:5 这行编译器输出就是它的线索。模型把这条结构化错误信息解析成”文件 + 行号 + 错误类型”,再决定下一步动作。
普通聊天机器人是一问一答:用户提问 → 模型回复 → 结束。一旦答错,只能靠用户纠正。
Agent 模式是循环:模型可以反复调用工具,看到工具结果后自己决定下一步。报错本身就是一个新输入,驱动模型进入”诊断→修复→重试”的子流程,全程不需要人介入。这就是”自我纠错(self-correction)“的本质——不是模型有什么特殊能力,而是循环给了它”看结果再调整”的机会。
和写代码那次一样,循环只在模型不再输出工具调用时结束。自动修 bug 场景下,模型通常会在两种情况停下:
后者是关键的安全阀:如果没有它,模型可能无限循环下去。成熟的 Agent 还会加最大轮次限制、重复检测等硬约束,防止卡死烧 token。