抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >
跳到正文

最近整理了一下 Biny Agent 的工具系统。

给 Agent 增加一个工具其实不难:定义参数 Schema,注册工具,再实现执行函数。但接入 MCP、插件和各种扩展以后,工具数量越来越多,问题就变了。

只读一个文件,有必要把上百个工具的定义都交给模型吗?缺少能力时,模型能不能自己找到工具?不同来源的调用,能不能共用一套权限和执行机制?如果一次写入中途断线,又该不该重试?

我现在更关心的是,工具从被发现到真正产生副作用,中间的每一步能不能说清楚。

这篇文章就沿着这条链路,聊聊我在 Biny 中的实现和取舍。

1. 注册了工具,不代表模型每轮都要看到它

我先区分了两个集合:系统拥有的工具,以及模型当前可用的工具。

在 Biny 中,内置工具以及 MCP、Plugin、Skill、Subagent 相关的工具入口,统一进入 ToolRegistry。注册表维护名称、描述、参数 Schema、来源和能力信息。

但注册只是说明系统知道这个工具,不意味着每一轮请求都要把它的完整定义放进模型上下文。Skill 的说明文档也有自己的加载流程,不能把它和工具 Schema 混为一谈。

假设接入几个 MCP Server,一共提供 100 个工具。用户只是想修改一个配置文件,此时把日历、邮件、GitHub 等工具的参数定义全部发给主模型,会产生很多与当前任务无关的输入。

所以我的处理方式是:工具目录保留完整能力,模型侧只开放当前需要的一部分。

自动工具选择模式下,会保留 Read、Write、Edit、Bash 等基础 Coding 能力,以及工具发现、结果回读等必要入口;其他工具按任务选择。具体如何呈现,还会随模型的工具交互模式调整。

工具目录负责“有什么”,模型工具集负责“现在用什么”。 两者不需要始终一样。

2. 先预选,缺少能力时再搜索

只给基础工具也有问题:主模型后来需要访问 GitHub,却没有相关工具,怎么办?

我采用了两阶段发现。

请求开始前:预选相关能力

在自动选择模式下,辅助模型根据当前请求和最近的对话,从目录中选择相关工具。它看到的主要是名称和简短描述,而不是全部工具的完整参数定义。

例如用户希望查看某个 PR,辅助模型可以先选中相关的 GitHub 能力。用户明确点名的有效工具,也会在本地处理,不必全部依赖模型猜测。

预选有超时和预算限制。失败时仍保留基础及有效的历史能力,不会退化成“把全部工具展开”。

这里还有一个容易误读的细节:当前预算主要约束新自动选入的工具,并不强制淘汰已有工具或必要工具。因此,不能把它理解成整个会话工具集的硬上限。

运行过程中:主动发现缺失能力

预选只是预测,多步任务仍然可能需要新的工具,所以 Biny 保留了 ToolSearch。

它区分两种情况:

  • 知道完整工具名: 从本地注册表精确查找。
  • 只知道要做什么: 把需求和候选工具的简短描述交给辅助模型,进行语义选择。

辅助模型返回的名称还要和真实注册表核对。凭空生成一个工具名,不能让系统获得不存在的能力。

成功发现后,只把匹配工具开放给后续模型步骤,而不是顺带展开整个 MCP Server。

Biny 的工具按需发现流程:完整目录经过预选形成当前工具集,缺少能力时通过 ToolSearch 补充

图 1:目录可以很大,但每一步只需要披露当前相关的工具。发现成功后,下一步模型才能使用新增能力。

举个例子,用户先要求查看 PR,随后又要求读取关联 Issue。第一步预选未必包含 Issue 工具,主模型可以在执行过程中再搜索并补充它。

成功发现的工具还可以在会话后续回合中复用,前提是注册仍然有效。这样能减少重复搜索,也会带来累积问题:任务不断变化,工具集可能越来越大。

这是一项需要继续评测的取舍,而不是已经解决了所有上下文管理问题。

3. MCP 连接、工具发现和实际调用,要分开处理

接入 MCP 后,我发现还有三个动作容易混在一起:连接服务、发现工具、执行工具。

它们的状态并不相同。

环节 回答的问题 不能据此推断什么
连接 远端服务现在能否通信? 所有工具都应该进入上下文
发现 目录中有哪些相关能力? 缓存中的工具现在一定可执行
执行 这次调用能否获得结果? 发出请求就代表操作已完成

Biny 会在 Runtime 启动时异步连接启用的 MCP Server,并给连接准备设置等待预算。慢服务可以继续在后台连接,不需要让整个 Agent 无限等待。

