首页
/ DeepSeek Harness 工具参数 schema DSL:从否决 Schemastery 到统一 JSON 值词汇的设计决策

DeepSeek Harness 工具参数 schema DSL:从否决 Schemastery 到统一 JSON 值词汇的设计决策

2026-09-03 15:41:05作者:卓艾滢Kingsley

本文围绕 DeepSeek Harness 仓库中一份已归档的架构决策笔记(2026-06-11-custom-schema-dsl.zh.md)展开,讲解"工具参数必须以标准 JSON Schema 到达模型,同时工具作者在 execute(args) 中拿到类型化参数且无需任何类型断言"这一双重目标是如何落地的。读完本篇,你可以理解 ParameterSchemaSpec / InferArgs / parameterSchemaSpecToJsonSchema() / defineTool() 四件套的职责分工,知道为什么项目否决了已 vendor 的 Schemastery,并能定位到 packages/core/tools/src/schema.ts 中的编译器实现与类型级回归测试。

问题的双重约束

架构笔记在"问题"一节把需求浓缩为一句话:

工具参数必须以标准 JSON Schema 形式到达模型,同时让工具作者在 execute(args) 中获得类型化的参数而无需类型断言。

这两个约束之间存在天然张力:

  • 模型侧要求的是"线上协议格式"(wire format)——一个能被 LLM 正确理解的 JSON Schema 对象,即 type: 'object'properties、独立的 required: string[] 数组;
  • 作者侧希望的是一种贴着属性的书写习惯——在每个属性旁边写 required: true,而不是在对象外面再维护一份 required 键名列表,否则两处声明很容易失同步。

值得注意的是,仓库此时已经 vendor 了 Schemastery(用于插件 Config 的校验/转换),但笔记明确指出:工具作者 API 需要的是逐属性的 required: true 布尔值,而非 JSON Schema 的独立 required 数组。这个"看起来只差一个字段"的差别,最终演变成整个自定义 DSL 的立项理由。

决策:自定义类型化工具 schema DSL

该决策的现行形态已被后续的统一 JSON 值 schema DSL取代,但原笔记中的四个核心构件全部保留,构成 dsh-tools 的类型化作者面:

构件 职责 源码位置
ParameterSchemaSpec 逐属性参数声明,保留 required: true 约定 schema.ts#L96-L106
InferArgs<S> 类型推导:必需键 → 非可选属性,其余 → 可选属性 schema.ts#L119-L131
parameterSchemaSpecToJsonSchema() 把隐式开放对象根编译为原始 JSON Schema schema.ts#L449-L458
defineTool() 把类型推导、编译与运行时校验串联起来 schema.ts#L545-L617

作者面:逐属性 required: true

ParameterSchemaSpec 在类型上就是"一个可带 required: true 标注的属性映射",映射本身是隐式开放的对象根

// 摘自 packages/core/tools/src/schema.ts
export type ParameterPropertySpec = ValueSchemaSpec & { required?: true }

/** 工具参数 schema。该映射本身是隐式开放的对象根;
 *  必需性仍然是逐属性的 `required: true` 标注。 */
export type ParameterSchemaSpec = {
  [key: string]: ParameterPropertySpec
  [key: symbol]: never   // 运行时编译拒绝 symbol 键
}

[key: symbol]: never 这一条不是装饰:类型层直接封死 symbol 键,运行时编译器也会拒绝非自有可枚举字符串键的输入(见统一 JSON 值 schema DSL 笔记中"自定义原型、继承的约束、symbol 和 JSON 不可见的附加内容都无法让编译、投影和校验观察到不同的声明")。

一个完整的第一方工具长这样(摘自 adding-a-tool.zh.md):

ctx.tools.register(defineTool({
  name: 'read_file',
  description: 'Read a file from disk.',          // 模型看到的内容
  parameters: {
    path: { type: 'string', required: true, description: 'Absolute path' },
    limit: { type: 'number' },                     // 默认可选
  },
  output: {
    schema: { type: 'string' },
    render: (_args, value) => [{ type: 'text', text: value }],
  },
  async execute(args, exec) {
    // args 类型自动推导为 { path: string; limit?: number }
    return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
  },
}))

execute 签名中的 args 类型由 InferArgs<S> 给出:required: true 的键推导为非可选属性,其余键推导为可选属性。作者全程不写 as,也不手写接口。

InferArgs 的类型级映射

