工具可见性与非侵入扩展
这篇先回答一个直觉问题:如果 DSH “万物皆插件”,是不是所有插件的工具都会在普通模式的第一轮一起灌给模型?答案取决于 Profile、agent 作用域、呈现方式和最终提示词组装:注册可以很多,当前 agent 能解析的集合不必很多;能解析也不等于模型会收到每个工具的原生 schema。
证据范围固定在上游提交 aa6c361a972c8369148dea7380bb5c21c24e07ec。官方 README、源码和测试支持下面的设计解释;本学习仓库没有做真实模型延迟、token 或回答质量基准。
如果你只想先看一张总图,不想自己拼接多篇文章,先读工具预算与插件责任决策卡;本篇再展开其中的工具可见性和扩展边界。
先记住一句话
“万物皆插件”回答的是能力怎样装配、替换和卸载;某个 agent 每轮向模型公开多少工具,则交给作用域和呈现规则决定。一个工具先存在于运行时,再经过作用域筛选和呈现规则,最后才进入某个模型请求。
不要把下面三句话混成一句:
- 已注册:插件把工具放进运行时注册表,宿主知道它存在。
- 当前 agent 可解析:同一 agent 作用域的
get()和工具分发查找能解析这个工具;这是该 agent 的能力查找集合,不单独说明模型已经收到这个工具的原生 schema。 - 执行允许:策略、审批、沙箱、宿主账号和操作系统能力允许这一次调用真正运行。
可以用这张图记忆:
插件加载 / 注册
|
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 作用域改变第二层,换执行策略改变第三层。
那是一个教学模型:Bundle 名、工具名、风险分级和策略都由组件定义,来自任何真实部署的配置都不算数。它演示三层的嵌套关系和收窄归属;真实模型看到工具后是否调用、审批界面和审批人如何行动,属于组件之外的运行时问题。
循环卫生提醒在执行侧落实这条规则,但它是建议性的:数的是「工具名 + 规范化参数」这个键连续出现的次数,到第 3、5、8 次各注入一条提醒——第一档温和,之后带工具名、次数和参数预览。只换键序不算新调用;换了值或用户插话才清零链条。下面的实验把这套计数做成可以亲手推的时间线,顺带看「调用全部照常执行」的账目。
为什么工具列表可能影响模型体验
官方工具文档把 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 任务画成一个最小循环:
观察上下文与状态
-> 决定下一步
-> 调用模型或工具
-> 接收结果
-> 更新状态并再次观察Skill、Browser、截图、记忆和自修改,并不会自动跳出这个循环。它们主要扩展观测面、动作面、状态面或控制策略;是否有价值,要看循环是否更可靠、更快或更便宜。
工具延迟加载改变的是这一轮的候选集合和 schema 成本:可能减少干扰,也可能让模型暂时找不到需要的工具。要同时比较发现成功率、漏选率、恢复调用和任务结果,再判断这次加载策略的收益。
复杂系统可以加入路由器、Planner、Executor、事件驱动状态机或多个 Agent。它们通常是多个循环或状态机的组合。“仍然是 loop”是一个有用的抽象,不等于所有设计的能力和可靠性都相同。
“万物皆插件”描述的是装配方式,效果要另行证明:任务能否完成还要经过提示词、模型决策、工具执行、外部环境和失败恢复。源码里出现一个工具名,只说明注册层有它;这一轮模型是否收到,要查呈现与组装记录。
普通模式应该怎样设计
如果一个产品真的有很多插件,优先按下面顺序治理:
- 先减少默认加载:正常 Profile 只装当前任务常用的 Bundle;开发、调试和实验能力放到单独 Profile。
- 再限制 agent 视野:用 agent 作用域的
restrict()只保留当前任务需要的工具。 - 再选择呈现方式:按模型和任务选择
native、code或both,不要把 Code Mode 当成自动减 token 的开关。 - 最后精简 schema:名称要区分,description 只解释模型需要的决策信息,参数和枚举不要无谓展开。
- 保持集合稳定:频繁注册、卸载、重排工具会改变模型请求前缀,应该记录发生原因。
“隐藏 UI 按钮”属于另一回事:用户界面隐藏一个按钮,schema 是否发给模型、执行权限是否保留都原样不变。这两件事分别由组装结果和执行策略决定,检查时要分开看。
把“工具很多”落到三个模式
下面是教材建议的分层,不是上游固定的 Profile 名称或现成命令。真正实现时,仍然要回到固定提交的 Profile、作用域和宿主策略核对。
| 模式 | 默认让模型看到什么 | 适合做什么 | 不应误解成什么 |
|---|---|---|---|
| 正常模式 | 当前任务所需的一小组稳定工具 | 日常对话、减少无关 schema | 不是把其他工具删除,也不是安全沙箱 |
| 开发模式 | 任务工具加上少量调试、观测工具 | 开发插件、看注册和结果事件 | 不是可以随意改私有 registry |
| 审计模式 | 工具可以全部注册,但按实验逐组暴露 | 比较 schema、权限和选择行为 | 不是把所有工具一次性塞给模型 |
初学者只需要记住一个动作:先固定“运行时注册了什么”,再固定“本次 agent 能解析什么”,再记录“它以 native、code 还是 both 呈现”,最后才检查“执行层允许什么”。如果这些状态没有分别记录,看到一个工具名称出现在代码里,不能推出它已经进入每一轮请求。
一个初学者能执行的判断题
假设系统里注册了 20 个工具,但某个 agent 只需要读文件、搜索和查看状态:
注册层: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。只打开下面三个固定版本文件,按顺序找一个符号,然后写三句话:
- 工具运行时入口:找到
register()、restrict()或schemas()的职责边界。 - 作用域测试:找到一个测试,说明被限制的工具从哪个作用域的 schema 结果消失。
- 工具子系统说明:核对文档是否把“注册、作用域、呈现、执行”分开。
把答案写成下面这个最小产物就算完成:
源码事实:哪个函数把工具登记到运行时,哪个函数改变 agent 作用域的解析集合?
测试证据:哪个断言支持“当前作用域看不到它”,但没有支持“工具已经从全局注册表删除”?
未验证项:我还没有启动真实 Profile、调用 provider,或证明模型实际收到的 token/延迟变化。如果三份文件的说法不一致,先记录“文档与源码需要人工复核”,不要自行补一个看起来合理的设计理由。这一练习训练的是从源码片段 → 测试断言 → 下一跳,而不是背诵 restrict() 的名字。
为什么“非侵入式”更适合作为默认路线
非侵入式扩展把代码放在宿主已经承诺的接口上:升级时跟着公开 API 走,卸载时按声明清理。优先顺序可以是:
| 需求 | 首选做法 | 插件作者应负责什么 |
|---|---|---|
| 观察或记录 | ctx.on() 与公开 live event | 监听器生命周期、日志和 dispose |
| 新增能力 | ctx.provide()、Service、ctx.tools.register() | 类型、配置、schema、失败、取消和卸载 |
| 修改当前决策 | 文档列出的 waterfall、guard()、审批接口 | 决策理由、顺序、单调拒绝和测试 |
| 组合功能 | Bundle、Profile、cordis.patch.yml | manifest、版本范围、加载顺序和回滚 |
| 外部协议接入 | Hook bridge 或兼容层 | 输入输出映射、超时、权限、失败和协议版本 |
这些方式共享 Cordis 的 Context、Fiber 和 Effect 规则。插件不需要私自摸宿主的模块缓存或内部 Map,也能在明确的生命周期内注册和清理自己的资源。
侵入式能力的责任不能偷偷下放
一种常见说法是“侵入的所有工作不能由插件制作者来做”,这个说法需要改准:普通插件作者不默认替宿主承担侵入式修改;一旦某人真的维护 patch/fork,他的身份就从插件作者变成宿主或发行版维护者。
责任可以这样分:
| 技术需求 | 应由谁拥有 | 对外应怎样写 |
|---|---|---|
| 公开 API 能完成 | 普通插件作者 | “第三方插件,支持 DSH 版本范围” |
| 缺一个公开扩展点 | 上游宿主维护者 | 提交 feature request 或维护公开 patch |
| 必须改变核心一致性 | 宿主/发行版维护者 | “patched fork”,公布基线、差异和同步策略 |
| 只需翻译外部 Hook 协议 | bridge 维护者 | “兼容层”,公布协议子集和权限 |
| 必须改私有对象、模块缓存或进程 | 专门的实验/集成维护者 | “非官方运行时补丁”,单独审计、锁版本、可回滚 |
插件作者可以提出需求、维护兼容层,甚至公开一个 patched fork;但方案里一旦出现核心改写、私有 registry 重建、进程注入或 Windows 注册表修改,对外身份就必须相应升级,不能沿用“安装插件”的说法。
安装说明至少要明确:修改了哪一层、需要什么权限、支持哪个上游 commit、如何卸载、失败怎样恢复、是否会写文件或启动子进程。没有这些信息,就不能把它和普通 Bundle 放在同一个信任等级。
从需求到分类:五问决策树
能否用公开 Service / Event / Tool API?
└─ 能:普通插件
能否只用官方 Bundle / Profile / patch 组合?
└─ 能:组合层扩展
是否只是翻译外部 Hook 协议?
└─ 是:Hook bridge / 兼容层
必须改变核心事件或一致性?
└─ 是:宿主维护的 patch / fork
必须摸私有对象、模块缓存、进程或系统配置?
└─ 是:非官方、版本敏感实验,不应伪装成普通插件一个项目可以同时属于两类。例如它的前半段用官方 Bundle 注册工具,后半段用私有 registry 注入另一个工具;审计时应拆开写,不能因为前半段合规,就给后半段自动背书。
插件作者提交前的八项自检
把下面清单贴到自己的插件 README 或审查记录中,比只写一句“支持 Hook”更容易复核:
- 我使用的是哪一个公开 Service、Event、Tool API 或 Bundle 入口?
- 我的工具默认是否进入所有 agent 的可解析或模型呈现集合?如果是,为什么不能按任务限制?
- 名称、description、参数和枚举是否只保留模型做决策所需的信息?
- 我是否把“模型收到原生 schema 或 Code Mode 入口”误写成“执行安全”?真正的文件、网络、子进程和凭据边界在哪里?
- 插件卸载时,监听器、服务、子进程、文件 watcher 和临时文件由谁清理?
- 是否读取或改写了未文档化字段、私有 Map、模块缓存、构建产物或进程?
- 如果改了核心源码,我是否公开写出上游 commit、差异、版本矩阵、权限和回滚方式?
- 我能提供哪些证据:源码、单元测试、安装日志、真实运行、卸载检查,哪些仍然没有?
第 6 项只要回答“是”,就不要把项目的全部能力包装成普通插件;第 7 项只要回答“是”,对外身份就应改成 patched fork、发行版或非官方兼容层维护者。
最小学习实验:只看证据,不急着运行 DSH
你可以在 GitHub 网页完成前四步;它们不需要 API key:
- 打开官方工具 README,找到“普通工具 schema”和
restrict()。 - 打开官方系统提示词 README,确认 schema token、可见子集和 Code Mode 组装规则;缓存命中仍要通过 provider 实验确认。
- 打开作用域测试,记录它证明的是筛选与作用域,不是 OS 沙箱。
- 打开工具执行测试,记录注册、查找、执行和清理线索。
- 写下一个未验证项:当前没有真实模型 token、首轮延迟、选择错误率或 KV cache 命中率。
如果以后做运行实验,固定模型、provider、工具集合、schema、上下文、温度、平台和时间;至少比较“20 个全部可见”和“20 个注册、3 个可见”两组,并记录输入 token、首字节延迟、总延迟、错误调用和输出质量。两组之外不要随意改变配置。
以后最值得做的三项研究
- 做一次真正的工具预算基准。 在同一模型、provider、上下文和提示词下,随机交替比较“20 个 native 呈现”和“20 个注册、3 个 native 呈现”,重复多轮并记录输入 token、缓存 token、首字节延迟、总延迟、错误工具调用和任务完成质量。没有这些记录,就只能说“有设计上的成本风险”。
- 把可见集合做成可观察证据。 在宿主或实验插件里记录“注册集合 → restrict 后解析集合 → presentation 与最终组装 → 执行策略结果”,同时脱敏参数和凭据。这样才能知道究竟是注册太多、模型呈现太多,还是权限层配置错误。
- 建立社区扩展的信任清单。 给每个项目记录仓库所有者、固定 commit、包名、安装入口、是否改源码、是否访问私有状态、所需权限、卸载方式和测试证据;发现冒用官方身份或隐藏注入时单独标红,而不是只按“能不能运行”排序。
这三项是后续工作,超出本仓库当前已经完成的运行时证明。当前教材提供源码、测试和设计层面的阅读路线;真实模型性能实验与社区项目的安全背书,留给有运行条件的读者和维护者。
读完后的自测
- [ ] 我能说出“已注册、agent 可解析、执行允许”三层状态,以及
presentAs()到最终组装这条呈现线跟三层的区别。 - [ ] 我能解释为什么
restrict()可能减少 schema 成本,但不能替代沙箱。 - [ ] 我能说明普通插件作者和 patched fork 维护者承担的责任不同。
- [ ] 我能把一个社区项目拆成公开插件部分和侵入式部分分别审计。
- [ ] 我知道当前教材有源码和测试证据,但没有真实模型性能基准。
接着读工具可见集合观测与性能实验,把三层状态和呈现线做成脱敏快照并学习 A/B 记录;再读官方工具插件完整契约学习工具的注册、呈现、执行、取消和卸载;最后回到社区生态与扩展边界核对具体项目的身份和安装行为。
如果你要把这些规则带去审核社区项目,使用工具预算与插件责任决策卡中的"五问决策卡"和"十分钟审计卡"。技能目录的渐进加载是上下文成本控制的另一个实例:技能目录实验演示摘要信封、digest 驱动的替换规则和 skill 工具的三种结局。