最近在使用vLLM或Spring AI等框架调用OpenAI代理时,不少开发者遇到了一个典型错误:
json复制{
"object": "error",
"message": "\"auto\" tool choice requires --enable-auto-tool-choice and --tool-call-parser to be set",
"type": "BadRequestError",
"param": null,
"code": 400
}
这个错误的核心在于:当客户端请求中设置了"tool_choice": "auto"参数时,服务端必须启用两个关键配置才能正确处理工具调用请求。这个机制是vLLM等框架为了优化工具调用流程而设计的特殊处理逻辑。
提示:工具调用(Tool Calling)是大模型应用中的高级功能,允许模型根据上下文自动选择并调用外部工具(如API、函数等),是实现复杂AI代理的关键技术。
对于使用vLLM作为推理引擎的场景,必须在启动API服务时添加以下两个参数:
bash复制--enable-auto-tool-choice \
--tool-call-parser hermes
这两个参数的具体作用如下:
--enable-auto-tool-choice
启用自动工具选择功能,允许模型根据对话上下文自主决定是否需要调用工具。未启用时,服务端会直接拒绝包含"auto"工具选择的请求。
--tool-call-parser hermes
指定使用Hermes格式解析工具调用。Hermes是一种优化的工具调用协议格式,能更高效地处理工具请求和响应。
完整启动命令示例(以Qwen2.5-32B-Instruct-AWQ模型为例):
bash复制nohup python -m vllm.entrypoints.openai.api_server \
--host 0.0.0.0 \
--port 3000 \
--model /data/Qwen/Qwen2.5-32B-Instruct-AWQ \
--tensor-parallel-size 2 \
--served-model-name Qwen25-32B-Chat-AWQ-A100 \
--quantization awq \
--gpu-memory-utilization 0.9 \
--trust-remote-code \
--enable-auto-tool-choice \
--tool-call-parser hermes \
--disable-log-request \
--enforce-eager \
--max-model-len 20000 \
>> /data/log/Qwen2.5-32B-Chat-AWQ.log 2>&1 &
并非所有模型都支持工具调用功能。使用前需确认:
优先选择带有Instruct或Chat后缀的模型,如:
避免使用明确不支持工具调用的模型系列,例如:
注意:即使模型理论上支持工具调用,不同版本间也可能存在实现差异。建议在模型文档中确认工具调用支持情况。
服务端配置正确后,客户端需要按照以下格式发起请求:
json复制{
"model": "Qwen25-32B-Chat-AWQ-A100",
"messages": [...],
"tools": [...],
"tool_choice": "auto"
}
关键字段说明:
tools: 定义可用的工具列表(函数签名)tool_choice:
"auto": 由模型自主决定是否调用工具"none": 强制不调用工具{"type": "function", "function": {"name": "get_weather"}}当无法修改服务端配置时,可以考虑以下替代方案:
放弃"auto"模式,直接在请求中指定要调用的工具:
json复制{
"tool_choice": {
"type": "function",
"function": {
"name": "get_weather",
"parameters": {...}
}
}
}
适用场景:
对于支持工具调用但未启用自动选择的模型,可以:
示例流程:
python复制# 构造特殊格式的提示词
prompt = f"""你需要调用工具时,请严格按以下格式响应:
TOOL_CALL: {{
"name": "tool_name",
"parameters": {{...}}
}}"""
# 发送请求并解析响应
response = client.chat.completions.create(
model=model,
messages=[...],
)
parse_tool_call(response.choices[0].message.content)
架构示意图:
code复制客户端 → 中间件(处理工具逻辑) → 模型服务 → 中间件 → 客户端
中间件核心职责:
优势:
对于完全不支持工具调用的模型,可采用结构化输出方案:
json复制{
"action": "tool_call",
"tool": "get_weather",
"params": {
"location": "Beijing"
}
}
实现示例:
python复制def chat_with_tools():
history = []
while True:
prompt = build_prompt(history)
response = model.generate(prompt)
try:
action = json.loads(response)
if action["action"] == "tool_call":
result = call_tool(action["tool"], action["params"])
history.append(f"Tool result: {result}")
except:
history.append(response)
典型工具调用流程:
vLLM默认集成的Hermes解析器具有:
启用工具调用会影响:
优化建议:
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 400 BadRequest | 服务端未启用auto-tool-choice | 添加启动参数或改用显式工具调用 |
| 工具调用被忽略 | 模型不支持或提示词不当 | 检查模型类型,优化工具描述 |
| 参数解析失败 | 工具定义与模型预期不符 | 统一使用OpenAI格式的工具定义 |
| 重复工具调用 | 上下文管理不当 | 确保工具结果正确拼接到下轮对话 |
--log-level debug参数获取详细日志python复制# 不佳示例
tool = {
"name": "get_current_weather",
"description": "这是一个获取当前天气信息的工具..." # 过长
}
# 优化示例
tool = {
"name": "get_weather",
"description": "获取天气: location(string)" # 简明扼要
}
python复制# 合并多个工具请求
requests = [
{"tool": "A", "params": {...}},
{"tool": "B", "params": {...}}
]
batch_response = model.batch_call(requests)
python复制from functools import lru_cache
@lru_cache(maxsize=100)
def get_weather(location):
# 实际调用代码
复杂任务可能需要多个工具协同工作。示例流程:
关键技术点:
高级系统可以实现工具的动态注册:
python复制class ToolBox:
def __init__(self):
self.tools = {}
def register(self, name, func, schema):
self.tools[name] = {
"function": func,
"schema": schema
}
def get_available_tools(self):
return list(self.tools.values())
使用工作流引擎管理复杂工具调用:
mermaid复制graph TD
A[用户输入] --> B{是否需要工具}
B -->|是| C[选择合适工具]
C --> D[执行工具]
D --> E[分析结果]
E --> F{需要更多工具}
F -->|是| C
F -->|否| G[生成最终响应]
(注:实际实现时应避免使用mermaid,改用文字描述)
| 框架 | 工具调用支持 | 备注 |
|---|---|---|
| vLLM | 需要显式启用 | 需添加本文所述参数 |
| Text Generation Inference | 原生支持 | 默认启用 |
| Spring AI | 依赖后端 | 需正确配置OpenAI客户端 |
| Llama.cpp | 有限支持 | 需要特定模型格式 |
| 模型系列 | 工具调用 | 自动选择 | 备注 |
|---|---|---|---|
| Qwen2-Instruct | 支持 | 需要参数 | 推荐使用最新版本 |
| Llama3-Instruct | 支持 | 部分支持 | 8B/70B表现不同 |
| GPT系列 | 全支持 | 是 | 行业标杆 |
| Mistral | 插件支持 | 需配置 | 社区方案多样 |
python复制from openai import OpenAI
client = OpenAI(base_url="http://localhost:3000/v1")
def chat_with_tools():
messages = [{"role": "user", "content": "北京今天天气怎么样?"}]
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名称"
}
},
"required": ["location"]
}
}
}]
response = client.chat.completions.create(
model="Qwen25-32B-Chat-AWQ-A100",
messages=messages,
tools=tools,
tool_choice="auto",
)
tool_calls = response.choices[0].message.tool_calls
if tool_calls:
for tool_call in tool_calls:
if tool_call.function.name == "get_weather":
location = json.loads(tool_call.function.arguments)["location"]
weather = fetch_real_weather(location)
messages.append({
"role": "tool",
"content": weather,
"tool_call_id": tool_call.id
})
second_response = client.chat.completions.create(
model="Qwen25-32B-Chat-AWQ-A100",
messages=messages,
)
return second_response.choices[0].message.content
else:
return response.choices[0].message.content
javascript复制async function callWithTools() {
const response = await fetch('http://localhost:3000/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: "Qwen25-32B-Chat-AWQ-A100",
messages: [...],
tools: [...],
tool_choice: "auto"
})
});
const data = await response.json();
if (data.choices[0].message.tool_calls) {
// 处理工具调用
const toolResults = await Promise.all(
data.choices[0].message.tool_calls.map(async call => {
const result = await executeTool(call.function.name, call.function.arguments);
return {
role: "tool",
content: JSON.stringify(result),
tool_call_id: call.id
};
})
);
// 发送工具结果回模型
const finalResponse = await fetch(...);
return finalResponse;
} else {
return data.choices[0].message.content;
}
}
输入验证:
python复制def safe_call_tool(name, args):
if name not in ALLOWED_TOOLS:
raise ValueError("工具未授权")
if not validate_args(name, args):
raise ValueError("参数校验失败")
return globals()[name](**args)
输出过滤:
python复制def sanitize_output(content):
return bleach.clean(
str(content),
tags=[],
attributes={},
strip=True
)
访问控制:
python复制@require_permission("weather_api")
def get_weather(location):
# 实际实现
pass
| 指标名称 | 类型 | 告警阈值 | 说明 |
|---|---|---|---|
| tool_call_rate | 率值 | >30% | 工具调用占比 |
| tool_failure_rate | 率值 | >5% | 工具调用失败率 |
| avg_tool_latency | 时延 | >500ms | 平均工具响应时间 |
| concurrent_tool_calls | 计数 | >10 | 并发工具调用数 |
json复制{
"timestamp": "2024-03-20T15:04:05Z",
"trace_id": "abc123",
"tool_name": "get_weather",
"parameters": {"location": "Beijing"},
"execution_time": 120,
"success": true,
"error": null,
"result_size": 256
}
python复制from opentelemetry import trace
tracer = trace.get_tracer("tool_tracer")
def call_tool_with_trace(name, args):
with tracer.start_as_current_span(name) as span:
span.set_attributes({
"tool.args": str(args),
"tool.source": "ai_model"
})
try:
result = call_tool(name, args)
span.set_status(Status(StatusCode.OK))
return result
except Exception as e:
span.record_exception(e)
span.set_status(Status(StatusCode.ERROR))
raise