跳到主内容
快讯直播
AI智模界
教程

修好 vLLM 工具调用:Schema、约束解码与回归测试

这篇能做出什么

按下面的步骤走完,你会得到一条可复现的工具调用修复链路:先跑一份小规模回归集,拿到“工具选对率、参数可解析率、参数匹配率”三个基线数字;再按“请求 Schema → 对话模板 → 工具解析器 → 约束解码 → 回归测试”逐层定位;最后把修复后的配置固化成启动参数和测试脚本。典型效果是:原来模型把“查天气”调成“搜新闻”、把 unit 填成中文“摄氏度”、或者返回一段带 <tool_call> 的纯文本,现在能稳定输出合法的 tool_calls,并且每次改 prompt 或换模型都能用同一套用例回归。

前置条件清单

  • 一台能跑 vLLM 的机器,模型已下载到本地路径,例如 /models/your-model。
  • 已安装 vLLM 与 OpenAI Python SDK,命令以官方文档当前版本为准。
  • 准备 20~50 条真实用户问法,覆盖每个工具的“该用”和“不该用”场景。
  • 明确模型家族对应的工具调用格式。vLLM 的 --tool-call-parser 可选值随版本变化,以官方文档当前版本为准;常见的有 hermes、mistral、llama3_json、pythonic 等。
  • 一个能发 HTTP 请求的终端,默认服务端口用 8000。

第一步:先跑基线,不要先改 prompt

启动一个带工具调用能力的服务。下面命令里的模型路径、parser 名称按你的模型替换:

```bash

vllm serve /models/your-model \

--served-model-name tool-model \

--host 0.0.0.0 \

--port 8000 \

--enable-auto-tool-choice \

--tool-call-parser hermes \

--chat-template /path/to/tool_chat_template.jinja

```

如果模型 tokenizer 自带的 chat template 已支持 tools,可以省略 --chat-template。但一旦出现“工具定义像没进 prompt”的现象,就要显式指定模板。

准备回归集 cases.jsonl,每行一条:

```jsonl

{"id":"weather-1","messages":[{"role":"user","content":"北京明天出门要带伞吗?"}],"expected_tool":"get_weather","expected_args":{"city":"北京","unit":"celsius"}}

{"id":"weather-2","messages":[{"role":"user","content":"帮我查一下上海今天多少度"}],"expected_tool":"get_weather","expected_args":{"city":"上海","unit":"celsius"}}

{"id":"ticket-1","messages":[{"role":"user","content":"登录一直报 500,帮我提个高优先级工单"}],"expected_tool":"create_ticket","expected_args":{"title":"登录报 500","priority":"high"}}

```

用 Python 跑基线,统计工具名准确率、参数 JSON 可解析率、参数子集匹配率:

```python

import json, requests

URL = "http://localhost:8000/v1/chat/completions"

TOOLS = json.load(open("tools.json", encoding="utf-8"))

def norm(v):

if isinstance(v, str):

return v.strip().replace("市", "")

return v

def subset_match(expected, actual):

for k, v in expected.items():

if k not in actual or norm(actual[k]) != norm(v):

return False

return True

total = tool_ok = json_ok = arg_ok = 0

for line in open("cases.jsonl", encoding="utf-8"):

case = json.loads(line)

total += 1

body = {

"model": "tool-model",

"messages": case["messages"],

"tools": TOOLS,

"tool_choice": "auto",

"temperature": 0,

"max_tokens": 512,

}

r = requests.post(URL, json=body, timeout=60).json()

msg = r["choices"][0]["message"]

calls = msg.get("tool_calls") or []

if not calls:

print(case["id"], "NO_TOOL_CALL", msg.get("content", "")[:200])

continue

name = calls[0]["function"]["name"]

if name == case["expected_tool"]:

tool_ok += 1

try:

args = json.loads(calls[0]["function"]["arguments"])

json_ok += 1

except Exception:

print(case["id"], "BAD_JSON", calls[0]["function"]["arguments"][:200])

continue

if subset_match(case["expected_args"], args):

arg_ok += 1

else:

print(case["id"], "ARG_MISMATCH", args)

print({"total": total, "tool_acc": tool_ok/total, "json_ok": json_ok/total, "arg_acc": arg_ok/total})

```

先记录这三个数字,后面每改一层都重跑同一份用例。

第二步:把失败样本分到四层

拿到失败样本后,不要笼统说“模型不准”,按下面四层归类:

1. 请求层:工具名、描述、参数 schema 写得太泛,模型分不清该用哪个工具。

2. 模板层:chat template 没有把 tools 渲染进 prompt,模型根本看不到工具定义。

3. 解析层:模型输出了工具调用格式,但 --tool-call-parser 与模型家族不匹配,服务返回的 tool_calls 为空。

4. 解码层:参数类型、枚举值、必填字段没有约束,模型自由发挥导致 JSON 不合法或字段缺失。

一个快速判断方法:看响应里的 finish_reason 和 message.content。如果 tool_calls 为空,但 content 里出现 <tool_call>、[TOOL_CALLS] 之类的标记,优先查解析器和模板;如果 tool_calls 有值但 arguments 不是合法 JSON,优先查 schema 和约束解码。

第三步:修工具 Schema,让“选哪个”和“填什么”都无歧义

工具描述要写“什么时候用、什么时候不用”,不要只写“查询数据”。参数尽量用 enum、required、additionalProperties: false 收口。示例:

