返回 FEED
AGENT2026-05-29

Agent Harness 不应该是一个框架,而应该是一堆可替换的 Worker

大多数 Agent 团队不会自己构建 Harness,而是直接采用现成的:LangChain、LangGraph、OpenAI Agents SDK、Anthropic SDK、CrewAI、AutoGen。编排循环、工具调用、记忆管理、状态机——所有东西作为一个整体被一次性决策 adoption。如果其中某个部分不合适,你只能 fork、对抗、或者 workaround。

Mike Piccolo 认为这个形状是错的。这也是每个长期运行的 Agent 团队最终都会从头重写 Harness 的原因。

Harness 不是一样东西,是十三样东西

如果你把生产级 Agent Harness 的职责剥开,会得到一个类似这样的清单:

  1. 接收客户端的 turn 请求并持久化
  2. 解析模型提供商的凭证
  3. 查询所选模型的能力(视觉、工具、流式、上下文窗口)
  4. 驱动 per-turn 状态机:provision、流式输出、运行工具、steer、teardown
  5. 加载和提供 Skill 定义(函数请求形状、错误码、使用说明)
  6. 组装系统提示词:mode paragraph、identity preamble、工作目录、默认 Skills
  7. 将 token 流式返回给客户端
  8. 每个工具调用前通过策略引擎检查权限
  9. 暂停需要人工决策的工具调用,将答案路由回正确的 turn
  10. 追踪 LLM 开销,按 workspace 或 agent 维度控制预算
  11. 工具调用前后运行 hooks(日志、脱敏、自定义副作用)
  12. 将 session 持久化为分支树,支持 fork 和 resume
  13. 上下文窗口满时压缩 session 历史
  14. 向 UI 发射事件流
  15. 跨每一步携带 OpenTelemetry trace,以便调试

每个严肃的 Agent Harness 都要处理其中大部分。昂贵的方案做全,便宜的方案砍角,然后在生产环境中把砍掉的角重新建起来。

框架的问题在于:它们把这些东西捆绑成单体,每个只提供一个版本。一年后你会发现框架自带的策略引擎不是你想要的,而替换它意味着替换整个 Harness。

iii 的赌注:Worker 栈

iii engine 的赌注是:这些不应该是一个块。应该是一组运行在共享引擎上的 Worker,每个可替换、每个独立版本化、每个通过单一原语互联:trigger。

Harness 变成一堆可安装的 Worker,"build your own" 不再意味着 "fork a framework",而是 "swap a few workers"。

实际生产栈:11 个 Worker,1 个引擎

iii 的 monorepo 中,实际生产栈包含 11 个 Worker,每个的职责一行说清:

Worker职责
harness::trigger接收 turn 请求,转发到 run::start,种子 OpenTelemetry span
turn-orchestrator驱动 per-turn 状态机,管理 TurnStateRecord
provisioning启动 microVM,下载 Skills,组装系统提示词
assistant_streaming调用 provider 流式输出,管理 SSE 通道
auth-credentials凭证解析和 token 获取
policy权限检查,读取 iii-permissions.yaml
approval-gate人工审批的暂停和恢复
context-compaction上下文压缩
budget-tracker开销追踪
hooks工具调用前后的副作用
session-storageSession 持久化为分支树

每个 Worker 发布独立版本。每个可以单独运行(pnpm dev:<worker>),也可以通过 iii worker add 安装为发布二进制,或作为复合入口点一起启动。

关键洞察:每个格子都是别人可以给你一个不同 Worker 的地方,而你可以保留其余。不喜欢静态模型目录?插入一个从 live API 读取的 Worker。不喜欢文件凭证?插入一个从 secrets manager 读取的 Worker。想要不同的 turn FSM?替换 turn-orchestrator,其他所有依赖只调用 run::start 和读取 turn_state,通过同一个 bus,所以栈的其余部分不变。

一次 Turn 的完整流程

1. 触发

浏览器/CLI/聊天 POST 一个 turn,通过 harness::trigger 携带 {session_id, message_id, payload}。Harness meta-worker 将 payload 转发到 run::start。这一步的存在是为了让 OpenTelemetry span wrapper 将 session 和 message ID 作为 baggage 种子化,传播到每个嵌套的 iii.trigger 调用。trace 树在另一端是一个连通的图。

2. 编排启动

run::start 落在 turn-orchestrator 上。它持久化 run 请求,在 iii state 的 session/<sid>/turn_state 中种子化初始 TurnStateRecord,然后立即返回。实际工作在 durable per-state machine 内部发生,由 turn-step FIFO 的 publish 唤醒。

两个终止状态:stopped(通过 finishSession() 干净退出)和 failed(意外 handler throw 路由到这里,ack 队列停止重试,抛出 message_complete{stop_reason:'error'}agent_end)。Teardown 是从任何 turn-end 路径内联调用的 finishSession(),不是单独的入队步骤。

