Function Calling:让 LLM 调真实工具
谁该读这一篇? 要做 agent / 工具调用 / OpenAI-compatible function call 的开发者。 前置阅读:
03-json-schema.md、01-dsl-walkthrough.md。 耗时: 30 分钟 学完能: 1. 用 OpenAI 兼容 API 让 SGLang 模型选 tool; 2. 解释 function call parser 是怎么实时识别 tool call 的; 3. 区分流式 vs 非流式 tool 调用; 4. 知道 SGLang 支持哪些模型的 tool call 格式; 5. 调试 "tool call 解析失败" 类问题。
1. 一个例子
from openai import OpenAI
import json
client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY")
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
}]
resp = client.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct",
messages=[{"role": "user", "content": "今天上海天气?"}],
tools=tools,
tool_choice="auto",
)
msg = resp.choices[0].message
if msg.tool_calls:
for call in msg.tool_calls:
name = call.function.name
args = json.loads(call.function.arguments)
print(f"调用 {name}({args})")
启动时:
python -m sglang.launch_server \
--model-path Qwen/Qwen2.5-7B-Instruct \
--tool-call-parser qwen25
--tool-call-parser 指定 parser,对应不同模型的 tool call 格式。
2. 不同模型的 tool call 格式
模型厂商各家有自己的 tool 表示法:
| 模型 | 格式 |
|---|---|
| Llama-3.1 | <\|python_tag\|>...<\|eot_id\|> 或 JSON |
| Qwen2.5 | <tool_call>{"name": "...", "arguments": {...}}</tool_call> |
| Qwen3 | XML-like + JSON arguments |
| DeepSeek-V3 / V3.1 / V3.2 | 自家格式(有差异) |
| Hermes / Mistral | 直接 JSON |
| GPT-OSS | 类 Llama |
| GLM-4 / 4.7 | XML 标记 |
| Gemma 4 | 类 Hermes |
| Hunyuan / InternLM / Kimi K2 / Pixtral | 各自专属 |
SGLang 在 srt/function_call/ 下为每个模型实现一个 detector:
function_call/
├── function_call_parser.py ← 总入口
├── base_format_detector.py ← 抽象基类
├── core_types.py ← 数据结构
├── qwen25_detector.py
├── qwen3_detector.py
├── deepseekv3_detector.py
├── deepseekv31_detector.py
├── deepseekv4_detector.py
├── hermes_detector.py
├── gpt_oss_detector.py
├── glm4_moe_detector.py
├── glm47_moe_detector.py
├── gemma4_detector.py
├── hunyuan_detector.py
├── internlm_detector.py
├── kimi_k2_detector.py
├── pixtral_detector.py
└── ...
启动时 --tool-call-parser 选对应的。
3. Parser 工作原理
Tool call parser 是个状态机,边接收 token 边判断"现在是不是在 tool call 里"。
class BaseFormatDetector:
def parse_streaming(self, new_text):
"""逐 chunk 调用,返回:
- 普通文本 chunk(继续给客户端)
- 一个完整 tool call(触发 tool)
- 还需要更多输入(继续等)
"""
伪代码(以 Qwen2.5 为例):
TOOL_START = "<tool_call>"
TOOL_END = "</tool_call>"
class Qwen25Detector:
def __init__(self):
self.buffer = ""
self.in_tool = False
def parse_streaming(self, new_text):
self.buffer += new_text
if not self.in_tool:
idx = self.buffer.find(TOOL_START)
if idx == -1:
# 没看到 tool start, 看似纯文本
# 但要小心 "<tool_ca" 这种半截,不能输出
safe_len = len(self.buffer) - (len(TOOL_START) - 1)
emit = self.buffer[:safe_len]
self.buffer = self.buffer[safe_len:]
return TextChunk(emit)
else:
# 看到了 tool start
text_before = self.buffer[:idx]
self.buffer = self.buffer[idx + len(TOOL_START):]
self.in_tool = True
return TextChunk(text_before)
else:
idx = self.buffer.find(TOOL_END)
if idx == -1:
return None # 还在 tool 内部,等更多
json_str = self.buffer[:idx]
self.buffer = self.buffer[idx + len(TOOL_END):]
self.in_tool = False
return ToolCall(json.loads(json_str))
4. 流式 vs 非流式
4.1 非流式
模型整段输出完才解析:
text = full_response_text
calls = parser.parse_full(text)
简单可靠。客户端等完整响应。
4.2 流式
边接收边解析:
for chunk in stream:
result = parser.parse_streaming(chunk.delta.content)
if isinstance(result, ToolCall):
execute_tool(result)
elif isinstance(result, TextChunk):
yield_to_client(result)
OpenAI API 兼容时 SGLang 给客户端的 chunk 是混合的:普通文本 chunk + tool call chunk。客户端按 OpenAI 协议处理。
5. 受约束 + Function Call 组合
为了避免模型乱编 tool name 或参数:
# 给 tool name 用 enum 约束(DSL)
@sgl.function
def call_tool(s, question, tools):
s += sgl.user(question)
s += sgl.assistant_begin()
s += "<tool_call>{\"name\": \""
s += sgl.select("name", choices=[t["function"]["name"] for t in tools])
s += "\", \"arguments\": "
# 根据选中的 name 取对应 schema
selected_schema = next(t for t in tools if t["function"]["name"] == s["name"])
s += sgl.gen("args", json_schema=json.dumps(selected_schema["function"]["parameters"]))
s += "}</tool_call>"
s += sgl.assistant_end()
DSL 把 select + json_schema 组合用,从协议层面杜绝错误。
OpenAI API 路径默认不开 schema 约束(信任模型自己输出),出错率比 DSL 高。
6. 多步 tool 调用(agent loop)
messages = [{"role": "user", "content": "查上海天气,把温度乘 2"}]
while True:
resp = client.chat.completions.create(
model="...", messages=messages, tools=tools, tool_choice="auto"
)
msg = resp.choices[0].message
if not msg.tool_calls:
print(msg.content)
break
messages.append(msg)
for call in msg.tool_calls:
result = execute(call.function.name, json.loads(call.function.arguments))
messages.append({
"role": "tool", "tool_call_id": call.id, "content": result
})
每轮 SGLang runtime 都 cache 之前的 messages 上下文 → 多步 agent 收益巨大。 RadixCache 命中率在 agent 场景常 > 95%。
7. 排障 cheatsheet
| 现象 | 排查 |
|---|---|
| Tool call 没识别 | parser 不匹配模型;改 --tool-call-parser |
| arguments JSON 错 | 模型没遵循 schema;试 DSL 强约束 |
| 输出混乱 | chat_template 错;输入 messages 格式 |
| 流式 chunk 卡顿 | parser 等待 close tag;正常现象 |
| Tool name 模型编 | 用 select 受限选择 |
8. 监控指标
sglang:tool_call_total{name="...", ...}
sglang:tool_call_parse_failure_total
sglang:tool_call_argument_invalid_total
parse_failure_rate < 1% 是健康范围。
9. 实战 tips
- Tool 数量超过 10:模型容易选错;考虑 hierarchical(先选类别再选具体 tool)。
- Tool 描述要简洁明确:模型靠 description 判断用不用;写得啰嗦反而模糊。
- arguments 字段加
description:让模型知道每个参数干啥。 - 错误参数返回友好 message:tool 报错时返回 "格式错误,请用 ...",模型能修正。
- 限制 max_steps:agent loop 必加上限,防止死循环。
10. 小结
- Function calling = 受约束输出 + parser 解析 + 状态化迭代。
- SGLang 为各家模型实现专属 parser(
srt/function_call/)。 - DSL 路径可以加约束让 tool name / 参数 100% 正确。
- 多步 agent 在 SGLang 上 cache 命中率高,性能优势明显。
- 监控 parse_failure_rate。
11. 自检
- SGLang 怎么知道用哪种 tool 格式解析?
答案
靠启动参数 `--tool-call-parser- 流式 parser 为什么要 buffer?
答案
tool 边界标签可能横跨多 chunk。 例:模型吐 token 流 `"- DSL + select 怎么强制 tool name 正确?
答案
关键代码: ```python s += "- 多步 agent 在 SGLang 上比 vLLM 快的原因?
答案
每一步 agent loop 都附带累积 messages 上下文,第 N 步的 prompt = 前 N-1 步全部内容拼接 + 新 user/system。 SGLang RadixCache 把前 N-1 步的 KV 完整保留(lock_ref 维持),第 N 步 `match_prefix` 命中率 95%+,只 prefill 末尾新增几十 token。 vLLM v1 也有 prefix cache,但 block hash 表是 16 token 整块匹配,agent 长上下文 + 多变 user input 的组合下命中率仅 60-70%。 实测多步 agent SGLang TTFT 比 vLLM 低 50-70%,单步 TPOT 接近。 配合 DSL 的 `s += sgl.gen(...)` 多轮在同 ProgramState 累积,命中确定性 100%;纯 OpenAI 客户端循环依赖 hash 盲匹配,命中率有方差。- tool 数量过多怎么处理?
答案
tool > 10 后模型选错概率显著上升,建议: (1) **Hierarchical routing**:先用一次 LLM 选 tool **类别**(5-10 类),再选具体 tool;总分支可控。 (2) **RAG-style retrieval**:业务侧用 embedding 把 user query 与 tool description 匹配,只把 top-5 tool 喂给 LLM。 (3) **Tool 合并**:相似 tool 合并为一个,用 `mode` 参数区分。 (4) **DSL `select` 显式枚举**:哪怕 100 个 tool,DSL 路径用 `sgl.select(choices=[...100个])` 也能正确选(运行时跑一次 forward 算 100 个 logprob,比 free-form gen 快);只是 tool name 长度有限制。 (5) **降低成本**:tool description 加 `priority` 字段,常用 tool 文字精简。 监控 `parse_failure_rate`,> 1% 就 hierarchical。12. 下一步
01-dsl-walkthrough.md— DSL 综合实战。03-json-schema.md— 受约束输出。09-advanced-features/02-structured-output.md— 结构化进阶。- 源码:
srt/function_call/。