如果说大模型是发动机,那么 agentic loop(Agent 循环)就是把发动机的动力传到轮子上的传动系统。一个能改代码、跑测试、读日志的编码 Agent,本质上就是这个循环在反复转动。很多人对 Agent 的想象停留在"模型很聪明",但真正让它"能干活"的,是循环里那套不起眼的执行与回灌机制。这篇我们把这个闭环拆开看清楚。

模型其实什么都没"做"

第一个反直觉的事实:模型本身不执行任何动作。它读不了文件、跑不了命令、发不了请求。它能做的只有一件事——根据上下文输出文本,其中包括一种特殊的结构化输出:tool_use

当你给模型挂上工具(tool definitions),模型在需要时会停下来,不再继续生成普通文本,而是输出一个 tool_use 块,里面是工具名和参数,比如"调用 run_bash,参数 command: pytest"。这时模型的本轮输出结束,stop_reason 变成 tool_use

注意:此刻没有任何命令被执行。模型只是"表达了一个意图"。真正去把 pytest 跑起来、把退出码和 stdout 收回来的,是模型外面的 harness(编排脚手架)。harness 执行完,把结果包成一个 tool_result 块,追加进对话历史,再发起下一次请求。模型这才"看到"测试结果,决定下一步。

这就是整个循环的核心动作:

1
2
3
模型输出 tool_use  →  harness 执行工具  →  把 tool_result 回灌  →  再次请求模型
↑ │
└────────────────────── 循环 ←──────────────────────────────┘

循环的终止条件是模型返回 stop_reason: end_turn——意味着它认为任务告一段落,输出的是给用户的最终答复,而不是又一个工具调用。

一个具体的转动过程

假设用户说:"修复 utils.py 里那个除零的 bug。"循环大致这样转:

  1. 模型输出 tool_use:读取 utils.py。harness 读文件,tool_result 回灌文件内容。
  2. 模型看到代码,输出 tool_use:把某行改成带保护的写法。harness 执行编辑,tool_result 回灌"修改成功"。
  3. 模型输出 tool_use:跑测试。harness 执行 pytesttool_result 回灌退出码与失败用例。
  4. 假设测试还红着,模型读到失败信息,继续输出 tool_use 二次修改……
  5. 测试转绿,模型输出普通文本"已修复并通过测试",stop_reason: end_turn,循环结束。

关键在于第 3、4 步:模型能根据执行的真实反馈纠正自己。这正是 Agent 区别于"一次性生成一段代码"的地方——它在闭环里基于事实迭代,而不是盲写。反馈的质量直接决定 Agent 的能力上限:一个把报错信息原样回灌的工具,远胜过一个只回灌"失败了"的工具。

两种实现:tool runner 与 manual loop

把这个循环落到代码上,有两条路。

第一种是 tool runner——SDK 帮你把循环跑完。你只管定义工具和它们的执行函数,SDK 内部自动完成"发请求→拿到 tool_use→调用你的函数→回灌 tool_result→再发请求",直到 end_turn 才把结果交还给你。代码简洁,适合标准场景,缺点是循环内部对你是黑盒。

第二种是 manual loop——你自己手写这个 while 循环。骨架大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
messages = [{"role": "user", "content": user_request}]

while True:
resp = client.messages.create(
model=MODEL,
messages=messages,
tools=TOOLS,
)
messages.append({"role": "assistant", "content": resp.content})

if resp.stop_reason != "tool_use":
break # end_turn,任务结束

tool_results = []
for block in resp.content:
if block.type == "tool_use":
# —— 这里是手控循环的价值所在 ——
if needs_approval(block): # 人机确认
if not ask_human(block):
result = "用户拒绝执行"
else:
result = run_tool(block.name, block.input)
else:
result = run_tool(block.name, block.input)
log(block, result) # 审计日志
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})

手控循环多写了几十行,换来的是对每一步的完全掌控:危险操作(删文件、推代码)插入人工审批;记录每次工具调用的审计日志;按条件改写或拦截结果;甚至在某些步骤注入额外上下文。编码 Agent 普遍选 manual loop,正是因为"改代码"这件事容错要求高,必须能在循环里随时刹车和介入。

一个实现细节:同一轮里模型可能一次输出多个 tool_use 块。harness 要把它们都执行,并把对应的多个 tool_result 放进同一条 user 消息里回灌,tool_use_id 一一对应,顺序无所谓但不能漏。

pause_turn:服务端循环也要回灌

还有一类工具运行在服务端,比如代码执行、web 搜索这类 server tool,模型侧的循环由 API 服务端自己转。但即便如此,长任务也可能在中途返回 stop_reason: pause_turn——表示"这一轮还没转完,先暂停"。

这时你不能当成结束。正确做法是把返回的内容原样回灌进 messages,再发起一次请求,让它从暂停点续跑。逻辑上和手控循环里"拿到 tool_use 就回灌再请求"是同构的,只是暂停的触发方从你的工具换成了服务端机制。把 pause_turn 误判为 end_turn,是接服务端工具时最常见的坑。

结尾

Agent 循环不复杂,但它是编码 Agent 全部能力的承载结构:模型只负责"想下一步做什么",harness 负责"真去做并把结果如实带回来",二者反复咬合,才让一堆 token 变成能改代码、能自我纠错的工程能力——闭环转得稳,Agent 才靠得住。