推导的核心是两个条件类型(schema.ts#L119-L131):

/** 属性映射中标记了 `required: true` 的键。 */
type RequiredKeys<S> = {
  [K in StringKeyOf<S>]: S[K] extends { required: true } ? K : never
}[StringKeyOf<S>]

/** 把隐式属性映射推导为"必需键 + 可选键"的对象类型。 */
type InferProperties<S, Depth extends unknown[]> = Simplify<
  & { [K in RequiredKeys<S>]: InferProperty<S[K], Depth> }
  & { [K in Exclude<StringKeyOf<S>, RequiredKeys<S>>]?: InferProperty<S[K], Depth> }
>

值推导走 InferValueAtschema.ts#L153-L172):标量优先匹配 const / enum 字面量约束,再退回宽类型;容器节点每深入一层消耗一次 Depth精确推导以 16 层容器为界,超过后回退到 JsonValue。这个有界设计是刻意的——它避免了 TypeScript 类型实例化栈深度反过来限制作者能声明的嵌套深度,而运行时的 schema 强制执行在任意深度仍然精确。

编译:parameterSchemaSpecToJsonSchema()

编译产物是一个对象根的原始 JSON Schema,逐属性的 required: true 在这里被收拢为标准 JSON Schema 的 required 数组:

// 摘自 packages/core/tools/src/schema.ts
export function parameterSchemaSpecToJsonSchema(spec: ParameterSchemaSpec): ParameterJsonSchema {
  const compiled = compilePropertyMap(spec, 'parameters')
  const schema: ParameterJsonSchema = {
    type: 'object',
    properties: compiled.properties,
    ...(compiled.required === undefined ? {} : { required: compiled.required }),
  }
  assertSupportedJsonSchema(schema)
  return schema
}

注意"隐式开放"体现在:生成的根对象不写 additionalProperties 限制,保留 JSON Schema 默认的开放语义;而显式声明的 object 节点则必须写明 additionalProperties: true | false,漏写会在编译期直接抛错(schema.ts#L365-L372)——这是笔记中"对象开放性规则"的落地,目的是让嵌套或输出对象"永远不获得一个意外的 JSON Schema 默认值"。

defineTool():推导、编译、校验的串联点

defineTool()schema.ts#L545-L617)是唯一需要作者接触的入口,它在定义阶段完成三件事:

  1. 编译参数 schemaparameterSchemaSpecToJsonSchema(options.parameters) 立即执行,格式错误的声明(不支持的键、requiredtrue、缺失 additionalProperties、循环引用)在定义或注册时快速失败,而不是拖到模型调用时才暴露;
  2. 编译输出 schemavalueSchemaSpecToJsonSchema(options.output.schema) 处理工具输出侧;
  3. 注入校验包裹:返回的 ToolDefinition.execute 先把原始 args 交给 validate()(内部即 validateJsonSchemaValue(parameters, args, '')),有任何违规就抛出带逐条 violationsToolArgsErrorschema.ts#L461-L470),校验通过后再以 InferArgs<S> 的类型断言移交用户回调——类型体操的代价被封死在核心包内部,作者侧零断言。这正是笔记"后果"一节所说的"符合 AGENTS.md 的类型安全策略"。

回放场景(presentCall / presentResult / isConcurrencySafe)对历史日志参数采用软校验:校验失败不抛错,而是回退到通用渲染或返回 false,避免旧 schema 的日志参数让 UI 回放崩溃(schema.ts#L594-L615)。

为什么否决 Schemastery

笔记的"曾考虑的替代方案"一节只列了一项,但结论干脆:

Schemastery(已作为 vendor 引入,用于插件 Config)经评估后被否决:它面向的是基于 StandardSchema 的校验/转换,而非 JSON Schema 生成,因此会增加间接层却无法干净地产出协议格式(wire format)。

拆开看这个否决理由:Schemastery 解决的是"给定一个 StandardSchema 形态的 validator,做校验与数据转换";而工具作者路径需要的是"从一份类型化声明生成模型可读的 JSON Schema"。方向相反,中间塞一个适配器只会多一层间接,产出物(wire format)反而不干净。

这个判断在后来的统一 JSON 值 schema DSL 笔记中被再次确认并扩展:"使用 Schemastery 处理工具参数:不予采纳。Schemastery 通过 Standard Schema 面向校验与转换,而不是生成 JSON Schema。采用它会增加一层适配器,却不能产出面向模型的协议 schema 或共享的输出词汇。" 该统一笔记还顺带否决了"采用完整 JSON Schema 或 Ajv"——理由是 harness 必须拒绝所有无法投影到生成 SDK 和校验器中的结构,接受更大的语言子集会让"强制执行能力"与事实不符。

仓库中 Schemastery 的位置可查:其声明出现在 apps/cli/package.jsonTHIRD_PARTY_NOTICES.md,使用场景集中在插件 Config 的校验(参见 config-catalog.md),与工具参数路径互不干扰。

源码纵深:为什么编译器是显式工作栈

阅读 runSchemaCompiler 会看到一个不寻常的实现选择:整个 schema 编译不用递归下降,而是把每个节点表示为 CompileTask 压入栈中迭代执行(schema.ts#L222-L240)。作者侧 schema 编译、原始 schema 断言、值校验、schema 到 TypeScript 的渲染全部采用这种显式工作栈策略,因此"运行时嵌套只受可用内存限制,不受 JavaScript 调用栈限制"。

同一处还处理了几个防御性细节,值得对照阅读:

  • 防原型污染:向 properties 写入编译节点时用 Object.defineProperty 而非直接赋值,避免 __proto__ 键名带来的赋值语义问题(schema.ts#L242-L263);
  • 词表校验assertAuthorKeys() 拒绝节点上出现的任何声明外键(例如在 string 节点上写 minLength),错误信息精确到路径,如 parameters.path.minLength is not supported by the value schema DSL
  • 循环检测seen 集合 + leave 任务实现 DFS 进出配对,重复对象直接报 is circular
  • required 语义收紧:属性上的 required 字段若存在,值必须严格为 truerequired: false 这类写法会被拒绝(schema.ts#L292-L295),从源头消灭"假可选"声明。

oneOf 分支的语义也在这里固化:要求恰好匹配一个分支,既不是"首个匹配即通过"(分支顺序会改变语义),也不容忍重叠分支同时命中——这保证了模型输出的歧义能被校验器抓住。

统一化演进与类型级回归测试

原笔记在"后果"中留了一个伏笔:InferArgs 映射在类型层面有回归测试,源于早期一个可选性 bug(即某个本应必需的键一度被推导成了可选属性)。

这个回归测试如今在 ts-types.spec.ts 中,直接对 parameterSchemaSpecToJsonSchema 的产物做渲染断言,把"必需键不得为可选"锁死在类型投影里:

const schema = parameterSchemaSpecToJsonSchema({
  path: { type: 'string', required: true, description: 'Absolute file path' },
  limit: { type: 'number' },
})
// 期望渲染结果:path: string;(无 ?)
// limit?: number;(可选)

测试还覆盖 jsonSchemaToTs 对全部统一构造的映射(ts-types.spec.ts#L9-L38):enum 展开为字面量联合、const 展开为单字面量、开放对象渲染为 & Record<string, JsonValue>、封闭对象渲染为 Record<string, never> 等。配合 schema.spec.tstools.spec.ts 的运行时用例,"类型推导、编译、校验三者一致"这一不变量同时被编译期和运行期两层测试守住。

演进脉络上,本笔记(2026-06-11,已归档)到统一 JSON 值 schema DSL(2026-07-20)的关键增量是:作者面从"只有参数"扩展到"参数与值共享一套词汇"——ValueSchemaSpec 成为可描述任意 JSON 根类型的节点集合(含 { type: 'json' } 这一作者侧语法糖,编译为仅注解、不约束的原始节点,schema.ts#L74-L77),output.schema 因此可以使用对象、数组、标量或 null 任意根类型,而不再被迫重复实现第二条推导/编译/校验路径。

逃生舱:原始 JSON Schema 直注册

笔记特别保留了一条外部通道:

原始 JSON Schema 的 ToolDefinition 仍是 ToolRegistry.register() 接受的输入,供 MCP 和其他外部工具使用。

对应实现是 ToolRegistry.register 直接接收 ToolDefinitionparameters 字段为原始 JSON Schema 对象)。这意味着:

  • defineTool() 的第一方工具享受类型推导、快速失败与统一校验;
  • 来自 MCP 等外部来源、自带 JSON Schema 的工具直接注册,自行负责输入校验;
  • 统一代码生成遇到不受支持的原始 schema 结构时"视为未知类型",不会假装自己能强制执行——这与前面否决完整 JSON Schema/Ajv 的理由一脉相承。

小结

这条决策线展示了 DeepSeek Harness 处理"模型协议格式"与"作者类型体验"冲突的工程手法:不在两侧各建一套系统,而是设计一个小的、词表受限的作者 DSL(逐属性 required: true、显式对象开放性、类型正确的 enum/const、恰好匹配一个分支的 oneOf),让类型推导(InferArgs/InferValue)、JSON Schema 编译(parameterSchemaSpecToJsonSchema)和运行时校验(validateArgs/ToolArgsError)从同一份声明派生,类型体操的成本全部收敛在 dsh-tools 核心包 内部,并以类型级回归测试锁定关键不变量。Schemastery 则回到它擅长的位置——插件 Config 的 StandardSchema 校验——继续服役。

延伸阅读:

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384