在不支持工具调用接口的大模型上实现工具调用

工具调用是大模型重要的能力之一,能够抑制大模型胡说八道,增加模型输出真实性可靠性。有些大模型在接口上不支持工具调用,但其实任何大模型本质上都是词语接龙,我们可以把工具调用指令拼接到提示词里面来实现。

添加必要的导入:

py 复制代码
import re
import os
import json
import openai

首先我们需要这么一个提示词模板,它描述了可用工具列表,工具调用的返回和回传格式,还有示例。示例算是小样本提示,在没有特别微调的模型上,能够引导模型理解和输出工具调用。其中可用工具列表是个占位符,之后填充。

py 复制代码
TOOLCALL_PMT = '''
## 工具调用指南

### 可用工具

在回答任何问题时,你可以调用一次或多次如下工具:

```
{tool_def}
```

### 返回格式

在决定调用工具时,请按照如下格式返回工具调用,确保内容包含在"[tool]...[/tool]"中,任何其它内容将会被忽略。如果不决定调用工具,不要输出任何"[tool]...[/tool]"内容。

[tool]
[{"id": "uuid", "tool": "tool name", "parameters": {"parameter name": "parameter value"}}]
[/tool]

用户调用工具后,将结果以如下格式传回:

[tool-result]
[{"id": "uuid", "result": "result"}]
[/tool-result]

### 示例

这是一个可用工具列表的示例:

```
{"tools": [{"name": "plus_one", "description": "Add one to a number", "parameters": {"type": "object","properties": {"number": {"type": "string","description": "The number that needs to be changed, for example: 1","default": "1",}},"required": ["number"]}},{"name": "minus_one", "description": "Minus one to a number", "parameters": {"type": "object","properties": {"number": {"type": "string","description": "The number that needs to be changed, for example: 1","default": "1",}},"required": ["number"]}}]}
```

如果你想计算`42 + 1`,可以返回:

[tool]
[{"id": "c3d16bba-9216-449e-8d46-d389fbca6cb5", "tool": "plus_one", "parameters": {"number": 42}}]
[/tool]

用户计算后,传回结果:

[tool-result]
[{"id": "c3d16bba-9216-449e-8d46-d389fbca6cb5", "result": 43}]
[/tool-result]

请注意,上述只是个示例,并不代表`plus_one`和`plus_minus`真实存在。
'''

定义工具字典,这里提供了两个工具,模拟查询航班号和航班价格:

py 复制代码
def get_flight_number(date: str, departure: str, destination: str):
    flight_number = {
        "北京": {
            "上海": "1234",
            "广州": "8321",
        },
        "上海": {
            "北京": "1233",
            "广州": "8123",
        }
    }
    return flight_number.get(departure, {}).get(destination, "0")

def get_ticket_price(date: str, flight_number: str):
    return "1000"

tool_dict = {
    "get_flight_number": get_flight_number,
    "get_ticket_price": get_ticket_price
}

工具信息按照 OpenAI 的格式写就可以了:

复制代码
tool_defs = [
    {
        "type": "function",
        "function": {
            "name": "get_flight_number",
            "description": "根据始发地、目的地和日期,查询对应日期的航班号",
            "parameters": {
                "type": "object",
                "properties": {
                    "departure": {
                        "description": "出发地",
                        "type": "string"
                    },
                    "destination": {
                        "description": "目的地",
                        "type": "string"
                    },
                    "date": {
                        "description": "日期",
                        "type": "string",
                    }
                },
                "required": ["departure", "destination", "date"]
            },
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_ticket_price",
            "description": "查询某航班在某日的票价",
            "parameters": {
                "type": "object",
                "properties": {
                    "flight_number": {
                        "description": "航班号",
                        "type": "string"
                    },
                    "date": {
                        "description": "日期",
                        "type": "string",
                    }
                },
                "required": ["flight_number", "date"]
            },
        }
    },
]

定义一个函数来调用大模型并处理工具调用。

  • 首先拼接提示词,并作为系统提示词插入到消息列表最上面
  • 然后调用一次大模型
  • 用正则匹配返回文本里是否带有[tool]...[/tool]
    • 如果是,用 JSON 库解析,找到合适的工具并调用

    • 然后把工具调用的结果回传,再次调用大模型,重复步骤 2~3

    • 否则直接返回文本

      def call_llm_with_toolcall(
      msgs, model_name,
      tool_defs, tool_dict,
      ):
      if isinstance(msgs, str):
      msgs = [{'role': 'user', 'content': msgs}]
      tool_defs_str = json.dumps(tool_defs)
      toolcall_pmt = TOOLCALL_PMT.replace('{tool_def}', tool_defs_str)
      msgs = [{
      'role': 'system',
      'content': toolcall_pmt
      }] + msgs
      client = openai.OpenAI(
      base_url=openai.base_url,
      api_key=openai.api_key,
      )
      print(f'msg: {msgs[-1]["content"]}')
      res = client.chat.completions.create(
      messages=msgs,
      model=model_name,
      ).choices[0].message.content.strip()
      print(f'res: {res}')

      复制代码
          TOOLCALL_RE = r'\[tool\]([\s\S]+)\[/tool\]'
          m = re.search(TOOLCALL_RE, res)
          while m:
              toolcalls = json.loads(m.group(1))
              toolcall_res_list = []
              for tc in toolcalls:
                  tc_res = tool_dict[tc['tool']](**tc['parameters'])
                  toolcall_res_list.append({'id': tc['id'], 'result': tc_res})
              toolcall_res_str = json.dumps(toolcall_res_list)
              msgs += [{
                  'role': 'assistant',
                  'content': res
              }, {
                  'role': 'user',
                  'content': f'[tool-result]{toolcall_res_str}[/tool-result]'
              }]
              print(f'msg: {msgs[-1]["content"]}')
              res = client.chat.completions.create(
                  messages=msgs,
                  model=model_name,
              ).choices[0].message.content.strip()
              print(f'res: {res}')
              m = re.search(TOOLCALL_RE, res)
          
          return res

测试代码:

py 复制代码
openai.api_key = os.environ.get('OPENAI_API_KEY')
openai.base_url = os.environ.get('OPENAI_BASE_URL')
model_name = os.environ.get('OPENAI_MODEL')

msgs = [{
    'role': 'system',
    'content': '你是一个...'
}, {
    'role': 'user',
    'content': '帮我查询2024年1月20日,从北京出发前往上海的航班号和价格',
}]
ans = call_llm_with_toolcall(msgs, model_name, tool_defs, tool_dict)
print(ans)

我们可以看到如下输出:

复制代码
ques: 帮我查询2024年1月20日,从北京出发前往上海的航班号和价格
res: [tool]
[{"id": "query_123", "tool": "get_flight_number", "parameters": {"departure": "北京", "destination": "上海", "date": "2024-01-20"}}]
[/tool]
ques: [tool-result][{"id": "query_123", "result": "1234"}][/tool-result]
res: [tool]
[{"id": "query_456", "tool": "get_ticket_price", "parameters": {"flight_number": "1234", "date": "2024-01-20"}}]
[/tool]
ques: [tool-result][{"id": "query_456", "result": "1000"}][/tool-result]
res: 2024年1月20日从北京出发前往上海的航班号为 1234,票价为 1000 元。
2024年1月20日从北京出发前往上海的航班号为 1234,票价为 1000 元。

可以看到模型首先决定调用get_flight_number工具,获取航班号之后,又调用get_ticket_price工具,顺利完成查询。