tool-call-parser

SGLang 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 glm47

API 调用

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:
        # 结构化标签信息
        pass

2. 注册到映射表

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 在中间做了统一转换。

设计模式上就是策略模式 + 工厂模式,代码结构挺清晰的。用起来也比较简单,启动时指定一下参数就行。