开源权重 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 executionschema-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 披露。

对工程落地的启发

  1. "独立评估通过"不等于"联合部署通过"——任何做 Agent 的团队,上线新组合约束(tools + schema + system prompt + RAG context)时都必须做 联合测试,这是 Agent 部署的硬要求。
  2. JSON Schema 编译到 grammar mask 时要审计 tool-call tokens:如果你在用 vLLM、TGI、Outlines、Guidance 等推理框架,应当审查生成的 grammar 是否覆盖了 tool-call 表达所需的 token 空间。
  3. Transparent Two-Pass 是 zero-retraining 的应急方案:遇到 Tool Suppression 又不能重训模型时,这是首选止血方案。
  4. 端到端测试应包含"约束组合矩阵":tools × schema × system prompt 的笛卡尔积测试在 Agent 工程里值得常态化。
  5. 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 未引具体表,此注与文件一致)

⚠️ 存疑与坑点

  1. CPI 是假设不是机制:论文原文明确声明"behavioral hypothesis rather than verified internal mechanism",文件解读已如实标注。工程团队不应以 CPI 为据设计训练侧干预;若要干预,应以 grammar mask 审计为依据。
  2. Transparent Two-Pass 两倍延迟:两段 LLM 调用 = 延迟 × 2、成本 × 2。abstract 未给具体数字,高频调用场景(如实时 agent)需实测后再决定是否上线。batch 场景(离线处理)影响相对小。
  3. GitHub URL 悬空:abstract 承诺 release 但未给链接;截至 2026-08-15 未核验。建议在上生产前主动搜索「Tool Suppression open source」确认代码是否已出。
  4. 闭源 API 是否受影响未测:abstract 仅覆盖「open-weight models」;若你在用 GPT-4o / Claude / Gemini,需要自行做联合约束测试验证是否也有 Tool Suppression。
  5. JSON Schema 类型覆盖:Transparent Two-Pass 对任意 schema 有效,还是仅对某些结构(如嵌套层数少、字段类型简单)有效,abstract 未明确。生产中若 schema 复杂(如金融/医疗级嵌套 JSON),应先做笛卡尔积矩阵测试。
  6. "多模型族"覆盖范围: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%)立即告警