# 代理运行框架

agent 运行框架是在运行时将用户消息（或 webhook 触发，或 cron 定时触发）转换为一次完整的、使用工具的 LLM 交互。它负责工具调用循环、子代理调度、触发器分流流水线，以及围绕它们的钩子接口。它不负责 **不** 负责提供方 HTTP 传输、工具实现、提示词分段组装或记忆存储——这些都是由运行框架组合的独立领域。

本页先讲解一次轮次中会发生什么，然后逐步深入各个组成部分。

## 一次轮次的形态

每一次轮次——无论是用户刚输入了一条消息、Telegram webhook 刚刚触发，还是早上 9 点的 cron 刚刚跳动——都遵循同一个生命周期：

```
┌─ 输入 ─────────────────────────────────────────────────────────┐
│ 用户消息 · 渠道输入 · webhook · cron · composio 事件 │
└──────────────────────────┬────────────────────────────────────────┘
                           │
                           ▼  （仅外部触发器）
                ┌──────────────────────┐
                │   触发器分流         │  分类 → 丢弃 / 通知 /
                │   （小型本地 LLM）   │  生成反应器 / 生成编排器
                └──────────┬───────────┘
                           │
                           ▼
            ┌──────────────────────────────┐
            │      Agent::turn()           │
            │  1. 恢复对话记录             │
            │  2. 构建系统提示词*          │
            │  3. 注入记忆上下文           │
            │  4. 进入工具调用循环 ────┼──► 提供方调用
            │  5. 分发工具调用  ────┼──► 工具执行 / 子代理生成
            │  6. 上下文守卫 / 压缩        │
            │  7. 检查停止钩子             │
            │  8. 最终助手文本             │
            └──────────┬───────────────────┘
                       │ 异步执行，在用户看到回复之后
                       ▼
              ┌─────────────────┐
              │  轮次后钩子     │  档案员 · 学习 · 成本日志 ·
              │  钩子          │  事件记忆索引
              └─────────────────┘

* 系统提示词只在第一轮构建——后续
  轮次会逐字复用已渲染的提示词，因此推理
  后端的 KV 缓存前缀仍然有效。
```

本页其余部分是同一张图的展开版。

## 会话与 `Agent::turn`

一个 **会话** 是指一个正在运行的 `Agent` 实例的实时对话。该 `Agent` 结构体拥有：

* 对话历史（系统 + 用户 + 助手 + 工具消息）。
* 要调用的提供方客户端（模型由 [模型路由器](/openhuman/zh/gong-neng/model-routing.md)).
* 解析）。
* 一个记忆加载器，用于在每条用户消息之前加载相关记忆。
* 每轮预算——最多工具迭代次数、最大载荷大小、最大 USD 成本。
* 本地动作预算——针对会产生副作用的工具动作的滚动小时上限，读取自 `config.autonomy.max_actions_per_hour`.

`Agent::turn(user_message)` 是热路径。在一次轮次中，它会：

1. **恢复会话对话记录** 如果这是一个新进程——从磁盘重新加载完全相同的提供方消息，以便推理后端的 KV 缓存前缀仍然命中。
2. **构建系统提示词** （仅在第一轮）。这会引入身份、灵魂、个人资料、记忆、已连接集成、可用工具、安全前言——由提示词分段构建器组装。
3. **注入记忆上下文** 针对新的用户消息，通过记忆加载器注入：来自 [记忆树](/openhuman/zh/gong-neng/obsidian-wiki/memory-tree.md)的相关片段，并附上引用，以便 UI 显示来源。
4. **进入工具调用循环** （下一节）。
5. **在后台生成轮次后钩子** ——用户会先拿到回复，然后档案员 / 学习 / 成本记录才完成。

系统提示词 **不** 不会在后续轮次中重新构建。即使是细微的字节变化也会使 KV 缓存前缀失效并强制完全重新预填充，因此动态的逐轮上下文（记忆回忆、新学到的片段）会作为用户可见的消息内容追加，而不是拼接进系统提示词。

## 工具调用循环

在 `Agent::turn`内部，工具调用循环是内核引擎。它最多运行 `max_tool_iterations` 轮（默认 10）：

