大多数 Agent 团队不会自己构建 Harness,而是直接采用现成的:LangChain、LangGraph、OpenAI Agents SDK、Anthropic SDK、CrewAI、AutoGen。编排循环、工具调用、记忆管理、状态机——所有东西作为一个整体被一次性决策 adoption。如果其中某个部分不合适,你只能 fork、对抗、或者 workaround。
Mike Piccolo 认为这个形状是错的。这也是每个长期运行的 Agent 团队最终都会从头重写 Harness 的原因。
Harness 不是一样东西,是十三样东西
如果你把生产级 Agent Harness 的职责剥开,会得到一个类似这样的清单:
- 接收客户端的 turn 请求并持久化
- 解析模型提供商的凭证
- 查询所选模型的能力(视觉、工具、流式、上下文窗口)
- 驱动 per-turn 状态机:provision、流式输出、运行工具、steer、teardown
- 加载和提供 Skill 定义(函数请求形状、错误码、使用说明)
- 组装系统提示词:mode paragraph、identity preamble、工作目录、默认 Skills
- 将 token 流式返回给客户端
- 每个工具调用前通过策略引擎检查权限
- 暂停需要人工决策的工具调用,将答案路由回正确的 turn
- 追踪 LLM 开销,按 workspace 或 agent 维度控制预算
- 工具调用前后运行 hooks(日志、脱敏、自定义副作用)
- 将 session 持久化为分支树,支持 fork 和 resume
- 上下文窗口满时压缩 session 历史
- 向 UI 发射事件流
- 跨每一步携带 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-storage | Session 持久化为分支树 |
每个 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 秒超时触发,consultBefore 以 gate_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_eventRPC 被移除
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 基础设施。