GPT5 容易被忽略的部分之 自定义工具

Posted on 8月 9, 2025

在最初的 OpenAI API 中,funcall 参数一直以 JSON 格式传递,这一方式曾被视为事实标准,国内外各大模型也都兼容这种调用方式。

但在实际使用中,我遇到了一些问题,比如让 LLM 输出合法的 JSON 格式仍存在困难(不仅如此,一些兼容性问题和边界情况也时有发生)。

在 GPT5 的 Blog 中 Openai 也有提到尽管我们的模型经过充分训练能够输出 JSON 格式,但当输入内容较长时(例如数百行代码或一份 5 页报告),其出错概率会显著上升。1

我正在考虑改用 XML 格式结合 React,据我所知,Cline 一直采用 XML 格式。我能想到的这种做法有助于 Agent 直接支持不兼容 OpenAI API 的模型。

OpenRoute openai/gpt-oss-120b for Groq

未能将工具调用参数解析为 JSON

alt text

alt text

GLM-4.5 zhipu

Github Issue

在调用工具时传入了工具描述中不存在的入参数

alt text

解析 XML 标签失败 funcall 内容出现在了 reasoning_content 中

alt text

自定义工具

Openai 在推出 GPT5 的同时推出了一种新的工具类型,自定义工具,它允许 GPT‑5 使用纯文本而不是 JSON 来调用工具。通过提供正则表达式 或 lark 信息无关文法 ⁠(context-free-grammars)来定义工具调用的格式。

自定义工具的工作原理

自定义工具的核心思想是摆脱严格的 JSON 格式限制,让模型能够以更自然的文本方式调用工具。这种方式有几个显著优势:

  1. 降低格式错误率:纯文本格式减少了因括号不匹配、引号逃逸等 JSON 格式问题导致的错误
  2. 更好的兼容性:不受特定 API 格式限制,可以适配各种后端系统
  3. 更直观的调试:人类可以直接阅读和修改工具调用,无需处理复杂的 JSON 结构

使用正则表达式定义工具格式

最简单的自定义工具实现方式是使用正则表达式来解析模型输出。例如:

import re

# 定义一个简单的计算器工具格式
pattern = r'calculate:\s*(\d+)\s*([+\-*/])\s*(\d+)'

def parse_calculator(text):
    match = re.search(pattern, text)
    if match:
        left, op, right = match.groups()
        return {
            "function": "calculate",
            "arguments": {
                "left": int(left),
                "operator": op,
                "right": int(right)
            }
        }
    return None

上下文无关文法(Context-Free Grammars)

对于更复杂的工具调用场景,OpenAI 推荐使用上下文无关文法来定义工具格式。CFG 提供了比正则表达式更强大的表达能力,能够处理嵌套结构和更复杂的语法规则。

CFG 基础概念

上下文无关文法由以下部分组成:

  • 终结符(Terminal symbols):基本的不可再分的符号
  • 非终结符(Non-terminal symbols):可以被替换的符号
  • 产生式规则(Production rules):定义如何从非终结符生成终结符序列
  • 开始符号(Start symbol):文法的入口点

实际应用示例

考虑一个文件操作工具的 CFG 定义:

<tool_call> ::= <function_name> "(" <arguments> ")"
<function_name> ::= "read_file" | "write_file" | "delete_file"
<arguments> ::= <argument> | <argument> "," <arguments>
<argument> ::= <param_name> "=" <value>
<param_name> ::= "path" | "content" | "mode"
<value> ::= <string> | <number> | <boolean>
<string> ::= "\"" <char_sequence> "\""

使用 Python 的lark库实现:

from lark import Lark

grammar = """
    ?tool_call: function_name "(" arguments? ")"
    function_name: "read_file" | "write_file" | "delete_file"
    arguments: argument ("," argument)*
    argument: param_name "=" value
    param_name: "path" | "content" | "mode"
    value: string | number | boolean
    string: ESCAPED_STRING
    number: NUMBER
    boolean: "true" | "false"

    %import common.ESCAPED_STRING
    %import common.NUMBER
    %import common.WS
    %ignore WS
"""

parser = Lark(grammar, start='tool_call')

# 解析模型输出
result = parser.parse('write_file(path="/tmp/test.txt", content="Hello World")')

实践建议

  1. 从简单开始:先用正则表达式处理简单场景,再逐步迁移到 CFG
  2. 提供示例:在系统提示中给出清晰的工具调用示例
  3. 错误处理:实现优雅的错误处理和重试机制
  4. 测试覆盖:为各种边界情况编写测试用例

性能考量

虽然自定义工具提供了更大的灵活性,但也需要注意:

  • 解析开销:复杂的 CFG 解析可能比 JSON 解析更耗时
  • 内存使用:大型文法可能占用更多内存
  • 缓存策略:考虑缓存解析结果以提高性能

碰巧最近在实现一个 QL 规则引擎,一直在写 lark 相关的东西。感觉这个特别适合用来写 安全策略 构建一个规则编写工具来写 nuclei 或者 xray 的策略

附录

参考文章

版权信息

本文原载于 not only security,复制请保留原文出处。

comments powered by Disqus