```
loop {
    1. 上下文守卫      - 如果历史太大，进行微压缩 / 自动压缩
    2. 停止钩子检查    - 预算上限、最大迭代数、自定义终止开关
    3. 提供方调用      - 发送消息 + 工具规格，流式返回响应
    4. 解析响应        - 将助手文本与工具调用分离
    5. 如果没有工具调用   - 返回最终文本
    6. 执行工具调用    - 分发每一个调用（下一节）
    7. 过大内容摘要    - 通过摘要器代理处理巨大的工具输出
    8. 追加结果       - 将工具结果推入历史，再循环一次
}
```

每次迭代都会发出一个实时的 `AgentProgress` 事件，因此 UI 可以渲染逐 token 流式输出、“正在调用工具 X”状态，以及每次迭代的成本更新。

### 工具分发与工具调用方言

不同的 LLM 使用不同的工具调用方言。运行框架通过一个 `ToolDispatcher` trait 对此进行抽象，它有三个具体实现：

* **Native** - 原生支持工具调用 API 的提供方（Anthropic、OpenAI）。工具调用会作为结构化字段返回，而不是出现在文本主体中。
* **XML** - 供那些并非原生训练为工具调用，但可以遵循指令的模型使用的回退方案。工具会被包装在 `<tool_call>{...}</tool_call>` 标签中，位于助手文本里。
* **P-Format** - 某些较小模型使用的紧凑文本格式。

分发器按提供方选择，这使得循环本身与方言无关。同一套循环代码可驱动 Claude、GPT、Gemini，以及本地 Ollama 模型。

### 循环中的上下文管理

冗长的工具调用链可能会超过上下文窗口。这里由两层机制处理：

* **工具结果预算** - 每个工具结果都会根据每次调用的字节预算进行检查。超出的内容会被强制截断，并附上说明标记，让模型知道它没有看到完整输出。
* **微压缩 / 自动压缩** - 当总历史逐渐逼近上下文窗口时，运行框架会在下一次提供方调用之前将较早的轮次压缩成摘要。压缩后的历史会保留系统提示词和最近的轮次不变（KV 缓存稳定性），并重写中间部分。

### 超大工具结果——摘要器绕行

某些工具调用会返回巨大的载荷——比如 Composio 动作吐出 200 KB 的 JSON，网页抓取返回 50 KB 的 markdown，或者一个 `file_read` 跨越数千行日志。强行在中途截断会丢掉落在截断点之后的任何内容。

当工具结果超过摘要器阈值时，在进入父代理历史之前，它会被路由到一个专门的 `摘要器` 子代理。摘要器会按照一份抽取契约压缩载荷，保留标识符和关键信息，而父代理只看到压缩后的摘要。如果摘要失败，或者载荷大得离谱，以至于为它支付一次 LLM 调用都不经济，那么下游仍然会以强制截断作为最后兜底。

### 缺失命令的自我修复

当代码执行子代理运行一个 shell 命令，而运行时返回“command not found”时，自我修复拦截器会捕获该错误，生成一个 `ToolMaker` 子代理来为缺失的命令编写 polyfill 脚本，并重试原始调用。每个命令都有尝试次数上限，因此一个真正不可能执行的命令不会无限循环。

## 子代理——编排器模式

OpenHuman 是 **多代理**的。与用户聊天的那个代理是 **编排器** ——一个高级、策略级的代理，决定何时直接回答、何时使用直接工具，以及何时生成一个专门的子代理。

### 为什么要多代理

一个什么都懂的单一代理，其系统提示词也会大到像一本小书。将工作拆分给专门代理意味着：

* 每个子代理都获得一个 **窄系统提示词** ，只包含它需要的部分（身份 / 记忆 / 安全前言都可以被剥离）。
* 每个子代理都获得一个 **经过筛选的工具注册表** - 集成代理不需要文件系统工具，编码代理不需要 Composio 目录。
* 子代理历史永远不会泄漏回父代理——父代理只看到一个压缩后的工具结果，而不是内部对话。
* 更便宜的模型可以处理叶子工作。编排器运行在强推理模型上；研究子代理可以运行在更快、更便宜的模型上。

### 内置原型

每个原型都位于 `agents/<name>/` 下，并包含一个 `agent.toml` （元数据、工具范围、模型提示）和一个提示词：

