跳转到内容

把合法集合告诉模型:有限论域缺口的两面与设计空间

状态:Research Note 版本:0.3.0 日期:2026-09-08 研究问题:系统已经知道哪些参数值、哪些标识符是合法的,为什么模型仍然可以写出不存在的规格与句柄?把“合法集合”表达给模型有哪些做法,各自的代价是什么,Aira 该走哪条? 起因:标准件 MVP 暴露 AMIR §6.1 的“可变符号参数”缺口;退役 provider projection 时发现 RFC-0002 §13.2.1 的“逐轮收窄”是同一缺口的模型接口一面。 证据来源:当前工作树实测(packages/aira-contracts、apps/server、apps/web)+ 外部论文与供应商文档。 0.2.0 修订:追加编译器实测(§2.3、§4.1),据此关闭待验证 #4、重新判定方案 D 与 E。 0.3.0 修订:方案 E 已落地(§2.4),replace-program 不再接收节点标识符;关闭待验证 #7。

  1. 缺口不是“我们不知道合法集合”,而是“我们知道却用错了信道”。 网关每轮已经把完整的模型上下文 (revisionId、全部 nodes、全部 parameters 及其值与 range)发给模型——见 packages/aira-cad-tools/src/tools.ts 的 exactModelContext。合法标识符已经在提示里,只是以数据形式, 不是以类型形式。[实测]
  2. 更根本的一层:计划工具当前 strict: false,schema 根本不是解码期约束。 连已有的枚举 (unit: mm|deg|scalar、side: inside|outside)都只是事后校验,不参与生成。所以 “把句柄物化成 enum”这条路,在打开严格模式之前是无效的。[实测]
  3. 严格模式打不开是有原因的:计划 schema 用了 pattern(7)、minLength(5)、maxLength(4)、 minItems(4)、maxItems(6)、uniqueItems(3)、exclusiveMinimum(1)、minimum(5)、maximum(4)、anyOf(2)。 主流严格结构化输出只支持 JSON Schema 的一个受限子集,这些关键字大多不在其中。[实测 + 供应商文档]
  4. 代价是真实的:计划被拒后走 needs-repair,增长基准 规定“单次任务最多 2 次修复,第 3 份计划被拒即失败”。一次编造的标识符就消耗掉三分之一的修复预算。[实测]
  5. 外部证据给出一个反直觉的约束:格式限制会损害推理。Tam 等的 Let Me Speak Freely?(arXiv 2408.02442)实测在格式限制下 LLM 推理能力显著下降, 且限制越严退化越大。所以“把所有合法值塞进 schema”不是无代价的胜利。[论文结论]
  6. 另一条外部证据指向更便宜的解法:Lee 等的 Don’t Adapt Small Language Models for Tools; Adapt Tool Schemas to the Models(ACL 2026) 报告仅通过改工具组件的命名使其贴近预训练分布,整体提升至多 17%, 而schema 错配错误(模型幻觉出 schema 里不存在的名字)下降 80%。这与本缺口是同一失败模式。[论文结论]
  7. 实测追加一:一个文档至多一个 exact.program 节点,这是编译器已经强制的。 createProgramPatch 在 document.bodies 非空时拒绝 build-program,理由是“在既有实体上新建程序会让它的模型图悬空”。 因此方案 E(结构性消除句柄引用)成立——replace-program 的 nodeId 是可推导的。[实测,已落地] 该字段已于 2026-09-08 从计划 schema 移除:宿主自行解析那个唯一节点,模型不再有机会写错它(§2.4)。
  8. 实测追加二:方案 D 不是“未做”,是“做了一半,且有一处报错方向相反”。 26 个 EXACT_CAD_PLAN_* 诊断里有 5 个已经回传合法集合或可操作细节,其中 BUILD_REQUIRES_EMPTY_MODEL 会列出现有程序节点 id 与全部可编辑参数。但模型真正编造标识符的三条路径都没有沿用这个写法, 其中 set-parameters 遇到不存在的 parameterId 时抛的是 PARAMETER_NOT_NUMERIC—— 把“名字不存在”报成“类型不对”,会把修复引向错误方向。[实测,已修]
  9. 判定:不走“把全部合法句柄物化成大枚举”这条路。分三层解决—— ① 命名与形状先改(最便宜、已有证据支持);② 把小而稳定的论域进 schema(标准件规格、单位、有限模式); ③ 大而易变的句柄集合留在上下文,但改用可校验的引用形式,并让诊断把合法集合当场回给模型。

