s01:Agent 循环

已更新于 2026-06-23 08:00:00
最小 Agent 只需一个 while 循环。掌握工具调用意图与 tool_result 的流转方式,理解 messages 为什么是 Agent 的唯一记忆载体,以及主循环如何成为后续所有章节的基础。
预计阅读:约 26 分钟 · 字数:4,618
先抓住一个循环和一个 Bash 工具如何构成最小 Agent Harness,再看本地笔记对消息历史、工具结果回流和生产级 State 的拆解。

来源与致谢

本节整理自 Learn Claude Code 官方站点 s01:Agent Loop 与 GitHub 源项目

shareAI-lab/learn-claude-code · s01_agent_loop

,按 MIT License 授权整理;同时迁入阿衡学习笔记 s01-Agent循环.md 的补充理解。本站内容为非官方学习笔记,不代表 shareAI-lab 或 Anthropic 官方立场。

最小的代理是一个循环,该循环调用模型、运行工具并将结果反馈回来。

学习目标

  1. 说清楚为什么“一个循环 + 一个 Bash 工具”就是最小 Agent。
  2. 区分模型的 tool_use 意图和 Harness 写回的 tool_result
  3. 理解 LLM 本身没有记忆,它能"知道"之前发生了什么,完全靠 Agent 把完整的 messages 每次都整个发过去。
  4. 说清楚 state 和真正发给 LLM 的 request body 有什么区别。
  5. 判断教学版 stop_reason 判断和生产级流式 follow-up 判断的差异。

Agent While Loop

这是 Agent Harness 的核心。它是一个不断重复的过程,模型在其中决定是否调用工具,工具的结果又被反馈回模型,形成一个持续推进任务的闭环。

while (stop_reason === "tool_use")
tool_useend_turnStartAPI Callstop_reason?Execute ToolAppend ResultBreak / Done
messages[]
[ empty ]
The While Loop
Every agent is a while loop that keeps calling the model until it says 'stop'.
1/7

运行该工具,将结果追加到 messages[] 中,然后将其回传。

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

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

没有循环,就没有 agent。

quote:核心原则

真正的 agent 起点,是把真实工具结果重新喂回模型。


问题

你提出了一个问题给大模型:“帮我读取下我的目录下有哪些文件,并且执行XXX.py”。

模型能输出一条 bash 命令,但输出完了就停了,它不会自己跑,也不会看到结果后继续推理。

你可以手动跑一遍,把输出粘贴回对话框,让它接着干。下一个命令出来,你再跑一遍、再贴回去。

每一个来回,你都在做中间层。而把它自动化,就是这一章要做的事。


这一章要解决什么问题

语言模型本身只会"生成下一段内容"。

它不会自己:

  • 打开文件
  • 运行命令
  • 观察报错
  • 把工具结果再接着用于下一步推理

如果没有一层代码在中间反复做这件事:

发请求给模型
  -> 发现模型想调工具
  -> 真的去执行工具
  -> 把结果再喂回模型
  -> 继续下一轮

那模型就只是一个"会说话的程序",还不是一个"会干活的 agent"。

这一章的核心目标只有一个:把"模型 + 工具"连接成一个能持续推进任务的主循环。


名词解释

loop(循环)

只要任务还没做完,系统就继续重复同一套步骤。

turn(轮)

最小版本里,一轮通常包含:

  1. 把当前消息发给模型
  2. 读取模型回复
  3. 如果模型调用了工具,就执行工具
  4. 把工具结果写回消息历史

tool_result(工具执行结果)

要重新写回对话历史,让模型在下一轮处理,能看见的结果块。

state(运行状态)

主循环继续往下走时,需要一直带着走的那份数据。

最小版本里最重要的状态:

  • messages
  • 当前是第几轮
  • 这一轮结束后为什么还要继续

名词流转时序图

标准的 Agent 架构的层次是:

User
  └── Agent(控制层)
        ├── loop(主循环)
        ├── state(循环状态)
        └── Tool(工具执行层)
              ↕ (Agent 向外调用)
          LLM(独立的模型服务)

LLM 不属于 Agent:

  • Agent = "跑在你机器上、管控循环的那段代码"(loop + state + tool executor)
  • LLM = 一个通过网络调用的推理服务,它本身不感知循环,也不管 state
  • Agent 负责"驱动",LLM 负责"推理",两者通过 API 边界分离。这也是为什么 Agent 可以换模型、可以限速、可以做权限拦截,而 LLM 对这些一无所知