| 原型                   | 编排器选择它时的情形                                          |
| -------------------- | --------------------------------------------------- |
| `orchestrator`       | 顶层代理。不会被其他编排器生成。                                    |
| `planner`            | 多步拆解——将复杂请求分解为有序子任务。                                |
| `researcher`         | 网页 / 文档查找，引用追踪。                                     |
| `code_executor`      | 在工作区中编写、运行和调试代码。                                    |
| `critic`             | 代码审查，对另一个代理输出进行质量检查。                                |
| `摘要器`                | 压缩过大的工具结果（由运行框架调用，通常不是模型调用）。                        |
| `archivist`          | 记忆提炼——哪些要保留，哪些要忘记。                                  |
| `tool_maker`         | 自我修复——为缺失的 shell 命令编写 polyfill。                     |
| `tools_agent`        | 用于任意工具绑定任务的通用专才。                                    |
| `integrations_agent` | 绑定到特定的 Composio 工具包（Gmail、GitHub、Slack…），用于该工具包的动作。 |
| `trigger_triage`     | 将传入的外部事件分类为丢弃 / 通知 / 生成反应器 / 生成代理。                  |
| `trigger_reactor`    | 对已分流的触发器做轻量级响应，不需要完整的编排器轮次。                         |
| `morning_briefing`   | 由 cron 运行的精选每日简报。                                   |
| `welcome` / `help`   | 引导流程。                                               |

自定义原型以 TOML 文件形式随 `$OPENHUMAN_WORKSPACE/agents/*.toml` （或者 `~/.openhuman/agents/*.toml` 用于用户全局专才）提供。ID 冲突时，自定义定义会覆盖内置定义。

### 运行子代理

当编排器调用 `spawn_subagent` （或以下 `delegate_*` 便捷工具之一）时，运行器会：

1. 从任务本地变量中读取父代理的执行上下文——父代理的提供方、沙箱模式、取消边界、对话记录根。
2. 解析子代理的模型——先看内联的 `model` 覆盖，然后看配置级别的固定设置（`[orchestrator].model`, `[teams.*].lead_model`, `[teams.*].agent_model`），再看原型提示或继承的父模型。
3. 按定义中的 `tools`, `disallowed_tools`以及 `skill_filter`过滤父代理的工具注册表。在 `fork` 模式下，父代理的完整注册表会原样继承。
4. 构建窄系统提示词，省略定义要求剥离的部分。
5. 使用与父代理相同的机制运行内部工具调用循环。
6. 返回一个压缩后的文本结果。子代理内部历史永远不会再拼回父代理——编排器只会看到一个工具结果，然后继续。

对于不需要阻塞编排器轮次的任务， `spawn_worker_thread` 会在后台运行子代理，而编排器会立即继续。

### 生成层级与分层

并非每个代理都允许生成每一个其他代理。运行框架建模了一个三层层级，它反映了模型之间的成本 / 延迟 / 思考深度划分：

```
Chat        （快速、面向 UX —— 例如编排器使用 `chat` 提示）
  │
  ├─► Worker      ◄─── 快速路径：一次委派，叶子节点执行工作
  │
  └─► Reasoning   （慢速、深度思考 —— 例如 planner 使用 `reasoning` 提示）
        │
        └─► Worker  ◄─── 深度路径：推理负责拆解，worker 负责执行
```

每个 `AgentDefinition` 都携带一个 `agent_tier` 字段（`chat` / `reasoning` / `worker`，默认 `worker`）。约定如下：

| 层级          | 可以生成                  | 不得生成                      | 典型成员                                                                               |
| ----------- | --------------------- | ------------------------- | ---------------------------------------------------------------------------------- |
| `chat`      | `reasoning`, `worker` | 另一个 `chat`                | `orchestrator`                                                                     |
| `reasoning` | `worker`              | 另一个 `reasoning`，任意 `chat` | `planner` （今天是规范的那个）                                                               |
| `worker`    | 无[^1]                 | 任何                        | researcher, code\_executor, critic, archivist, tool\_maker, integrations\_agent, … |

**为什么有这些规则。**

* *Chat → chat 没有意义。* chat 层的存在是为了快速 UX。一个 chat 代理再生成另一个 chat 代理，只会让 TTFT 翻倍并烧掉 token，却不会获得任何新能力。
* *Reasoning → reasoning 会让深度失控。* reasoning 层成本很高。推理代理的链条往往会反复拆解同一个问题，并产生失控的层级。
* *Worker → anything 会把执行与编排混在一起。* worker 是叶子节点，因此父代理总是只看到一个压缩后的结果，而不是嵌套委派的对话记录。

**执行约束。** 两层机制：

