开源权重 LLM 中的"约束税":结构化输出约束下工具调用被抑制的实证研究
- 关联论文:2606.25605
- 作者:spark
- 更新:2026-07-22
一句话结论
当 Tool Calling 与 JSON Schema 结构化输出约束同时启用时,多个开源权重(open-weight)LLM 会出现 Tool Suppression——尽管工具执行与 schema 合规各自独立都能工作,联合部署下模型干脆不再调用工具;论文把原因归到"JSON Schema 被编译成 grammar-based token mask,使 tool-call tokens 在解码时不可达",并提出 Transparent Two-Pass Execution(透明两段执行)这种推理时策略来修复。
解决的真问题
Agent 系统同时依赖两个能力:
- Tool Calling:模型决定要不要调用哪个工具、传什么参数;
- Structured Output:模型输出严格符合 JSON Schema,以便下游系统解析。
业界通常认为这两件事是"正交"的,只要分别评估都通过,组合起来也没问题。但论文在生产 Agent 系统里反复观测到一个可复现的现象:两个能力各自 OK,联合启用时工具调用率塌掉。这种"单独测都过、组合就挂"的故障模式是 Agent 部署里最难定位的一类——它不是 bug,是行为层漂移。
论文把这种故障命名为 Tool Suppression(工具抑制),把根因机制命名为 Constraint Priority Inversion (CPI)(约束优先级反转),并给出 inference-time 缓解策略。
核心方法
1. 现象的复现与刻画
- 在多个开源权重模型族、多类部署设置上做受控实验;
- 一致复现:联合启用 Tool Calling + JSON Schema 时,工具调用率显著下降,schema 合规率仍高;
- 单独启用任一约束时,各项指标正常。
2. 机制解释:Grammar-based token mask 让 tool-call 不可达
论文给出的实现层解释非常具体:
JSON Schema 约束在推理引擎里通常被编译成 grammar-based token mask,每一步解码时只有满足 schema 的 token 会被放行。
问题来了:tool-call tokens(模型用来声明"我要调用工具"的特殊 token / 字段)在严格的 JSON Schema mask 下可能根本不被允许生成。结果就是:
- 模型"想"调用工具,但 mask 不允许它表达"我要调用工具";
- 它只能输出一个"语法上正确但不调工具"的响应;
- 这在外层观察里就是 Tool Suppression。
这把问题从"模型不会调工具"拉到了"模型想调但被生成器挡了"——这是一个纯部署层、推理引擎层的 bug,与模型本身能力无关。
3. 行为层假设:Constraint Priority Inversion (CPI)
CPI 是论文给出的一个 行为层假设(behavioral hypothesis),而非已验证的内部机制。论文明确表述:"We present CPI as a behavioral hypothesis consistent with the observed evidence rather than a verified internal mechanism."
CPI 的直觉:当多个约束同时存在时,schema 满足可能在内部优先级上压过 action-selection,导致模型为了"输出合法"而牺牲"该调工具"。
CPI 不是被证实,而是被观察所支持——这是一种诚实的论述方式,也提醒读者不要把它当成机制证据。
4. 缓解:Transparent Two-Pass Execution
- 思路:把 tool execution 和 schema-constrained response generation 解耦到两段执行里;
- 关键性质:
- 在第一段允许模型自由生成 tool-call token(此处不施加 JSON Schema mask);
- 拿到 tool 调用结果后,第二段才施加 JSON Schema 约束,生成最终结构化响应;
- 全程不需要任何模型重训练。
- 效果:实验显示该策略恢复了工具调用行为,同时保留结构化输出保证。
Transparent Two-Pass 的"透明"指:对调用方来说,接口契约不变,模型仍然输出符合 schema 的最终响应,只是内部被拆成两段。
伪代码骨架:
输入:user request q,可用工具集 T,目标 schema S
阶段 1:Tool Resolution(不施加 schema mask)
response_1 = LLM(q, tools=T, mode="free") # 允许 tool-call tokens
if response_1 包含 tool_call:
tool_result = execute(tool_call)
构造 q' = q + tool_result
阶段 2:Schema-Constrained Generation
response_2 = LLM(q', schema=S, mode="constrained") # 施加 schema mask
return response_2 # 满足 S
不变量:response_2 满足 S;tool_call 一定在阶段 1 表达,不受 schema mask 阻挡
具体 mask 切换、阶段间 prompt 拼接的细节,论文未在 abstract 完全披露,标注「原文未明确」。
关键实验与数据
- 实验对象:多个开源权重 LLM 家族(论文 abstract 未列具体名单,标注「原文未明确」);
- 场景:生产 Agent 系统的 Tool Calling + JSON Schema 联合部署;
- 核心观测:联合启用时工具调用率显著下降,但 tool execution 与 schema compliance 在独立评估下功能正常;
- 缓解效果:Transparent Two-Pass 恢复工具调用,且保持结构化输出保证;
- 不变量:无需模型重训练。
(具体模型名称、tool call 率绝对值、schema 合规率数字,原文未明确;详细表格在原 PDF 中,本次未下载。)
亮点与局限
亮点
- 现象可复现且具普遍性——这不是某个推理引擎的 bug,而是一类部署模式下普遍存在的失败模式,值得所有做 Agent 的人警惕。
- 机制解释具体到 token mask 层面——把行为问题归结到推理引擎实现细节,这对推理引擎作者尤其有价值:他们可以反过来审查自己的 mask 编译是否合理。
- Transparent Two-Pass 不需要重训——对已经在生产中用开源模型的团队是 zero-cost fix,立刻可上。
- 诚实区分"现象 / 假设 / 机制"——CPI 作为 behavioral hypothesis 被明确标注,没有越界声称内部因果,这是好学术规范。
- 代码、数据、文档承诺开源(GitHub 链接已在 abstract 给出),可复现性比一般实证论文高。
局限
- 聚焦开源权重 LLM:闭源 API 模型是否同样存在 Tool Suppression?论文未明确跨模型族比较。
- "机制"是实现层推断,非因果证明——是"tool-call tokens 在 grammar mask 下不可达"还是"模型在训练时就学到这种偏好"?两者在外部观测上无法区分。
- CPI 是假设不是机制——论文自己强调了这一点,读者不应直接据此设计训练侧干预。
- Transparent Two-Pass 的代价:两段执行 = 两倍 LLM 调用,延迟与成本翻倍;论文 abstract 未给出延迟数字,标注「原文未明确」。
- 覆盖 schema 类型:abstract 未明确 Transparent Two-Pass 对任意 JSON Schema 都成立,还是仅对某些结构有效。
- 样本规模 / 评测基准细节未在 abstract 披露。
对工程落地的启发
- "独立评估通过"不等于"联合部署通过"——任何做 Agent 的团队,上线新组合约束(tools + schema + system prompt + RAG context)时都必须做 联合测试,这是 Agent 部署的硬要求。
- JSON Schema 编译到 grammar mask 时要审计 tool-call tokens:如果你在用 vLLM、TGI、Outlines、Guidance 等推理框架,应当审查生成的 grammar 是否覆盖了 tool-call 表达所需的 token 空间。
- Transparent Two-Pass 是 zero-retraining 的应急方案:遇到 Tool Suppression 又不能重训模型时,这是首选止血方案。
- 端到端测试应包含"约束组合矩阵":tools × schema × system prompt 的笛卡尔积测试在 Agent 工程里值得常态化。
- CPI 假设暗示训练侧干预方向:如果 schema 满足在训练时反复压制 action-selection,模型学到"为合规而放弃行动"是有可能的;为训练侧干预(数据构造 / 偏好对齐)打开了方向,但目前只是 hypothesis。
与同方向工作的关系
- Tool Calling / Function Calling 系列(OpenAI Function Calling、ToolLLM、ToolBench):这些工作关注 怎么让模型调工具,本文关注 工具调用在组合约束下为什么会失效。
- Structured Output / JSON Mode 系列(Outlines、Guidance、LMQL、JSON Schema enforcement):这些工作关注 怎么强制 schema,本文揭示了 强制 schema 可能踩到的工具调用坑。
- Agent Reliability / Failure Mode 系列(SayCan-style 失败分析、ToolBench 评测、AgentBench):本文给出了 联合约束 这一个新的失败维度,补全了可靠性研究的一块。
- Inference Engine Internals(vLLM / TGI / SGLang 文档):本文指出的 grammar mask 与 tool-call 冲突,是推理引擎需要正面回应的实现问题。
- Prompt Engineering 实证研究:CPI 与"prompt 里加 schema 让模型变乖"的经验性观察相呼应,但给出了更具体的机制语言。
适合谁读
- 在生产环境跑开源权重 LLM 做 Agent 的人;
- 设计或维护 LLM 推理引擎(vLLM、Outlines、Guidance、SGLang 等)的人;
- Agent 平台 / Agent 框架作者(Tool Calling + JSON Schema 是基本盘);
- 对 LLM 行为假设与机制证据之间的边界感兴趣的 ML 研究者;
- 做 LLM 评测 / red-teaming,关注"联合失败模式"的工程师。
工程落地与核查(Jay)
事实核查
| 核查项 | 原文声明 | 核查结果 |
|---|---|---|
| Tool Suppression 现象 | JSON Schema + Tool Calling 联合启用时工具调用率塌缩 | ✅ abstract 原文确认 |
| CPI 为假设非机制 | 「CPI as a behavioral hypothesis consistent with observed evidence rather than a verified internal mechanism」 | ✅ abstract 原文显式声明 |
| Transparent Two-Pass | 推理时两段执行,不需重训练 | ✅ abstract 原文确认 |
| grammar-based token mask | JSON Schema 编译为 grammar-based token mask | ✅ abstract 原文确认 |
| 开源代码/数据/文档 | 「Code, data, and docs will be released at this https URL」 | ⚠️ abstract 有 URL 占位但未给出具体链接;截至 2026-08-15 文件未核查实际 release 状态 |
| 实验规模 | 多个开源权重模型族(具体名单 abstract 未列) | ❌ abstract 缺具体模型名称列表 |
| 缓解效果数字 | abstract 未给具体召回率 / 工具调用率恢复幅度 | ❌ abstract 仅有方向性声称,无量化数字 |
| figures/tables | abstract 注「2 figures, 14 tables」 | ✅ abstract 原文确认(文件 §3 未引具体表,此注与文件一致) |
⚠️ 存疑与坑点
- CPI 是假设不是机制:论文原文明确声明"behavioral hypothesis rather than verified internal mechanism",文件解读已如实标注。工程团队不应以 CPI 为据设计训练侧干预;若要干预,应以 grammar mask 审计为依据。
- Transparent Two-Pass 两倍延迟:两段 LLM 调用 = 延迟 × 2、成本 × 2。abstract 未给具体数字,高频调用场景(如实时 agent)需实测后再决定是否上线。batch 场景(离线处理)影响相对小。
- GitHub URL 悬空:abstract 承诺 release 但未给链接;截至 2026-08-15 未核验。建议在上生产前主动搜索「Tool Suppression open source」确认代码是否已出。
- 闭源 API 是否受影响未测:abstract 仅覆盖「open-weight models」;若你在用 GPT-4o / Claude / Gemini,需要自行做联合约束测试验证是否也有 Tool Suppression。
- JSON Schema 类型覆盖:Transparent Two-Pass 对任意 schema 有效,还是仅对某些结构(如嵌套层数少、字段类型简单)有效,abstract 未明确。生产中若 schema 复杂(如金融/医疗级嵌套 JSON),应先做笛卡尔积矩阵测试。
- "多模型族"覆盖范围:abstract 说「多个开源权重模型族」但未列名单;这个「普遍性」是工程决策的重要依据,建议下载 PDF 核验具体覆盖了哪些模型家族。
工程落地三步走
第一步:复现 Tool Suppression——上线前必做 在正式部署 Tool Calling + JSON Schema 组合前,先跑一个诊断:
import json
def diagnose_tool_suppression(model, tools, schema, test_queries):
"""测试 model 在 JSON Schema 约束下是否仍调工具"""
results = []
for q in test_queries:
response = model.chat(
q,
tools=tools,
response_format={"type": "json_object", "schema": schema}
)
has_tool_call = any(
msg.get("tool_calls") or msg.get("function_call")
for msg in response["messages"]
)
has_schema_valid = is_valid_json(response["content"], schema)
results.append({
"query": q,
"tool_call": has_tool_call,
"schema_valid": has_schema_valid,
"suppressed": not has_tool_call and has_schema_valid
})
return results
# 触发 Tool Suppression 的特征组合:
# - schema 有深层嵌套(≥3 层)
# - schema 字段名与 tool 名接近(如 "action" / "tool" 冲突)
# - schema enum 值与 tool 参数选项重叠
第二步:Transparent Two-Pass 实现(已知 URL 悬空,先自行实现) GitHub 未出,先用两段执行止血:
def two_pass_inference(user_request, tools, target_schema, model):
# 阶段 1:自由 tool-call
response_1 = model.chat(
user_request,
tools=tools,
# 不施加 JSON Schema 约束
)
tool_calls = extract_tool_calls(response_1)
if not tool_calls:
# 没有 tool call,直接走阶段 2
final_response = model.chat(
user_request,
response_format={"type": "json_object", "schema": target_schema}
)
return final_response
# 执行工具
tool_results = [execute(tc) for tc in tool_calls]
# 阶段 2:schema 约束生成
enriched_request = user_request + "\n\nTool results:\n" + json.dumps(tool_results)
final_response = model.chat(
enriched_request,
response_format={"type": "json_object", "schema": target_schema}
)
return final_response
第三步:grammar mask 审计(面向推理引擎开发者) 若你在用 vLLM / TGI / SGLang / Outlines,应检查 grammar 生成逻辑是否无意中屏蔽了 tool-call tokens:
# 伪代码:检查 grammar mask 是否包含 tool_call 关键 token
def audit_grammar_for_tool_tokens(grammar, tool_schema):
# 1. 解析 grammar mask 允许的 token 集合
allowed_tokens = parse_grammar(grammar)
# 2. 提取工具调用所需的特殊 token
required_tokens = extract_tool_call_tokens(tool_schema)
# 3. 检查交集
blocked = [t for t in required_tokens if t not in allowed_tokens]
if blocked:
print(f"[ALERT] Grammar mask blocks tool-call tokens: {blocked}")
return False
return True
生产部署检查项: - ✅ 任何 Tool Calling + JSON Schema 组合上线前必须跑 diagnose_tool_suppression - ✅ 高频实时 agent 场景先实测 Two-Pass 延迟是否可接受(batch 场景优先) - ✅ 关注 vLLM / SGLang 版本升级日志,grammar mask 行为可能在升级后改变 - ✅ 监控工具调用率与 schema 合规率的比值;一旦出现工具调用率塌缩(<5%)立即告警