跳至内容

工具可见集合观测与性能实验

这篇把“工具太多会不会影响模型”变成可以检查的问题。先记住边界:本篇给出观测格式和实验方法,不声称已经测出了某个模型的 token、首字节延迟、总延迟或回答质量。

如果你还没分清“插件、patch、fork 和注入”分别是谁负责,先读工具预算与插件责任决策卡;本篇只负责把工具集合变成可记录、可比较的实验对象。

本篇的三层来自工具可见性与非侵入扩展,那一页有一个可以逐层收窄的组件:勾掉 Bundle 改变已注册集合,换 agent 作用域改变可解析集合,换策略改变允许执行的集合。先在那里看清三层怎样嵌套,再回到本篇设计观测格式。组件给的是教学模型,不是任何真实部署的工具清单。

先把一个问题拆成三层和一条呈现线

同一个工具在 DSH 里要分成三层状态,再加一条“怎样呈现给模型”的线。只看到工具名称出现在代码里,不能推出它已经进入每一轮模型请求。

层次初学者可以怎样理解适合记录什么
已注册运行时知道这个工具存在,可能可以查到它的定义工具名、来源、版本、注册或注销事件
当前 agent 可解析同一作用域的 get() 和工具分发查找能解析这个工具工具名、作用域、查找结果
实际模型呈现presentAs(native / code / both) 与最终提示词组装决定模型收到原生 schema、Code Mode 入口,还是两者呈现方式、最终工具字段、schema 摘要、顺序
执行允许这一次调用经过策略、审批、guard、沙箱和宿主权限检查工具名、允许/拒绝、策略原因、执行结果

官方的 ToolProviderResult 还把“模型可见的 schemas”和“限制前的 knownNames”分开;工具 registry 的 schemas(scope) 则是作用域解析结果的投影。这样工具顺序检查可以知道完整候选集合,而最终组装仍然可以按呈现方式收束。这个设计正好说明:注册集合、解析集合、模型呈现和执行权限不能混成一个列表。

可以把一次调用前后的关系画成这样:

text
插件加载
  -> 注册集合
  -> agent 作用域与 restrict()
  -> 当前 agent 可解析的工具
  -> presentAs(native / code / both)
  -> 实际组装出的模型工具字段
  -> 模型选择工具
  -> pre-execute / guard / approval / sandbox
  -> execute / post-execute
  -> result 观察与 Session 记录

restrict() 会同时影响某个 agent 作用域的 get() 和由该 agent 发起的 execute() 查找,使被排除工具在这一作用域中表现为未知;在 native 呈现下,它也不会进入该次原生 schema 组装。presentAs() 主要改变工具怎样呈现给模型,同样不增加执行权限。文件系统、网络、子进程和凭据各有专门的策略与宿主能力边界,第 22 课对这条边界有完整表述。

不要只测 schema 字节数

schema 的 UTF-8 字节数是一个好用的起点,但它只回答“序列化后的描述有多大”。它不能单独回答模型是否更会选工具、任务是否更容易完成,或者 provider 是否真的更快。

指标层至少记录什么不能单独推出什么
上下文成本可见 schema 字节、估算 token、最终 prompt 字节真实 provider token 或缓存命中
工具发现与选择找到工具的成功率、漏选率、选错率、重复调用率工具越少一定越好
时间成本发现耗时、首个工具调用时间、首 token、总延迟变短一定来自工具集合
任务效果成功率、正确性、证据完整度、人工盲评一次成功案例的普遍收益
稳定性超时、重试、取消、失败恢复、循环次数没有错误就代表设计完美
安全边界越权尝试、错误工具暴露、敏感参数泄漏、审批结果可见集合就是 OS 沙箱
运行成本provider token、调用次数、缓存字段、费用本地字节数就是账单成本

工具延迟加载至少可以比较几种策略:全部暴露、按 Profile 暴露、按任务作用域暴露、先给轻量目录再展开 schema、先由路由器筛选候选,或失败后扩大搜索范围。每种策略都要记录“找得到、选得对、用得成、失败能恢复”这四个结果。

宿主应该观测哪四个时点

如果你是宿主维护者,或者维护一个明确标注为 patched fork 的发行版,建议至少保留下面四个快照。普通插件作者只能使用宿主公开的观测接口,不能为了补齐快照而偷偷读取私有 Map、模块缓存或内部 Loader 状态。