1. **加载时（静态）。** [`agents::loader::validate_tier_hierarchy`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/agent/agents/loader.rs) 会遍历合并后的注册表（内置 + 工作区 TOML），并拒绝启动一个列出同层级或“worker 还带子代理”条目的注册表。内置原型在编译测试时检查；用户提供的 TOML 在工作区加载时检查。
2. **运行时深度门禁（动态）。** 与层级无关，子代理运行器会将总生成链深度限制为 `MAX_SPAWN_DEPTH = 3` ，通过一个在 `run_subagent`之间递增的任务本地计数器实现，并在错误层面表现为 `SpawnDepthExceeded` 代理错误。这意味着即使用户提供的 TOML 去掉了层级注释，也仍然无法递归超过三跳。

> **状态：** 加载时层级检查、 `agent_tier` 字段和运行时深度计数器任务本地变量都已启用。深度同时受静态加载约定和运行时 `MAX_SPAWN_DEPTH = 3` 守卫限制。

### 工具包专用专家

对于拥有数百个动作的 Composio 工具包（仅 GitHub 就有 500+），将每个动作都加载进子代理的工具集会把提示词长度撑爆。运行框架使用一个廉价的纯 CPU 过滤器（动词检测、token 重叠、动词对齐加权）将工具包的动作与父代理精炼后的任务提示进行排名，只把排名靠前的子集加载到子代理中。没有模型调用，纯启发式——快速且可解释。

## 分流——处理外部触发器

当 webhook 触发、cron 跳动，或者 Composio 事件到达时，系统不能直接把它交给编排器。大多数触发器都是噪音；有些值得通知；只有少数值得完整的一次代理轮次。 **触发器分流流水线** 就是这个门槛。

```
TriggerEnvelope ──► run_triage ──► TriageDecision ──► apply_decision
                       │                                     │
                       │                                     ├─► 丢弃（噪音）
                       │                                     ├─► 仅通知
                       │                                     ├─► 生成 trigger_reactor
                       │                                     └─► 生成 orchestrator
                       │
                       └── 小型本地 LLM（带云端 LLM 重试回退）
```

评估器刻意保持廉价——优先使用可用的小型本地模型，重试时回退到远程模型。决策会被缓存，因此相同的触发器不会重复分类。只有升级为“生成编排器”的触发器才会进入完整的 `Agent::turn` 机制。

## 钩子——可观测性与策略控制杆

两个钩子接口从相对两端包裹着循环：

### 停止钩子（轮次中）

停止钩子会在 **工具调用循环的迭代之间** 触发。它们是预算上限、速率限制和自定义终止开关的策略控制杆。内置钩子：

* **预算停止钩子** - 使用每次迭代的成本累加器，以 USD 为单位限制累计轮次成本。
* **最大迭代停止钩子** - 从代理持久配置之外限制迭代次数。
* **动作预算策略** - `SecurityPolicy` 强制执行 `config.autonomy.max_actions_per_hour` 针对会产生副作用的工具操作。用户可以在 Settings -> Advanced -> Agent autonomy 中调节它，或者运维人员可以通过 `OPENHUMAN_MAX_ACTIONS_PER_HOUR`.

返回 `Stop` 的钩子会以明确原因中止循环，调用方可以将其展示给用户。停止钩子不同于中断（下一节）：它们由策略驱动，而不是由用户驱动。

### 轮次后钩子

轮次后钩子会在 **轮次完成之后** 于后台触发。它们会获得一个 `TurnContext` 快照——用户消息、助手回复、每一次工具调用及其参数和结果、总耗时、迭代次数、会话 ID。内置消费者包括：

* **Archivist** - 提炼出轮次中哪些事实值得持久化到长期记忆。
* **Learning** - 反馈反思、工具追踪器和用户画像更新。
* **Cost log** - 最终的每轮成本记录。
* **事件记忆索引** - 将该轮写入 [记忆树](/openhuman/zh/gong-neng/obsidian-wiki/memory-tree.md) 作为未来回忆的一个块。

钩子通过 `tokio::spawn`运行，因此用户会先拿到回复，然后它们才会全部完成。

## 中断——优雅取消

一个 `InterruptFence` 会在循环中的固定安全点被检查——每次工具执行之前、每次子代理生成之前、每次提供方调用之前。当用户按下 Ctrl+C 或发送 `/stop`:

