跳至内容

工具可见性与非侵入扩展

这篇先回答一个直觉问题:如果 DSH “万物皆插件”,是不是所有插件的工具都会在普通模式的第一轮一起灌给模型?答案取决于 Profile、agent 作用域、呈现方式和最终提示词组装:注册可以很多,当前 agent 能解析的集合不必很多;能解析也不等于模型会收到每个工具的原生 schema。

证据范围固定在上游提交 aa6c361a972c8369148dea7380bb5c21c24e07ec。官方 README、源码和测试支持下面的设计解释;本学习仓库没有做真实模型延迟、token 或回答质量基准。

如果你只想先看一张总图,不想自己拼接多篇文章,先读工具预算与插件责任决策卡;本篇再展开其中的工具可见性和扩展边界。

先记住一句话

“万物皆插件”回答的是能力怎样装配、替换和卸载;某个 agent 每轮向模型公开多少工具,则交给作用域和呈现规则决定。一个工具先存在于运行时,再经过作用域筛选和呈现规则,最后才进入某个模型请求。

不要把下面三句话混成一句:

  1. 已注册:插件把工具放进运行时注册表,宿主知道它存在。
  2. 当前 agent 可解析:同一 agent 作用域的 get() 和工具分发查找能解析这个工具;这是该 agent 的能力查找集合,不单独说明模型已经收到这个工具的原生 schema。
  3. 执行允许:策略、审批、沙箱、宿主账号和操作系统能力允许这一次调用真正运行。

可以用这张图记忆:

text
插件加载 / 注册
        |
        v
运行时工具全集 -- 作用域 restrict / 遮蔽 --> 当前 agent 可解析的工具
                                                |                    |
                                                |                    +--> 同一作用域的 get / execute 查找
                                                |
                                                +--> presentAs(native / code / both)
                                                      + systemPrompt.assemble
                                                      --> 模型实际收到的原生 schema 或 Code Mode 入口
                                                |
                                                +--> 执行流水线、审批、guard、沙箱、OS 权限

restrict() 会让同一 agent 作用域里被排除的工具从该作用域的 get() 和工具分发查找中一起消失;在 native 呈现下,它也不会进入该次原生 schema 组装。presentAs('native' | 'code' | 'both') 主要改变模型侧如何呈现:native 直接提供可见 schema,code 主要提供 run_code 和生成的 SDK,both 同时提供两者。它不是给工具增加权限,也不替你完成文件、网络、子进程或凭据隔离。

亲手把三层拆开

“注册”“模型可见”“允许执行”是三层嵌套,不是同一件事的三种说法。下面这个组件用一份 12 个工具的教学清单演示三道收窄:勾掉一个 Bundle 改变第一层,换 agent 作用域改变第二层,换执行策略改变第三层。

互动组件工具可见性三层集合实验单独打开

正在载入互动组件……

不看组件也能读的说明

不打开组件也能得到结论:默认输入是“全部可见”配“只允许读”,于是 12 个工具全部已注册、全部模型可见,但只有 4 个允许执行——剩下 8 个(write_filedelete_pathrun_shellkill_process 等)出现在工具清单里,却被策略挡在执行之外。这就是本页要分开的两层。组件里的每一次判定都在它自己的表格里逐行给出,包括每个工具被谁挡住;点击任意一个工具(图里的色块或表格行),它会高亮自己的判定链路——走到哪层、被哪道收窄拦下。

那是一个教学模型:Bundle 名、工具名、风险分级和策略都由组件定义,来自任何真实部署的配置都不算数。它演示三层的嵌套关系和收窄归属;真实模型看到工具后是否调用、审批界面和审批人如何行动,属于组件之外的运行时问题。

循环卫生提醒在执行侧落实这条规则,但它是建议性的:数的是「工具名 + 规范化参数」这个键连续出现的次数,到第 3、5、8 次各注入一条提醒——第一档温和,之后带工具名、次数和参数预览。只换键序不算新调用;换了值或用户插话才清零链条。下面的实验把这套计数做成可以亲手推的时间线,顺带看「调用全部照常执行」的账目。

互动组件循环卫生实验单独打开

正在载入互动组件……

不看组件也能读的说明

组件是固定教学模型:四次以内的重复发出、一个固定阈值、一条注定无效的撤销尝试。它不运行真实工具,也不代表任何 Profile 的真实阈值配置。

为什么工具列表可能影响模型体验

官方工具文档把 native、code、both 分开定义:native 提供每个可见工具的 schema,code 主要把 run_code 和生成的 SDK 交给模型,both 同时提供两种形式。官方系统提示词文档把 PromptAssembly.tools 定义为一次组装结果。因此,“模型实际收到什么”必须结合呈现方式和最终组装结果判断,不能只看 schemas(agent)