时点需要回答的问题不要越界解释成什么
注册集合运行时到底装载了哪些工具?来源和版本是什么?不等于本次模型已经看见
作用域解析/restrict() 后集合这个 agent 的作用域筛掉了哪些工具?同一作用域的 getexecute 是否也把它视为未知?不等于 OS 权限已经收紧
实际模型呈现与组装本次模型请求最终拿到了哪些原生 schema、Code Mode 入口和顺序?不等于模型一定会调用它
执行策略决定调用到达后为什么允许、拒绝或等待审批?不等于工具身体已经成功运行

工具注册/注销或作用域限制发生变化时,官方 tools/change 事件可以作为变化提示。tools/pre-executetools/executetools/post-execute 分别位于执行流水线的不同阶段,tools/result 适合观察最终结果。它们可以帮助你记录生命周期,但不能替宿主提供不存在的“实际 prompt 快照”。

宿主可以直接导出一份脱敏调试快照

本学习仓库在 ToolRuntime 上增加了只读的 ctx.tools.debugSnapshot(scope?),用于让宿主观察工具集合,而不是让普通插件读取私有 registry。它返回深度冻结、可 JSON 序列化的名称和成本信息,不执行工具,不替换提示词,也不包含 execute、presenter 回调、参数、凭据或用户内容;设计记录见脱敏 ToolRuntime 调试快照

把返回值先按下面六个字段读,不要一上来把它当成模型请求的完整 prompt:

字段初学者解释适合得出的结论
registered被检查的那一层自己注册了哪些名称哪些名称由这一层拥有
known作用域遮蔽完成、限制之前的候选名称限制前这个 agent 可能继承哪些工具
visible限制、遮蔽和呈现 transport 加入后的运行时解析集合这一路径能否解析这些名称
hiddenByRestriction被作用域 allow/deny 隐藏的继承名称哪些名称因限制从候选集合消失
visibleSchemas真正投影给模型 wire 层的名称和 UTF-8 字节数原生 schema 或 run_code 入口的大小
visibleSchemaUtf8BytesvisibleSchemas 字节数之和两份固定快照的本地序列化成本差异

Code Mode 下尤其要分开看:visible 可能同时包含 SDK 可到达的业务工具和 run_code,而 visibleSchemas 可能只包含直接给模型的 run_code schema。UTF-8 字节数是稳定的序列化大小指标,不是 provider tokenizer 的真实 token 数,也不等于完整系统提示词大小。

这份 API 解决的是“宿主到底组合出了什么”的观测问题,不是权限隔离 API。即使 visible 数量减少,文件系统、网络、子进程、凭据和操作系统权限仍要由各自的宿主策略单独证明;普通插件也不应为了补充字段而读取私有 Map 或修改 Loader。

脱敏快照应该长什么样

下面是一个可以交给离线检查器的最小示例。它只放工具名、来源、版本和 schema 形状,不放完整 prompt、参数值、凭据、用户内容、绝对路径或文件正文。

json
{
  "profile": "normal",
  "agent": "default",
  "registered": [
    { "name": "read_file", "source": "example", "version": "1.0.0" },
    { "name": "search_text", "source": "example", "version": "1.0.0" }
  ],
  "visible": [
    {
      "name": "read_file",
      "presentation": "native",
      "schema": {
        "type": "object",
        "properties": { "path": { "type": "string" } },
        "required": ["path"]
      }
    }
  ],
  "execution": [
    { "name": "read_file", "allowed": true, "reason": "policy" }
  ]
}

这里的 registered 有 2 个工具,visible 只有 1 个模型呈现条目,execution 只说明这一次 read_file 的策略结果。它没有证明 search_text 被删除,也没有证明 read_file 的路径一定安全;这正是分层记录的价值。若实验使用 code 模式,还要另行记录 run_code、SDK 段和绑定工具集合,不能把 visible 直接解释成原生 schema 数量。

仓库附带的离线检查器只是一个离线检查器。它读取你导出的 JSON,不启动 DSH、不连接模型、不修改注册表,也不会替你生成真实 token。第一次使用可以这样做:

sh
node study-tools/inspect-tool-visibility.mjs study-tools/tool-visibility-snapshot.example.json