一个 Prompt 从用户输入到任务完成,完整走一遍所有名词:

正在渲染图示...

时序图的「可单步播放」版,一步步看 messagesturn_counttransition_reason 如何随两个 Turn 变化:

agent_loop(state)
AgentTurn 1 · 模型决定调工具Turn 2 · 模型直接给出答案发送初始 Prompt写入初始消息messages + toolsstop_reason = tool_use追加 assistantrun_bash("ls")原始输出追加 tool_result再次发送 messagesstop_reason = end_turn返回最终答案UserloopstateToolLLM
state
[ empty ]
① 用户把 Prompt 交给 Agent
用户只跟 loop 入口打交道,不直接碰 state,也不认识 LLM。
1/8

逐步拆解

① 用户把 Prompt 交给 Agent

用户只跟 Agent 的入口(loop)打交道,不直接碰 state,更不认识 LLM。

② Agent 把消息写进 state

loop 把用户的 Prompt 存进 state.messages。state 是整个循环的"记忆",它唯一的职责就是把上下文带到下一轮。

③ Agent 调用 LLM(Turn 1)

loop 把当前 messages + 工具定义(tools schema)一起发给 LLM。

注意:LLM 在 box 外面,这是一次跨越 API 边界的网络调用,LLM 完全不知道有个循环在等它。

④ LLM 回来说"我要调工具"

LLM 返回的 stop_reason = tool_use,意思是:"我不打算直接给你答案,我需要先执行一个工具。" 这一步 LLM 只是表达意图,它自己不会真的去跑任何东西。

⑤ Agent 真正执行工具

loop 解析出 LLM 想调的工具,把任务交给 Tool 执行层,拿到原始输出,再包装成规范的 tool_result 结构(带上 tool_use_id,让 LLM 下一轮知道这条结果对应哪次调用)。

⑥ 工具结果写回 state,准备下一轮

loop 把 tool_result 追加进 state.messages。此刻 messages 里已经有三条:

#role内容
1user用户原始提问
2assistantLLM 刚才那条回复——内容是"我要调 run_bash('ls')",这就是所谓的"assistant 意图":LLM 没有直接给答案,而是在回复里声明了自己要用哪个工具、传什么参数,本质上还是一条普通的 assistant 消息
3user工具的真实执行结果(以 tool_result 块的形式写回,role 仍是 user)

同时,loop 把 transition_reason 标记为 "tool_result"。这是 loop 自己用来判断"要不要继续转"的标志:如果这一轮结束时有工具结果需要送回模型,就打上这个标记,下一轮继续;如果 LLM 直接给了最终答案(stop_reason = end_turn),标记就是 None,loop 退出。

⑦ Agent 再次调用 LLM(Turn 2) loop 把更新后的完整 messages 再次发给 LLM。LLM 这次看到了工具的真实结果,可以给出最终答案,stop_reason = end_turn

⑧ Agent 把答案返回给用户 loop 把 LLM 的最终回复交还给用户,循环结束。

note:读图要点

  • state 贯穿始终,是唯一"记忆载体" - turn 是每次"模型调用 → 可能执行工具"的完整一轮 - tool_result 必须写回 state.messages,否则下一个 turn 的模型看不到执行结果,下一轮 LLM 读到 tool_result 才知道"上一步执行了什么" - loop 靠 LLM 返回的 stop_reason 决定走哪条路,tool_use 就执行工具继续,end_turn 就退出 - transition_reason 是 loop 自己写的记录,是 loop 内部记录,LLM 不可见,loop 自己用来追踪"为什么还在跑"

state 与 request body 的区别

state.messages 是每次调用 LLM 时的 request body 的核心字段,每轮都把它整个发出去

state 里还有两个字段是 LLM 永远看不到的:

字段会发给 LLM 吗作用
messages✅ 是,每轮整个发出去LLM 的上下文输入
turn_count❌ 否loop 自己记"跑了几轮了"
transition_reason❌ 否loop 自己判断"要不要继续转"

所以更准确的理解是:

state 是 loop 的工作台,它既放着"下次要发给 LLM 的材料"(messages),也放着"loop 自己用来决策的内部变量"(turn_count、transition_reason)。