所以工具列表变大时,至少会出现四种可能的成本:

成本发生了什么能否只靠静态代码下结论
输入成本名称、描述、参数、枚举和嵌套 schema 占用请求上下文可以确认会进入组装,但不能得到实际 token 数
选择负担模型要在更多相似名称和参数之间判断只能提出合理风险,不能证明质量下降幅度
缓存形状工具集合、顺序或 schema 改变,可能影响前缀复用可以确认设计规则,不能证明 provider 的实际命中率
传输与处理更大的请求和响应结构可能增加处理工作需要按模型、provider 和网络实测

因此更准确的说法是:工具数量和 schema 复杂度可能增加输入成本、选择负担和缓存变化;实际延迟与效果必须基准测试。“工具越多一定越慢”或“工具越多一定让模型变笨”都超出了当前证据。

首轮是否感觉列表更长,可能同时受到 schema 大小、上下文、网络、模型和 provider 缓存行为影响。provider 是否建立了前缀缓存、命中多少以及是否影响延迟,必须在固定 provider 和模型下实测;本教材不把“首轮通常没有缓存”当成事实。

从 Agent loop 看工具暴露

可以先把一次 Agent 任务画成一个最小循环:

text
观察上下文与状态
  -> 决定下一步
  -> 调用模型或工具
  -> 接收结果
  -> 更新状态并再次观察

Skill、Browser、截图、记忆和自修改,并不会自动跳出这个循环。它们主要扩展观测面、动作面、状态面或控制策略;是否有价值,要看循环是否更可靠、更快或更便宜。

工具延迟加载改变的是这一轮的候选集合和 schema 成本:可能减少干扰,也可能让模型暂时找不到需要的工具。要同时比较发现成功率、漏选率、恢复调用和任务结果,再判断这次加载策略的收益。

复杂系统可以加入路由器、Planner、Executor、事件驱动状态机或多个 Agent。它们通常是多个循环或状态机的组合。“仍然是 loop”是一个有用的抽象,不等于所有设计的能力和可靠性都相同。

“万物皆插件”描述的是装配方式,效果要另行证明:任务能否完成还要经过提示词、模型决策、工具执行、外部环境和失败恢复。源码里出现一个工具名,只说明注册层有它;这一轮模型是否收到,要查呈现与组装记录。

普通模式应该怎样设计

如果一个产品真的有很多插件,优先按下面顺序治理:

  1. 先减少默认加载:正常 Profile 只装当前任务常用的 Bundle;开发、调试和实验能力放到单独 Profile。
  2. 再限制 agent 视野:用 agent 作用域的 restrict() 只保留当前任务需要的工具。
  3. 再选择呈现方式:按模型和任务选择 nativecodeboth,不要把 Code Mode 当成自动减 token 的开关。
  4. 最后精简 schema:名称要区分,description 只解释模型需要的决策信息,参数和枚举不要无谓展开。
  5. 保持集合稳定:频繁注册、卸载、重排工具会改变模型请求前缀,应该记录发生原因。

“隐藏 UI 按钮”属于另一回事:用户界面隐藏一个按钮,schema 是否发给模型、执行权限是否保留都原样不变。这两件事分别由组装结果和执行策略决定,检查时要分开看。

把“工具很多”落到三个模式

下面是教材建议的分层,不是上游固定的 Profile 名称或现成命令。真正实现时,仍然要回到固定提交的 Profile、作用域和宿主策略核对。

模式默认让模型看到什么适合做什么不应误解成什么
正常模式当前任务所需的一小组稳定工具日常对话、减少无关 schema不是把其他工具删除,也不是安全沙箱
开发模式任务工具加上少量调试、观测工具开发插件、看注册和结果事件不是可以随意改私有 registry
审计模式工具可以全部注册,但按实验逐组暴露比较 schema、权限和选择行为不是把所有工具一次性塞给模型

初学者只需要记住一个动作:先固定“运行时注册了什么”,再固定“本次 agent 能解析什么”,再记录“它以 native、code 还是 both 呈现”,最后才检查“执行层允许什么”。如果这些状态没有分别记录,看到一个工具名称出现在代码里,不能推出它已经进入每一轮请求。

一个初学者能执行的判断题

假设系统里注册了 20 个工具,但某个 agent 只需要读文件、搜索和查看状态:

text
注册层:20 个工具仍然可以被运行时管理
可见层:这个 agent 只保留 3 个工具的 schema
权限层:读文件、搜索和查看状态仍各自经过策略与宿主权限