3. 供应

provisioning 做三件事:

  • 如果 run 需要隔离执行,启动 microVM
  • system_default_skills 中的每个 namespace 调用 directory::skills::download,让 iii-directory 预缓存 run 启动时需要的 skill bodies
  • 分三层组装系统提示词:从 run_request.mode 选取的 mode paragraph(plan、ask 或 agent)、iii identity preamble(教模型 agent_trigger 约定和 directory::skills::get 按需发现模式)、以及默认 skills 的索引附录

调用者可以在 run::start 上传递 system_prompt 覆盖整个提示词;否则 orchestrator 构建它。函数 schema 来自 live engine catalog。

4. 流式输出

assistant_streaming 调用匹配 run 的 provider 字段的 provider::<name>::stream。provider worker 通过 auth::get_token(auth-credentials)拉取凭证,将模型的 SSE 响应流式注入 iii channel,orchestrator 排空该 channel 并在 agent::events 上发射 message_update 事件供 UI fanout。

Channel 创建和读取循环活在 provider-stream.ts 的 pull-based MessagePump 后面,所以流式状态保持聚焦在转换上。

5. 工具调用与策略检查

当 assistant 返回工具调用时,FSM 进入 function_execute。每个工具调用通过 orchestrator 中的单一阻塞点 dispatchWithHook

consultBefore 直接调用 policy::check_permissions,5 秒超时。policy worker(默认栈中的 harness meta-worker)读取 iii-permissions.yaml,将调用的 function_id 匹配规则集,返回三种结果之一:

  • allow:dispatch 继续,orchestrator 触发目标函数并写入结果
  • deny:dispatch 短路,返回 DenialEnvelope,结果变成 denial record
  • needs_approval:单个调用 park 到 turn 的 awaiting_approval 列表。batch 的其余部分继续 dispatch。当有一个或多个条目 pending 时,turn 才过渡到 function_awaiting_approval

6. 审批唤醒

审批唤醒是反应式的、共享的。orchestrator 在 scope approvals 上注册恰好一个 turn::on_approval 状态触发器。当 console 调用 approval::resolve,approval-gate worker 将 approvals/<sid>/<cid> = {decision, reason} 写入 iii state。该写入触发 turn::on_approval,推进受影响的 session。

function_awaiting_approval 只读取刚刚落地的决策,逐个 dispatch(allow 变成预批准的 dispatch,deny 或 aborted 变成 synthetic denial),当 awaiting_approval[] 为空时推进。没有 per-call resume 函数要注册,没有启动时重扫来恢复 pending approvals。一个触发器覆盖每个 session。

Fail-closed by construction:如果 policy worker 不可达或 5 秒超时触发,consultBeforegate_unavailable envelope deny 调用。如果 iii::durable::publish 本身出错,hook fanout 返回 publish_failed: true,orchestrator 将其视为 deny。

7. 延迟优化

这种架构带来一些延迟收益:

  • after-function-call hook 通过 subscriber-presence cache 短路 publish_collect,当没有 durable subscriber 注册该 topic 时,每个执行的函数调用减少约 500ms
  • tearing_down 内联到 finishSession(),每个 turn 减少一个 durable queue hop
  • context-compaction 订阅 orchestrator 在 turn 边界发射的专用 agent::turn_end 流,所以 compactor 唤醒是 per-turn 而非 per-event
  • session-create fanout 状态触发器只按 scope 门控并匹配 in-process,所以之前的 per-write harness::session::is_create_event RPC 被移除

8. Steer 与结束

batch 完成后,steering_check 决定继续、停止、或达到 max_turns。如果 continue,循环回 assistant_streaming。如果 stop 或 max,finishSession() 内联运行:发射 agent_end,释放 sandbox,过渡到 stopped

为什么这很重要

当前 Agent 生态的框架思维是一种隐性 tradeoff:你得到一个打包好的解决方案,但失去了在每个层级做独立决策的能力。iii 的 Worker 栈模式把 Harness 变成一组可替换的组件,每个组件只负责一件事,通过统一协议互联。

这不是说框架没有价值——对于快速原型和标准化场景,框架仍然是好选择。但对于长期运行的生产系统,可组合性 beats 便利性。当你需要在策略引擎、凭证管理、预算控制或上下文压缩上做自定义时,Worker 栈让你只替换那一个 Worker,而不是重写整个 Harness。

iii 的 trigger 原语是这个架构的关键:它让每个 Worker 说同一种语言,无论它是用 TypeScript、Python 还是 Rust 写的。这种语言无关的互操作性,加上独立版本化和独立部署,构成了真正可扩展的 Agent 基础设施。