request body 只是从 state.messages 里取出一部分构建出来的,state 本身比 request payload 大。

关于 stop_reason

stop_reason 就是 LLM API 直接返回的,它是响应体里的一个字段,不是 Agent 自己算出来的。

Anthropic 的响应结构大概长这样:

{
  "role": "assistant",
  "content": [...],
  "stop_reason": "tool_use"
}

Agent 拿到响应后,直接读 response.stop_reason 这个字段来决定下一步怎么做。

这个模式是业界标准,但字段名不统一:

厂商字段名"要调工具"的值"正常结束"的值
Anthropicstop_reason"tool_use""end_turn"
OpenAIfinish_reason"tool_calls""stop"
Google Geminifinish_reason"STOP" + 看 content"STOP"

语义完全一致,叫法不同而已。本质都是:LLM 在回复里告诉 Agent "我为什么停下来了",Agent 凭这个信号决定走哪条分支。

stop_reason 是响应 JSON 里的一个字段,Agent 解包后读取它,LLM 本身不知道 Agent 会拿它做什么判断。


解决方案

正在渲染图示...

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

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

最小心智模型

user message
   |
   v
LLM
   |
   +-- 普通回答 ----------> 结束
   |
   +-- tool_use ----------> 执行工具
                              |
                              v
                         tool_result
                              |
                              v
                         写回 messages
                              |
                              v
                         下一轮继续

important:关键点

工具结果必须重新进入消息历史,成为下一轮推理的输入。 如果少了这一步,模型就无法基于真实观察继续工作。


工作原理

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

第 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

warning:初学者常见错误

很多初学者会只关心"最后有没有答案",忽略把 assistant 回复本身写回历史。这样一来,下一轮上下文就会断掉。

第 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 harness 内核。模型负责决策(要不要调工具、调哪个),harness 负责执行(调了就跑、结果喂回去)。后面的章节都在这个循环上叠加机制,循环本身始终不变。

用 state 对象包装

教学版直接传 messages 列表已经够用。生产版通常把它包进一个 state 对象,同时带上 loop 自己需要的内部变量:

state = {
    "messages": [...],   # 发给 LLM 的上下文
    "turn_count": 1,     # loop 内部计数,LLM 看不到
    "transition_reason": None,  # loop 内部记录,LLM 看不到
}

完整循环升级为:

def agent_loop(state):
    while True:
        response = client.messages.create(
            model=MODEL,
            system=SYSTEM,
            messages=state["messages"],
            tools=TOOLS,
            max_tokens=8000,
        )
 
        state["messages"].append({
            "role": "assistant",
            "content": response.content,
        })
 
        if response.stop_reason != "tool_use":
            state["transition_reason"] = None
            return
 
        results = []
        for block in response.content:
            if block.type == "tool_use":
                output = run_tool(block)
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": output,
                })
 
        state["messages"].append({"role": "user", "content": results})
        state["turn_count"] += 1
        state["transition_reason"] = "tool_result"

turn_counttransition_reason 是 loop 的内部变量,不会出现在发给 LLM 的 request body 里。


关键数据结构

1. Message

{"role": "user", "content": "..."}
{"role": "assistant", "content": [...]}

Agent 里的 messages 是 LLM 下一次推理的唯一输入

LLM 本身没有记忆,每次调用都是全新的。它能"记住"之前发生的事,唯一的原因就是 Agent 把完整的 messages 列表整个发过去了。它读到的不是"对话记录",而是一份完整的任务日志:用户要求什么、自己之前说了什么、工具返回了什么结果。

一个具体的对比:

聊天展示层:  "删掉这条消息" → 界面变干净,没别的影响

Agent messages:  "删掉这条消息" → LLM 下一轮就再也看不到这个信息
                                    → 它会当作这件事从没发生过
                                    → 推理结果可能完全错误

在 Agent 里操作 messages,本质上是在编辑 LLM 的工作记忆。每一条消息的存在与否,直接决定 LLM 下一轮能看到什么、能推理出什么。这也是为什么"工具结果必须写回 messages"是整章最强调的事。少了这个,LLM 的下一轮推理就是在无中生有。

2. Tool Result Block

{
    "type": "tool_result",
    "tool_use_id": "...",
    "content": "...",
}

tool_use_id 的作用:告诉模型"这条结果对应的是你刚才哪一次工具调用"。