* 时，边界会翻转。
* 每个正在运行的子代理都会看到同一个标志（它通过 `Arc`共享），并在下一个检查点退出。
* 进行中的提供方流会被丢弃。
* 档案员仍然会用现有的任何部分上下文执行，因此对话不会丢失。

中断由用户驱动；停止钩子由策略驱动。它们共享底层的“干净地停止循环”管道，但入口不同。

## 成本核算

每个提供方响应都带有一个 `UsageInfo` 区块——输入 token、输出 token、缓存输入 token，以及由 OpenHuman 后端填充的权威 `charged_amount_usd` 。 `TurnCost` 会把一次轮次中的所有提供方调用累加起来，从而使运行框架能够：

* 通过进度通道发出每次迭代的成本遥测。
* 为预算停止钩子提供数据，以便失控的轮次在循环中途自我终止。
* 记录准确的轮次结束成本行。

当后端没有显示收费金额时（旧版本、未通过它计费的提供方），会用一个按层级划分的小费率表提供 token 费率的下限估计。只要可用，后端的直接成本总是优先。

## Fork 上下文——整个运行框架中的 KV 缓存复用

运行框架使用一个任务本地的 `ParentExecutionContext` 把父状态传递给子代理，而不至于让每个函数签名都膨胀。相同模式还会携带当前沙箱模式、中断边界和停止钩子列表。继承父代理提供方、模型和提示词前缀的子代理可以在推理后端上 **共享父代理的 KV 缓存前缀** ——这比从头重新预填充要便宜得多。

## 自我修复回顾

在主循环之上还有几个小型自适应系统：

* **缺失命令的自我修复** - `ToolMaker` polyfill、受限重试次数。
* **载荷摘要器断路器** - 会话中连续三次子代理失败会禁用摘要，回退到截断。
* **分流本地 vs 远程重试** - 先用本地 LLM；解析失败时回退远程。

这些都不会改变循环的形态——它们只是让常见失败模式能够在无需用户介入的情况下恢复。

## 在代码中查看的位置

运行框架完全位于 `src/openhuman/agent/`下。该目录中的 README 列出了公共接口；最关键的文件有：

| 文件 / 目录                         | 内容                             |
| ------------------------------- | ------------------------------ |
| `harness/session/turn.rs`       | `Agent::turn` - 上面描述的生命周期。     |
| `harness/tool_loop.rs`          | 内部工具调用循环。                      |
| `harness/subagent_runner/`      | `run_subagent`、fork 模式、过大结果移交。 |
| `harness/definition.rs`         | `AgentDefinition` - 原型所声明的内容。  |
| `harness/tool_filter.rs`        | 集成子代理的工具包动作排名。                 |
| `harness/payload_summarizer.rs` | 超大工具结果绕行。                      |
| `harness/self_healing.rs`       | 缺失命令拦截器。                       |
| `harness/interrupt.rs`          | 取消边界。                          |
| `dispatcher.rs`                 | 工具调用方言抽象。                      |
| `triage/`                       | 外部触发器分类 + 升级。                  |
| `agents/`                       | 内置原型——每个代理一个子目录。               |
| `hooks.rs` / `stop_hooks.rs`    | 轮次后和轮次中钩子接口。                   |
| `cost.rs`                       | 每轮 USD/token 核算。               |
| `progress.rs`                   | 发往 UI 的实时进度事件。                 |
| `memory_loader.rs`              | 按用户消息注入 Memory Tree 上下文。       |

## 另见

* [架构概览](/openhuman/zh/kai-fa/architecture.md) - 运行框架在整体架构中的位置。
* [记忆树](/openhuman/zh/gong-neng/obsidian-wiki/memory-tree.md) - 记忆加载器从哪里读取，以及轮次后钩子写到哪里。
* [自动模型路由](/openhuman/zh/gong-neng/model-routing.md) - 如何 `model: "hint:reasoning"` 解析为一个具体的 provider+model。
* [原生工具——代理协调](/openhuman/zh/gong-neng/native-tools/agent-coordination.md) - 面向用户的 `spawn_subagent`, `delegate_*`, `todo_write`.

[^1]: 技能通配项条目（`{ skills = "*" }`）是例外，因为它们会收敛为单一的 `delegate_to_integrations_agent` 工具，其目标是一个 worker——它们是扇出式委派接口，而不是递归生成。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://tinyhumans.gitbook.io/openhuman/zh/kai-fa/architecture/agent-harness.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