系统里存在两类“有限集合”,都无法表达给模型:

面 合法集合 当前形态 后果
参数侧 标准件规格 M1.6…M64、材料牌号、螺纹标记、型材系列 只存在于程序源码字面量与 TS 类型 改规格 = 改源码,不是改参数(AMIR §6.1)
句柄侧 当前 Revision 的 nodeId、parameterId、namedSubEntities 名 以 JSON 上下文发给模型,schema 里是自由字符串 模型可编造不存在的 id,只能事后拒绝

两者同源:AMIR 支持“可变的数值”与“不可变的符号”,不支持“可变的符号 + 有限论域” (Parameter.type 只有 7 种数值/布尔类型,range 只表达连续区间;ConstAtom 有符号枚举但不可变)。

2.1 实测:计划里哪些字段是自由字符串

Section titled “2.1 实测:计划里哪些字段是自由字符串”

EXACT_CAD_PLAN_SCHEMA 中以 id = { type: 'string', pattern: '^[A-Za-z0-9._:-]+$' } 定义的字段, 本文写作时有四个:requestId、nodeId(replace-program)、parameterId 与 parameter(set-parameters)。 其中 nodeId 已按 §2.4 取消,下文对它的分析保留为该判定的推导过程。 另有两组名称数组同样是自由字符串(^[A-Za-z][A-Za-z0-9_.:-]{0,127}$):affects(守恒声明)与 expectation.named(创建期的量测声明)。

这些名称必须取自当前 evaluation 的 namedSubEntities 或当前文档的节点/参数表——这个集合在提交计划的那一刻 是完全确定的,宿主知道,模型也被告知了,但 schema 不知道。

2.2 实测:合法集合已经发给模型了

Section titled “2.2 实测:合法集合已经发给模型了”

exactModelContext 的注释原文写明它投影的正是“计划能够引用的东西”:identity、roots、bodies、 带 source 的 nodes、带值的 parameters、以及 evaluation。也就是说:

  • 模型收到了全部合法 nodeId 与 parameterId;
  • 模型收到了每个参数的 type、value、unit、range;
  • 但计划 schema 对这些字段只说“是一个符合正则的字符串”。

所以这不是信息缺失问题,是信息形态问题。 同一份事实,以数据形式给了模型(可被忽略), 没有以约束形式给解码器(不可被违反)。

2.3 实测:句柄集合小到不需要枚举——它只有一个元素

Section titled “2.3 实测:句柄集合小到不需要枚举——它只有一个元素”

apps/web/src/authoring/exact-cad-plan.ts 的 createProgramPatch 在文档已有 body 时拒绝 build-program, 注释原文说明理由是“在既有实体上新建程序会让它的模型图悬空”。也就是说每个文档至多一个 exact.program 节点。

这个事实改变了整个问题的量级:replace-program 的 nodeId 不是“从一个大集合里选一个”, 而是“填写那个唯一确定的值”。让模型填一个它必须从上下文抄来、且只有一个正确答案的字段, 是纯粹的失败机会——方案 E(消除该字段)优于任何约束该字段的做法。

参数侧不同:p.* 参数数量随模型声明增长,是真正的多元素集合,方案 E 不适用,只能靠 D/B/C。

2.4 落地:replace-program 不再接收节点标识符

Section titled “2.4 落地:replace-program 不再接收节点标识符”

2026-09-08 的变更把上一节的推论执行到底。三处同时改:

位置 变更
packages/aira-contracts/src/exact-cad-plan.ts replace-program 的两个变体(source 与 edits)都从 required 与 properties 去掉 nodeId;additionalProperties: false 因此把仍然发送该字段的旧计划判为不合法
apps/web/src/authoring/exact-cad-plan.ts 编译器改为自己解析唯一的 exact.program 节点,node.put 与守恒报告都用解析结果;PROGRAM_NOT_FOUND 从“你给的 id 不对”变成“这个模型没有程序,改发 build-program”
packages/aira-cad-tools/src/tools.ts、apps/web/src/ai/agent-client.ts 指令与上下文提示同步改写为“它总是作用于本模型唯一的程序,不接收节点标识符”

守恒报告的 ExactCadConservationReport.nodeId 保留,语义从“模型声明的目标”改为“宿主解析到的目标”—— 它是诊断在说明自己量测了什么,不是计划的输入。核查确认它的 12 个消费者(网关输出投影、 工作台执行、守恒摘要等)全部只做透传与展示,没有一个要求这个值来自模型。

