AgentRunner 的模型—工具循环
对应源码
| 主题 | 源码 |
|---|---|
| 输入和输出契约 | nanobot/agent/runner.py::AgentRunSpec、AgentRunResult |
| 生命周期 hook 外壳 | nanobot/agent/runner.py::AgentRunner.run、nanobot/agent/hook.py |
| 模型—工具主循环 | nanobot/agent/runner.py::AgentRunner._run_core |
| Provider 请求 | nanobot/agent/runner.py::AgentRunner._request_model、nanobot/providers/base.py::LLMResponse |
| Tool 执行 | nanobot/agent/runner.py::AgentRunner._execute_tools、AgentRunner._run_tool |
学习目标
读完后,你应当能够:
- 解释
AgentRunSpec和AgentRunResult; - 画出 AgentRunner 每个 iteration 的主要分支;
- 说明 Tool Call 如何回到下一轮模型请求;
- 找到上下文治理、checkpoint 和 injection 的位置。
前置知识
- 理解 Tool Calling 完整闭环;
- 已阅读 AgentLoop 状态机和 ContextBuilder;
- 知道 callback 和 async Task 的基本概念。
1. AgentRunner 不负责产品层问题
类定义说明:
Run a tool-capable LLM loop without product-layer concerns.
它不决定:
- 消息来自哪个 Channel;
- Session key 如何产生;
- OutboundMessage 发给谁;
- WebUI 显示什么协议;
- Session 文件放在哪里。
这些由 AgentLoop 和外围模块负责。
2. AgentRunSpec 是本次执行契约
核心字段可分为六组:
| 分组 | 字段示例 |
|---|---|
| 输入 | initial_messages、tools、model |
| 生成参数 | temperature、max_tokens、reasoning_effort |
| 边界 | max_iterations、max_tool_result_chars、context budget |
| 策略 | concurrent_tools、fail_on_tool_error、retry mode |
| 上下文 | workspace、session_key |
| 回调 | progress、stream、retry wait、checkpoint、injection |
| 长任务 | goal predicate、continue message、max-iteration finalization |
它将 AgentLoop 的配置和当前 Turn 状态压缩成 Runner 可以使用的最小输入。
3. AgentRunResult 是统一结果
包括:
final_contentmessagestools_usedusagestop_reasonerrortool_eventshad_injections
AgentLoop 不需要理解 Runner 内部每个 Provider 分支,只消费这个统一结果。
4. 主循环
AgentRunner._run_core() 核心形式:
for iteration in range(spec.max_iterations):
...每轮大致如下:
flowchart TD A["准备 messages_for_model"] --> B["Hook: before_iteration"] B --> C["请求 Provider"] C --> D["解析 reasoning / usage"] D --> E{"should_execute_tools?"} E -->|是| F["追加 assistant tool_calls"] F --> G["checkpoint: awaiting_tools"] G --> H["执行 Tools"] H --> I["追加 role=tool 结果"] I --> J["checkpoint: tools_completed"] J --> K["排空 pending injections"] K --> A E -->|否| L{"空响应 / length / error?"} L -->|需要恢复| M["retry / finalization / continue"] M --> A L -->|最终文本| N["生成 AgentRunResult"]
5. 每轮先做上下文治理
Runner 不直接把累积的 messages 原样发送。它创建 messages_for_model:
- 删除孤立 tool results;
- 为缺失 tool results 补回放占位;
- microcompact 较早内容;
- 应用 tool result budget;
- snip history;
- 再次修复可能产生的工具消息边界。
重要设计:
治理作用于发送给模型的临时视图,不应破坏用于保存本轮增量的原始 messages 边界。
如果治理失败,Runner 记录异常并尝试最小修复,而不是立即丢弃整个 Turn。
6. Provider 响应的三条主路
最终文本
没有可执行 Tool Call,content 非空,进入最终化。
Tool Call
response.should_execute_tools 为真:
- 构造 assistant tool-call 消息;
- 保存
awaiting_toolscheckpoint; - 执行工具;
- 为每个调用追加 role=tool 消息;
- 保存
tools_completedcheckpoint; - 继续下一 iteration。
异常或不完整响应
可能进入:
- 空响应重试;
- finalization retry;
finish_reason=length恢复;- Provider error;
- fallback;
- max iterations finalization。
7. should_execute_tools 比 has_tool_calls 更严格
Provider 可能返回 tool calls,但 finish reason 是 refusal、content filter 或 error。Runner 只在以下 finish reason 下执行:
tool_callsfunction_callstop
否则记录警告并忽略这些调用。
这是安全和协议一致性边界,不能简化成:
if response.tool_calls:
execute()8. Checkpoint 的位置
Runner 在两个关键阶段回调 AgentLoop:
awaiting_tools
assistant tool call 已产生
工具结果尚未完成
tools_completed
本批工具结果已完成
下一次模型请求尚未开始这些 checkpoint 用于中断恢复。Runner 只发布结构化 checkpoint,持久化到 Session metadata 的工作由 AgentLoop 回调完成。
9. Injection 的位置
同一 Session 在当前 Turn 期间收到的新消息,可能进入 pending queue。Runner 在安全边界排空:
- 工具执行后;
- 某些错误后;
- 最终回复前。
注入消息会追加为新的 user 消息;如果最后一条也是 user,则合并内容,保持 role 序列合法。
这种设计允许用户在长 Turn 中补充信息,而不启动第二个竞争 Turn。
10. Hook 的作用
Runner 通过组合 Hook 处理:
- before/after run;
- before/after iteration;
- before execute tools;
- progress;
- reasoning;
- stream;
- usage;
- file edit events。
核心循环发布通用生命周期,具体入口或产品层选择如何显示。
11. 停止条件
常见 stop_reason 包括:
completedtool_errorerrorempty_final_response- max-iteration 相关结果
- continuation 或 goal 相关结果
具体取值应以当前 _run_core() 分支和测试断言为准。不要在外部扩展中假设未知 stop reason 永远不会出现。
观察任务
在 _run_core() 中记录每轮:
iteration
len(messages)
len(messages_for_model)
response.finish_reason
len(response.tool_calls)
tools_used
stop_reason分别发送:
- 一个无需工具的问题;
- 一个必须读取 workspace 文件的问题;
- 一个工具参数故意不完整的问题。
比较三条路径。
源码与测试
nanobot/agent/runner.pyAgentRunSpecAgentRunResultAgentRunner.runAgentRunner._run_core
nanobot/providers/base.py::LLMResponsetests/agent/test_runner_core.pytests/agent/test_runner_governance.pytests/agent/test_runner_injections.pytests/agent/test_runner_persistence.pytests/agent/test_runner_goal_continue.py
自检
- 为什么 Runner 使用临时
messages_for_model,而不是直接修改原列表? - Checkpoint 为什么由 Runner 产生、由 AgentLoop 持久化?
- 为什么 Tool Call 完成后要继续下一 iteration?
- 新消息为什么不能在任意时刻插入 messages?
has_tool_calls与should_execute_tools有什么区别?
下一步
阅读 Tool 注册、校验与执行。