这不是删除工具,也不是 OS 沙箱。它只是把“模型这次能提出哪些工具调用”缩小了。若要限制文件路径或网络目标,还需要对应的 filesystem、web、sandbox、approval 和 OS 证据。

把这个例子再走一遍会更清楚:若 terminal 仍在注册全集中、却被当前 agent 的 restrict() 排除,那么这个作用域的查找会把它当作未知工具;在 native 呈现下,它也不会进入最终原生 schema。若 read_file 能被该 agent 解析,native 组装可能把它的 schema 交给模型,code 组装则可能只交给模型 run_code 和 SDK;两种情况下,实际调用仍要经过 guard、审批和宿主权限。可见性让“选择和查找”一致,不能单独让“执行”变安全。

5 分钟源码练习:找出“注册不等于可见”的证据

不要先读完整个 ToolRuntime。只打开下面三个固定版本文件,按顺序找一个符号,然后写三句话:

  1. 工具运行时入口:找到 register()restrict()schemas() 的职责边界。
  2. 作用域测试:找到一个测试,说明被限制的工具从哪个作用域的 schema 结果消失。
  3. 工具子系统说明:核对文档是否把“注册、作用域、呈现、执行”分开。

把答案写成下面这个最小产物就算完成:

text
源码事实:哪个函数把工具登记到运行时,哪个函数改变 agent 作用域的解析集合?
测试证据:哪个断言支持“当前作用域看不到它”,但没有支持“工具已经从全局注册表删除”?
未验证项:我还没有启动真实 Profile、调用 provider,或证明模型实际收到的 token/延迟变化。

如果三份文件的说法不一致,先记录“文档与源码需要人工复核”,不要自行补一个看起来合理的设计理由。这一练习训练的是从源码片段 → 测试断言 → 下一跳,而不是背诵 restrict() 的名字。

为什么“非侵入式”更适合作为默认路线

非侵入式扩展把代码放在宿主已经承诺的接口上:升级时跟着公开 API 走,卸载时按声明清理。优先顺序可以是:

需求首选做法插件作者应负责什么
观察或记录ctx.on() 与公开 live event监听器生命周期、日志和 dispose
新增能力ctx.provide()、Service、ctx.tools.register()类型、配置、schema、失败、取消和卸载
修改当前决策文档列出的 waterfall、guard()、审批接口决策理由、顺序、单调拒绝和测试
组合功能Bundle、Profile、cordis.patch.ymlmanifest、版本范围、加载顺序和回滚
外部协议接入Hook bridge 或兼容层输入输出映射、超时、权限、失败和协议版本

这些方式共享 Cordis 的 Context、Fiber 和 Effect 规则。插件不需要私自摸宿主的模块缓存或内部 Map,也能在明确的生命周期内注册和清理自己的资源。

侵入式能力的责任不能偷偷下放

一种常见说法是“侵入的所有工作不能由插件制作者来做”,这个说法需要改准:普通插件作者不默认替宿主承担侵入式修改;一旦某人真的维护 patch/fork,他的身份就从插件作者变成宿主或发行版维护者。

责任可以这样分:

技术需求应由谁拥有对外应怎样写
公开 API 能完成普通插件作者“第三方插件,支持 DSH 版本范围”
缺一个公开扩展点上游宿主维护者提交 feature request 或维护公开 patch
必须改变核心一致性宿主/发行版维护者“patched fork”,公布基线、差异和同步策略
只需翻译外部 Hook 协议bridge 维护者“兼容层”,公布协议子集和权限
必须改私有对象、模块缓存或进程专门的实验/集成维护者“非官方运行时补丁”,单独审计、锁版本、可回滚

插件作者可以提出需求、维护兼容层,甚至公开一个 patched fork;但方案里一旦出现核心改写、私有 registry 重建、进程注入或 Windows 注册表修改,对外身份就必须相应升级,不能沿用“安装插件”的说法。

安装说明至少要明确:修改了哪一层、需要什么权限、支持哪个上游 commit、如何卸载、失败怎样恢复、是否会写文件或启动子进程。没有这些信息,就不能把它和普通 Bundle 放在同一个信任等级。

从需求到分类:五问决策树

text
能否用公开 Service / Event / Tool API?
  └─ 能:普通插件
能否只用官方 Bundle / Profile / patch 组合?
  └─ 能:组合层扩展
是否只是翻译外部 Hook 协议?
  └─ 是:Hook bridge / 兼容层
必须改变核心事件或一致性?
  └─ 是:宿主维护的 patch / fork
必须摸私有对象、模块缓存、进程或系统配置?
  └─ 是:非官方、版本敏感实验,不应伪装成普通插件