取消字段本身会制造一个新的坏诊断,必须一并修掉。 计划步骤是 9 个变体的 anyOf, additionalProperties: false 意味着一个多余字段会让九个变体同时失败, 而 ajv.errorsText(allErrors) 会把每个变体的抱怨全部回放——实测 54 条子句, 其中没有任何一条说“把 nodeId 删掉”。也就是说:本变更之后最可能发生的错误 (模型带着旧习惯继续发 nodeId)恰好落进全仓库最差的诊断路径。这与 §4.1 的判据直接冲突。

修法不是给 nodeId 开特例,而是补一条通用规则:被所有变体拒绝的字段,就不属于计划语言; 被部分变体接受的字段(如 edits)拒绝次数更少,计数本身就把两者精确分开。同一条计数规则 顺带覆盖“编造步骤 kind”——此时每个变体都在 kind 上报 const 失配,于是回报合法 kind 列表 而不是把该步骤的每个字段都说成未知。实测结果:

输入 修订前 修订后
replace-program + 多余 nodeId 54 条子句,不指向修复 plan/steps/0.nodeId is a field of no step kind; remove it and resend. 并说明 replace-program 不接收它
字段拼写错误(affcts) 同上 直接点名 affcts
编造的 kind(bore-hole) 同上 回报 8 个合法 kind
缺少必填 affects 54 条子句 不变——缺字段无法用同一条计数规则识别,留作后续

顺带消掉一处重复:BUILD_REQUIRES_EMPTY_MODEL 原本内联重建了一份可编辑参数清单, 与 PARAMETER_NOT_FOUND 用的 numericParameterList 逻辑逐字相同,现在合并为同一个函数。 该诊断也不再回报程序节点 id——模型已经无处可用它。

净效果:计划里的自由标识符字段从 4 个降到 3 个(requestId、parameterId、parameter), 其中 requestId 由宿主而非模型的先验决定其正确性,真正需要模型从上下文抄写的只剩参数侧两个。

3. 更根本的一层:schema 现在不是解码期约束

Section titled “3. 更根本的一层:schema 现在不是解码期约束”

exactCadPlanTool 声明 strict: false,其 inputSchema 只带一个 validate 回调—— schema 的作用是生成之后校验,不是生成之时约束。

这意味着:即使现在就把 nodeId 写成 enum: ['n.program', 'n.finish'],模型也照样可以写别的, 只是会在校验阶段被拒。“物化成 enum”要产生实效,前提是严格模式(解码期 grammar masking)。

严格结构化输出的工作方式是编译一份 grammar,在解码时屏蔽任何会破坏 schema 的 token; 它保证模型不会产出无效枚举值。但它只支持 JSON Schema 的一个受限子集,而计划 schema 大量使用 子集外的关键字(§1.3 的计数)。因此打开严格模式需要先简化 schema,这本身是一次有取舍的改动: 被移除的 pattern/minItems/maximum 等约束会从“解码期不可违反”退回“事后校验”, 净效果取决于哪些约束更重要。

[待验证] DeepSeek 在当前 AI SDK 路径上对严格模式的实际支持程度未实测。多数供应商提供 OpenAI 兼容端点,但是否真正执行 strict 与 schema 约束并不一致,需要按实际 provider 实测, 不能按文档推定。

4. 代价:一次编造的标识符值多少

Section titled “4. 代价:一次编造的标识符值多少”

计划被宿主拒绝后进入 needs-repair,模型得到诊断并重写。增长基准的判据是 “单次任务最多 2 次修复,第 3 份计划被拒即失败”。所以:

  • 一次编造的 nodeId 消耗 1/3 的修复预算;
  • 修复轮要重新发送完整上下文与重新推理,成本按 AI 通道计量,是当前产品目标里被明确测量的量;
  • 失败模式还会污染诊断质量——模型看到的是“这个 id 不存在”,而不是“合法的是这几个”。

[待验证] 现有基准结果里没有按“失败原因”分类的统计,因此编造标识符在全部失败中的占比未知。 这是决定投入优先级的关键数字,应在下一轮基准运行时单独记录。

4.1 实测:诊断质量本身是变量,而且曾经指错方向

Section titled “4.1 实测:诊断质量本身是变量,而且曾经指错方向”

编译器的 26 个 EXACT_CAD_PLAN_* 诊断分成两类:

后续更新(2026-09-09):下表记录当日状态。REWRITE_DISCARDS_SOURCE 的源码行相似度检查已退役; 当前完整源码与片段编辑共用几何约束,保留要求通过 affects 和 expect.preserve 表达,见工作进度 §7。