```json

{

"type": "function",

"function": {

"name": "get_weather",

"description": "查询指定城市当前或未来天气。仅当用户询问天气、温度、降雨、穿衣建议时使用;不要用于新闻、交通、股票查询。",

"parameters": {

"type": "object",

"properties": {

"city": {

"type": "string",

"description": "城市中文名,例如“北京”“上海”,不要带省份前缀"

},

"unit": {

"type": "string",

"enum": ["celsius", "fahrenheit"],

"description": "温度单位。中文用户默认使用 celsius"

}

},

"required": ["city", "unit"],

"additionalProperties": false

}

}

}

```

同时做三件事:

  • 工具名用动词开头,近义工具要拉开描述差异,例如 search_docs 只查内部文档,search_web 只查公开网页。
  • 工具数量多时,先做一层路由或分组,不要把几十个工具一次性塞进请求。
  • 参数里能枚举就枚举,能写 pattern 就写 pattern,例如工单优先级用 ["low","medium","high"]。

改完 schema 先只重跑回归集,不要同时改解码参数,否则无法判断收益来自哪一层。

第四步:对齐 chat template 与 tool-call-parser

模板和解析器是一对。模板决定工具定义以什么格式进入 prompt,解析器决定从模型输出里怎么抽 tool_calls。两者不匹配的典型现象是:模型明明在按格式输出,接口却返回空 tool_calls。

检查方法:

```bash

curl -s http://localhost:8000/v1/chat/completions \

-H "Content-Type: application/json" \

-d '{

"model": "tool-model",

"messages": [{"role":"user","content":"北京天气"}],

"tools": [{"type":"function","function":{"name":"get_weather","description":"查天气","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],

"tool_choice": "auto",

"temperature": 0

}' | python -m json.tool

```

如果 tool_calls 为空,换一个与模型家族匹配的 parser 再试;如果换 parser 后仍为空,检查启动日志里是否有模板渲染或工具解析相关警告。不要凭模型名字猜 parser,直接查官方文档当前版本的对应表。

第五步:用约束解码收口参数

工具调用链路里,不建议对整个 tool_calls 输出做强 JSON 约束,因为这会和模型特有的工具调用特殊 token 冲突。更稳的做法是“两阶段”:

  • 阶段 A:只让模型选工具名。可以用 guided_choice(当前版本支持时)或 response_format 的 JSON Schema 约束一个只含 tool 字段的对象。
  • 阶段 B:拿到工具名后,用该工具的参数 schema 做 guided_json 或 response_format 约束,只生成参数。

阶段 B 的请求示意:

```json

{

"model": "tool-model",

"messages": [

{"role": "user", "content": "北京明天出门要带伞吗?"},

{"role": "assistant", "content": "{\"tool\":\"get_weather\"}"},

{"role": "user", "content": "只输出 get_weather 的参数 JSON"}

],

"response_format": {

"type": "json_schema",

"json_schema": {

"name": "get_weather_args",

"strict": true,

"schema": {

"type": "object",

"properties": {

"city": {"type": "string"},

"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}

},

"required": ["city", "unit"],

"additionalProperties": false

}

}

},

"temperature": 0

}

```

约束 schema 尽量保持简单:少用深层 oneOf/anyOf,避免运行时编译失败;字符串枚举、数字范围、必填字段通常就够。若使用 vLLM 的 guided_json 扩展参数,确认当前版本的 OpenAI 兼容接口支持该字段,具体以官方文档为准。

第六步:把回归测试固化下来

修复后把评测脚本放进 CI 或每次发版前的手动清单。建议至少保留这些指标:

  • 工具选择准确率:只看 function.name。
  • 参数 JSON 可解析率:arguments 能否 json.loads。
  • 参数 schema 通过率:用 JSON Schema 校验器跑一遍。
  • 参数匹配率:期望参数是否为实际参数的子集,字符串做去空格、去“市”等归一化。
  • 混淆矩阵:记录“期望工具 → 实际工具”,看清是把天气错成新闻,还是把工单错成搜索。

回归时固定 temperature=0,并保留每次请求的原始 content、tool_calls、finish_reason。如果 finish_reason 是 length,先加 max_tokens,不要误判成模型能力问题。

常见坑与排错

现象优先查处理
启动报错提示需要 tool parser启动参数同时加 --enable-auto-tool-choice 和 --tool-call-parser
tool_calls 为空,content 里有工具标记解析器/模板换匹配模型家族的 parser,显式指定支持 tools 的 chat template
模型答“我无法查询天气”模板工具定义没有渲染进 prompt,检查模板是否处理 tools 字段
工具名对了,参数多出字段SchemaadditionalProperties: false,并把不必要字段从 properties 去掉
参数枚举值写成中文Schema/解码用 enum 收口,描述里给默认值示例
JSON 被截断解码提高 max_tokens,看 finish_reason 是否为 length
约束解码报 schema 编译失败解码简化 schema,减少深层联合类型,先用小 schema 验证
多工具场景只返回一个调用请求参数检查 parallel_tool_calls 设置与用例预期

下一步建议

先把这份回归集扩到每个工具至少 10 条“该用/不该用”样本,再加入多轮对话和AI 词典:并行工具调用">并行工具调用。线上采样时记录解析失败率、参数校验失败率和工具名混淆矩阵,按周回放失败样本。工具数量继续增长时,加一层轻量路由:先用小模型或规则把候选工具缩到 3~5 个,再走完整工具调用链路。这样每次换模型、改模板、调 schema,都有数字可对比,不靠感觉判断“好像准了”。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。