试一下

教学 demo 提示:代码会执行模型生成的 shell 命令。建议在一个临时测试目录中运行,避免影响你的项目文件。s03 会讲真正的权限系统。

下载代码

  • s01-agent-loop.zip

    含 code.py · requirements.txt · .env.example

    下载

准备(首次运行):

python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
# 官方 API:填入 ANTHROPIC_API_KEY 和 MODEL_ID
# 第三方 API:填入 BASE_URL、API_KEY 和 MODEL_ID

运行

python code.py

试试这些 prompt:

  1. 创建一个叫 hello.py 的文件,打印 "Hello, World!"
  2. 列出当前目录里所有的 Python 文件
  3. 当前的 git 分支是什么?

观察重点

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


接下来

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

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

扩展:深入 Claude Code 源码

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

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

一、循环结构差异

教学版检查 response.stop_reason。CC 不把它作为循环继续的唯一依据。

流式响应中 stop_reason 可能还没更新,但内容里已经有 tool_use 块了。CC 用 needsFollowUp 标志:接收到流式消息时(query.ts:830-834),只要检测到 tool_use 块就设为 trueQueryEngine.ts 会从 message_delta 捕获真实 stop_reason 用于其他逻辑,但 query loop 本身靠 needsFollowUp 决定是否继续。

// query.ts:554-558
// stop_reason === 'tool_use' is unreliable.
// Set during streaming whenever a tool_use block arrives.
let needsFollowUp = false;

二、State 对象 10 字段(教学版只用 messages)

#字段用途对应章节
1messages当前迭代的消息数组s01
2toolUseContext工具、信号、权限上下文s02
3autoCompactTracking压缩状态追踪s08
4maxOutputTokensRecoveryCounttoken 恢复尝试次数(上限 3)s11
5hasAttemptedReactiveCompact本轮是否已尝试响应式压缩s08
6maxOutputTokensOverride8K→64K 的升级覆盖s11
7pendingToolUseSummary后台 Haiku 生成的 tool use 摘要s08
8stopHookActive停止钩子是否产生阻塞错误s04
9turnCount轮次计数(maxTurns 检查)s01
10transition上一次继续原因s11

注:taskBudgetRemainingquery.ts:291)是 loop-local 局部变量,不在 State 上。源码注释明确写了 "Loop-local (not on State)"。

三、多条退出和继续路径

教学版只有 1 条退出路径(模型不调工具就结束)。生产版有多条退出和继续路径,覆盖 blocking limit、prompt too long、model error、abort、hook stop、max turns、token budget continuation、reactive compact retry 等场景。每种场景都有对应的恢复或退出策略。

四、流式工具执行和 QueryEngine

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

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

它如何接进整个系统

从现在开始,后面所有章节本质上都在做同一件事:往这个循环里增加新的状态、新的分支判断和新的执行能力。

章节增加的能力
s02往里面接工具路由
s03往里面接权限判断
s06往里面接子 Agent 隔离
s07往里面接按需加载的知识
s11往里面接错误恢复

初学者最容易犯的错

  1. 把工具结果打印出来,但不写回 messages → 模型下一轮根本看不到真实执行结果
  2. 只保存用户消息,不保存 assistant 消息 → 上下文会断层
  3. 不给工具结果绑定 tool_use_id → 模型会分不清哪条结果对应哪次调用
  4. 一上来就把流式、并发、恢复、压缩全塞进第一章 → 让主线变得非常难学
  5. 以为 messages 只是聊天展示 → 在 agent 里,messages 更像"下一轮工作输入"

小结

最小 Agent Harness 不是把模型提示词写得更长,而是把模型的 tool_use 意图交给真实工具执行,再把 tool_result 写回 messages,让下一轮模型调用能基于真实观察继续推理。后续章节增加工具、权限、Hook、任务状态和压缩机制,都是围绕这个循环补控制面。

summary:一句话记住

Agent Loop 的本质,是把"模型的动作意图"变成"真实执行结果",再把结果送回模型继续推理。


代码下载

本节完整源码,支持官方 Anthropic API 和任何兼容 Anthropic 协议的第三方服务。

  • s01-agent-loop.zip

    含 code.py · requirements.txt · .env.example

    下载

这一章对你有帮助吗?

s01:Agent 循环 | 阿衡的AI笔记