它会输出已注册数量、可见数量、注册但不可见的工具、可见但未登记的工具、schema 的 UTF-8 字节数和执行允许/拒绝数量。输出中的粗略 token 数只用来帮助比较快照,必须看成启发式估计,不是任何 provider 的 tokenizer 结果。

你第一次运行后应该看到什么

当前仓库提交的教学夹具(study-tools/tool-visibility-snapshot.example.json,比上面的最小示例多一个 run_command)会得到一份类似下面的摘要。数字属于这份夹具文件,不是 DSH 默认工具数量,也不是上面那个两工具示例的输出;以后夹具改变时,先看字段含义,再比较数字:

text
profile: normal        agent: default
registered: 3          visible: 1
registeredButNotVisible: run_command, search_text
visibleSchemaUtf8Bytes: 77
execution: allowed 1 / denied 1 / unknown 0

如果你看到“注册 3 个、可见 1 个”,正确的第一句话是“这份离线快照记录了 3 个注册项,其中 1 个进入了可见集合”;不能直接说“模型已经收到 1 个 schema”。还要继续看 presentation、最终 prompt 组装和 provider 返回的 token 字段。若命令报“找不到文件”,先确认当前目录是仓库根目录,并把命令中的示例路径写完整;不要为了绕过路径问题去运行真实 DSH。

先做 A/B 结构预检,再谈性能

把两个 JSON 快照直接拿去比较,容易把“工具 schema 变了”“Profile 变了”或“注册集合也变了”误当成可见数量的效果。仓库附带的A/B 结构预检器把比较接口收窄为两个文件:它要求 profileagentregistered 和可选的 fixed 实验条件一致;profileagent 不能省略或写空,只允许 visible 集合和执行结果出现差异;共同可见工具的 schema、呈现方式和相对顺序也必须保持不变。

先用仓库里的两组教学快照练习:

sh
node study-tools/compare-tool-visibility-ab.mjs study-tools/tool-visibility-ab.a.example.json study-tools/tool-visibility-ab.b.example.json

输出中的 valid: true 只表示“这两组快照适合进入下一步实验”,不是 provider 性能已经测完。命令会同时报告可见数量、schema 字节数和新增/移除工具;如果 valid: false,先修复实验条件,不要继续收集延迟数字。

这层预检器故意不读取 DSH 私有 registry,也不启动模型。它位于宿主观测接口和真实 provider 实验之间:宿主负责导出脱敏快照,预检器负责检查比较条件,实验运行器再负责记录 provider token、缓存字段、首 token 延迟、总延迟、工具错误、盲评质量和成本。

先建立不依赖 provider 的本地基线

如果还没有 API key,可以先运行离线性能基准。它复用上面的 A/B 结构预检,然后重复测量 JSON 解析、可见名称集合差异和 visible 列表序列化:

sh
node study-tools/benchmark-tool-visibility-ab.mjs study-tools/tool-visibility-ab.a.example.json study-tools/tool-visibility-ab.b.example.json --iterations 1000 --warmup 100

这一步会给出可见工具数、visible JSON 字节数、schema 字节数和 bytes / 4 粗略代理,也会报告当前 Node、平台和架构。它能回答“这两份固定快照的本地准备成本如何”,不能回答 provider tokenizer 的真实 token、网络延迟、首 token、总延迟或模型质量。先保存这份基线,未来拿到真实 provider 后,再用同一批脱敏快照和固定任务补做真正的 A/B 实验。

再往前一步:让真实宿主参与,但仍不调用模型

仓库还提供一个更接近 DSH 运行时的离线实验:

sh
pnpm run build:lib:host
pnpm run study:runtime-benchmark -- --iterations 200 --warmup 25

如果完整的 build:lib:host 因为你本地的站点类型依赖失败,可以先按构建日志修复依赖,再运行;不要把“脚本找不到 debugSnapshot()”误解成宿主 API 不存在。这个实验直接加载当前工作树已经构建出的 @deepseek-ai/dsh-tools@deepseek-ai/dsh-system-prompt@deepseek-ai/dsh-scope,然后:

  1. 用真实 ToolRuntime 注册 24 个学习夹具工具。
  2. A 组读取全局 native 视图。
  3. B 组在同一个 agent 作用域调用公开 restrict({ allow }),只保留 read_filesearch_textshow_status
  4. 分别调用 debugSnapshot()schemas()SystemPrompt.assemble(),记录工具数量、wire schema 的 UTF-8 字节数和本地准备阶段耗时。

