大模型 Agent 中的 Loop:从基本循环到 Claude Code / Codex 的工程实现
Agent Loop 不是让模型无限思考,而是由运行时管理的反馈循环:模型提出下一步行动,Harness 检查并执行,环境返回结果,模型据此继续或结束。
一份面向 Agent 开发者的技术讲义。本文重点解释:Loop 是什么、一次任务实际如何运行、模型与 Harness 各自控制什么、这套机制如何演进,以及怎样用 Python 写出一个最小版本。
阅读结论: Agent Loop 不是“让模型无限思考”,而是一个由运行时管理的反馈循环:模型提出下一步行动 → Harness 检查并执行 → 环境返回结果 → 模型根据新信息继续行动或结束。
1. 先建立直觉:Loop 到底是什么
假设用户给编码 Agent 一个任务:
修复这个 Python 项目的测试失败问题,并验证修复有效。
普通问答模型可能直接根据经验猜测原因,回答一段修改建议。但编码 Agent 通常需要真正操作项目:读文件、定位实现、编辑代码、运行测试,再根据测试结果决定下一步。
一个典型的过程是:
用户提出任务
│
▼
模型根据指令和当前上下文决定下一步
│
├── 返回最终答复 ──────────────────────┐
│ │
└── 请求调用工具 │
│ │
▼ │
Harness 检查工具、权限和参数 │
│ │
▼ │
执行工具:读文件 / 改文件 / 跑测试 │
│ │
▼ │
将工具结果写回对话状态 │
│ │
└────再次请求模型 ◀─────────────┘
│
├── 还有问题:继续循环
└── 任务已完成:结束本轮这里的关键并不是循环语法本身,而是环境反馈改变了模型下一步决策时所依据的信息。
例如:
- 模型请求读取
calculator.py。 - 工具返回代码,模型发现减法函数误用了
-。 - 模型请求修改文件。
- Harness 执行修改。
- 模型请求运行测试。
- 测试报告仍然失败,模型得到新的错误信息,继续调查。
- 第二次修改后测试通过,模型选择结束任务。
如果没有“执行—反馈”这一环,模型只能给出建议,无法确认建议是否真的奏效。Loop 把模型从单次文本生成,变成了能够在外部环境中采取多步行动的系统。
一个简洁的定义
Agent Loop = 模型决策 + 工具/环境执行 + 结果反馈 + 继续或终止的控制逻辑。
在实际工程中,通常还要加上状态管理、权限审批、异常处理、调用预算和上下文管理。OpenAI 在介绍 Codex CLI 时,把 Agent Loop 描述为协调用户、模型与工具交互的核心机制;Anthropic 的工具使用文档则强调,模型产生结构化工具请求,但实际执行工具的是应用程序或托管工具运行环境。[1][2]
2. 不要混淆的四个层次:Turn、Model Call、Tool Call、Loop
“模型调用一次”“工具调用一次”“Agent 执行一个任务”并不相同。
| 概念 | 含义 | 示例 |
|---|---|---|
| Model Call / Inference | 向模型发起一次推理请求 | 把当前消息、工具定义和上下文发给模型 |
| Tool Call | 模型请求执行某个工具 | read_file({"path": "app.py"}) |
| Iteration / Step | Loop 中的一次推进。不同框架的定义可能略有不同 | 一次模型决策后执行一批工具,再更新状态 |
| Turn | 用户发起的一轮交互任务 | 用户说“修复测试”,直到 Agent 交回结果 |
| Session / Thread | 可持续多轮交互的会话 | 修复测试结束后,用户继续要求增加一个功能 |
一个 Turn 可以包含很多次模型推理,也可以包含多个工具调用。在支持并行工具调用的系统中,一次模型响应甚至可以请求执行多项互不依赖的工具。
还可以把 Loop 分成三个相互嵌套的层次:
- 任务级循环(outer loop):围绕用户目标持续工作,直到目标完成、需要用户输入或触发停止条件。
- 工具级循环(tool-use loop):模型请求一个或多个工具,Harness 执行,然后把结果交给模型。
- 工具内部循环(inner loop):例如模型请求执行一段 Python 代码,而这段代码内部自己循环处理一万条记录。若使用支持程序化工具调用的运行时,模型还可能生成程序来编排多次工具调用。
因此,“一个 Agent 只有一个 while 循环”只是教学模型。生产系统可能有多层控制流程、异步任务、子 Agent、权限暂停和恢复等机制。
3. 一个标准 Agent Loop 是怎么运行的
3.1 初始化:把任务转换成模型可用的上下文
运行时通常会组合这些信息:
- 用户当前的任务;
- 系统指令与工作规则;
- 当前会话中有用的历史消息;
- 可用工具清单、工具说明和参数 Schema;
- 工作目录、操作系统、项目结构等环境上下文;
- 权限策略、限制条件以及相关技能或项目规则。
这些信息组成模型当前决策的依据。编码 Agent 通常不会在每次推理时把整个电脑交给模型,而是向模型暴露一个经过约束的工具接口,例如读取文件、搜索代码、编辑文件、运行命令等。
3.2 模型做出决策:输出文本,还是请求工具
模型响应通常会走向两类结果:
- 最终消息:模型认为当前任务可以结束,并给用户答复。
- 工具请求:模型选择某个工具,并提供结构化参数。
例如,概念上的工具请求可能是:
{
"name": "read_file",
"arguments": {
"path": "src/calculator.py"
}
}这不是模型直接读取磁盘,而是模型根据工具定义生成一项请求。实际文件访问仍由本地程序、沙箱或者远程工具服务执行。
Anthropic 把这称为模型与应用之间的工具使用契约:应用声明可用操作及输入输出形式;模型决定何时、如何请求这些操作;执行代码的一方返回结果。[2]
3.3 Harness 接管工具请求
Harness 是围绕模型运行的控制层。它一般需要完成这些工作:
- 检查工具名称是否存在;
- 验证输入参数是否符合 Schema;
- 判断该动作是否需要拒绝、自动允许或等待用户审批;
- 为工具提供工作目录、沙箱、超时等运行环境;
- 执行工具,并记录输出、错误码、耗时等信息;
- 把结果按协议格式回传给模型。
在编码场景中,工具可能是 Read、Grep、Edit、Write、Bash,也可能是 MCP 工具、浏览器或专门的代码执行服务。具体工具名称属于产品实现,核心模式相同。
3.4 将工具结果回写上下文
假设工具返回:
AssertionError: expected 5, got -1如果 Harness 只把这个错误打印给用户,而没有让模型看到,模型下一步就无法基于该结果修正策略。
所以运行时会把工具结果追加到会话状态,再发起下一次模型请求。对话历史可以抽象成:
user: 修复这个测试失败的问题
assistant: 调用 read_file(path="app.py")
tool: 文件内容是 def add(a, b): return a - b
assistant: 调用 run_tests()
tool: AssertionError: expected 5, got -1
assistant: 调用 write_file(...修正后的内容...)
tool: 写入成功
assistant: 调用 run_tests()
tool: 3 passed
assistant: 测试已通过,问题已修复。不同厂商的真实消息格式不同,但结构化工具调用与工具结果匹配的思想是相似的。Claude 的 Messages API 使用 tool_use / tool_result 及对应的调用 ID;OpenAI 的 API 则有自己的工具调用和响应项结构。[2][3]
3.5 判断继续还是结束
最常见的教学逻辑是:
if model_requested_tools:
execute_tools_and_continue()
else:
return_final_answer()真实系统还必须考虑其他终止或暂停原因:
- 模型给出最终消息;
- 达到最大步数或时间预算;
- 用户中断任务;
- 工具请求被拒绝;
- 模型 API 出错或限流;
- 上下文空间即将耗尽,需要压缩历史;
- 需要用户补充信息或批准敏感操作;
- 模型响应处于可恢复的暂停状态,而不是已经完成。
例如,在 Claude 的工具使用协议中,客户端执行工具时,常见流程是一直处理 stop_reason == "tool_use",直到模型返回其他停止原因;不过“不是工具调用”不一定代表任务成功,还需要检查实际停止原因及错误状态。[2]
4. 代码示例一:最小 Loop 的伪代码
先不绑定某个厂商的 API,可以把核心机制缩减为下面的 Python 伪代码:
history = [{"role": "user", "content": user_task}]
while True:
# 1. 模型根据对话历史和工具定义决定下一步
response = model.generate(history, tools=tool_schemas)
# 2. 如果模型不再请求工具,就结束本轮
if not response.tool_calls:
return response.text
# 3. 保存模型的工具请求,保持对话协议完整
history.append(response.as_assistant_message())
# 4. Harness 检查权限,然后执行工具
for call in response.tool_calls:
authorize_or_raise(call)
result = execute_tool(call.name, call.arguments)
# 5. 把结果交还给模型,以便下一轮决策
history.append(make_tool_result(call.id, result))这个循环有两个最核心的原则:
原则 A:工具结果必须进入下一次决策的上下文。 否则模型无法根据现实反馈调整策略。
原则 B:Loop 的终止不能只依赖“模型看起来说完了”。 应当检查协议状态、任务条件和运行时安全边界。
这段伪代码省略了并行工具、参数验证、重试、权限等待、异步任务、上下文压缩等生产细节。接下来通过一个可以直接运行的模拟程序,看看完整反馈如何推动循环。
5. 代码示例二:可运行的 Python 教学模拟器
下面的示例不需要 API Key,也不需要安装任何第三方库。它模拟一个编码 Agent 修复 add() 函数的过程:先读文件,检查当前代码,再写入修复,最后运行测试。
注意: 这是教学模拟器。
FakeModel用人工编写的规则模拟模型的工具选择,不是真正的大模型。它的作用是让 Loop 的控制流一目了然。
将代码保存为 mini_agent_loop.py,使用 Python 3.10+ 执行:
python mini_agent_loop.pyfrom __future__ import annotations
from dataclasses import dataclass
from typing import Any
@dataclass
class ToolCall:
"""模型输出的结构化工具请求。"""
name: str
arguments: dict[str, Any]
call_id: str
@dataclass
class ModelDecision:
"""模型的一次决策:请求工具,或者返回最终答案。"""
tool_calls: list[ToolCall]
final_text: str | None = None
class FakeModel:
"""用简单规则模拟模型根据工具反馈持续决策。"""
def decide(self, history: list[dict[str, Any]]) -> ModelDecision:
tool_results = [
item for item in history
if item.get("type") == "tool_result"
]
completed = {
item["name"]: item["content"]
for item in tool_results
}
# 第一步:先获取源代码
if "read_file" not in completed:
return ModelDecision([
ToolCall(
name="read_file",
arguments={"path": "app.py"},
call_id="call_read_1",
)
])
source = completed["read_file"]
# 第二步:根据源代码生成修复操作
if "write_file" not in completed:
fixed_source = source.replace(
"return a - b",
"return a + b",
)
if fixed_source == source:
return ModelDecision(
tool_calls=[],
final_text="没有找到预期的错误表达式,需要进一步人工检查。",
)
return ModelDecision([
ToolCall(
name="write_file",
arguments={"path": "app.py", "content": fixed_source},
call_id="call_write_1",
)
])
# 第三步:运行测试验证,而不是凭感觉宣称修好了
if "run_tests" not in completed:
return ModelDecision([
ToolCall(
name="run_tests",
arguments={},
call_id="call_test_1",
)
])
test_result = completed["run_tests"]
if "PASS" in test_result:
return ModelDecision(
tool_calls=[],
final_text="已修复 add() 的运算符错误,测试通过。",
)
# 这里为了保持例子简短,不再实现第二轮自动修复。
return ModelDecision(
tool_calls=[],
final_text=f"测试仍然失败,需要继续调查:{test_result}",
)
class MiniHarness:
"""最小运行时:工具路由、执行、反馈和步数限制。"""
def __init__(self) -> None:
self.files = {
"app.py": "def add(a, b):\n return a - b\n"
}
self.tools = {
"read_file": self.read_file,
"write_file": self.write_file,
"run_tests": self.run_tests,
}
def read_file(self, path: str) -> str:
if path not in self.files:
raise FileNotFoundError(path)
return self.files[path]
def write_file(self, path: str, content: str) -> str:
# 示例只允许操作 app.py,体现工具执行范围限制。
if path != "app.py":
raise PermissionError("只允许写入 app.py")
self.files[path] = content
return "写入成功"
def run_tests(self) -> str:
source = self.files["app.py"]
if "return a + b" in source:
# 代表测试断言 add(2, 3) == 5 成功。
return "PASS: 1 test passed; add(2, 3) == 5"
return "FAIL: expected 5, got -1"
def execute(self, call: ToolCall) -> str:
if call.name not in self.tools:
raise ValueError(f"未知工具:{call.name}")
# 真正的生产系统还需要验证 JSON Schema、权限和超时。
return self.tools[call.name](**call.arguments)
def run(self, task: str, model: FakeModel, max_steps: int = 8) -> str:
history: list[dict[str, Any]] = [
{"role": "user", "content": task}
]
for step in range(1, max_steps + 1):
print(f"\n--- Loop step {step} ---")
decision = model.decide(history)
if not decision.tool_calls:
print(f"[FINAL] {decision.final_text}")
return decision.final_text or "任务结束。"
# 模型的工具请求也属于对话历史的一部分。
history.append({
"role": "assistant",
"type": "tool_calls",
"calls": [call.__dict__ for call in decision.tool_calls],
})
for call in decision.tool_calls:
print(f"[MODEL -> TOOL] {call.name}({call.arguments})")
try:
result = self.execute(call)
except Exception as exc:
# 生产系统通常会将错误作为 tool_result 反馈给模型,
# 而不是只打印后丢弃。
result = f"ERROR: {type(exc).__name__}: {exc}"
print(f"[TOOL -> AGENT] {result}")
history.append({
"role": "tool",
"type": "tool_result",
"name": call.name,
"call_id": call.call_id,
"content": result,
})
raise RuntimeError(f"超过最大步数限制:{max_steps}")
if __name__ == "__main__":
harness = MiniHarness()
result = harness.run(
task="修复 app.py 中 add 函数的错误,并运行测试验证。",
model=FakeModel(),
)
print("\n任务输出:", result)
print("最终文件:\n" + harness.files["app.py"])运行时应该看到什么
输出大致如下:
--- Loop step 1 ---
[MODEL -> TOOL] read_file({'path': 'app.py'})
[TOOL -> AGENT] def add(a, b):
return a - b
--- Loop step 2 ---
[MODEL -> TOOL] write_file({'path': 'app.py', 'content': 'def add(a, b):\n return a + b\n'})
[TOOL -> AGENT] 写入成功
--- Loop step 3 ---
[MODEL -> TOOL] run_tests({})
[TOOL -> AGENT] PASS: 1 test passed; add(2, 3) == 5
--- Loop step 4 ---
[FINAL] 已修复 add() 的运算符错误,测试通过。从这个例子看 Loop 的本质
- 模型不是一次性决定完整的所有步骤。 它每次根据已有证据决定下一步。
- 工具返回的是环境结果。 写入成功、测试失败、文件不存在,都可能改变下一次决策。
- Harness 不等于模型。 即便模型请求调用不存在的工具,执行端也可以拒绝。
- 循环需要硬边界。
max_steps防止模型或工具流程永远无法结束。 - 成功必须有外部证据。 代码写入成功并不等于修复成功;运行测试是独立的验证步骤。
本例为了可读性用了确定性模拟。真实 Agent 将 FakeModel.decide() 替换为模型 API 调用,并按照 API 协议解析模型响应即可;但权限、沙箱、超时、重试等边界依然应该由 Harness 负责,而不是期待模型自行遵守。
6. 代码示例三:连接真实模型时的工具调用骨架
下面展示真实工具调用协议的基本形状。它使用 Anthropic Python SDK 的 Messages API 风格;需要先安装 SDK,并设置 ANTHROPIC_API_KEY。示例把 read_file 作为工具,重点在 Loop 协议,而不是构建完整的代码编辑器。
uv add anthropic
export ANTHROPIC_API_KEY="你的 API Key"import json
import os
from pathlib import Path
import anthropic
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
# 本示例将文件访问限制在指定工作目录内;生产环境还要防范符号链接等边界。
WORKSPACE = Path("./demo-project").resolve()
WORKSPACE.mkdir(parents=True, exist_ok=True)
def read_file(path: str) -> str:
target = (WORKSPACE / path).resolve()
if target != WORKSPACE and WORKSPACE not in target.parents:
raise PermissionError("路径超出工作目录")
return target.read_text(encoding="utf-8")
def dispatch_tool(name: str, arguments: dict) -> str:
if name == "read_file":
return read_file(**arguments)
raise ValueError(f"未授权或不存在的工具:{name}")
tools = [
{
"name": "read_file",
"description": "读取工作目录中的文本文件。只能读取项目内文件。",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
}
]
messages = [
{
"role": "user",
"content": "请读取 app.py,并说明其中定义了什么函数。不要修改任何文件。",
}
]
MAX_TOOL_ROUNDS = 8
for round_number in range(1, MAX_TOOL_ROUNDS + 1):
response = client.messages.create(
# 使用你账户当前可用、支持工具调用的模型名称。
model=os.environ["ANTHROPIC_MODEL"],
max_tokens=2048,
tools=tools,
messages=messages,
)
tool_uses = [block for block in response.content if block.type == "tool_use"]
# 没有工具请求时,通常意味着模型正在给出普通答复;
# 生产环境还应区分 end_turn、max_tokens 等停止原因。
if not tool_uses:
final_text = "\n".join(
block.text for block in response.content if block.type == "text"
)
print(final_text or f"模型停止,stop_reason={response.stop_reason}")
break
# 先保存模型发出的原始 assistant 内容,再回传 tool_result。
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for tool_use in tool_uses:
try:
result = dispatch_tool(tool_use.name, tool_use.input)
is_error = False
except Exception as exc:
result = f"{type(exc).__name__}: {exc}"
is_error = True
tool_results.append(
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": result,
"is_error": is_error,
}
)
messages.append({"role": "user", "content": tool_results})
else:
raise RuntimeError("超过最大工具轮数;任务被安全停止")这段代码在做什么
- 用
tools声明模型可请求的操作,以及操作参数的 Schema。 - 每轮把累积的消息发送给模型。
- 如果返回
tool_use,由本地 Python 代码执行工具。 - 通过
tool_use_id把每项结果对应回具体调用。 - 将结果放入下一轮消息,让模型继续决策。
- 如果不再请求工具,则输出模型答复;同时设置最大轮数限制。
此代码是协议骨架,不是生产级安全模板。它没有实现并行调用、用户审批、复杂的停止原因处理、请求重试、上下文压缩、审计记录或细粒度工具授权。若打算让模型执行 Bash、任意 Python、文件写入或联网操作,必须进一步引入权限策略和隔离执行环境。Anthropic 官方工具使用教程也采用相同的基本循环:处理工具请求、运行工具、把 tool_result 放回消息,再重复直到工具调用阶段结束。[4]
另外,示例中的 ANTHROPIC_MODEL 需要设置为你的账号当前可用且支持工具调用的模型名称;模型名称和 SDK 版本可能变化,应以官方文档为准。
7. Claude Code 与 Codex:Loop 的共性和差异
Claude Code 与 Codex 的具体实现并不完全公开,也会随版本变化。下面区分官方公开的运行机制和从产品行为抽象出的通用架构,不把概念图当成某个产品内部全部代码的精确复刻。
7.1 共性:模型决定行动,Harness 控制执行
可以用一个共同的抽象来理解二者:
User Task
│
▼
Prompt / Context Builder
│
▼
Model Inference
│
├── 最终消息 ──> 输出结果、结束当前任务
│
└── 工具请求
│
▼
Permission / Policy
│
▼
Tool Runtime / Sandbox
│
▼
Tool Result / Error
│
└────回到上下文,再次调用模型Loop 本身很简单;工程难度集中在模型周围的运行时:如何提供恰当上下文、怎样可靠地执行文件和命令、何时向用户申请权限、如何处理错误、如何避免任务失控,以及在长任务中怎样保持足够的状态。
7.2 Claude Code:循环之外还有工具权限与执行策略
理解 Claude Code 时,最重要的是不要把它看作一个“模型可以随便调用电脑”的黑盒。
从 Claude Code / Claude Agent SDK 的公开文档,可以确认以下设计思想:
- 工具调用有明确的协议;模型负责提出工具请求,运行时负责执行。
- 工具使用可以受权限模式、允许/拒绝规则、Hook 和运行时审批回调约束。
- 执行过程中可以通过 Hook 在工具调用前后执行自定义逻辑,进而做策略检查、记录或调整。
- Agent SDK 可以封装相当一部分循环逻辑;开发者不一定需要手写底层
while循环,但仍然需要设计工具、权限和运行规则。
Claude Agent SDK 文档描述的权限评估流程中,Hook、拒绝规则、权限模式、允许规则以及 canUseTool 回调共同决定工具调用是否能够执行。[5] 这意味着模型产生了一个动作,不等于动作必然被执行。
可以把 Claude Code 的一个典型编码任务理解为:
- 读取项目规则和用户任务;
- 模型先调查代码,选择读文件或搜索工具;
- Harness 按工具权限和策略执行;
- 模型拿到结果后,选择编辑文件或运行命令;
- 对具有风险的操作,系统可能要求确认或直接拒绝;
- 运行测试并处理失败反馈;
- 在模型给出完成消息、达到停止条件或被用户中断时结束/暂停任务。
这是一种架构层面的描述,而不是声称每次任务都严格采用这七个固定步骤。模型可能改变顺序、并行请求工具或选择完全不同的调查路径。
要点: SDK 帮你托管 Loop,不代表你不再需要关心 Agent 的执行行为。你仍然需要决定可开放哪些工具、如何处理审批、如何阻止危险动作、使用哪些 Hook,以及如何管理超时和上下文。
7.3 Codex:把 Loop 看作连接模型推理和工作环境的 Harness
OpenAI 于 2026 年 1 月发布的《Unrolling the Codex agent loop》专门拆解了 Codex CLI 的 Loop。文中指出,Codex 的一次用户任务会包含多次模型推理和工具执行;当模型请求工具时,Harness 执行工具并把输出接回后续上下文。对于编码 Agent,主要产物往往是被实际写入或修改的文件,而不只是最后显示给用户的文字。[1]
那篇文章还特别讨论了上下文管理:随着工具调用和对话不断追加,模型的输入会增长,可能接近上下文窗口限制。因此 Codex 需要在适当时机压缩历史,保留有助于后续执行的摘要与状态。[1]
从工程视角看,Codex 的 Agent Loop 不只是:
while not done:
response = model(prompt)它还需要维护会话和工作目录状态、发起模型请求、流式接收输出、路由工具请求、管理命令执行权限与沙箱、处理中断,以及让下一次推理获得必要的环境反馈。
7.4 最值得理解的差别:用户用产品,还是自己造运行时
| 使用方式 | 谁主要负责 Loop | 开发者仍需承担的责任 |
|---|---|---|
| 直接调用模型 API + 自定义工具 | 你的应用 | 手写循环、消息协议、工具路由、权限、重试、上下文管理等 |
| 使用 Agent SDK / Agent 框架 | SDK 或框架提供核心 Runner | 配置工具、权限、状态、失败策略、人工审批、业务约束 |
| 使用 Claude Code / Codex 这类编码 Agent 产品 | 产品自带 Harness | 主要通过提示、项目规则、权限设置、可用工具及工作区约束来引导和限制行为 |
| 使用托管 Agent 运行时 | 平台负责一部分托管执行 | 定义任务、授权连接器、配置运行环境、评估结果和处理业务风险 |
核心区别不是“一个有 Loop,另一个没有”,而是Loop 的控制逻辑和运行状态由谁维护,以及你可以把控制细化到什么程度。
8. Loop 演进史:从固定流程到 Harness 驱动的 Agent
这段历史并不是一条“新技术彻底替代旧技术”的直线。很多系统至今仍混用固定工作流、条件分支和模型驱动循环。下面的时间线主要用于理解思路如何逐步叠加。
阶段一:传统程序与固定工作流
在大模型普及前,程序也有循环、状态机和工作流:
for item in items:
validate(item)
transform(item)
save(item)或者开发者显式定义状态:读取 -> 校验 -> 处理 -> 保存 -> 结束。
特点是:下一步基本由代码规定,过程可预测、易测试;缺点是面对开放任务时,开发者必须提前枚举大量可能分支。它并不是 Agent Loop,但为后来的 Agent 提供了循环、状态、异常处理和控制流等工程基础。
阶段二:单次 LLM Completion
最早的一类大模型应用通常是:
输入 Prompt -> 调用模型一次 -> 返回文本模型可以写计划、解释错误或生成代码,但应用本身不一定会执行这些行动,也不能自然地根据执行结果修订操作。
如果用户说“帮我修复 bug”,单次 Completion 可能给出一段建议;但它不会自动知道修改是否成功,除非后续有程序去执行、收集结果并再次调用模型。
阶段三:ReAct——将推理与行动交替起来
2022 年的 ReAct 研究提出在模型轨迹中交替组织推理和行动,使模型可以根据环境返回的观测更新计划,而不是只输出一段内部推理或一系列预先写好的动作。[6]
概念示意:
Thought / 分析:需要查证某个信息
Action / 行动:调用搜索工具
Observation / 观测:工具返回结果
Thought / 分析:现有信息还不够
Action / 行动:再次查询
Observation / 观测:返回补充信息
Final / 答复:整合证据后给出结论今天的 Agent 不一定使用 ReAct 论文中的特定文本格式;重要的贡献是推理与行动交替,并以观察结果推动后续决策。这就是现代 Tool-Using Agent Loop 的核心直觉之一。
阶段四:模型学会何时、如何使用工具
2023 年的 Toolformer 研究探索让模型学习何时调用 API、传递什么参数,以及如何把工具返回结果用于之后的预测。[7] 同期,模型服务商也逐渐提供原生 Function Calling / Tool Calling 接口,让模型可以通过结构化输出请求应用函数,而不是只输出一段需要应用自行解析的自然语言。
这一步把“模型说它想做什么”变成一个更稳定的程序接口:
{
"name": "get_weather",
"arguments": {"location": "Paris"}
}应用执行函数,将输出返回给模型,然后模型继续生成。OpenAI 的 Function Calling 文档也将其描述为一个多步交互:请求模型、接收工具调用、由应用执行、回传工具输出,再拿到最终响应或更多工具调用。[3]
阶段五:Agent 框架把 Loop 变成可复用运行时
当任务变复杂后,开发者需要解决比“调用工具”更多的问题:
- 对话状态如何保存;
- 多个工具如何路由;
- 工具失败如何重试;
- 哪些步骤必须由代码保证;
- 哪些步骤可以由模型动态选择;
- 人工审批或进程中断后如何恢复;
- 怎样测试和观测每一步。
Agent 框架逐步将 Loop 封装成可复用组件,同时提供状态、回调、工具路由、记忆和可观测性。LangGraph 用 State、Nodes 和 Edges 表达有状态工作流;边可以是固定转移,也可以依据状态进行条件路由,因此它能把确定性逻辑与模型驱动步骤组合起来。[8]
这和 Agent Loop 不矛盾。Loop 是运行机制的一种核心形态;图结构则是表达更大范围执行流程的方式。
阶段六:编码 Agent 的 Harness 工程化
Claude Code、Codex 这类产品所面对的是高度开放的编码任务。单靠模型会调用 read_file 已经不够,还要考虑:
- 大量文件与代码搜索;
- 修改后运行命令和测试;
- 操作系统、目录和权限边界;
- 危险命令的授权;
- 任务可能连续执行几十甚至上百个步骤;
- 上下文窗口不能无限增长;
- 工具失败和环境差异;
- 用户中断、澄清、审批和恢复;
- 通过规则文件、Skills、Hooks 和子 Agent 扩展能力。
所以现代编码 Agent 的 Loop 更像是一个“受控执行运行时”:模型可以动态选择路径,但执行动作仍要穿过 Harness 的策略和工具接口。
阶段七:托管执行、程序化工具调用与多 Agent
较新的运行时进一步把工作拆成多层:
- 托管 Harness:平台负责 Loop、状态、工具编排或恢复;
- 沙箱工作区:Agent 在隔离环境中读写文件、运行命令、安装依赖或产出文件;
- 程序化工具调用:允许模型生成小段程序,通过循环和条件语句编排多次工具调用,避免每个微小步骤都重新请求模型;
- 多 Agent / Handoff:把搜索、实现、审查等子任务委托给其他 Agent,再汇总结果。
OpenAI 的 Agent 文档区分了由平台托管 Harness 的 Agents API、在应用内运行 Runner 的 Agents SDK,以及允许开发者直接控制模型调用的 Responses API;沙箱文档也明确区分 Harness 作为控制平面与 sandbox 作为执行平面。[9][10] 其 Programmatic Tool Calling 文档则描述了模型编写程序来协调多次工具调用的模式。[11]
这些发展并没有让传统 Loop 消失,而是形成了分层 Loop:外层控制任务生命周期,中层管理模型与工具之间的反馈,内层可能运行程序、子流程或子 Agent。
9. 为什么有了 Loop,仍然需要 Harness
这是理解 Claude Code / Codex 时最重要的问题之一。
9.1 Loop 解决“如何持续行动”,不自动解决“行动是否安全”
模型可能建议运行:
rm -rf ./build也可能错误地猜测某个环境变量存在,或者生成了错误的文件路径。即便模型已经输出结构化工具调用,也不代表调用一定合理、安全或成功。
因此运行时需要权限层:
模型提出工具请求
│
▼
参数验证 / 工具白名单
│
▼
权限策略检查
┌───┼────────┐
拒绝 自动允许 询问用户
│ │ │
停止/反馈 执行 等待批准/拒绝Anthropic 的 Agent SDK 文档给出了权限模式、Hook、允许/拒绝规则和 canUseTool 回调等控制方式。[5] Codex 开源仓库也包含执行审批和沙箱策略相关实现,说明执行控制是 Harness 的独立工程责任,而不是模型生成工具调用的附带结果。[12]
9.2 Loop 解决“能够继续”,不自动解决“知道何时结束”
模型可能重复执行:
运行测试 -> 同样失败 -> 修改 -> 运行测试 -> 同样失败 -> 修改 -> ...常见对策包括:
- 最大模型调用数或最大工具步数;
- 总时间和 Token 预算;
- 对连续重复的工具调用进行检测;
- 对重复错误设置熔断阈值;
- 对“完成”要求定义外部验证条件;
- 多次无进展后转向用户澄清或返回部分结果。
最大步数不是完整的解决方案,但应该有。否则,即使模型通常表现良好,边缘输入、坏工具结果或模型异常也可能导致资源无限消耗。
9.3 Loop 解决“能把结果交回去”,不自动解决“上下文永远装得下”
每轮都追加工具输出会使上下文不断膨胀。大量日志、整个文件内容、冗长的搜索结果都可能挤占后续推理需要的空间。
生产系统通常需要多种策略:
- 限制工具输出长度;
- 只返回相关片段而不是整个文件;
- 对重复信息去重;
- 将持久状态保存在文件、数据库或任务状态中;
- 接近上下文上限时压缩历史,保留完成目标所需的关键信息;
- 在必要时重新读取权威数据,而不是依赖过时摘要。
Codex 的工程文章明确将上下文窗口管理列为 Agent Loop 的职责,并解释了通过压缩对话项目来继续长任务的思路。[1]
9.4 Loop 解决“可以调用工具”,不自动解决“工具操作可靠”
真实工具可能超时、断网、返回非零退出码、返回格式异常或在执行后中断。Harness 应当:
- 为每个工具定义输入和输出契约;
- 验证参数;
- 设置超时和取消机制;
- 将失败作为明确的工具结果反馈;
- 对适合重试的操作采用退避策略;
- 对写文件、付款、删除、部署等副作用操作采用幂等键、二次确认或事务;
- 记录审计信息,以便追溯模型为什么执行了某个动作。
还要注意:重试并非总是安全。只读查询通常适合重试,但“发送邮件”“创建订单”这类动作,如果没有幂等保护,重试可能造成重复副作用。
10. Loop 的工程难题与常用对策
| 难题 | 典型现象 | 处理思路 |
|---|---|---|
| 无限循环 | 同一命令重复运行 | max_steps、时间预算、重复动作检测、无进展熔断 |
| 盲目自信 | 修改完就宣称成功 | 用测试、构建、断言等外部证据验证 |
| 错误级联 | 第一步路径错,后面持续基于错误假设 | 校验工具结果;关键状态重新读取;错误反馈清晰具体 |
| 上下文膨胀 | 长任务越来越慢、超出上下文 | 限制输出、摘要压缩、持久状态与按需检索 |
| 权限过宽 | 模型可执行任何命令或访问任意文件 | 最小权限、工作目录约束、沙箱、拒绝策略、人工审批 |
| 非确定性 | 同样输入得到不同工具路径 | 对关键流程加确定性节点、强制前置/后置检查、保留审计轨迹 |
| 执行成本高 | 每个微小步骤都要再调用模型 | 并行独立工具;缓存;批量操作;在合适场景用程序编排工具 |
| 人工介入难恢复 | 用户中断后不知已完成哪些操作 | 保存会话状态、任务检查点、工具执行结果与副作用记录 |
| 评价困难 | 最终回答看起来合理但实际失败 | 以目标达成率、测试通过率、误操作率、成本和耗时评估 |
10.1 为什么模型决定下一步并不意味着“全靠模型控制”
一个典型的系统可以同时具备两种控制:
- 模型驱动的软控制:模型判断先看哪个文件、要不要搜索、下一步采用什么实现方式。
- 代码驱动的硬控制:运行时规定不能写入哪些目录、必须经过哪些检查、最多调用多少次、哪些工具需要用户审批。
两者互补。模型适合处理开放世界中难以提前枚举的选择;代码适合执行必须明确、可审计、不可越过的约束。
10.2 并行工具调用并不等于所有操作都能并行
如果模型同时请求读取三个互不相关的文件,这三个读取操作可能并行执行。但如果第二个动作依赖第一个动作的结果,就需要先执行第一步,然后让模型基于结果作出后续决策。
可以粗略区分:
- 独立的信息获取:常可并行;
- 存在数据依赖的动作:必须按依赖顺序执行;
- 互相覆盖的写入或外部副作用:需要串行化、锁或事务保护。
并行化的目标是缩短等待时间,而不是消除数据依赖。
11. 它和 LangChain / LangGraph 的关系
一个常见误区是:既然 Claude Code 或某个 Agent SDK 有自动 Loop,是不是就不再需要 LangGraph 里那些 Node、Edge 和代码控制了?
答案是:它们处在不同的抽象层,解决的问题有重叠,但并不等价。
11.1 纯 Agent Loop:把下一步交给模型动态决定
概念上:
while not finished:
decision = model(state, tools)
if decision.is_tool_call:
result = run_tool(decision.tool_call)
state = append_result(state, result)
else:
finished = True优点是灵活。用户给出开放任务后,模型可以根据发现的新信息不断调整计划,不要求开发者提前规定每一条路线。
缺点是路径不完全固定;如果任务有严格业务顺序、必须满足的审批要求或复杂分支,单靠模型来决定所有步骤不够稳妥。
11.2 LangGraph:用图表达状态、节点和转移
LangGraph 将流程建模为共享状态、执行节点和边;边可以固定,也可以依据状态决定下一节点。[8]
例如:
输入任务
│
▼
需求分类(确定性规则)
│
├── 不清晰 -> 请求补充信息
│
└── 清晰
│
▼
Agent 研究代码(模型驱动)
│
▼
安全检查(确定性规则)
│
├── 不通过 -> 拒绝/要求修订
│
└── 通过
│
▼
执行修改
│
▼
测试验证
│
├── 失败 -> 返回修复步骤
└── 通过 -> 总结并结束在这个设计中,Agent Loop 可以发生在“研究代码”或“修复步骤”节点内部;图控制则决定更大的业务流程何时进入安全检查、测试、审批或结束。
11.3 更实用的组合方式
可以把系统分成三个层级:
- 业务工作流层:通过确定性节点表达必须遵守的顺序与分支,例如支付前确认、发布前审批、数据变更后校验。
- Agent Loop 层:给模型有限的工具集合,让它在明确任务边界内自行调查、规划和执行多步操作。
- 工具与环境层:实现读写文件、运行代码、访问 API、查询数据库、沙箱隔离、权限审批与审计。
不要为了使用 Agent 而把所有逻辑都交给模型。可以确定性实现的流程就使用确定性代码;真正需要根据上下文灵活判断的环节,再使用模型驱动的 Loop。
12. 设计自己的 Agent 时,应该把控制权放在哪里
可以从以下问题开始做架构决策。
12.1 让模型决定什么?
适合交给模型的部分通常包括:
- 根据任务选择下一项调查动作;
- 从多种解决方案中选择候选方案;
- 结合工具反馈调整原有计划;
- 判断还缺什么信息;
- 归纳结果并向用户解释。
这些决策往往难以通过固定规则穷举。
12.2 让代码决定什么?
应该优先由程序强制执行的部分通常包括:
- 哪些工具在当前任务里可用;
- 参数和输出格式是否合法;
- 可访问的文件、目录、网络和凭证范围;
- 哪些动作需要审批或双重确认;
- 最大步数、总预算、超时和取消条件;
- 必须通过哪些测试或验证才算完成;
- 重要副作用是否成功、是否重复执行;
- 日志、审计和状态持久化规则。
例如,“尽量别删除用户文件”属于模型能理解的指令,但它不应成为唯一防护。实际系统还应该限制可访问范围,并在高风险动作上引入显式策略或确认。
12.3 一个可操作的最小生产架构
┌─────────────────────────────────┐
│ API / CLI / UI │
└────────────────┬────────────────┘
▼
┌─────────────────────────────────┐
│ Task Manager / Session State │
│ 任务状态、预算、取消、检查点 │
└────────────────┬────────────────┘
▼
┌─────────────────────────────────┐
│ Agent Runner / Loop │
│ 调模型、解析决策、继续/终止 │
└──────────┬───────────────┬──────┘
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ Policy / Approvals│ │ Context Manager│
│ 权限、规则、审批 │ │ 历史、摘要、检索 │
└────────┬───────┘ └────────────────┘
▼
┌─────────────────────────────────┐
│ Tool Router / Executor │
│ Schema 校验、执行、超时、错误处理 │
└────────────────┬────────────────┘
▼
┌─────────────────────────────────┐
│ Tools / Sandbox / External APIs │
└─────────────────────────────────┘这个架构图不是唯一正确答案。小型原型可以把这些组件放在一个 Python 文件里;复杂系统则应该逐步把状态、工具执行、权限和可观测性拆开。重要的是在设计阶段就把这些职责区分清楚,避免让一个巨大的 while True 同时承担模型调用、权限、安全、存储和业务逻辑。
13. 总结
可以用以下几句话记住 Agent Loop:
- Loop 是反馈闭环,不是重复生成文本。 模型发起行动,工具作用于环境,环境结果反过来影响模型下一步决策。
- 模型不等于 Agent。 模型提供决策能力;Harness 管理执行、状态、权限、上下文与停止条件。
- 一次用户 Turn 可以包含多次模型调用和工具执行。 不要把 Turn、Model Call 和 Tool Call 混为一谈。
- Loop 可以托管,但不能因此忽略控制。 使用 Claude Agent SDK、Codex 或其他 Agent 运行时,可以少写底层编排代码;业务权限、工具边界和完成标准仍然需要明确设计。
- 现代 Agent 通常是多层控制系统。 外层工作流负责可靠的业务顺序,内层 Loop 负责灵活的探索和行动,工具层负责具体执行。
- 正确结束和安全执行与继续行动同样重要。 最大步数、权限策略、测试验证、错误处理和上下文管理不是外围附加功能,而是稳定 Loop 的一部分。
如果你以前用 LangGraph 自己定义大量 Node、Edge 来控制 Agent,那么值得采用的思路不是把这些代码全部删除,而是重新划分边界:把适合模型动态选择的部分交给 Agent Loop,把必须确定性执行的规则保留在程序和工作流里。
14. 参考资料
以下是官方文档或原始论文。产品和 SDK 持续更新,代码接口、模型名称及可用功能请以当前文档为准。
-
OpenAI, Unrolling the Codex agent loop (2026-01-23):解释 Codex CLI 的 Agent Loop、模型推理、任务 Turn 和上下文管理。
https://openai.com/index/unrolling-the-codex-agent-loop/ -
Anthropic, How tool use works:解释工具使用契约、客户端工具与
tool_use/tool_result循环。
https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works -
OpenAI, Function calling:工具 Schema、工具调用与应用端执行的多步流程。
https://developers.openai.com/api/docs/guides/function-calling -
Anthropic, Build a tool-using agent:从单次调用逐步构建包含多轮工具调用、错误处理和并行工具的 Loop。
https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent -
Anthropic, Claude Agent SDK — Permissions:权限模式、允许/拒绝规则、Hooks 与运行时审批。
https://code.claude.com/docs/en/agent-sdk/permissions -
Shunyu Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models (2022).
https://arxiv.org/abs/2210.03629 -
Timo Schick et al., Toolformer: Language Models Can Teach Themselves to Use Tools (2023).
https://arxiv.org/abs/2302.04761 -
LangChain / LangGraph, Graph API overview:状态、节点、边和条件路由。
https://langchain-ai.github.io/langgraph/concepts/low_level/ -
OpenAI, Agents:比较 Agents API、Agents SDK 与 Responses API 的运行时责任划分。
https://developers.openai.com/api/docs/guides/agents -
OpenAI, Sandbox Agents:区分 Harness 控制平面与沙箱执行平面。
https://developers.openai.com/api/docs/guides/agents/sandboxes -
OpenAI, Programmatic Tool Calling:让模型生成程序以编排多次工具调用、循环和条件分支。
https://developers.openai.com/api/docs/guides/tools-programmatic-tool-calling -
OpenAI, Codex CLI source code:公开的 Codex 实现,可进一步阅读执行、审批和沙箱相关模块。
https://github.com/openai/codex