大模型 Agent 中的 Loop:从基本循环到 Claude Code / Codex 的工程实现

Agent Loop 不是让模型无限思考,而是由运行时管理的反馈循环:模型提出下一步行动,Harness 检查并执行,环境返回结果,模型据此继续或结束。

一份面向 Agent 开发者的技术讲义。本文重点解释:Loop 是什么、一次任务实际如何运行、模型与 Harness 各自控制什么、这套机制如何演进,以及怎样用 Python 写出一个最小版本。

阅读结论: Agent Loop 不是“让模型无限思考”,而是一个由运行时管理的反馈循环:模型提出下一步行动 → Harness 检查并执行 → 环境返回结果 → 模型根据新信息继续行动或结束。

1. 先建立直觉:Loop 到底是什么

假设用户给编码 Agent 一个任务:

修复这个 Python 项目的测试失败问题,并验证修复有效。

普通问答模型可能直接根据经验猜测原因,回答一段修改建议。但编码 Agent 通常需要真正操作项目:读文件、定位实现、编辑代码、运行测试,再根据测试结果决定下一步。

一个典型的过程是:

text
用户提出任务
    │
    ▼
模型根据指令和当前上下文决定下一步
    │
    ├── 返回最终答复 ──────────────────────┐
    │                                      │
    └── 请求调用工具                       │
            │                              │
            ▼                              │
      Harness 检查工具、权限和参数          │
            │                              │
            ▼                              │
      执行工具:读文件 / 改文件 / 跑测试     │
            │                              │
            ▼                              │
      将工具结果写回对话状态                │
            │                              │
            └────再次请求模型 ◀─────────────┘
                  │
                  ├── 还有问题:继续循环
                  └── 任务已完成:结束本轮

这里的关键并不是循环语法本身,而是环境反馈改变了模型下一步决策时所依据的信息。

例如:

  1. 模型请求读取 calculator.py。
  2. 工具返回代码,模型发现减法函数误用了 -。
  3. 模型请求修改文件。
  4. Harness 执行修改。
  5. 模型请求运行测试。
  6. 测试报告仍然失败,模型得到新的错误信息,继续调查。
  7. 第二次修改后测试通过,模型选择结束任务。

如果没有“执行—反馈”这一环,模型只能给出建议,无法确认建议是否真的奏效。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 / StepLoop 中的一次推进。不同框架的定义可能略有不同一次模型决策后执行一批工具,再更新状态
Turn用户发起的一轮交互任务用户说“修复测试”,直到 Agent 交回结果
Session / Thread可持续多轮交互的会话修复测试结束后,用户继续要求增加一个功能

一个 Turn 可以包含很多次模型推理,也可以包含多个工具调用。在支持并行工具调用的系统中,一次模型响应甚至可以请求执行多项互不依赖的工具。

还可以把 Loop 分成三个相互嵌套的层次:

  1. 任务级循环(outer loop):围绕用户目标持续工作,直到目标完成、需要用户输入或触发停止条件。
  2. 工具级循环(tool-use loop):模型请求一个或多个工具,Harness 执行,然后把结果交给模型。
  3. 工具内部循环(inner loop):例如模型请求执行一段 Python 代码,而这段代码内部自己循环处理一万条记录。若使用支持程序化工具调用的运行时,模型还可能生成程序来编排多次工具调用。

因此,“一个 Agent 只有一个 while 循环”只是教学模型。生产系统可能有多层控制流程、异步任务、子 Agent、权限暂停和恢复等机制。

3. 一个标准 Agent Loop 是怎么运行的

3.1 初始化:把任务转换成模型可用的上下文

运行时通常会组合这些信息:

  • 用户当前的任务;
  • 系统指令与工作规则;
  • 当前会话中有用的历史消息;
  • 可用工具清单、工具说明和参数 Schema;
  • 工作目录、操作系统、项目结构等环境上下文;
  • 权限策略、限制条件以及相关技能或项目规则。

这些信息组成模型当前决策的依据。编码 Agent 通常不会在每次推理时把整个电脑交给模型,而是向模型暴露一个经过约束的工具接口,例如读取文件、搜索代码、编辑文件、运行命令等。

3.2 模型做出决策:输出文本,还是请求工具

模型响应通常会走向两类结果:

  • 最终消息:模型认为当前任务可以结束,并给用户答复。
  • 工具请求:模型选择某个工具,并提供结构化参数。

例如,概念上的工具请求可能是:

json
{
  "name": "read_file",
  "arguments": {
    "path": "src/calculator.py"
  }
}

这不是模型直接读取磁盘,而是模型根据工具定义生成一项请求。实际文件访问仍由本地程序、沙箱或者远程工具服务执行。

Anthropic 把这称为模型与应用之间的工具使用契约:应用声明可用操作及输入输出形式;模型决定何时、如何请求这些操作;执行代码的一方返回结果。[2]

