如何构建可靠的代理工具框架(阅读约48分钟)

TLDR AI 工具

摘要

这篇文章作为构建可靠代理工具框架的事后分析和指南,详细介绍了从omp到omp²的架构经验教训,并倡导有效管理复杂性的设计原则。

本文详细阐述了代理工具框架的架构,涵盖了状态管理、运行时、控制平面、推理、工具、界面和语言选择。核心论点是,不可避免的复杂性应被核心抽象吸收,而不是反复推给扩展和用户。
查看原文
查看缓存全文

缓存时间: 2026/09/03 23:45

# 工具链实战手册 来源:https://stencil.so/blog/harness-playbook *在开始之前,请允许我表达谢意。你们中有数十万人使用了omp,反馈了问题,提出了缺失的功能,并共同塑造了它的样貌。这篇文章和omp2的存在,都是因为你们。* 当听到omp2时,许多人都会立刻问:"但为什么要这样做?"围绕一次网络请求构建一个循环看似简单,但OpenCode、Pi、OpenClaw和omp都在同时进行完整重构是有原因的:这类软件以前不存在,只有从简单版本开始,我们才能发现裂缝,从而打造更好的版本。不可避免的复杂性需要所有者。目前,复杂性守恒定律(https://en.wikipedia.org/wiki/Law_of_conservation_of_complexity)倾向于扩展和用户,这使得在omp或Pi之上构建可靠软件变得不可能。我仿佛已经听到*“什么,它那么简单,而且扩展起来如此愉悦”*的声音了。请给我几章的时间来说服你。 Dijkstra曾写道“简洁是可靠性的前提”(https://www.cs.virginia.edu/~evans/cs655/readings/ewd498.html),然而他以算法解决寻路问题而闻名。为何不采用暴力法?他根本不是在主张我们现在常重复的**简单就好,复杂就糟**。这条建议是为了帮助实现者进行推理。而我们可耻地用它来为实现者逃避推理开脱。 Ousterhout在他斯坦福讲座的笔记中给出了缺失的另一半。他告诉模块作者要“拥抱痛苦”(https://web.stanford.edu/~ouster/cgi-bin/cs190-spring16/lecture.php?topic=modularDesign)。承担困难问题,彻底解决它们,并让结果易于其他人使用。将复杂性压入模块内部。让少数实现者来承担,而不是让每个调用者都携带一个略有不同的更小副本。 --- 我相信许多读者还记得那条将Claude Code比作游戏引擎的推文引发的 meme 潮。这个类比听起来有些牵强,但如果你列出工具链的职责,除了渲染部分,它确实非常匹配。它维护一个权威的世界,记录变更日志,运行不受信任的操作,将状态复制到多个视图,调度角色,解释命令,适配不兼容的协议,并渲染实时界面。听起来很熟悉吗?游戏引擎似乎已经花了数十年时间来处理同样类别的复杂性。 接下来的内容既是事后分析,也是一本实战手册: - **omp教会了我们什么**指出了在一个人们实际使用的系统中遇到的失败。 - **omp2的改变**描述了替代架构——部分已构建,部分仍在推进中。 1. [设计边界](https://stencil.so/blog/harness-playbook#the-design-envelope) 2. [状态](https://stencil.so/blog/harness-playbook#the-state) 3. [运行时](https://stencil.so/blog/harness-playbook#the-runtime) 4. [控制平面](https://stencil.so/blog/harness-playbook#the-control-plane) 5. [推理](https://stencil.so/blog/harness-playbook#the-inference) 6. [工具表面](https://stencil.so/blog/harness-playbook#the-tool-surface) 7. [界面](https://stencil.so/blog/harness-playbook#the-interface) 8. [技术栈](https://stencil.so/blog/harness-playbook#the-stack) 9. [结语](https://stencil.so/blog/harness-playbook#closing-notes) 10. [附录A:官方示例中的状态失败](https://stencil.so/blog/harness-playbook#appendix-a-state-failures-in-the-official-examples) 11. [附录B:弹性推测插槽](https://stencil.so/blog/harness-playbook#appendix-b-elastic-speculative-slots) ## 设计边界 在讨论智能体工具链的任何子系统之前,请设想四个截然不同的产品将依赖它: - **多路复用工作区** *一个本地环境,多个智能体和子智能体位于同一文件夹中。* - **远程驱动器** *一个远程客户端通过手机驱动云中的智能体——或他们办公桌下的机器。* - **观察者** *一个Web客户端观看Claude智能体工作。* - **自动化软件工厂** *使用SDK针对不受信任的输入进行自动化软件生产。* 这些不是市场用户画像,而是架构测试。它们共同改变了使工具链超越简单聊天循环的维度:一个仅适用于第一种情况的设计,往往会将控制器嵌入TUI,将状态保留在闭包中,让扩展在引擎进程中执行,并假设人类可以从一次无界调用中恢复。一个能适应所有四种情况的设计则被迫建立更好的边界。 本书其余部分围绕五个推论展开: 1. **一个权威会话。** 回退、分叉、恢复、复制和检查必须全部源自同一个日志化的状态。 2. **一个受信任的控制平面。** 策略和会话所有权留在主机;沙盒只接收有界执行请求。 3. **有界工作。** 工具调用、子智能体和后台任务都是可取消的流,具有中心限制和可观测性。 4. **显式兼容性。** 模型和服务提供商的特性是有结构的知识,而不是散布在调用点各处的分支。 5. **视图是投影。** TUI、Web客户端、远程客户端和子智能体检查器渲染的是相同的状态,而不是成为额外的权威源。 这些约束是后续所有内容的连接组织。当后面章节提出一个DOM、一个状态变量、一个Director、一个小型VM存根或一个组件渲染器时,它正在解决这五个要求之一——而不是为了自身的巧妙而引入一个子系统。第一个要求是基础:在决定代码在哪里运行或如何渲染之前,工具链需要知道什么是真实的。 ## 状态 ### 什么必须持久化 如果你希望某些东西是持久的、可回退的、能容错的和可分叉的,你有三个选择: 1. 保留产生它的历史。 2. 保留你关心属性的变更。 3. 保留机器本身。 “你需要序列化状态”的meme,三格漫画:事件溯源(哭泣的Wojak被埋在事件中,需要重放全部)、增量快照(平静的Wojak在对比两个属性快照)、巨神级来源(对比WASM内存,恢复机器状态)。 Source引擎(Valve的游戏引擎)使用了第二种方法的变体进行网络同步。omp和Pi目前...没有一致地使用其中任何一种。有事件,但状态并非真正源自这些事件,违反了事件溯源的第一原则:**状态必须仅从事件中推导出来**。 *Pi的状态模型有两个权威,从未产生delta——但它*就是*状态,回退无法到达树外的东西。* **权威,非派生** **变更单元** *消息 · 自定义 · custom_message*(仅覆盖树) **真实来源** *消息树*(仅包含ID和消息) **磁盘格式** *.jsonl*(树,且仅此而已) **重放** *移动叶子指针* **重放(.jsonl) ≠ 原始状态** *回退 · 分叉 · 恢复全都是谎言* **Source引擎:单一权威,从不产生delta——很好,它是派生的,在实体列表之外** **客户端预测:派生,从不权威** **变更单元** *{ Δ 实体 ... }*(覆盖每个字段) **真实来源** *实体列表*(规则、插件、全局变量——全部) **磁盘格式** *.dem*(全部状态) **重放** *寻找时间点,重新推导* **重放(.dem) == 原始状态** *外部没有任何东西可以泄露* 一个权威 vs 两个权威:Source中的一切都是实体delta,因此`replay(.dem) == 原始状态`。Pi的日志仅覆盖消息树,而权威状态存在于其外部——回退、分叉和恢复全都是谎言。到达这种状态有可以理解的原因。在每个日志中重复系统提示和`AGENTS.md`会很浪费;这可以通过哈希模板并存储其变量来解决。而且这种状态建模风格在TypeScript中并不常见,因为它实际上没有运行时类型。 然而,结果仍然是两个真实来源:全局变量行变得有趣起来。Source没有会话全局变量;它们只是实体的属性。而我们的全局变量有自己的层次结构: *日志化为树条目?* | *可通过自定义条目日志化?* | *未日志化?* | 这个事实在树里吗? ---|---|---|--- A ✓ | 有福的约3个 | model_change · thinking_level_change · session_info · label | 重放正确 B ~ | 手工实现 | 每个扩展编写自己的derive | ≈15个生命周期bug,见下文 C ✗ | 在历史之外 | AGENTS.md · 扩展集 · 工具名册 · 设置 · 服务提供商配置 · MCP服务器 | 编辑AGENTS.md → 重放使用的是今天的副本。你录制的会话已丢失。 *会话全局变量的三个层级,其中一个有效。* Source获得正确性并非通过编写精心的协调器或优秀的文档。它使得不可重放的状态*无法被表示*。**正确性源于这个约束**,而不是要求每个扩展作者记住注册两个钩子并定义更新形状。 ### 证据:正确性在API中是可选的 我们查看了78个官方的Pi扩展示例。六十个是无状态的;在有状态的17个中,只有两个是正确的。详情见附录A (https://stencil.so/blog/harness-playbook#appendix-a-state-failures-in-the-official-examples),但重要的是,文档无法修复这种错误分布。引擎需要一个状态可以存在的单一位置。 *`tic-tac-toe.ts`:下X,在O回应前崩溃,恢复后,X消失了。实时写入和恢复读取使用了不同的条目类型。* ### omp2的改变:一个物化的会话 如果整个会话被物化为**一个DOM**会怎样?当然你也可以使用带序列化的ECS系统,或其他任何你想要的表示格式。我主要选择XML,因为它使得状态非常易于组合、检查和调试。 ```xml <session> <todo status="completed">...</todo> <todo status="in_progress">...</todo> </session> ``` 它的事件是一个属性变更流: `: todo.done event: patch@1 by: e41 data: {"ops":[["set",412,"status","completed"],["set",415,"status","in_progress"]]}` 树是权威;日志存储其增量更改。运行时对象可以缓存或索引它,但它们不会成为第二个真实存在的地方。在任何日志点,工具链都可以物化——因此可以快照——整个会话。 ### 一个权威带来了什么 状态和记录都在同一个树中,几个困难问题简化为相同的操作。 **回退是DOM差异。** 将当前物化状态与目标状态进行差异比较。一个``元素消失了?通过销毁该元素来终止它。一个出现了?通过创建该元素来恢复或生成它。delta本身就是完整的工作清单。 > 添加有状态功能永远不会增加回退、分叉、恢复或复制的调用点。 **提示变成投影。** 不再有100行的状态对象传递给每个模板。系统提示和其他一切一样读取同一个树: `- {{ count(select("todo item[status!=completed]")) }} open items` **复制变成订阅。** 我们已经有了应用及其派生。远程客户端消费补丁流,而不是跟踪文件。远程驱动和观察者场景不再需要单独的状态管道。 **渲染变成投影。** 一个组件注册表可以从相同的状态元素渲染`Read`、`Bash`、一条消息或一个子智能体。流式参数修改``;流式输出修改``。第七章将其转化为一个类型化的界面,而不是另一个定制的渲染器。 ### 控制器与角色 这种分离也使子智能体可检查。Pi的视图直接读取实时会话状态——页脚调用`sessionManager.getEntries()`——因此添加“检查子智能体”意味着需要将控制器状态通过UI内部传递。保持控制器和角色完全分离:控制器拥有会话状态;角色只渲染其快照和补丁流。TUI、远程客户端和子智能体检查器成为同级实体。检查一个子智能体意味着将同一个角色指向该子智能体的状态。 一个真实的状态模型是基础,但如果不受信任的代码拥有变更它的策略,它仍然会被破坏。下一章将划定运行时边界。 ## 运行时 状态章节确立了工具链所相信的内容。运行时章节决定谁可以改变它,不受信任的工作在哪里运行,以及当执行可以持续数小时、流式输出或忽略礼貌的停止请求时,“工具调用”意味着什么。 ### 沙盒应该执行,而不是决策 从设计边界中的*自动化软件工厂*案例开始。假设我们克隆roboomp,要求gpt spark将其名称的每次提及都替换为CodeWhatever,并开始向用户收取我们神奇技术的数千美元费用。谁运行工具?当然是虚拟机。 是的,没错。这就是当我们将执行器放入虚拟机时发生的情况: *工具很复杂?驱动器是受信任的工具链受信任边界虚拟机是不受信任的:代码执行、网页内容?每个工具住在哪里?1) TODO工具链状态 2) FILE R/W哪一边?3) IMAGE-GEN输出在这里 4) PY-EXEC执行状态需要密钥?编程化工具使用冲突!?* 嗯,这不行。因为: - 编程化工具使用需要访问所有工具;所以我们不能任意分割工具链状态工具和环境状态工具 - 我们需要构建一个双向网关,允许虚拟机调用主机工具;这会:1. 违背目的(要么你启用DoS;要么你需要对某些操作限制自己的虚拟机速率) 2. 让这变得更加复杂,不,谢谢。 好吧,让我们把驱动应用放入虚拟机! *如果驱动器在虚拟机内部会怎样?虚拟机(不受信任)驱动器应用源代码 + 提示现在你必须构建和托管这个LLM网关代理——密钥留在这里 (a)连接错误 (b)OOM自杀攻击?外部无法分辨!不受信任的提示应用源代码泄露!移动了边界,保留了痛苦* - 现在我们泄露了应用提示和内部源代码,除非我们把应用移到虚拟机外部,并通过网络RPC连接到工具链,同时也会话存储也移到外部——但是,会话存储在外部意味着我们需要授予虚拟机写入权限,这再次让我们回到问题#1和#2。 解决方案是在虚拟机内部放置一个单一的、顺从的存根,并且非常非常小心,限制流回的最大数据量(你不希望一个被误用的Read工具产生2GB的响应): *存根留在里面,其他一切留在外面。主机(受信任)驱动器工具链LLM网关密钥在这里会话存储单一写入者类型化RPC唯一的门虚拟机(不受信任)如果弹出:攻击者得到一个存根、python和grep执行器存根py + rg只读镜像就是这样。没有其他了。最小可行性囚犯* 这些图表导向一个边界: - **主机**拥有会话状态、推理、策略、工具路由、审批、限制和日志记录。 - **沙盒**通过一个小型、顺从的协议拥有环境执行权限。 - 每个跨越回界的流在到达不受信任的一方耗尽主机内存或上下文之前都是有界的。 这种安排在不使本地使用变差的情况下满足了自动化软件工厂的需求。同一主机可以将存根指向本地进程、容器、虚拟机或远程机器。 ### 子智能体跨越相同的边界 放置不仅仅是主机与虚拟机。子智能体需要在文件系统层具有相同的边界:工作树仅隔离被跟踪的文件,而`pi-iso`使用APFS、btrfs、ZFS、overlayfs、ProjFS或复制回退为每个子智能体提供整个工作区的写时复制视图。子智能体产生分歧;父级接收差异。子智能体接收一个视图并返回更改。它不共享父级的可变权限。这是同一主机/沙盒规则的文件系统形式。 ### omp教会我们什么:一个调用,三个脱节的API 好吧,但我们如何定义一个工具?我们将在后面讨论我们最初所做的更改,但我们大多保持了核心契约不变: ```typescript export const myCustomTool: ToolDefinition = { name: "my_tool", parameters: mySchema, // 1. 在参数流式传输期间调用 // ... } ```

相似文章

面向长时应用开发的Harness设计

Anthropic Engineering

Anthropic工程师详细介绍了一种多智能体Harness设计,利用生成器与评估器智能体提升Claude在长时间内自主构建完整、高质量前端应用的能力。

最好的智能代理工具会这样做……

Reddit r/AI_Agents

作者分享了构建高效智能代理工具的见解:最好的工具最大限度地减少对大语言模型(LLM)在琐碎任务上的依赖,将其保留用于复杂推理,从而将真正的代理工具与简单的包装器区分开来。

Harness Handbook:使不断演化的智能体Harness可读、可导航、可编辑

arXiv cs.AI

Harness Handbook是一种以行为为中心的表示,通过静态程序分析和LLM辅助从智能体harness代码库中合成,帮助开发者和编码智能体定位实现特定行为的代码。它引入了行为引导的渐进式披露(BGPD),引导智能体从高层描述到相关实现细节,提高了定位准确性和编辑计划质量。

Building an Advanced Agentic Harness

Hacker News Top

A technical blog post that walks through building a production-grade agentic harness around a basic LLM loop, covering typed tools, plan DAGs, tiered memory, verification hierarchies, budgets, and tracing.