Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

s01: Agent Loop — 一个循环就够了

中文 · English · 日本語

s01 → s02 → s03 → s04 → ... → s19

"One loop & Bash is all you need" — 一个工具 + 一个循环 = 一个 Agent。

Harness 层: 循环 — 模型与真实世界的第一道连接。


问题

语言模型能推理代码,但碰不到真实世界——不能读文件、不能跑命令、不能看报错。

你可以给它一个工具(比如 bash),让它第一次调用拿到了结果。但然后呢?

你自己把结果复制粘贴回对话框,再让它继续。 那你就是那个循环。我们要做的,就是把这个"复制粘贴"自动化。


解决方案

Agent Loop

一个 while True 循环,模型调用工具就继续,不调用就停。整个过程只有两个信号:

信号 含义 循环动作
stop_reason == "tool_use" 模型举手说"我要用工具" 执行 → 结果喂回去 → 继续
stop_reason != "tool_use" 模型说"我做完了" 退出循环

工作原理

将这个过程翻译成代码。分步来看:

第 1 步:把用户的问题作为第一条消息。

messages = [{"role": "user", "content": query}]

第 2 步:将消息和工具定义一起发给 LLM。

response = client.messages.create(
    model=MODEL, system=SYSTEM, messages=messages,
    tools=TOOLS, max_tokens=8000,
)

第 3 步:追加模型回答,检查它是否调了工具。没调 → 结束。

messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
    return

第 4 步:执行模型要求的工具,收集结果。

results = []
for block in response.content:
    if block.type == "tool_use":
        output = run_bash(block.input["command"])
        results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": output,
        })

第 5 步:把工具结果作为新消息追加,回到第 2 步。

messages.append({"role": "user", "content": results})

组装为一个完整函数:

def agent_loop(messages):
    while True:
        response = client.messages.create(
            model=MODEL, system=SYSTEM, messages=messages,
            tools=TOOLS, max_tokens=8000,
        )
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason != "tool_use":
            return

        results = []
        for block in response.content:
            if block.type == "tool_use":
                output = run_bash(block.input["command"])
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": output,
                })
        messages.append({"role": "user", "content": results})

不到 30 行,这就是整个 Agent。后面 18 个章节都在这个循环上叠加机制——循环本身始终不变。


试一下

cd learn-claude-code
python s01_agent_loop/code.py

试试这些 prompt:

  1. Create a file called hello.py that prints "Hello, World!"
  2. List all Python files in this directory
  3. What is the current git branch?

观察重点:模型什么时候调用工具(循环继续),什么时候不调用(循环结束)?


接下来

现在模型手里只有 bash 一个工具——读文件要 cat,写文件要 echo ... >,找个文件要 find,又丑又容易出错。

s02 Tool Use → 给它 5 个真正的工具,会发生什么?模型会不会一次调用多个工具?几个工具同时跑会不会互相踩?

深入 CC 源码

以下内容基于 CC 源码 src/query.ts(1729 行)的完整分析。核心差异就两个:CC 不看 stop_reason 字段而是检查内容里有没有 tool_use 块(因为流式响应中 stop_reason 不可靠);CC 有更多的退出路径和恢复策略做生产级保护。

教学版的 30 行 while True 就是 CC 1729 行的核心。 下面每一项都是在这个核心上叠加的保护机制。

一、循环结构差异

教学版检查 response.stop_reason。CC 不用这个字段——流式响应中 stop_reason 可能还没更新但内容里已经有 tool_use 块了。CC 用 needsFollowUp 标志:接收到流式消息时(query.ts:832-835),只要检测到 tool_use 块就设为 true

// query.ts:554-558
// stop_reason === 'tool_use' is unreliable.
// Set during streaming whenever a tool_use block arrives.
let needsFollowUp = false
二、State 对象完整 11 字段(教学版只用 messages)
# 字段 用途 对应章节
1 messages 当前迭代的消息数组 s01
2 toolUseContext 工具、信号、权限上下文 s02
3 autoCompactTracking 压缩状态追踪 s08
4 maxOutputTokensRecoveryCount token 恢复尝试次数(上限 3) s11
5 hasAttemptedReactiveCompact 本轮是否已尝试响应式压缩 s08
6 maxOutputTokensOverride 8K→64K 的升级覆盖 s11
7 pendingToolUseSummary 后台 Haiku 生成的 tool use 摘要 s08
8 stopHookActive 停止钩子是否产生阻塞错误 s04
9 turnCount 轮次计数(maxTurns 检查) s01
10 transition 上一次继续原因 s11
11 taskBudgetRemaining 跨压缩边界的 task_budget s05
三、退出路径(10 个)和继续路径(7 个)

教学版只有 1 条退出路径。CC 有 10 个 return 点和 7 种"继续"方式(不退出但以不同原因进入下一轮)。这是生产级 Agent 必须的保护层——超时、超预算、用户中断、工具执行中被中止等等。每种场景都有对应的恢复或退出策略。

四、流式工具执行和 QueryEngine

CC 的 StreamingToolExecutorquery.ts:561)让工具在模型还在生成时就开始并行执行。QueryEngine.ts 额外加了费用超限、结构化输出验证失败等保护。教学版不实现这些——目标是概念清晰,不是性能极致。

一句话:1729 行的 query.ts 核心就是 30 行 while True。所有复杂字段和退出路径都是保护机制。先理解核心循环,后面的一切自然展开。