这比“手写两个 JSON 快照”多了一层运行时证据,但仍然不是模型性能实验。它没有加载真实 Profile、安装 Bundle、执行文件或网络工具、调用 provider,也没有得到 tokenizer、缓存、首 token、总延迟或任务质量。报告里的计时只适合在相同机器和相同版本上做相对观察;不要把一次 A 组比 B 组快的输出写成“工具少一定让模型更快”。

此实验使用的是学习仓库当前工作树中新增的 debugSnapshot() 观测接口,不是固定上游提交已经承诺的公共 API。若把它带入产品,应由宿主维护者审阅 API 稳定性、隐私字段、版本兼容和删除策略;普通插件不应自行读取私有 registry。

本仓库的一次真实宿主运行记录

下面不是手写 JSON 的演示,而是本学习仓库在 Windows 本机实际运行 ToolRuntime、作用域限制和系统提示词组装得到的一次记录。这样读者可以先学会读报告,再决定是否要接入真实 provider。

本轮最新运行命令:

sh
pnpm run build:lib:host
pnpm run study:runtime-benchmark -- --iterations 200 --warmup 25

运行环境:Node v24.15.0、Windows win32x64。本轮复跑时间为 2026-08-17(Asia/Shanghai),预热 25 次,正式测量 200 次。实验使用同一个 ToolRuntime 和同一个 24 个工具的注册集合:A 组全部 native 可见;B 组仍注册 24 个工具,但在同一个 agent 作用域通过公开的 restrict({ allow }) 只保留 read_filesearch_textshow_status。没有删除注册,也没有执行工具。

观测项A 组:全部可见B 组:限制为 3 个这列能说明什么
注册工具数2424两组的注册集合相同
当前可见工具数243作用域限制改变了当前可见集合
可见 schema UTF-8 字节4,524533本地原生 schema 序列化规模不同
prompt assembly 工具数243组装阶段采用的工具数量不同
toolWireUtf8Bytes4,549537本地工具 wire 字段的序列化规模不同

在 200 次测量(预热 25 次)中,三类本地准备操作的平均耗时如下。这里的单位是纳秒;它是本机相对基线,不是 provider 延迟。

操作A 组每次B 组每次本次 B 相对 A 的观察
debugSnapshot()309,234 ns160,564 nsB 较低
schemas()338,379.5 ns96,022 nsB 较低
SystemPrompt.assemble()158,517.5 ns89,516.5 nsB 较低;仍应按本机相对基线解读
200 次测量下 A/B 两组本地准备操作平均耗时(纳秒)A 组:24 个工具全部可见B 组:restrict 后可见 3 个debugSnapshot()schemas()SystemPrompt.assemble()309,234160,564338,38096,022158,51889,5170340,000 ns · 每次操作平均,越短越快

图中每一根条都来自上表同一个数字;比例尺以 schemas() 的 A 组为满刻度。它是同一台机器、同一工作树里的相对观察,不是 provider 延迟,也不能跨机器比较。

这次报告同时记录了 providerCalls: 0modelCalls: 0dshProcessStarted: false,并且 comparison.valid: true。因此本次运行支持的结论是:在这台机器、这个工作树、这个固定夹具和这个公开作用域 API 下,注册集合可以保持不变,而可见 schema、工具 wire 字段和本地准备阶段出现可观测差异;本轮三项准备操作的 B 组计时都低于 A 组。这是 2026-08-17 本机复跑的一次相对观察,不是跨机器或 provider 的性能承诺。

本次运行没有证明以下任何一项:provider input tokens、cached tokens、首 token 延迟、网络延迟、真实 Profile 默认工具数量、模型回答质量、工具执行耗时。它同样没有证明“工具越少模型一定更聪明”,也没有证明“在所有机器上一定更快”。要研究这些问题,必须接入固定的 provider、模型、任务和评分表,并把失败、成本和质量一起记录。由于 debugSnapshot() 是本学习仓库当前工作树新增的观测接口,这条记录也不能改写成“固定上游 DSH 已经承诺该公共 API”。

此前一次 1,000 次复跑记录