类别 例子 是否给出修复所需信息
已回传合法集合/可操作细节(5 个) BUILD_REQUIRES_EMPTY_MODEL(列出程序节点 id 与全部可编辑参数)、SOURCE_EDIT_NOT_FOUND(给出最接近的源码)、SOURCE_EDIT_AMBIGUOUS(给出出现次数)、REWRITE_DISCARDS_SOURCE(给出保留行数)、PARAMETER_EXISTS 是
模型编造标识符的三条路径 PROGRAM_NOT_FOUND、PARAMETER_NOT_NUMERIC、CREATED_PARAMETER_UNKNOWN 修订前:否

其中一处是方向性错误而非信息不足:

const parameter = document.parameters[parameterId];
if (!parameter || parameter.type === 'Bool')
throw new Error(`EXACT_CAD_PLAN_PARAMETER_NOT_NUMERIC:${parameterId}`);

模型写了一个不存在的 parameterId,得到的诊断是“这个参数不是数值型”。模型据此会去换值或换类型, 而真正的问题是名字不存在。这类诊断不只是浪费一轮修复,它把修复引向错误方向。

三处已于 2026-09-08 按 BUILD_REQUIRES_EMPTY_MODEL 的既有写法修正:不存在的名字单列 PARAMETER_NOT_FOUND 并附上本模型声明的全部可编辑参数(label=id);PROGRAM_NOT_FOUND 直接给出 该文档唯一的程序节点 id,或在没有程序节点时告诉模型改用 build-program; CREATED_PARAMETER_UNKNOWN 列出该步骤实际声明的参数名。

这说明方案 D 的成本比预估更低:模式已经存在于同一个文件里,缺的只是把它用在最需要的三处。

严格结构化输出通过编译 grammar、在解码时屏蔽非法 token 来保证合规,因此模型不可能产出无效枚举值。 代价是:schema 复杂度直接转化为延迟,深层嵌套、长描述与大枚举集合都会增加开销, 并提高模型拒答或截断的概率。

Let Me Speak Freely?(Tam 等,arXiv 2408.02442)的核心结论: 在格式限制下 LLM 推理能力显著下降,且限制越严格退化越大。

这条对本缺口是直接的反向约束:把“当前 Revision 的全部合法句柄”物化成一个大枚举, 既增加延迟又收紧格式,可能把“编造标识符”换成“整体建模质量下降”。换来的未必划算。

5.3 引擎之间的覆盖差异是真实的

Section titled “5.3 引擎之间的覆盖差异是真实的”

JSONSchemaBench(Geng 等,2025)用 1 万个真实 schema 评测了 Guidance、Outlines、llama.cpp、XGrammar、OpenAI、Gemini 六个系统,三个维度(效率、约束类型覆盖、输出质量) 上差异显著;论文明确指出“保证合规”与“实践中有效”之间存在认知空白。

结论:不能假设“写进 schema 就一定被执行”。 任何依赖严格模式的设计都必须对实际 provider 实测, 并保留宿主侧权威校验作为唯一真实边界——这也正是 RFC-0002 原规则 2 的要求(投影只能收窄,不构成有效性证据)。

5.4 更便宜的杠杆:改命名,不是加约束

Section titled “5.4 更便宜的杠杆:改命名,不是加约束”

Adapt Tool Schemas to the Models(Lee 等,ACL 2026)提出 PA-Tool: 用“peakedness”(预训练熟悉度信号)为工具组件重新命名,使其贴近模型预训练分布。结果: MetaTool 与 RoTBench 上整体提升至多 17%,而schema 错配错误下降 80%—— schema 错配的定义正是“模型幻觉出 schema 里不存在的名字”,与本缺口是同一失败模式。

这条给出一个重要提示:在加约束之前,先看命名与形状是否让模型容易做对。 当前的 n.program、p.width 这类 id 是否对模型友好、replace-program 与 set-parameters 的 职责切分是否符合模型的先验,都是零成本可测的。

