# M01 最小Agent执行循环与模型协议

> 本章产出：一个可测试、有预算、有类型边界的最小执行循环。使用脚本模型离线验证执行语义，再选择性接入真实模型。

## 1. 一个while循环里藏着什么

最小循环看起来只有“调用模型→调用工具→回传结果”。真正需要设计的是每条箭头的契约：模型可能拒绝、输出截断、返回多个工具调用、参数不合法，工具可能超时或产生副作用，用户也可能取消。

SWE-agent团队的极简教程适合观察这个闭环，但把模型输出直接当Shell在宿主机运行不属于本课程默认做法。本章只开放无副作用的合成商店查询。[S18](../../references/SOURCES.md#s18)

## 2. 把供应商协议归一化，不抹掉重要差异

我们的内部模型输出是一个带标签的联合类型：`Final(text)`、`ToolCalls(calls)`、`Refusal(reason)`、`Incomplete(reason)`。工具调用有稳定的`call_id`、`name`与已解析的参数对象。一个模型响应同时带说明文本与工具调用时，说明文本只作为展示性输出，不能误判为任务最终完成。

适配层负责把具体API转换成这个类型。不要假设所有“OpenAI-compatible”服务都完全支持同样的schema、并行工具、流事件、usage统计或拒绝字段。通过契约测试确认你实际使用的能力。

Tool Calling让模型提出调用意图，客户端负责执行并将结果按关联标识返回；结构化输出约束格式，但不替你核验订单归属、金额或商业政策。[S34](../../references/SOURCES.md#s34) [S35](../../references/SOURCES.md#s35)

课程的Provider接口故意不固定某一个模型名。真实运行必须记录provider、model、SDK版本、配置和响应标识；凭证只能来自服务端环境或密钥管理系统，不写入课程网页或Trace。

## 3. 最小循环的正确控制路径

```text
检查取消/截止时间/预算
  → 构建本轮上下文
  → 请求模型
  → 分类响应
      Final → 返回候选最终回答
      Refusal → 记录拒绝并安全结束
      Incomplete → 按原因恢复或明确失败
      ToolCalls → 逐个校验和授权 → 执行 → 追加结果 → 下一轮
```

“Final”只是模型停止行动的信号。业务层还需要核验目标是否完成。循环的职责不应扩展到自行修改业务账本。

一个工具调用必须恰好产生一个可关联的工具结果事件，即使结果是参数错误。否则下一轮模型会看到悬空调用或错误对应关系。为了简单，本章串行执行工具；M08才讨论哪些调用可以并行。

预算至少包含最大模型轮次、最大工具调用数、截止时间、最大输出量。`max_turns=5`不是“保证五轮内完成”，只是最坏资源边界。超限应成为可诊断的终态，不应悄悄把半成品标为成功。

<!-- interaction:I02 -->

## 4. 可复现的脚本模型与真实模型有什么区别

`ScriptedModel`按照预先准备的响应序列返回，不做语言理解。它能精确复现“第二轮输出坏参数”“第三个工具超时”等问题，适合测试执行器。但脚本模型通过测试不能证明模型能够理解自然语言。

我们把证据分成两种。**机制正确性证据**来自离线确定性测试；**任务能力证据**来自实际模型在固定任务集上的多次试验。两者必须分开报告。

例如离线脚本为：查询A100，查询物流，给出带证据的回复。你要检查三轮模型调用、两次工具调用、call_id对应、工具失败不会被当成成功。接上模型后还要检查它是否会选择正确工具、是否编造订单，以及是否会遗漏澄清。

## 5. 错误不是一个大而无当的Exception

参数错误属于调用协议问题，可以将安全、简短的诊断反馈给模型；身份不符属于权限错误，不应提示另一个租户资源的存在；读工具暂时超时可按预算重试；写工具结果未知必须进入核对流程；模型拒绝应该被保留为独立结果；取消属于控制信号，不应被宽泛异常处理吞掉。

建议内部错误对象包含`code`、`retryable`、`safe_message`和`details_ref`。详细堆栈保留在受控日志，模型只看到必要信息。可重试并不表示无限重试，重试次数也消耗同一任务预算。

一个错误设计是：捕获所有异常，向模型返回“请再试一次”。它会把权限失败变成攻击面，把永久错误变成费用循环，把取消请求变成无效按钮。

## 6. 流式输出：可以先显示，不能先执行

模型可能先输出工具名，再逐块输出JSON参数。收到`{"amount_cents":1`时，不知道最后是1、100还是10000。中间字符串不是可以执行的操作。

正确流程是按响应和call_id聚合增量，等待该工具调用的完成事件，再解析完整JSON、校验schema、检查授权，最后执行。界面可以显示“正在准备退款申请”，但不能在参数尚未闭合时启动退款。两个并行工具的增量可能交错，缓冲区不能只用一个全局字符串。

还要处理响应整体失败：收到若干增量后连接中断，不代表调用已经完整。缓冲区需要状态`collecting/completed/failed`；重复增量、重复完成事件和断线重连由适配层的协议测试覆盖。课程交互卡只模拟这一状态，不连接供应商。[S34](../../references/SOURCES.md#s34)

<!-- interaction:I03 -->

## 7. 上下文与日志最小结构

每一轮记录：`run_id`、`step_id`、输入消息引用、工具schema版本、模型响应分类、usage是否可得、耗时、错误类型。工具事件再记录`call_id`、名称、参数摘要、结果引用和权限决策。

不要求记录模型不可见的内部推理，也不把“推理文本”当事实解释。保存可观察的输入、输出、工具行为与业务状态即可支持本课程的调试。对可能含凭证和个人信息的内容，优先存哈希、摘要和受访问控制的对象引用。

一个容易踩的坑是所有消息都放进`user`角色。这样无法保留谁提出调用、哪个结果对应哪个调用，也可能把工具文档中的指令误当用户要求。内部事件模型与供应商消息角色要有明确映射。

## 8. 如何组织代码，避免一开始就造大框架

先保留四个模块：`model`只产生候选行动；`tools`定义工具与鉴权；`runner`负责循环和预算；`events`负责可观察记录。领域规则不塞进模型适配器，数据库事务不塞进Prompt模板。

给Runner注入模型、工具注册表和时钟。这三个依赖可被替换，就足够开始故障测试。不要提前抽象复杂插件系统、十种记忆后端或任意图语言。

初版代码最重要的性质是“每条分支都能测试”。当你不能轻易制造模型拒绝、坏JSON或工具超时，说明依赖边界还不清楚。

## 9. 本章过关检查

你应能从空文件写出基本循环，并解释哪一个层次终止任务、哪里检查预算、哪个字段关联工具结果。提交至少四条Trace：正常查询、坏参数、模型拒绝、预算耗尽。

完成L01。其参考实现仅是可运行的教学内核，不是多租户生产服务；代码里的离线脚本不应被包装成真实LLM评测。下一章开始处理模型提出了“合法JSON但不合法操作”的情况。

<!-- knowledge:K15 -->