成功获取的工具目录,会在同一 Runtime Host 内短期缓存。符合缓存条件的服务重连时,可以先利用已有目录进行发现,实际执行仍然需要等待连接就绪。

缓存的元数据,不是服务在线的证明。

真正调用时,要检查连接和工具定义。如果服务更新了 Schema,旧调用需要重新发现和准备,不能直接沿用。单个服务的连接异常也独立处理,尽量避免影响其他服务。

这里还需要分清两种收益:目录缓存主要减少重复发现的工作;减少主模型无关输入的,是工具预选和按需披露。

辅助模型本身也消耗 Token,并增加等待时间。主模型输入变小,只能说明其中一项成本下降,不能直接证明总成本更低。

4. 调用形式可以不同,执行入口必须统一

模型发现一个工具,只代表它具备被调用的条件,不代表获得了执行权限。

我不希望内置文件工具、MCP 工具和脚本组合调用各自维护一套权限逻辑。来源越多,分散的执行边界就越难核对。

因此,Biny 用 ToolExecutionCoordinator 协调工具调用。把主要步骤简化后,大致是:

参数校验 → 执行准备 → 权限判断 → 资源调度 → 目标复核 → 执行与记录 → 返回结果。

执行准备阶段会确定真实路径、操作目标、风险和资源访问范围。权限判断尽量绑定具体操作,例如修改哪个文件、修改什么内容,而不只是批准一个工具名称。

调度层根据工具声明的访问范围处理冲突。涉及文件修改时,执行前还会复核目标,避免审批之后文件已经变化,却继续按旧预览写入。

不同来源的工具调用经过统一执行协调器,依次处理参数、目标、权限、调度、执行与记录

图 2:这是主要执行步骤的概念图。状态记录贯穿调用过程,并不只在工具结束时发生。

Code Mode 也是同样的原则。模型可以用 JavaScript 组织多个调用,但内部允许调用的 tools.* 仍经过协调器,使用相同的权限、预算和状态记录机制。脚本入口并不获得直接访问文件系统、网络或进程的能力。

当然,这也不意味着任何工具都可以任意嵌套。哪些能力可以进入 Code Mode,仍有明确的准入范围。

对我来说,这个设计的价值是:调用可以由模型编排,实际操作的边界仍由程序维护。

5. 文件编辑:既要改对位置,也要确认版本

文件编辑是我投入比较多的一部分。

最直接的方案,是让模型输出修改后的完整文件,再整体写回。它容易实现,但为了改几行代码重新生成大型文件,输出成本高,也可能误改无关内容。

所以我保留了几种局部编辑协议。

字符串替换:简单,但要处理歧义

模型提供 old_string 和 new_string,工具找到旧文本并替换。它适合明确的小范围修改,对模型也比较直观。

问题是,同一段代码可能出现多次,复制的缩进、空白或换行也可能不同。

Biny 优先精确匹配,必要时有限度地容忍格式差异。默认要求匹配位置明确;如果需要替换所有相同片段,则必须显式使用 replace_all。

出现歧义时,我更倾向于报错,让模型补充上下文,而不是静默选择第一个位置。

编辑失败可以重新定位,错误地修改成功反而更难发现。

Hashline:用行号和摘要核对锚点

Hashline 在读取时给每行附上类似 12#A1B2C3D4 的引用标识,编辑时重新核对当前行。

它解决的是:模型依据旧文件定位,但文件已经变化。如果对应行的位置或内容不再匹配,锚点就会被拒绝。

这里的示例摘要只是格式演示。它也不是对整份文件语义的证明,更不能代替写入前的版本检查。

Hashline 目前仍是实验性模式,我没有足够评测数据证明它在整体任务成功率上优于其他方式。

结构化 Patch:适配支持对应协议的模型

对于支持原生结构化 Patch 的模型,Biny 也提供 apply_patch 入口。模型通过路径、上下文和修改片段表达变更。

我没有强行把所有模型统一到一种格式,而是依据接口和明确声明的协议适配。几种方式可以放在一起看:

方式 模型如何描述修改 主要需要核对什么
字符串替换 旧文本、新文本 匹配是否明确,是否要全部替换
Hashline 行号与摘要锚点、修改操作 锚点是否仍有效,操作是否冲突
Patch 文件路径、上下文、增删片段 协议是否合法,上下文能否定位

但正确定位,还没有解决写入安全。

假设 Agent 读取文件后,用户在编辑器里改了同一个文件。Agent 继续写入,就可能覆盖用户的新内容。

