# 三行速读

  1. 这一章把 “会聊天” 变成 “会执行”:核心是 tool_use -> tool_result 回写闭环。
  2. 你要真正看懂的是 run_one_turn 的停止 / 继续条件,而不只是最终回答文本。
  3. 后面所有章节都在这个 loop 上加层,不会推翻它。

# 先修知识

  • Python 基础:函数、循环、字典、异常处理。
  • 命令行基础:知道 subprocess.run 在做什么。

# 读完后你应该能做到(可检验清单)

# 本篇要解决什么

这篇只做一件事:把 “会聊天的模型” 变成 “能持续做事的智能体”。

很多新手第一次做 Agent,会卡在同一个点:模型能给建议,但你还要手动执行命令、复制结果、再贴回去。这个过程一旦超过几轮,就很容易乱。

s01 的目标就是把这个人工往返自动化,形成最小闭环:

  1. 用户提问。
  2. 模型决定是否调用工具。
  3. 程序执行工具。
  4. 工具结果写回会话。
  5. 模型继续,直到完成。

# 用一个类比先理解

把它想成 “客服工单流转”:

  • 用户提需求。
  • 客服把需求交给执行同事(工具)。
  • 执行同事返回结果。
  • 客服继续处理下一步。

如果没有这条流转链,客服只能停在 “建议你这样做”,但工单永远不会真正办完。

# 为什么这一步必须现在做

因为后面所有能力(权限、记忆、任务、多智能体)都依赖这条主循环。如果循环本身不稳定,后续只是往不稳定底座上加功能。

所以学习顺序是对的:先把 loop 跑通,再谈扩展。

# 关键代码怎么读

# 1) 把状态显式放进 LoopState

1
2
3
4
5
@dataclass
class LoopState:
messages: list
turn_count: int = 1
transition_reason: str | None = None

这三个字段分别回答:现在聊到哪、第几轮、为什么继续下一轮。新手最容易忽视的是第三个字段,它对后续错误恢复非常关键。

# 2) 工具执行后一定要回写 tool_result

1
2
3
4
5
6
7
def execute_tool_calls(response_content) -> list[dict]:
...
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})

tool_use_id 是关联键。没有这层对应关系,模型不知道哪条结果对应哪次调用。

# 3) 单轮函数 + while 循环

1
2
3
4
5
6
7
8
9
10
11
def run_one_turn(state: LoopState) -> bool:
...
if response.stop_reason != "tool_use":
return False
...
return True


def agent_loop(state: LoopState) -> None:
while run_one_turn(state):
pass

这个拆分非常适合新人理解:单轮负责 “做一轮”,主循环负责 “反复做”。

# 你可以怎么复现

  1. 只保留一个工具( bash )先跑通闭环。
  2. print 打出每次 tool_usetool_result
  3. 人工输入一个需要两步以上的任务,观察 loop 是否自然停止。

# 常见误区

  • 误区 1:把最终答案当核心,忽略中间工具回写。
  • 误区 2:为了 “看起来聪明” 先加很多工具,结果循环逻辑没站稳。
  • 误区 3:把状态藏在全局变量里,后续难调试。

# 一句话总结

s01 的本质不是 “调用一次模型”,而是建立一个可持续执行的闭环,这就是后续所有章节的地基。

# 补充解读:把 s01 的代码完整走一遍

这一节专门补 “之前略过的代码细节”。如果你是第一次学 Agent,建议按这个顺序读源文件:

  1. 先看常量: SYSTEMTOOLS
  2. 再看状态: LoopState
  3. 再看工具执行: run_bashexecute_tool_calls
  4. 最后看循环: run_one_turn -> agent_loop

# A. run_bash 为什么要先做危险命令拦截

1
2
3
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
if any(item in command for item in dangerous):
return "Error: Dangerous command blocked"

这一段虽然很朴素,但它在教学里有两个意义:

  • 告诉你 “工具执行” 不是直接裸跑命令,安全门要前置。
  • 告诉你 “工具层可以失败”,而失败也要作为 tool_result 回写,不能吞掉。

很多新人第一次写 Agent 会漏掉 “失败也要回写”。结果模型只看到 “没结果”,容易重复发同一个命令。

# B. extract_text 为什么单独存在

1
2
3
4
5
6
7
8
9
def extract_text(content) -> str:
if not isinstance(content, list):
return ""
texts = []
for block in content:
text = getattr(block, "text", None)
if text:
texts.append(text)
return "\n".join(texts).strip()

这函数看起来像工具函数,但它在告诉你一件现实:模型响应是结构化 block,不是永远一段纯文本。把 “渲染展示层” 和 “内部 block 层” 分开,是后面扩展工具类型的基础。

# C. run_one_turn 的停止条件其实是状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
if response.stop_reason != "tool_use":
state.transition_reason = None
return False

results = execute_tool_calls(response.content)
if not results:
state.transition_reason = None
return False

state.messages.append({"role": "user", "content": results})
state.turn_count += 1
state.transition_reason = "tool_result"
return True

你可以把它理解成一个极小状态机:

  • 不是 tool_use -> 停止。
  • tool_use 且有结果 -> 继续。
  • 回写成功后把 transition_reason 标成 tool_result

这个 transition_reason 在教学版里只是一个字段,但在真实系统里会发展成恢复策略、可观测指标的一部分。

# D. 时序图(建议照着打印日志自己验证)

  1. messages 加入用户输入。
  2. 调用模型。
  3. 如果模型要工具:执行工具、回写结果。
  4. 继续下一轮。
  5. 直到 stop_reason != tool_use

建议你在 run_one_turn 前后各加一行日志,打印 turn_countstop_reason ,会非常直观。

# 进阶练习(巩固本章)

  1. run_bash 中增加 stderrstdout 分开返回的模式,观察模型后续行为变化。
  2. LoopState 里新增 last_tool_name ,每轮记录最近一次工具调用,验证状态是否更可调试。
  3. 强制让模型执行 3 次以上工具调用,检查 loop 是否稳定收敛。

# 统一术语口径(本章)

  • Agent Loop :智能体每一轮 “思考 -> 调工具 -> 回写 -> 继续” 的主循环。
  • tool_use :模型提出的工具调用请求。
  • tool_result :工具调用完成后返回给模型的结果块。

# 章节衔接(从易到难)

  • 本章先解决 “能不能形成闭环执行”。
  • 下一章 s02 解决 “闭环不变的前提下,如何扩展更多工具能力”。