方案 做法 有效前提 代价 判定
A. 全量物化 把当前合法句柄集合逐轮编译进 schema 的 enum 严格模式可用 大枚举增加延迟、收紧格式、按 §5.2 可能损害推理;schema 需逐轮重建 不采用(作为默认)
B. 小论域进 schema 只把小且稳定的集合进 schema:单位、模式枚举、标准件规格 严格模式可用 极小;这些集合本就是常量 采用
C. 命名与形状对齐 按 §5.4 调整 id 与步骤命名、精简 schema 关键字以便开启严格模式 无 一次性重构;需实测 优先采用
D. 诊断回传合法集合 拒绝时不只说“不存在”,把当前合法集合与最接近的候选一并返回 无 极小 已做(§4.1,三处;其余诊断按需跟进)
E. 结构性消除 让计划不需要引用句柄:replace-program 作用于唯一的程序节点,set-parameters 用参数名而非 id 无——唯一性已由编译器强制(§2.3) 一次 schema 变更 + 计划编译器改动 已做(节点侧,§2.4);参数侧待验证 label 唯一性

方案 E 现在有了实证依据(§2.3):文档至多一个 exact.program 节点是编译器已经强制的不变量, 所以 replace-program 的 nodeId 是可推导的,字段本身可以取消——不能编造一个不存在的字段。 消除引用比约束引用更彻底,且不依赖严格模式、不增加 schema 复杂度、不触发 §5.2 的推理退化。

节点侧已经执行完(§2.4):代价确实只是一次 schema 变更加编译器改动,且核查确认该字段的四个消费点 (节点查找、两条诊断、node.put、守恒报告)全部可由宿主推导,没有第二个消费者依赖模型显式指定。

[待验证] set-parameters 的 parameterId 能否同样以参数名(label)取代——参数名由模型自己在 build-program/replace-program 里声明,可能比宿主生成的 p.<runId>.<n> 更贴近模型的先验, 但需先确认 label 在文档内唯一。这一条不能照搬节点侧的结论:节点侧成立是因为候选只有一个, 参数侧候选是多个,消除的是“id 形状”而不是“选择本身”。

  1. D 先做 → 已做:三处编造标识符路径的诊断现在回传合法集合,其中一处方向性错误一并修正(§4.1)。 其余 21 个诊断按实际失败分布逐步跟进,不预先统一改写。
  2. C 次之:命名对齐 + schema 关键字精简。后者是打开严格模式的前提,前者按 §5.4 是已被量化的杠杆。 两者都要先测 provider 的严格模式实际行为(§3 的待验证项)。
  3. B 随参数系统一起:Parameter 增加符号类型与 choices 之后,标准件规格这类小论域自然进 schema。 这条与 AMIR §6.1 的裁决是同一件事。
  4. E 提到句柄侧首选 → 节点侧已做(§2.4):replace-program 不再接收 nodeId。 set-parameters 的参数名替代仍待验证 label 唯一性,且性质不同(消除 id 形状,不消除选择本身)。
  5. A 不作为默认:仅在 D/C/E 之后仍有可测量的编造率时,对特定小集合局部启用。

边界:以上任何方案都不改变权威边界——宿主校验、守恒检查与事务校验始终是唯一的有效性判据, 模型侧约束永远只是前置过滤(RFC-0002 原规则 2)。

  1. [待验证] DeepSeek 在当前 AI SDK 路径上是否真正执行 strict 与 schema 约束。
  2. [待验证] 编造标识符在全部计划拒绝中的占比——基准需按失败原因分类记录。
  3. [待验证] 精简 schema 关键字以开启严格模式的净收益:失去的事后约束 vs 得到的解码期约束。
  4. 文档是否恒为单 exact.program 节点 → 已确认:编译器在文档已有 body 时拒绝 build-program, 该不变量是被强制的,不是巧合(§2.3)。
  5. [待验证] §5.4 的 peakedness 方法在本项目 id 命名上的可迁移性;论文对象是小模型与通用工具集, 与本项目的单一专用工具不完全同构。
  6. [待验证] 参数 label 在文档内是否唯一(决定 set-parameters 能否用名字取代 parameterId)。
  7. 取消 replace-program.nodeId 后是否有非模型消费者依赖它 → 已确认无:四个消费点全部可由宿主 推导,守恒报告的同名字段改由宿主填写(§2.4)。
  8. 旧计划形状的过渡成本 → 已处理:多余字段现在得到点名诊断而不是 54 条子句(§2.4)。 仍**[待验证]**的是频次:下一轮基准应记录“多余 nodeId”这一类拒绝的实际占比,用来判断 是否需要更强的手段(例如在指令里显式说明该字段已取消)。
  9. [待验证] 缺少必填字段仍走 errorsText 回放路径。同一条“被所有变体拒绝”的计数规则对 required 不成立(affects 只被 2 个变体要求),需要按 kind 定位变体才能改善。