在同一工作树、Node v24.15.0、Windows win32x64 上,本轮又以预热 100 次、正式测量 1,000 次复跑。A 仍是 24 个 native 工具全部可见,B 仍注册 24 个但只在 agent 作用域保留 read_filesearch_textshow_statuscomparison.valid: trueproviderCalls: 0modelCalls: 0dshProcessStarted: false

操作A 组每次B 组每次本次 B 相对 A 的观察
debugSnapshot()165,596.1 ns63,184.2 nsB 较低
schemas()140,185.3 ns32,164.6 nsB 较低
SystemPrompt.assemble()74,318.4 ns45,690.9 nsB 较低

这次复跑与前面的 200 次记录方向一致,但绝对纳秒值不同。因此它加强的是“在这个固定本地夹具中,缩小可见集合会改变准备阶段”的机制证据,不是跨机器性能承诺。它仍没有测 provider token、缓存、首 token、网络、任务质量或真实插件 Profile。

此前两次 1,000 次复跑记录

下面保留此前两次使用 1,000 次测量、预热 100 次的记录,用来展示同一条件下的抖动;它们不是上面最新 200 次运行的替代品。注册数量、可见集合、schema 字节数、providerCallsmodelCallsdshProcessStarted 没有改变;准备阶段的纳秒数字发生了变化:

操作第一次 A / B第二次 A / B应该怎样读
debugSnapshot()125,716.5 / 56,189.1 ns152,205.6 / 61,810.4 ns两次都是 B 较低,但绝对值随运行状态波动
schemas()124,129.3 / 26,966.0 ns119,988.5 / 27,507.7 ns集合规模差异仍可观察,数值不是跨机器承诺
SystemPrompt.assemble()66,603.9 / 38,914.6 ns68,488.1 / 36,253.9 ns只能作为本机同条件的相对观察

这几次复跑证明了计时会受 JIT、CPU、垃圾回收和机器负载影响,而且单个操作可能反向波动。以后如果要把工具集合与 provider token、首 token、总延迟或任务质量关联起来,应固定模型、任务、并发、预热、随机化顺序和失败处理,并至少交错运行多轮;这里的本地准备阶段计时不能替代那项实验。

normal、development、audit 不是官方固定 Profile

为了让初学者容易操作,本教材使用下面三档称呼。这是研究方法的命名,不是上游已经承诺存在的三个命令或三个内置 Profile。

模式建议暴露方式适合谁主要风险
normal只加载常用 Bundle,并让当前任务需要的少量工具可见日常对话和稳定产品误以为“未显示”就等于“没有权限”
development额外记录注册变化、schema 组装和结果事件插件作者、宿主开发者为了调试而改私有 registry
audit工具可以全部注册,但按实验逐组改变模型呈现集合性能和安全审计同时改变模型、prompt、schema 和权限,导致实验无法比较

推荐的默认顺序是:先减少不需要的 Bundle,再按 agent 作用域限制解析集合,再检查呈现方式和 schema,最后才考虑是否需要源码级修改。把所有工具注册着不一定有问题;让所有工具在所有 agent 的每一轮都进入模型呈现,才是需要用数据验证的设计选择。

A/B 实验:20 个全部 native 呈现,还是 20 个注册、3 个 native 呈现

最小实验可以采用下面两组:

  • A 组:注册 20 个工具,在 native 呈现且最终组装不再替换的条件下,20 个都向模型提供名称、描述和 schema。
  • B 组:仍然注册 20 个工具,在相同 native 呈现条件下当前 agent 只提供 3 个工具的 schema;其余工具不从注册表删除。

两组都完成同一个任务,例如“找到一个文件中的配置项并说明它的用途”。为了让结果能比较,至少固定这些变量:

必须固定为什么
model 和 provider不同模型或服务商的 tokenizer、缓存和工具调用策略不同
用户 prompt 与上下文问题难度和历史消息会直接改变输入量与质量
20 个工具的名称、description、schema 和顺序否则你测到的是文案变化,不是可见数量变化
temperature、其他生成参数和平台随机性、网络和机器状态会影响延迟与回答
Profile、权限、沙箱、工具实现和时间窗口不能让 B 组顺便获得更严格或更快的执行路径

建议先做少量预热,再让 A、B 交错运行;避免先把 A 组全部跑完、隔很久再跑 B。每次运行都写入同一个实验表;失败、超时和错误工具调用不能静默丢掉。