一个项目可以同时属于两类。例如它的前半段用官方 Bundle 注册工具,后半段用私有 registry 注入另一个工具;审计时应拆开写,不能因为前半段合规,就给后半段自动背书。

插件作者提交前的八项自检

把下面清单贴到自己的插件 README 或审查记录中,比只写一句“支持 Hook”更容易复核:

  1. 我使用的是哪一个公开 Service、Event、Tool API 或 Bundle 入口?
  2. 我的工具默认是否进入所有 agent 的可解析或模型呈现集合?如果是,为什么不能按任务限制?
  3. 名称、description、参数和枚举是否只保留模型做决策所需的信息?
  4. 我是否把“模型收到原生 schema 或 Code Mode 入口”误写成“执行安全”?真正的文件、网络、子进程和凭据边界在哪里?
  5. 插件卸载时,监听器、服务、子进程、文件 watcher 和临时文件由谁清理?
  6. 是否读取或改写了未文档化字段、私有 Map、模块缓存、构建产物或进程?
  7. 如果改了核心源码,我是否公开写出上游 commit、差异、版本矩阵、权限和回滚方式?
  8. 我能提供哪些证据:源码、单元测试、安装日志、真实运行、卸载检查,哪些仍然没有?

第 6 项只要回答“是”,就不要把项目的全部能力包装成普通插件;第 7 项只要回答“是”,对外身份就应改成 patched fork、发行版或非官方兼容层维护者。

最小学习实验:只看证据,不急着运行 DSH

你可以在 GitHub 网页完成前四步;它们不需要 API key:

  1. 打开官方工具 README,找到“普通工具 schema”和 restrict()
  2. 打开官方系统提示词 README,确认 schema token、可见子集和 Code Mode 组装规则;缓存命中仍要通过 provider 实验确认。
  3. 打开作用域测试,记录它证明的是筛选与作用域,不是 OS 沙箱。
  4. 打开工具执行测试,记录注册、查找、执行和清理线索。
  5. 写下一个未验证项:当前没有真实模型 token、首轮延迟、选择错误率或 KV cache 命中率。

如果以后做运行实验,固定模型、provider、工具集合、schema、上下文、温度、平台和时间;至少比较“20 个全部可见”和“20 个注册、3 个可见”两组,并记录输入 token、首字节延迟、总延迟、错误调用和输出质量。两组之外不要随意改变配置。

以后最值得做的三项研究

  1. 做一次真正的工具预算基准。 在同一模型、provider、上下文和提示词下,随机交替比较“20 个 native 呈现”和“20 个注册、3 个 native 呈现”,重复多轮并记录输入 token、缓存 token、首字节延迟、总延迟、错误工具调用和任务完成质量。没有这些记录,就只能说“有设计上的成本风险”。
  2. 把可见集合做成可观察证据。 在宿主或实验插件里记录“注册集合 → restrict 后解析集合 → presentation 与最终组装 → 执行策略结果”,同时脱敏参数和凭据。这样才能知道究竟是注册太多、模型呈现太多,还是权限层配置错误。
  3. 建立社区扩展的信任清单。 给每个项目记录仓库所有者、固定 commit、包名、安装入口、是否改源码、是否访问私有状态、所需权限、卸载方式和测试证据;发现冒用官方身份或隐藏注入时单独标红,而不是只按“能不能运行”排序。

这三项是后续工作,超出本仓库当前已经完成的运行时证明。当前教材提供源码、测试和设计层面的阅读路线;真实模型性能实验与社区项目的安全背书,留给有运行条件的读者和维护者。

读完后的自测

  • [ ] 我能说出“已注册、agent 可解析、执行允许”三层状态,以及 presentAs() 到最终组装这条呈现线跟三层的区别。
  • [ ] 我能解释为什么 restrict() 可能减少 schema 成本,但不能替代沙箱。
  • [ ] 我能说明普通插件作者和 patched fork 维护者承担的责任不同。
  • [ ] 我能把一个社区项目拆成公开插件部分和侵入式部分分别审计。
  • [ ] 我知道当前教材有源码和测试证据,但没有真实模型性能基准。

接着读工具可见集合观测与性能实验,把三层状态和呈现线做成脱敏快照并学习 A/B 记录;再读官方工具插件完整契约学习工具的注册、呈现、执行、取消和卸载;最后回到社区生态与扩展边界核对具体项目的身份和安装行为。

如果你要把这些规则带去审核社区项目,使用工具预算与插件责任决策卡中的"五问决策卡"和"十分钟审计卡"。技能目录的渐进加载是上下文成本控制的另一个实例:技能目录实验演示摘要信封、digest 驱动的替换规则和 skill 工具的三种结局。

固定版本入口