tool-call-parser

tool-call-parser
gogongxtSGLang Tool Call Parser 深度解析
为什么需要 Tool Call Parser?
在 LLM 应用开发中,工具调用(Function Calling / Tool Calling)是一个核心能力。
然而,不同模型厂商训练的时候,工具调用的输出格式完全是各玩各的:
GLM 系列输出的是这种 XML 风格:
1
2
3
4
get_weather
city北京
date2024-06-27
(实际使用的是
<?>、<?>、<?>、<?>
这类特殊标签)
Qwen 系列用的是特殊 token 包 JSON:
1
<tool_call>{"name": "get_weather", "arguments": {...}}DeepSeek 直接输出 JSON:
1
{"name": "get_weather", "arguments": {...}}应用层需要适配多种格式 - 每接入一个新模型就要写一套解析逻辑
解决思路
核心思想:将模型特定的输出格式转换为统一的 OpenAI 兼容格式。
所以 --tool-call-parser 就是告诉
SGLang:这个模型用的是哪种工具调用格式,我好知道怎么解析。
1
模型输出 → Parser 解析 → OpenAI 格式统一输出格式:
1
2
3
4
5
6
7
8
{
"id": "call_xxx",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\"}"
}
}架构设计
核心组件
SGLang 的 Tool Call Parser 采用策略模式 + 工厂模式设计:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
python/sglang/srt/function_call/
├── function_call_parser.py # 主解析器,工厂类
├── base_format_detector.py # 抽象基类
├── glm4_moe_detector.py # GLM-4.5 解析器
├── glm47_moe_detector.py # GLM-4.7/5 解析器
├── qwen25_detector.py # Qwen2.5 解析器
├── qwen3_coder_detector.py # Qwen3 Coder 解析器
├── deepseekv3_detector.py # DeepSeek V3 解析器
├── llama32_detector.py # Llama 3.2 解析器
├── mistral_detector.py # Mistral 解析器
├── hermes_detector.py # Hermes 解析器
├── kimik2_detector.py # Kimi K2 解析器
├── pythonic_detector.py # Pythonic 风格解析器
└── ... # 其他模型解析器解析器注册表
FunctionCallParser
类中定义了解析器映射表(类属性):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class FunctionCallParser:
ToolCallParserEnum: Dict[str, Type[BaseFormatDetector]] = {
"deepseekv3": DeepSeekV3Detector,
"deepseekv31": DeepSeekV31Detector,
"deepseekv32": DeepSeekV32Detector,
"glm": Glm4MoeDetector,
"glm45": Glm4MoeDetector,
"glm47": Glm47MoeDetector,
"gpt-oss": GptOssDetector,
"kimi_k2": KimiK2Detector,
"lfm2": Lfm2Detector,
"llama3": Llama32Detector,
"mimo": MiMoDetector,
"mistral": MistralDetector,
"pythonic": PythonicDetector,
"qwen": Qwen25Detector,
"qwen25": Qwen25Detector,
"qwen3_coder": Qwen3CoderDetector,
"step3": Step3Detector,
"step3p5": Qwen3CoderDetector,
"minimax-m2": MinimaxM2Detector,
"trinity": TrinityDetector,
"interns1": InternlmDetector,
"hermes": HermesDetector,
"gigachat3": GigaChat3Detector,
}基类接口
所有解析器继承自
BaseFormatDetector,必须实现以下接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class BaseFormatDetector(ABC):
def has_tool_call(self, text: str) -> bool:
"""检测文本是否包含工具调用"""
pass
def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult:
"""一次性解析完整文本"""
pass
def parse_streaming_increment(self, new_text: str, tools: List[Tool]) -> StreamingParseResult:
"""流式增量解析(基类提供默认实现,子类可覆盖)"""
pass
def supports_structural_tag(self) -> bool:
"""是否支持结构化标签约束(默认返回 True)"""
pass
def structure_info(self) -> _GetInfoFunc:
"""返回结构化标签信息(用于 constrained generation)"""
pass基类还维护了流式解析的状态:
_buffer: 累积未处理的文本prev_tool_call_arr: 已解析的工具调用信息current_tool_id: 当前正在流式输出的工具索引streamed_args_for_tool: 每个工具已输出的参数 JSON 字符串
深入 GLM-4.7 解析器
以 glm47 解析器为例,深入理解解析流程。
GLM 模型的输出格式
GLM-4.7/5 模型输出的工具调用采用 XML 风格,使用特殊标签:
1
2
3
4
get_weather
city北京
date2024-06-27
其中:
<?>是工具调用开始标签<?>包裹参数名?>包裹参数值?>是工具调用结束标签
连续调用:
1
2
3
4
5
6
get_weather
city北京
get_weather
city上海
解析流程
1. 检测工具调用
1
2
def has_tool_call(self, text: str) -> bool:
return self.bot_token in text # bot_token = "<?"2. 一次性解析
1
2
3
4
5
6
7
8
9
10
11
12
13
def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult:
# 1. 使用正则提取所有工具调用
match_result_list = re.findall(self.func_call_regex, text, re.DOTALL)
# 2. 解析每个工具调用
for match_result in match_result_list:
# 提取函数名
func_name = ...
# 提取参数
arguments = self._parse_argument_pairs(pairs, func_name, tools)
# 3. 返回 OpenAI 格式
return StreamingParseResult(normal_text=normal_text, calls=calls)3. 流式增量解析(核心难点)
流式场景下,数据逐字符到达,需要状态机处理:
1
2
状态流转:
INIT → BETWEEN → IN_KEY → WAITING_VALUE → IN_VALUE → BETWEEN → ...1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def _process_xml_to_json_streaming(self, raw_increment: str, func_name: str, tools: List[Tool]) -> str:
"""XML → JSON 流式转换"""
json_output = ""
for char in raw_increment:
self._xml_tag_buffer += char
if self._stream_state in [StreamState.INIT, StreamState.BETWEEN]:
if self._xml_tag_buffer.endswith("<?"):
self._stream_state = StreamState.IN_KEY
json_output += "{" if self._is_first_param else ", "
elif self._stream_state == StreamState.IN_KEY:
if self._xml_tag_buffer.endswith("?>"):
key = self._xml_tag_buffer[:-10].strip()
json_output += json.dumps(key) + ": "
self._stream_state = StreamState.WAITING_VALUE
# ... 更多状态处理
return json_output类型推断
解析器会根据工具定义的 JSON Schema 推断参数类型:
1
2
3
4
5
6
7
8
def get_argument_type(func_name: str, arg_key: str, defined_tools: List[Tool]) -> Optional[str]:
"""从工具定义获取参数类型"""
name2tool = {tool.function.name: tool for tool in defined_tools}
tool = name2tool.get(func_name)
params = getattr(tool.function, "parameters", None)
properties = params.get("properties", {})
arg_spec = properties.get(arg_key, {})
return infer_type_from_json_schema(arg_spec) # 支持 anyOf/oneOf/allOf/enum 等如果没有显式定义类型,解析器会根据值的内容自动推断(如检测是否为 JSON object/array、是否为数字等)。
实际使用
启动服务
1
2
3
python -m sglang.launch_server \
--model-path glm-5 \
--tool-call-parser glm47API 调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import openai
client = openai.Client(base_url="http://localhost:30000/v1", api_key="xxx")
response = client.chat.completions.create(
model="default",
messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
tools=[{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取城市天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"}
},
"required": ["city"]
}
}
}]
)
# 输出已转换为 OpenAI 格式
tool_call = response.choices[0].message.tool_calls[0]
print(tool_call.function.name) # "get_weather"
print(tool_call.function.arguments) # '{"city": "北京"}'统一格式的好处
1. 应用层无感知
上层的 Agent 框架、RAG 系统只需处理一种格式:
1
2
3
# 无需关心底层是 GLM、Qwen 还是 DeepSeek
for tool_call in response.message.tool_calls:
result = execute_tool(tool_call.function.name, tool_call.function.arguments)2. 流式体验一致
流式场景下,不同模型的输出统一转换为 SSE 格式:
1
2
3
data: {"choices": [{"delta": {"tool_calls": [{"function": {"name": "get_weather"}}]}}]}
data: {"choices": [{"delta": {"tool_calls": [{"function": {"arguments": "{\"city\":"}}]}}]}
data: {"choices": [{"delta": {"tool_calls": [{"function": {"arguments": " \"北京\"}"}}]}}]}4. 生态兼容
直接兼容所有基于 OpenAI API 的工具和框架:
- LangChain
- AutoGen
- CrewAI
- OpenAI SDK
- …
扩展新的解析器
为新增模型添加解析器只需三步:
1. 实现 Detector
1
2
3
4
5
6
7
8
9
10
11
12
13
# python/sglang/srt/function_call/my_model_detector.py
class MyModelDetector(BaseFormatDetector):
def has_tool_call(self, text: str) -> bool:
# 检测逻辑
pass
def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult:
# 解析逻辑
pass
def structure_info(self) -> _GetInfoFunc:
# 结构化标签信息
pass2. 注册到映射表
1
2
3
4
5
6
# python/sglang/srt/function_call/function_call_parser.py
class FunctionCallParser:
ToolCallParserEnum: Dict[str, Type[BaseFormatDetector]] = {
# ...
"my_model": MyModelDetector,
}3. 使用
1
python -m sglang.launch_server --model-path xxx --tool-call-parser my_model小结
--tool-call-parser
这个参数,本质上就是个适配层。不同模型输出的格式不一样,但上层应用不想关心这些细节,所以
SGLang 在中间做了统一转换。
设计模式上就是策略模式 + 工厂模式,代码结构挺清晰的。用起来也比较简单,启动时指定一下参数就行。