每次至少记录什么

指标记录方式解释边界
input tokens优先记录 provider 返回的数值不要用字符数替代 provider token
cached input tokens如果 provider 提供就单独记录没有字段时写“未提供”,不要猜命中
首字节或首 token 延迟记录请求开始到第一个有效响应的时间网络和排队时间也会进入这个数字
总延迟记录请求开始到完成的时间工具执行耗时要单独拆开
工具调用错误记录未知工具、参数错误、拒绝、超时和重复调用“最终回答成功”不能抹掉中间错误
任务质量用固定评分表或人工盲评不要只凭一次回答下结论
成本记录输入、输出和工具相关费用字段没有 provider 价格或 token 就写未测

UTF-8 字节数可以帮助发现 schema 是否明显膨胀。bytes / 4 只能作为非常粗略的启发式估计,因为不同 tokenizer 对中文、英文、标点、JSON 结构和特殊 token 的切分都不同。它不能代替真实的 provider token 统计,更不能直接推出延迟或质量。

结果表模板

复制下面的表格到学习工作簿与首个实验,每一行代表一次请求。没有 API key 或模型服务时,可以先填写快照大小,其他栏写“未测”。

编号组别可见工具数schema UTF-8 字节provider input tokenscached tokens首 token ms总 ms错误工具调用质量分备注
1A20待填未测未测未测未测0待填预热/正式
2B3待填未测未测未测未测0待填预热/正式

实验结论也要分层写:

text
源码事实:工具注册、作用域筛选和 schema 组装分别存在。
快照事实:这次导出的注册集合有 20 个,native 模型呈现集合有 3 个。
模型事实:provider 报告的 input tokens 和延迟是多少。
质量事实:固定评分表下 A、B 的任务结果如何。
未验证项:哪些模型、provider、平台或插件组合还没有测。

如果只有第一层或第二层,就只能说“架构允许按集合治理”或“快照显示集合不同”;不能写成“B 组一定更快”或“模型一定更聪明”。

谁可以做什么

普通插件作者可以:

  1. 通过公开工具、服务、事件和作用域 API 注册自己的能力。
  2. 在宿主提供公开观测接口时记录自己的注册、注销和执行结果。
  3. 提供脱敏快照或测试夹具,让宿主维护者可以复核解析集合和模型呈现集合。

普通插件作者不应为了观察模型集合而直接改私有 registry、重建 Loader、替换模块缓存、修改构建产物或写入 Windows 注册表。若这些动作不可避免,维护者的身份已经变成宿主、发行版、patched fork 或非官方注入器维护者,必须单独公开基线、差异、权限、版本矩阵和回滚方式。

这也是“侵入式工作不能默认交给插件作者”的准确说法:不是说插件作者永远不能维护 fork,而是不能把宿主级修改藏在普通插件安装步骤里,让用户误以为它和公开 Bundle 具有同一信任等级。

初学者的 9 步练习

  1. 打开工具可见性与非侵入扩展,用自己的话写出“已注册、agent 可解析、实际模型呈现、执行允许”。
  2. 复制示例快照文件,只改工具名和数量,不填真实参数。
  3. 运行离线检查器,查看“注册但不可见”和 schema 字节数。
  4. 运行上面的 A/B 结构预检器,确认 registered 数量不变、native 模式下的 visible 数量减少、共同 schema 没有变化。
  5. 运行离线性能基准,记录本地快照处理成本,并把它和真实 provider 指标分开。
  6. 如果本地宿主库已构建,再运行 pnpm run study:runtime-benchmark -- --iterations 20 --warmup 5,比较 debugSnapshotschemasSystemPrompt.assemble 的 A/B 准备阶段。
  7. 记录一项不能从快照或宿主准备阶段推出的事实,例如“没有真实 provider token”。
  8. 如果以后拿到模型服务,再按 A/B 表固定变量、交错运行并记录失败。
  9. 把结果写回工作簿,注明模型、provider、平台、时间和清理动作。

完成这 9 步,你学到的是怎样把一个架构判断变成可复核的实验问题。

实验前可以再用工具预算与插件责任决策卡的审计卡检查:本次快照由谁导出、是否改动了宿主、哪些权限和运行证据仍然缺失。

固定版本入口