---
title: Agent 循环与执行器
url: https://doc.liz6.com/ai/03-agent-systems/01-agent-loop
locale: zh
area: ai
tags:
- 大模型与 Agent
- Agent 执行系统
date: 2026-06-30
modified: 2026-09-10
description: 让模型调用工具，需要把建议变成可核验的执行记录。本文从一次库存查询建立最小循环，再加入结果关联、停止状态、依赖与未知写结果。读完应能指出某个动作只是被提出、已经执行，还是已经通过任务验收。
---

# Agent 循环与执行器

让模型调用工具，需要把建议变成可核验的执行记录。本文从一次库存查询建立最小循环，再加入结果关联、停止状态、依赖与未知写结果。读完应能指出某个动作只是被提出、已经执行，还是已经通过任务验收。

## 从固定流程到反馈决策

固定工作流由程序预先安排主要步骤，例如读取订单、抽取字段、校验、入库；模型可以参与某一步，但主要路径由代码控制。Agent 则让模型根据观察动态决定下一步，例如测试失败后自行定位相关文件，再选择新的修复动作。两者可以组合：一个确定性外层流程也能包含受限的 Agent 子任务。[Building effective agents](https://www.anthropic.com/engineering/building-effective-agents)

ReAct 研究将推理与行动交替组织，让模型依据环境反馈更新后续处理。[ReAct](https://arxiv.org/abs/2210.03629) 这不是按“有没有聊天界面”划分产品：聊天系统也可以调用工具，Agent 也可能一次回答就完成简单请求。

模型推理产生表示和输出，外部动作由某个执行环境落实。客户端工具由应用执行；服务端工具可能由供应商执行。因此权限和审计边界分布在实际执行组件中，不能假定所有操作都在本地宿主，也不能把模型生成的命令文本视为已经执行。

<svg viewBox="0 0 760 391.60760498046875" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="模型提出动作，执行器核验并执行，结果进入下一次决策" style="max-width:100%;height:auto" font-family="Source Han Sans CN,Microsoft YaHei,sans-serif">
<defs><marker id="agent-execution-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 Z" fill="#64748b"></path></marker></defs>
<rect width="760" height="391.60760498046875" rx="12" fill="#f8fafc"></rect>


<g transform="translate(0 0)"><rect x="30" y="85" width="200" height="75" rx="8" fill="#e0e7ff" stroke="#c7d2fe"></rect><text x="130.0" y="119.5" font-size="15" fill="#1e293b" text-anchor="middle" font-weight="600">模型响应</text><text x="130.0" y="141.5" font-size="12" fill="#475569" text-anchor="middle" font-weight="400">文本或结构化调用</text><rect x="280" y="85" width="200" height="75" rx="8" fill="#e0e7ff" stroke="#c7d2fe"></rect><text x="380.0" y="119.5" font-size="15" fill="#1e293b" text-anchor="middle" font-weight="600">执行前核验</text><text x="380.0" y="141.5" font-size="12" fill="#475569" text-anchor="middle" font-weight="400">名称、参数、权限、预算</text><rect x="530" y="85" width="200" height="75" rx="8" fill="#e0e7ff" stroke="#c7d2fe"></rect><text x="630.0" y="119.5" font-size="15" fill="#1e293b" text-anchor="middle" font-weight="600">工具执行</text><text x="630.0" y="141.5" font-size="12" fill="#475569" text-anchor="middle" font-weight="400">本地或远端服务</text><rect x="280" y="230" width="200" height="75" rx="8" fill="#e0e7ff" stroke="#c7d2fe"></rect><text x="380.0" y="264.5" font-size="15" fill="#1e293b" text-anchor="middle" font-weight="600">结果与任务状态</text><text x="380.0" y="286.5" font-size="12" fill="#475569" text-anchor="middle" font-weight="400">调用 ID、返回值、证据</text><line x1="230" y1="122" x2="275" y2="122" stroke="#64748b" stroke-width="1.8" marker-end="url(#agent-execution-arrow)"></line><line x1="480" y1="122" x2="525" y2="122" stroke="#64748b" stroke-width="1.8" marker-end="url(#agent-execution-arrow)"></line><line x1="630" y1="165" x2="630" y2="267" stroke="#64748b" stroke-width="1.8" marker-end="url(#agent-execution-arrow)"></line><line x1="630" y1="267" x2="485" y2="267" stroke="#64748b" stroke-width="1.8" marker-end="url(#agent-execution-arrow)"></line><line x1="280" y1="267" x2="130" y2="267" stroke="#64748b" stroke-width="1.8" marker-end="url(#agent-execution-arrow)"></line><line x1="130" y1="267" x2="130" y2="165" stroke="#64748b" stroke-width="1.8" marker-end="url(#agent-execution-arrow)"></line><text x="28" y="366" font-size="13" fill="#475569" text-anchor="start" font-weight="400">模型响应结束、一次工具返回、业务目标完成，是三个不同的完成边界。</text></g><text x="24" y="29" font-size="19" fill="#0f172a" text-anchor="start" font-weight="700"><tspan x="24" dy="0">模型提出动作，执行器核验并执行，结果进入下一次决策</tspan></text>
</svg>

### 完成声明与完成证据

完成是一项待验证的声明。这个模型允许在没有执行、没有观察时直接宣称完成，验收层仍独立检查证据。观察也有时间边界：修改前的读取不能证明修改后的状态。

**完成声明与完成证据**

执行成功、读到结果、声明完成是三件事；验收必须检查实际证据。


## 一次工具调用的完整路径

### 工具定义同时是模型接口和执行契约

名称帮助模型识别动作，描述解释用途与限制，schema 定义参数结构。以只读库存查询为例，描述应说明查的是可售数量、商品使用哪个标识，以及结果的时间或版本含义；“获取库存”四个字无法表达这些边界。

```json
{
  "name": "lookup_stock",
  "description": "按商品 SKU 查询当前可售数量；只读，不预留库存。",
  "input_schema": {
    "type": "object",
    "properties": {"sku": {"type": "string"}},
    "required": ["sku"],
    "additionalProperties": false
  }
}
```

这是 Claude 风格的工具定义；其他协议的字段名可能不同。schema 约束形状，不自动验证身份、商品是否属于当前租户或操作者能否查看。结构化输出也不能代替执行端验证：执行端必须只分派已注册工具，并重新核验输入与权限。[Tool use with Claude](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)

一次查询可以按下面的记录追踪：

| 阶段 | 示例记录 | 必须保留的含义 |
|---|---|---|
| 模型提出动作 | 调用 ID c17，lookup_stock，sku=A1 | 这是一项请求，尚未执行 |
| 执行前核验 | 工具存在、参数有效、主体有读权限 | 允许执行的范围 |
| 工具实际返回 | available=3，库存版本 v83 | 这是哪次读取的结果 |
| 回传观察 | 调用 ID c17 对应上述结果 | 防止结果错配 |
| 模型继续决策 | 回答可售数量或提出下一步 | 查询不等于已预留库存 |

执行结果最好包含明确状态、必要数据与来源定位。错误也应区分无此商品、参数无效、权限不足、限流或结果未知；笼统返回“失败，重试”会诱导无意义循环。

### 消息协议必须完整

Claude 客户端工具通过 `tool_use` 与匹配的 `tool_result` 关联；同一响应可包含多个调用。应用应保留完整 assistant 响应，并按协议组织所有相应结果。thinking、不透明状态和非文本内容不能被“只保存 text”的简化逻辑丢掉。

错误格式反馈可能影响之后的模型行为，但不是在请求中“悄悄训练了模型权重”。协议要求与推测性的行为解释应分开，具体工具消息排列规则以 [工具接口文档](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview) 为准。

## 响应停止与任务状态

模型没有再请求工具，只说明这次模型响应已停止。它可能正确完成了任务，也可能遗漏验证、需要用户补充信息、被截断，或错误地宣称成功。应用需要独立记录任务状态，例如 running、waiting、succeeded、failed、budget_exhausted，并用任务验收条件决定何时 succeeded。

下表是 Claude 接口中的常见分支，完整枚举和后续请求格式见 [Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)。

| 停止原因 | 意义 | 处理方向 |
|---|---|---|
| `tool_use` | 请求客户端工具 | 完成核验、执行与结果回传 |
| `end_turn` | 模型结束本轮 | 检查任务验收条件后收尾 |
| `max_tokens` | 输出达到限制 | 检查完整性；不要执行截断参数 |
| `pause_turn` | 服务端长工具流程暂停 | 按该接口要求续接，应用仍负责循环限额 |
| `refusal` | 模型拒绝当前生成 | 记录原因，按业务策略终止或提供替代 |

流式传输不会解除 `max_tokens`。如果工具参数 JSON 只收到一半就断流，不能猜测剩余字段后执行；如果服务端工具仍在运行，也不能把 pause 当作客户端应再次执行同一副作用的信号。

以“修复导入重复提交”为例，验收条件可能是补丁存在、普通测试通过、重启恢复测试通过。模型输出“已修复”但只跑了普通测试时，任务仍是未完成。验收标准应在开始时明确，并关联到实际产物版本。

## 依赖、重放与副作用

### 并行资格来自依赖关系

同时读取两份独立文档通常可以并行；“查询库存后按结果预留”有数据依赖，不能先猜参数并行发出。即便两个动作输入独立，若同时修改同一个文件或同一资源，也可能发生冲突。

执行器需要同时考虑数据依赖、共享资源、服务并发上限与权限。并行节约的是可重叠的等待时间，代价包括峰值负载和结果合并；不能因为模型同一轮提出多个调用就无条件全部并发。

### 调用 ID 不等于业务幂等键

调用 ID 用来把响应关联回提议。模型重试时可能产生新的调用 ID，但表达同一个业务动作；这时仅按调用 ID 去重，仍可能重复创建订单或发送通知。业务操作键应由应用按明确事务语义产生、保存，并由支持幂等的服务核验同一键与同一请求是否一致。

<svg viewBox="0 0 760 346.60760498046875" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="超时后的关键问题：动作失败，还是结果未能返回？" style="max-width:100%;height:auto" font-family="Source Han Sans CN,Microsoft YaHei,sans-serif">
<defs><marker id="agent-unknown-outcome-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 Z" fill="#64748b"></path></marker></defs>
<rect width="760" height="346.60760498046875" rx="12" fill="#f8fafc"></rect>


<g transform="translate(0 0)"><rect x="25" y="62" width="150" height="48" rx="8" fill="#e0e7ff" stroke="#c7d2fe"></rect><text x="100.0" y="91.0" font-size="15" fill="#1e293b" text-anchor="middle" font-weight="600">执行器</text><rect x="315" y="62" width="150" height="48" rx="8" fill="#e0e7ff" stroke="#c7d2fe"></rect><text x="390.0" y="91.0" font-size="15" fill="#1e293b" text-anchor="middle" font-weight="600">订单服务</text><rect x="585" y="62" width="150" height="48" rx="8" fill="#e0e7ff" stroke="#c7d2fe"></rect><text x="660.0" y="91.0" font-size="15" fill="#1e293b" text-anchor="middle" font-weight="600">数据库</text><line x1="100" y1="114" x2="100" y2="280" stroke="#64748b" stroke-width="1.8" stroke-dasharray="5 4"></line><line x1="390" y1="114" x2="390" y2="280" stroke="#64748b" stroke-width="1.8" stroke-dasharray="5 4"></line><line x1="660" y1="114" x2="660" y2="280" stroke="#64748b" stroke-width="1.8" stroke-dasharray="5 4"></line><line x1="100" y1="150" x2="390" y2="150" stroke="#64748b" stroke-width="1.8" marker-end="url(#agent-unknown-outcome-arrow)"></line><text x="245.0" y="140" font-size="13" fill="#334155" text-anchor="middle" font-weight="400">请求：操作键 K42</text><line x1="390" y1="200" x2="660" y2="200" stroke="#64748b" stroke-width="1.8" marker-end="url(#agent-unknown-outcome-arrow)"></line><text x="525.0" y="190" font-size="13" fill="#334155" text-anchor="middle" font-weight="400">提交已完成</text><line x1="390" y1="250" x2="230" y2="250" stroke="#64748b" stroke-width="1.8" stroke-dasharray="5 4"></line><text x="245" y="223" font-size="13" fill="#b45309" text-anchor="middle" font-weight="400">响应丢失</text><text x="208" y="256" font-size="22" fill="#b91c1c" text-anchor="start" font-weight="400">×</text><text x="28" y="321" font-size="13" fill="#475569" text-anchor="start" font-weight="400">不能把超时直接当作未执行；用业务操作键查询或按服务的幂等协议重试。</text></g><text x="24" y="29" font-size="19" fill="#0f172a" text-anchor="start" font-weight="700"><tspan x="24" dy="0">超时后的关键问题：动作失败，还是结果未能返回？</tspan></text>
</svg>

| 失败状态 | 是否可直接重试 | 合理处理 |
|---|---|---|
| 只读查询超时 | 通常可以，但数据可能变化 | 有限重试并保留读取时点 |
| 参数校验失败 | 原样重试无意义 | 返回可修正的具体字段错误 |
| 无权限 | 不通过改措辞绕过 | 在授权范围内调整或报告阻塞 |
| 写操作超时、结果未知 | 不能假定没执行 | 按操作键查询结果或使用服务幂等协议 |
| 已完成动作，回传丢失 | 重做可能重复副作用 | 重放已保存结果并核对业务状态 |

取消同样不是回滚：停止模型生成或取消本地等待，不保证远端已经开始的动作被撤销。执行器应把“确认未执行”“已完成”“结果未知”区分开，恢复时先消除未知状态。

### 超时以后，重试会写几次？

把网络结果与业务结果分开：请求没有返回，不等于没有写入。这里服务端事实始终可见，调用方却只有未知状态；查询或同键重试才取得确认。去重必须由服务端保证，模型记住“我已调用”不能提供同等保证。

**超时以后，重试会写几次？**

调用 ID 标识尝试，操作键标识业务操作；服务端去重才能使相同操作键不重复产生副作用。


## 一个可运行的本地执行器示例

下面用预设模型响应模拟一次只读库存查询。它验证工具白名单、参数、调用结果关联与请求上限，不调用模型 API，也不执行真实业务写入。生产系统还需要持久状态、身份与权限、超时、完整协议适配和审计。

```python
STOCK = {"A1": 3}

def execute(call):
    if call.get("name") != "lookup_stock":
        return {"ok": False, "error": "unknown_tool"}
    args = call.get("input")
    if not isinstance(args, dict) or set(args) != {"sku"}:
        return {"ok": False, "error": "invalid_arguments"}
    if not isinstance(args["sku"], str):
        return {"ok": False, "error": "invalid_sku"}
    if args["sku"] not in STOCK:
        return {"ok": False, "error": "not_found"}
    return {"ok": True, "available": STOCK[args["sku"]]}

def run(model, max_requests=3):
    observations, seen = [], set()
    for _ in range(max_requests):
        response = model(list(observations))
        if response["kind"] == "final":
            # 模型结束不等于业务验收成功。
            return {"state": "model_ended", "text": response["text"],
                    "observations": observations}
        if response["kind"] != "tool":
            return {"state": "invalid_response"}
        call = response["call"]
        call_id = call.get("id")
        if not isinstance(call_id, str) or not call_id or call_id in seen:
            return {"state": "invalid_call_id"}
        seen.add(call_id)
        observations.append({"call_id": call_id, "result": execute(call)})
    return {"state": "budget_exhausted", "observations": observations}

def scripted(observations):
    if not observations:
        return {"kind": "tool", "call": {
            "id": "c17", "name": "lookup_stock", "input": {"sku": "A1"}}}
    assert observations[0] == {
        "call_id": "c17", "result": {"ok": True, "available": 3}}
    return {"kind": "final", "text": "当前查询到可售数量为 3，尚未预留。"}

result = run(scripted)
assert result["state"] == "model_ended"
assert execute({"name": "delete_stock"})["error"] == "unknown_tool"
assert execute({"name": "lookup_stock", "input": {"sku": 1}})["error"] == "invalid_sku"
assert run(scripted, max_requests=1)["state"] == "budget_exhausted"
print(result["text"])
```

例子在一次工具调用后、还没取得最终响应时耗尽请求预算，会明确返回 budget_exhausted。这比将任何循环退出都映射为“成功”更准确。真实应用也应持久保存已经完成的调用，避免进程重启后丢失副作用记录。

## 工具边界与循环收敛

把“最多运行十轮”作为最后保险还不够。应观察每轮是否获得新证据、修改了产物、消除了错误；连续得到相同失败且条件没有变化时，重复动作通常无法推进。可以返回更具体的错误、改变检索范围，或报告缺失条件，而不是无限重发。

工具输出需要限制大小并保留可继续读取的位置。截断日志时明确标注截断区间、总量及文件定位，不能让模型误以为拿到了完整证据。上下文管理见 [上下文工程](/ai/02-context-and-interfaces/02-context-engineering.md)。

专用工具将参数与业务语义暴露给执行器，便于校验、授权和记录。Shell 工具提供更通用的能力，但命令允许列表本身不足以限制程序所能做的全部操作；仍需通过执行身份、文件和网络访问范围、隔离环境与资源限制控制实际权限。具体策略应匹配已经授权的任务，不要让每一步可逆操作都退化为重复确认，详见 [安全与防护](/ai/03-agent-systems/05-authority-and-tool-boundaries.md)。

## 补充：环境模拟器与真实执行

Agent 训练和评估可以使用环境模拟器：策略提出动作，模拟器产生观察，再让策略继续。语言世界模型尝试学习这种环境响应，例如 [Qwen-AgentWorld 模型卡](https://huggingface.co/Qwen/Qwen-AgentWorld-35B-A3B) 描述了面向多类交互环境的模拟能力。这与真正修改文件或调用业务服务属于不同执行路径。

模拟器可能生成貌似合理但状态不一致的结果。评估时不仅看单次观察是否像真，还要看连续动作的因果一致性、资源约束与长期状态是否保持；模拟环境中的高分不能直接证明真实工具执行成功率。是否适合作为模拟器或 Agent，应分别测量，不能用激活参数量断言某类角色天然可行或不可行。

继续阅读：[工具接口与 MCP](/ai/03-agent-systems/02-tools-and-mcp)。
