@mfpiccolo: https://x.com/mfpiccolo/status/2060069083878408689
摘要
文章认为,当前像 LangChain 和 CrewAI 这样的智能体编排框架将独立关注点捆绑成一个整体模块,导致缺乏灵活性。文章介绍了 iii 引擎,其中每个职责都是一个独立的、可替换的工作单元,通过共享总线和单一触发原语连接,使开发者能够通过替换工作单元而非分叉框架来组合自己的编排方案。
查看缓存全文
缓存时间: 2026/05/29 14:10
如何构建你自己的代理工具框架???
大多数代理团队并不构建工具框架。他们采用现成的。LangChain、LangGraph、OpenAI Agents SDK、Anthropic SDK、CrewAI、AutoGen、循环、工具、记忆和编排都被当作一个单一决策,从货架上拿下来。工具框架就是你导入的一个框架。如果里面有些东西不合适,你要么分叉它,要么对抗它,要么绕开它。
我认为这种形态是错误的,这也是为什么每一个长期运行的代理团队最终都会从头重写其工具框架的原因。工具框架不是一件东西。它是十到十二件不同东西的捆绑包,因为周围的生态系统没有给你组合它们的方式。
Pi Agent 包走在正确的轨道上,但它们仍然处于“添加另一个服务并将其与所有其他服务集成”的范式中。iii 引擎将所有工作器视为等同,并完全移除了集成逻辑。提供者路由器、凭据保险库、策略引擎、审批关口、模型目录、会话存储、预算追踪器、调用后钩子分发器和持久化轮转循环是独立的关注点。所有这些都可以与你的队列、HTTP/API服务器、流式传输,甚至浏览器工作器互操作。一个将它们作为一个整体模块发布的框架,是在向你兜售一个你本不必做出的取舍。
iii 背后的赌注是,它们不应是一个整体模块。应该有一套运行在共享引擎上的工作器,每个可替换,每个独立版本化,每个通过一个单一原语连接:一个触发器(iii.trigger()),每个其他工作器也使用它。工具框架变成了一个可安装工作器的堆栈,而“构建你自己的”不再意味着“分叉一个框架。”它意味着“交换几个工作器。”
这篇文章会带你看看这实际是什么样子。驱动当前 iii 代理轮转的完整堆栈,为什么每一层都是其自己的工作器,以及如何替换其中任何一个。
一个代理工具框架必须完成的15项工作
如果你将一个生产级代理工具框架剥离到其职责,你会得到一份大致如下所示的列表:
- 接受来自客户端的轮转请求并持久化它
- 为将要调用的任何模型提供者解析凭据
- 查找所选模型实际能做什么(视觉、工具、流式传输、上下文窗口)
- 驱动每轮状态机:预配、流式助手、运行工具、引导、拆除
- 加载并提供技能主体,描述每个函数请求的形状、错误代码和使用说明
- 组装系统提示、模式段落、身份序言、工作目录和默认技能附录
- 在模型生成令牌时,将令牌流式传输回客户端
- 在每次工具调用(就是一个函数)运行之前,根据策略检查它
- 暂停需要人工决策的工具调用,并将答案路由回正确的轮次
- 按工作区或按代理预算追踪 LLM 支出
- 在工具调用前后运行钩子(日志记录、编辑、自定义副作用)
- 将会话持久化为分支树,以便支持分叉和恢复
- 在上下文窗口填满时压缩会话历史
- 发出事件流,供 UI 订阅
- (缺失的部分,我在每个代理公司的构建中都会看到)在每一步上携带一个 OpenTelemetry 跟踪,以便你能够调试它
每个严肃的代理工具框架都会实现大部分这些功能。昂贵的框架会实现全部。便宜的框架会偷工减料,然后在进入生产环境后再补上那些缺失的部分。这些框架将它们捆绑成一个单体,并为每个部分发布一个版本。
最后这部分就是让你付出代价的地方,因为一年后,你会发现你想要的那个策略引擎并不是框架自带的那个,而替换它意味着替换整个工具框架。
iii 工具框架将这十三项工作中的每一项都作为一个独立的工作器,发布在 workers.iii.dev 注册表上。每个工作器都使用相同的 WebSocket 协议。每个工作器都在同一个引擎总线上注册函数和触发器。每个工作器都可以通过 iii worker add 添加、可交换,并且可以用任何带有 SDK 的语言编写。
堆栈,按工作器划分
以下是来自 iii-hq/workers 单体仓库的实际生产堆栈,每个工作器的工作只用一行说明。整个捆绑包在 github.com/iii-hq/workers/harness 发布:
十一个工作器。一个引擎。每个都已发布版本。每个都可以作为独立进程运行(开发模式下用 pnpm dev:,作为发布二进制文件用 iii worker add),或者作为将它们一起启动的组合入口点的一部分。
这之所以重要:那个表格中的每一个框都是一个地方,别人可以给你一个不同的工作器,而你保留其余部分。不喜欢静态模型目录?插一个注册了 models::list 并从实时 API 读取的工作器。不喜欢基于文件的凭据?插一个注册了 auth::get_token 并从密钥管理器读取的工作器。想要一个不同的轮转 FSM 来处理以不同方式分支的工作流?替换 turn-orchestrator,每个依赖方通过同一个总线调用 run::start 并读取 turn_state,因此堆栈的其余部分不变。
循环实际如何运行
一轮的形态如下所示,按照工作器触发的顺序遍历它们。
浏览器/CLI/聊天通过 harness::trigger POST 一个轮转,包含 {session_id, message_id, payload}。工具框架元工作器将 payload 转发到 run::start。这个跃点存在是为了让 OpenTelemetry 跨度包装器能够将会话和消息 ID 作为 baggage 注入,这些 baggage 会传播到堆栈中每个工作器上的每个嵌套的 iii.trigger 调用。另一端的跟踪树是一个连接起来的图。
run::start 落在 turn-orchestrator 上。它持久化运行请求,在 iii 状态中的 session//turn_state 处种子化初始的 TurnStateRecord,然后立即返回。实际工作发生在持久的每轮状态机内部,由发布到 turn-step FIFO 的消息唤醒。两个终端状态是 stopped(通过 finishSession() 干净退出)和 failed(未预期的处理程序抛出会路由到这里,确认队列以便停止重试,并发送 message_complete{stop_reason:'error'} 加上 agent_end,以便 UI 显示原因)。拆除是一个内联的 finishSession() 端口,从任何轮转结束路径调用,而不是一个单独的入队步骤。
provisioning 做三件事。如果运行需要隔离执行,它会启动一个 iii-sandbox 微VM。它会为 system_default_skills 中的每个命名空间调用 directory::skills::download(默认是 ["iii://iii-directory/index"]),以便 iii-directory 预缓存运行开始时拥有的技能主体。它还会组装系统提示,分为三层:从 run_request.mode(plan、ask 或 agent)选取的模式段落,教授模型 agent_trigger 约定和 directory::skills::get 按需发现模式的 iii 身份序言,以及代理启动时附带的默认技能索引。调用者可以通过在 run::start 上传递 system_prompt 来覆盖整个提示;否则 orchestrator 会构建它。函数模式来自实时引擎目录。
assistant_streaming 调用 provider::::stream,具体是哪个提供者工作器取决于运行的 provider 字段。提供者工作器通过 auth::get_token(auth-credentials)拉取凭据,将模型的 SSE 响应流式传输到 iii 通道中,orchestrator 排空该通道,在 agent::events 上发出 message_update 事件,供 UI 分发。通道创建和读取循环位于 provider-stream.ts 中基于拉取的 MessagePump 后端,因此流式状态可以专注于转换。
当助手返回工具调用时,FSM 进入 function_execute。每个工具调用都通过 dispatchWithHook,这是 orchestrator 中的唯一阻塞点。consultBefore 直接以 5 秒超时调用 policy::check_permissions。策略工作器(在默认堆栈中是工具框架元工作器)读取 iii-permissions.yaml,将调用的 function_id 与规则集匹配,并返回三个结果之一:
allow:分派继续进行;orchestrator 触发目标函数并写入结果deny:分派短路,返回一个DenialEnvelope,结果成为一条拒绝记录needs_approval:单个调用进入轮次的awaiting_approval列表。批次的其余部分继续分派。只有当有一个或多个条目待处理时,轮次才转换为function_awaiting_approval。
审批唤醒是反应式的且共享的。Orchestrator 在 approvals 范围内恰好注册一个 turn::on_approval 状态触发器。当控制台调用 approval::resolve 时,approval-gate 工作器将 approvals// = {decision, reason} 写入 iii 状态。该写入会触发 turn::on_approval,从而推进受影响的会话。function_awaiting_approval 只读取刚刚落地的决定,每到达一个就分派一个(allow 成为预先批准的分派,deny 或 aborted 成为合成的拒绝),当 awaiting_approval[] 为空时推进。无需注册每个调用的恢复函数。无需启动时重新扫描来恢复待处理的审批。一个触发器覆盖所有会话。
默认为故障关闭:如果策略工作器不可达或 5 秒超时触发,consultBefore 会以 gate_unavailable 信封拒绝该调用。如果 iii::durable::publish 本身出错,钩子分发器会返回 publish_failed: true,orchestrator 将其视为拒绝。
这种形态带来了一些延迟上的优势。调用后钩子通过一个订阅者存在缓存来短路 publish_collect,当该主题没有注册的持久化订阅者时,每个执行的函数调用可以节省大约 500ms。tearing_down 被内联到 finishSession() 中,每轮减少一个持久化队列跃点。context-compaction 订阅 orchestrator 在轮次边界发出的专用 agent::turn_end 流,因此压缩器唤醒是按轮次而非按事件进行的。session-create 分发状态触发器仅按范围进行门控并在进程内匹配,因此之前每次写入的 harness::session::is_create_event RPC 已不复存在。
批次完成后,steering_check 决定是继续、停止还是达到 max_turns。如果继续,则循环回 assistant_streaming。如果停止或达到最大轮次,finishSession() 会内联运行:发出 agent_end,释放沙箱,转换为 stopped。
在整个运行过程中,每个参与的工作器都会发出带有 iii.session.id、iii.message.id 和 iii.function.id 标签的 OTel 跨度。这些标签正是引擎的 engine::traces::group_by 所读取的,以便在跟踪 UI 中填充“按会话分组”/“按消息分组”/“按函数分组”。仪表化是自动的: src/runtime/worker.ts 将每个 registerFunction 包裹在一个 Proxy 中,因此每个工作器代码无需记住添加跨度。
构建你自己的
有趣的是,上述工作器没有一个特殊的。每个都是一个进程,打开一个 WebSocket 连接到引擎,注册一些函数和触发器,然后运行。契约与每个应用工作器使用的契约相同。工具框架建立在与你业务逻辑相同的基础原语之上。
这意味着“构建你自己的工具框架”可以分解为与“编写任何工作器”相同的操作。你选择要替换的层,编写一个在总线上注册相同函数的工作器,执行 iii worker add,然后堆栈的其余部分开始使用你的工作器。
有两层没有出现在上面的工作器表格中,但对工具框架的行为很重要。技能是每个工作器广告其函数功能的方式。每个工作器可以在 iii:/// 发布一个技能,代理在首次调用该函数之前通过 directory::skills::get 获取它。系统提示是每轮从模式段落、iii 身份序言和运行配置的默认技能主体组装而成的。两者都是总线驱动的:技能由 iii-directory 工作器提供,系统提示由 turn-orchestrator 组装。两者都是可替换的。
五个具体例子。
用实时 API 替换模型目录。 编写一个工作器,注册 models::list、models::get、models::supports。让它每 N 分钟从你的提供者目录端点获取一次并缓存。发布它。iii worker add your-org/dynamic-models-catalog。停止静态的 models-catalog 工作器。Turn-orchestrator 永远不会知道区别。它调用 iii.trigger('models::list'),引擎路由到最近注册了该函数 ID 的工作器。
添加一个新的提供者。 provider-kimi 和 provider-lmstudio 已经证明了这种形态。每个都是一个工作器,注册 provider::::stream 和 provider::::complete,将上游 API 的 SSE 流排入 iii 通道,并通过 budget::record 将其模型使用情况写入 llm-budget。添加第五个提供者就是编写一个包含一个 iii.worker.yaml 和一个 register.ts 的文件夹。发布到注册表,或保留在本地。Turn-orchestrator 根据运行的 provider 字段选择提供者;新提供者在工作器连接的那一刻立即可用。
从私有工件仓库提供技能。 编写一个工作器,注册 directory::skills::get 和 directory::skills::list,由你的内部文档系统或私有 S3 存储桶支持。断开或重命名默认的 iii-directory 工作器。Orchestrator 的引导程序会按命名空间调用 directory::skills::download;你的工作器回答。代理“在调用新函数之前获取每个函数技能”的模式保持不变,因为线格式相同。
完全覆盖系统提示。 run::start 接受一个可选的 system_prompt 字段。传递它,orchestrator 会逐字使用你的字符串,跳过模式段落 + 身份序言 + 技能附录的组装。当你有现成的提示资产并且希望工具框架在不修改的情况下遵守时很有用。技能下载仍在引导程序中运行,因此即使使用自定义提示,代理仍然拥有 directory::skills::get 按需发现的能力。
替换审批关口 UI 表面。 默认的 approval-gate 工作器注册 approval::resolve。线路模式是一个函数调用:处理程序将 approvals// = {decision, reason} 持久化到 iii 状态。Orchestrator 的单一 turn::on_approval 状态触发器会接收该写入并唤醒正确的会话。如果你想从 Slack 而不是控制台驱动审批,编写一个 Slack 工作器,监听 /approve 和 /deny 斜杠命令,然后使用正确的负载调用 approval::resolve。Orchestrator 永远不会知道区别。整个 approval-gate 工作器保持不动。你添加了一个新工作器;你没有替换现有的那个。
如果你想要一个不同的策略引擎(OPA、Cedar、你自己的 DSL),编写一个工作器,注册 policy::check_permissions 并返回 { decision, rule_id?, matched_constraint? }。断开默认的策略工作器(它被包裹在工具框架元工作器内部,因此你可以禁用该处理程序或运行一个精简版的元工作器)。Orchestrator 的 consultBefore 不知道区别。相同的 5 秒超时,相同的故障关闭语义,相同的线格式。
这些例子的重点不在于具体的替换。而在于操作的形式。iii 堆栈中的每个工具框架层都可以通过总线上的一个或两个函数 ID 访问。替换一层就是编写一个注册这些 ID 的工作器。系统的其余部分保持不变。
工具框架是一个滑块,不是一个分岔路口
经典的工具框架辩论将其框架化为薄与厚。Anthropic 的薄循环与 LangGraph 的显式 DAG。这种框架假设你选择一边并忍受它。当工具框架由同一总线上的工作器组成时,薄与厚只是你安装的工作器数量的计数。
一个薄的工具框架就是 turn-orchestrator 加上 provider-anthropic 加上 auth-credentials 加上一个最小的工具框架元工作器。仅此而已。没有审批,没有预算,没有策略引擎,没有钩子分发器。运行任何东西。信任模型。对自主研究代理、实验有用。
相似文章
@XAMTO_AI: 想自己从零搭一个生产级 Agent Harness?别做梦了,以为随便挑个框架就能收工的,基本全翻车了。 真相是这玩意儿压根不是"选框架"能摆平的事,它背后藏着15项你绕不开的硬核职责: 每一项都得做成能安装、能版本化、还能换语言跑的 w…
The article argues that production agent harnesses should not be monolithic frameworks but rather a stack of independent, replaceable workers connected by a shared trigger primitive, outlining 15 core responsibilities and how the iii engine implements this approach.
@mfpiccolo: https://x.com/mfpiccolo/status/2064358779940995141
本文介绍了iii,一个开源平台,用于使用可组合的worker构建生产级智能后端,取代传统的服务集成,为后端工程提供类似React的范式。
@Potatoloogs: https://x.com/Potatoloogs/status/2057391224592667051
本文深度拆解了Agent Harness的概念,即包裹在LLM外部的工程基础设施,包括编排循环、工具调用、记忆系统、上下文管理等12个组件。文章引用Anthropic、OpenAI、LangChain等公司的实践,论证了harness对生产级AI Agent的关键作用。
@mfpiccolo:Kaffu的‘富人的玩具’这句话是我今年读到关于工具框架的最犀利的评论之一。他对症状的判断是对的……
该推文讨论了AI代理框架中的臃肿问题,赞同Kaffu提出的框架变成‘富人的玩具’的批评,并倡导一种由小型可替换工作者组成的可组合架构,以减少漂移,保持系统廉价且易于调试。
@hwchase17: https://x.com/hwchase17/status/2053157547985834227
文章概述了一个系统的“智能体开发生命周期”(构建、测试、部署、监控),以有效创建和管理 AI 智能体,重点介绍了 LangChain、LangGraph 和 CrewAI 等关键框架。