3.3 Harness 接管工具请求

Harness 是围绕模型运行的控制层。它一般需要完成这些工作:

  1. 检查工具名称是否存在;
  2. 验证输入参数是否符合 Schema;
  3. 判断该动作是否需要拒绝、自动允许或等待用户审批;
  4. 为工具提供工作目录、沙箱、超时等运行环境;
  5. 执行工具,并记录输出、错误码、耗时等信息;
  6. 把结果按协议格式回传给模型。

在编码场景中,工具可能是 Read、Grep、Edit、Write、Bash,也可能是 MCP 工具、浏览器或专门的代码执行服务。具体工具名称属于产品实现,核心模式相同。

3.4 将工具结果回写上下文

假设工具返回:

text
AssertionError: expected 5, got -1

如果 Harness 只把这个错误打印给用户,而没有让模型看到,模型下一步就无法基于该结果修正策略。

所以运行时会把工具结果追加到会话状态,再发起下一次模型请求。对话历史可以抽象成:

text
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 判断继续还是结束

最常见的教学逻辑是:

python
if model_requested_tools:
    execute_tools_and_continue()
else:
    return_final_answer()

真实系统还必须考虑其他终止或暂停原因:

  • 模型给出最终消息;
  • 达到最大步数或时间预算;
  • 用户中断任务;
  • 工具请求被拒绝;
  • 模型 API 出错或限流;
  • 上下文空间即将耗尽,需要压缩历史;
  • 需要用户补充信息或批准敏感操作;
  • 模型响应处于可恢复的暂停状态,而不是已经完成。

例如,在 Claude 的工具使用协议中,客户端执行工具时,常见流程是一直处理 stop_reason == "tool_use",直到模型返回其他停止原因;不过“不是工具调用”不一定代表任务成功,还需要检查实际停止原因及错误状态。[2]


4. 代码示例一:最小 Loop 的伪代码

先不绑定某个厂商的 API,可以把核心机制缩减为下面的 Python 伪代码:

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+ 执行:

bash
python mini_agent_loop.py
python
from __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"])

运行时应该看到什么

输出大致如下:

text
--- 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 协议,而不是构建完整的代码编辑器。

bash
uv add anthropic
export ANTHROPIC_API_KEY="你的 API Key"
python
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("超过最大工具轮数;任务被安全停止")

这段代码在做什么

  1. 用 tools 声明模型可请求的操作,以及操作参数的 Schema。
  2. 每轮把累积的消息发送给模型。
  3. 如果返回 tool_use,由本地 Python 代码执行工具。
  4. 通过 tool_use_id 把每项结果对应回具体调用。
  5. 将结果放入下一轮消息,让模型继续决策。
  6. 如果不再请求工具,则输出模型答复;同时设置最大轮数限制。

此代码是协议骨架,不是生产级安全模板。它没有实现并行调用、用户审批、复杂的停止原因处理、请求重试、上下文压缩、审计记录或细粒度工具授权。若打算让模型执行 Bash、任意 Python、文件写入或联网操作,必须进一步引入权限策略和隔离执行环境。Anthropic 官方工具使用教程也采用相同的基本循环:处理工具请求、运行工具、把 tool_result 放回消息,再重复直到工具调用阶段结束。[4]

另外,示例中的 ANTHROPIC_MODEL 需要设置为你的账号当前可用且支持工具调用的模型名称;模型名称和 SDK 版本可能变化,应以官方文档为准。


7. Claude Code 与 Codex:Loop 的共性和差异

Claude Code 与 Codex 的具体实现并不完全公开,也会随版本变化。下面区分官方公开的运行机制和从产品行为抽象出的通用架构,不把概念图当成某个产品内部全部代码的精确复刻。

7.1 共性:模型决定行动,Harness 控制执行

可以用一个共同的抽象来理解二者:

text
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 的一个典型编码任务理解为:

  1. 读取项目规则和用户任务;
  2. 模型先调查代码,选择读文件或搜索工具;
  3. Harness 按工具权限和策略执行;
  4. 模型拿到结果后,选择编辑文件或运行命令;
  5. 对具有风险的操作,系统可能要求确认或直接拒绝;
  6. 运行测试并处理失败反馈;
  7. 在模型给出完成消息、达到停止条件或被用户中断时结束/暂停任务。

这是一种架构层面的描述,而不是声称每次任务都严格采用这七个固定步骤。模型可能改变顺序、并行请求工具或选择完全不同的调查路径。

要点: 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 不只是:

python
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

这段历史并不是一条“新技术彻底替代旧技术”的直线。很多系统至今仍混用固定工作流、条件分支和模型驱动循环。下面的时间线主要用于理解思路如何逐步叠加。

阶段一:传统程序与固定工作流

在大模型普及前,程序也有循环、状态机和工作流:

python
for item in items:
    validate(item)
    transform(item)
    save(item)

或者开发者显式定义状态:读取 -> 校验 -> 处理 -> 保存 -> 结束。

特点是:下一步基本由代码规定,过程可预测、易测试;缺点是面对开放任务时,开发者必须提前枚举大量可能分支。它并不是 Agent Loop,但为后来的 Agent 提供了循环、状态、异常处理和控制流等工程基础。

阶段二:单次 LLM Completion

最早的一类大模型应用通常是:

text
输入 Prompt -> 调用模型一次 -> 返回文本

模型可以写计划、解释错误或生成代码,但应用本身不一定会执行这些行动,也不能自然地根据执行结果修订操作。

如果用户说“帮我修复 bug”,单次 Completion 可能给出一段建议;但它不会自动知道修改是否成功,除非后续有程序去执行、收集结果并再次调用模型。

阶段三:ReAct——将推理与行动交替起来

2022 年的 ReAct 研究提出在模型轨迹中交替组织推理和行动,使模型可以根据环境返回的观测更新计划,而不是只输出一段内部推理或一系列预先写好的动作。[6]

概念示意:

text
Thought / 分析:需要查证某个信息
Action / 行动:调用搜索工具
Observation / 观测:工具返回结果
Thought / 分析:现有信息还不够
Action / 行动:再次查询
Observation / 观测:返回补充信息
Final / 答复:整合证据后给出结论

今天的 Agent 不一定使用 ReAct 论文中的特定文本格式;重要的贡献是推理与行动交替,并以观察结果推动后续决策。这就是现代 Tool-Using Agent Loop 的核心直觉之一。

阶段四:模型学会何时、如何使用工具

2023 年的 Toolformer 研究探索让模型学习何时调用 API、传递什么参数,以及如何把工具返回结果用于之后的预测。[7] 同期,模型服务商也逐渐提供原生 Function Calling / Tool Calling 接口,让模型可以通过结构化输出请求应用函数,而不是只输出一段需要应用自行解析的自然语言。

这一步把“模型说它想做什么”变成一个更稳定的程序接口:

json
{
  "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 解决“如何持续行动”,不自动解决“行动是否安全”

模型可能建议运行:

bash
rm -rf ./build

也可能错误地猜测某个环境变量存在,或者生成了错误的文件路径。即便模型已经输出结构化工具调用,也不代表调用一定合理、安全或成功。

因此运行时需要权限层:

text
模型提出工具请求
      │
      ▼
参数验证 / 工具白名单
      │
      ▼
权限策略检查
  ┌───┼────────┐
拒绝  自动允许  询问用户
  │      │       │
停止/反馈 执行   等待批准/拒绝

Anthropic 的 Agent SDK 文档给出了权限模式、Hook、允许/拒绝规则和 canUseTool 回调等控制方式。[5] Codex 开源仓库也包含执行审批和沙箱策略相关实现,说明执行控制是 Harness 的独立工程责任,而不是模型生成工具调用的附带结果。[12]

9.2 Loop 解决“能够继续”,不自动解决“知道何时结束”

模型可能重复执行:

text
运行测试 -> 同样失败 -> 修改 -> 运行测试 -> 同样失败 -> 修改 -> ...

常见对策包括:

  • 最大模型调用数或最大工具步数;
  • 总时间和 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:把下一步交给模型动态决定

概念上:

python
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]

例如:

text
输入任务
   │
   ▼
需求分类(确定性规则)
   │
   ├── 不清晰 -> 请求补充信息
   │
   └── 清晰
        │
        ▼
  Agent 研究代码(模型驱动)
        │
        ▼
  安全检查(确定性规则)
        │
        ├── 不通过 -> 拒绝/要求修订
        │
        └── 通过
             │
             ▼
        执行修改
             │
             ▼
        测试验证
             │
             ├── 失败 -> 返回修复步骤
             └── 通过 -> 总结并结束

在这个设计中,Agent Loop 可以发生在“研究代码”或“修复步骤”节点内部;图控制则决定更大的业务流程何时进入安全检查、测试、审批或结束。

11.3 更实用的组合方式

可以把系统分成三个层级:

  1. 业务工作流层:通过确定性节点表达必须遵守的顺序与分支,例如支付前确认、发布前审批、数据变更后校验。
  2. Agent Loop 层:给模型有限的工具集合,让它在明确任务边界内自行调查、规划和执行多步操作。
  3. 工具与环境层:实现读写文件、运行代码、访问 API、查询数据库、沙箱隔离、权限审批与审计。

不要为了使用 Agent 而把所有逻辑都交给模型。可以确定性实现的流程就使用确定性代码;真正需要根据上下文灵活判断的环节,再使用模型驱动的 Loop。


12. 设计自己的 Agent 时,应该把控制权放在哪里

可以从以下问题开始做架构决策。

12.1 让模型决定什么?

适合交给模型的部分通常包括:

  • 根据任务选择下一项调查动作;
  • 从多种解决方案中选择候选方案;
  • 结合工具反馈调整原有计划;
  • 判断还缺什么信息;
  • 归纳结果并向用户解释。

这些决策往往难以通过固定规则穷举。

12.2 让代码决定什么?

应该优先由程序强制执行的部分通常包括:

  • 哪些工具在当前任务里可用;
  • 参数和输出格式是否合法;
  • 可访问的文件、目录、网络和凭证范围;
  • 哪些动作需要审批或双重确认;
  • 最大步数、总预算、超时和取消条件;
  • 必须通过哪些测试或验证才算完成;
  • 重要副作用是否成功、是否重复执行;
  • 日志、审计和状态持久化规则。

例如,“尽量别删除用户文件”属于模型能理解的指令,但它不应成为唯一防护。实际系统还应该限制可访问范围,并在高风险动作上引入显式策略或确认。

12.3 一个可操作的最小生产架构

text
┌─────────────────────────────────┐
│          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:

  1. Loop 是反馈闭环,不是重复生成文本。 模型发起行动,工具作用于环境,环境结果反过来影响模型下一步决策。
  2. 模型不等于 Agent。 模型提供决策能力;Harness 管理执行、状态、权限、上下文与停止条件。
  3. 一次用户 Turn 可以包含多次模型调用和工具执行。 不要把 Turn、Model Call 和 Tool Call 混为一谈。
  4. Loop 可以托管,但不能因此忽略控制。 使用 Claude Agent SDK、Codex 或其他 Agent 运行时,可以少写底层编排代码;业务权限、工具边界和完成标准仍然需要明确设计。
  5. 现代 Agent 通常是多层控制系统。 外层工作流负责可靠的业务顺序,内层 Loop 负责灵活的探索和行动,工具层负责具体执行。
  6. 正确结束和安全执行与继续行动同样重要。 最大步数、权限策略、测试验证、错误处理和上下文管理不是外围附加功能,而是稳定 Loop 的一部分。

如果你以前用 LangGraph 自己定义大量 Node、Edge 来控制 Agent,那么值得采用的思路不是把这些代码全部删除,而是重新划分边界:把适合模型动态选择的部分交给 Agent Loop,把必须确定性执行的规则保留在程序和工作流里。


14. 参考资料

以下是官方文档或原始论文。产品和 SDK 持续更新,代码接口、模型名称及可用功能请以当前文档为准。

  1. OpenAI, Unrolling the Codex agent loop (2026-01-23):解释 Codex CLI 的 Agent Loop、模型推理、任务 Turn 和上下文管理。
    https://openai.com/index/unrolling-the-codex-agent-loop/

  2. Anthropic, How tool use works:解释工具使用契约、客户端工具与 tool_use / tool_result 循环。
    https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works

  3. OpenAI, Function calling:工具 Schema、工具调用与应用端执行的多步流程。
    https://developers.openai.com/api/docs/guides/function-calling

  4. Anthropic, Build a tool-using agent:从单次调用逐步构建包含多轮工具调用、错误处理和并行工具的 Loop。
    https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent

  5. Anthropic, Claude Agent SDK — Permissions:权限模式、允许/拒绝规则、Hooks 与运行时审批。
    https://code.claude.com/docs/en/agent-sdk/permissions

  6. Shunyu Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models (2022).
    https://arxiv.org/abs/2210.03629

  7. Timo Schick et al., Toolformer: Language Models Can Teach Themselves to Use Tools (2023).
    https://arxiv.org/abs/2302.04761

  8. LangChain / LangGraph, Graph API overview:状态、节点、边和条件路由。
    https://langchain-ai.github.io/langgraph/concepts/low_level/

  9. OpenAI, Agents:比较 Agents API、Agents SDK 与 Responses API 的运行时责任划分。
    https://developers.openai.com/api/docs/guides/agents

  10. OpenAI, Sandbox Agents:区分 Harness 控制平面与沙箱执行平面。
    https://developers.openai.com/api/docs/guides/agents/sandboxes

  11. OpenAI, Programmatic Tool Calling:让模型生成程序以编排多次工具调用、循环和条件分支。
    https://developers.openai.com/api/docs/guides/tools-programmatic-tool-calling

  12. OpenAI, Codex CLI source code:公开的 Codex 实现,可进一步阅读执行、审批和沙箱相关模块。
    https://github.com/openai/codex

杨彦星 后端工程师 · 序语程言主理人

做了八年后端,最近在 AI 行业折腾。喜欢把复杂的事情拆开讲清楚,讨厌「显而易见」这四个字。这里记录我踩过的坑和攒下的经验,愿你少踩一点。

[email protected]

查看主页