因此,编辑链路还会核对相关文件快照,并检查准备或审批后的目标变化。写入时先生成临时文件,再按创建或覆盖场景提交,同时记录变更证据。

字符串替换、Hashline 和 Patch 汇入文件校验与安全提交链路;版本变化时拒绝继续写入

图 3:定位协议回答“改哪里”,文件校验回答“现在还能不能改”。图中是共用约束的归纳,各协议的具体校验步骤并非完全相同。

这不等于绝对的并发安全。版本校验和文件系统提交之间仍有竞态窗口,尤其是外部进程同时原子保存文件时;多文件修改也不能因此视为一个原子事务。

我目前追求的是减少明显的过期写入和错误覆盖,并在无法确认状态时返回错误。编辑协议可以变化,底层的一致性约束不能随之丢掉。

6. 工具结果也需要按需读取

工具 Schema 只是上下文的一部分,返回结果同样可能很大。

搜索整个项目、读取日志、执行一个输出很多内容的 Shell 命令,都可能一次产生大量文本。全部塞进后续请求,会挤占真正用于推理的空间。

Biny 对超过预算的结果采用归档与受限预览:可归档的完整结果保存在本地,模型先拿到长度受限的内容预览和归档引用,需要更多内容时再通过 read_tool_result 读取。

这里的预览主要是截取内容,不应理解成辅助模型已经完整读过并做了语义总结。归档本身也有大小和保留数量限制,引用并非永久有效。

例如命令返回一大段日志,模型可以先看预览定位报错,再回读相关区段。它不必为了查看几条异常,把整份日志反复带进上下文。

这和按需发现是同一个思路:工具定义按需披露,工具结果按需读取。

7. 报错不代表没执行,结果未知也不是普通失败

工具恢复最容易出问题的地方,是把“客户端收到错误”理解成“操作没有发生”。

例如,一个 MCP 工具已经向远端发送创建请求,但收到响应前连接断开。远端可能已经创建成功,只是客户端不知道。

直接重试,就可能重复创建。

所以 Biny 会记录执行状态,包括未开始、运行中、副作用已提交、成功、失败、取消和结果未知等。状态记录用于判断执行走到了哪里,而不是只保留一句报错。

远端请求派发后断线,可能已经执行也可能尚未执行;客户端应记录结果未知并核查证据

图 4:客户端看见的是同一次断线,远端却可能处于不同状态。因此,断线本身不足以支持安全重试。

对于派发后发生的 MCP 连接错误,当前实现会返回结果未知,而不是自动重放原调用。后续需要结合操作 ID、执行日志和远端已有对象等证据核查。

操作 ID 能帮助关联记录,但只有远端协议真正支持幂等或去重,它才可能进一步帮助防止重复操作。单有一个本地 ID,并不能保证安全重试。

同样,记录了文件提交,也不代表后续测试通过;拿到了工具响应,也不一定能证明目标应用中的业务效果符合预期。

模型说“完成了”,不能替代执行证据。 这也是我希望统一执行链路承担的职责:恢复任务时,依据可核查的事实继续。

8. 这些设计值不值得,还需要实验回答

目前这套设计仍然有取舍。

辅助模型预选减少了主模型携带的无关定义,也增加了额外请求。历史工具复用减少重复发现,也可能让长会话的工具集持续累积。目录缓存减少重复工作,却不能保证服务始终可用。

几种编辑协议也是一样:接口看起来精确,不代表实际任务成功率更高;单次调用输出更短,也不代表完成任务的总成本更低。

接下来我更想通过同一任务集做消融实验,逐项改变工具选择、历史复用和编辑策略,观察:

  • 主模型与辅助模型的总 Token;
  • 预选、发现和调用的延迟,以及完成任务的总耗时;
  • 任务成功率、编辑首次成功率和重试次数;
  • 错误修改、过期写入与重复副作用的情况。

只有这样,才能判断哪些复杂度确实值得保留。本文整理的是设计与实现边界,还不是统一性能评测的结论。

最后

做 Biny 的过程中,我逐渐觉得,工具系统的难点不在于接入多少工具,而在于管理好它们从被发现到真正执行的整个过程。

注册,不等于模型必须看到;看到,不等于获得执行权限;发出请求,也不等于任务已经成功。

我现在的设计基本围绕这几个边界展开:让模型按需获得能力,让工具共用一致的执行机制,让文件修改和外部操作有据可查。

相比继续增加工具数量,我觉得这些底层设计更值得投入。


项目地址: Biny Agent

实现索引: