返回博客

API 与 SDK指南

使用 OpenAI 兼容 API 在 Python 中进行函数调用

发送 tools,读取 tool_calls,并用 role tool 返回结果:使用 MiniMax-M2.7 在 Python 中实现两步函数调用循环。

作者:CheapTokens Team发布于 更新于 5 分钟阅读

两步工具调用循环示意图:发送 tools,读取 tool_calls,将结果作为 tool 消息返回
本页内容
  1. 函数调用如何工作
  2. 设置客户端
  3. 声明函数并发送第一个请求
  4. 你应该看到什么
  5. 多个调用和更长的循环
  6. 为什么 MiniMax-M2.7 适合工具循环
  7. 如果它不工作
  8. 后续步骤

函数调用(工具调用)让模型请求你的代码运行一个函数。你在请求中发送一个 tools 列表,模型返回 tool_calls 而不是文本,你自己运行该函数,然后通过一条带有 role: tool 的消息把结果发回去。模型随后写出最终答案。本指南展示了在 Python 中针对 OpenAI 兼容端点 https://api.cheaptokens.dev/v1/chat/completions 使用 MiniMax-M2.7(id 为 MiniMaxAI/MiniMax-M2.7,180K 上下文,每次回答最多 8,192 个输出 token)的完整循环。

目录中的每个模型都列出了支持工具调用(CheapTokens 文档,于 2026 年 10 月 22 日检查)。你需要一个 API key;如果你还没有发起过请求,请先从你的第一个 API 请求开始。

函数调用如何工作#

模型从不运行任何东西。它只生成一个结构化请求:“用 {\"city\": \"Paris\"} 调用 get_weather”。你的程序完成实际工作并回报结果。这就形成了一个包含两次请求的循环:

  1. 请求 1。 你发送用户消息以及 tools 列表。模型要么用文本回答,要么返回一个或多个 tool_calls。
  2. 你的代码。 你解析每个调用,运行匹配的函数并收集结果。
  3. 请求 2。 你再次发送整个对话:原始消息、带有 tool_calls 的 assistant 消息,以及每个调用对应的一条 role: tool 消息。模型使用这些结果写出最终答案。

每次请求都会重新发送整个历史记录,因此工具循环比单个问题使用更多输入 token。如果你对此不熟悉,请阅读什么是 token。

设置客户端#

安装 OpenAI SDK(pip install openai),并把你的 key 放在环境变量中,而不是源代码里:

bash
export CHEAPTOKENS_API_KEY="your-key-here"

base URL 是 https://api.cheaptokens.dev/v1。从 CheapTokens 文档中获取模型 id:不同提供商对同一模型的命名不同。本指南使用 MiniMaxAI/MiniMax-M2.7。

声明函数并发送第一个请求#

函数在 tools 数组中描述。每个条目都有一个 type: function 和一个 function 对象,其中包含 name、description 以及以 JSON Schema 编写的 parameters。描述是模型用来决定何时调用函数的内容,因此请把它写成一句简短、具体的句子。

下面是一个完整脚本。天气函数是一个带有假数据的桩,因此该示例不需要额外服务:

python
import json
import os

from openai import OpenAI

client = OpenAI(
    base_url='https://api.cheaptokens.dev/v1',
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'MiniMaxAI/MiniMax-M2.7'


def get_weather(city: str) -> dict:
    # Stub with fake data: replace with a real lookup
    fake = {'Paris': 14, 'Tokyo': 19}
    return {'city': city, 'temp_c': fake.get(city, 10)}


FUNCTIONS = {'get_weather': get_weather}

tools = [
    {
        'type': 'function',
        'function': {
            'name': 'get_weather',
            'description': 'Get the current temperature in Celsius for a city.',
            'parameters': {
                'type': 'object',
                'properties': {
                    'city': {'type': 'string', 'description': 'City name, for example Paris'},
                },
                'required': ['city'],
            },
        },
    }
]

messages = [{'role': 'user', 'content': 'What is the weather in Paris right now?'}]

# Step 1: the model decides whether to call a function
first = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
message = first.choices[0].message

if not message.tool_calls:
    print(message.content)
    raise SystemExit

# Step 2: run every requested call and send the results back
messages.append(message)
for call in message.tool_calls:
    args = json.loads(call.function.arguments)
    result = FUNCTIONS[call.function.name](**args)
    messages.append({
        'role': 'tool',
        'tool_call_id': call.id,
        'content': json.dumps(result),
    })

final = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
print(final.choices[0].message.content)

有三个细节决定循环能否工作:

  • call.function.arguments 是一个 JSON 字符串,不是字典。用 json.loads 解析它。
  • 每条 role: tool 消息都必须携带它所回答调用的 tool_call_id,即 call.id。
  • 工具消息的 content 是字符串。结构化结果请使用 json.dumps。

你应该看到什么#

如果你在第一次请求后打印这些字段,它们看起来像这样(调用 id 每次运行都不同):

text
finish_reason: tool_calls
call.id: call_abc123
call.function.name: get_weather
call.function.arguments: {"city": "Paris"}

然后脚本会打印最终答案,一句类似“It is currently 14 °C in Paris.”的普通句子。措辞每次运行都会变化;数字来自你的函数。

多个调用和更长的循环#

模型可以在一次响应中返回多个调用,例如当你询问两个城市的天气时。这就是示例遍历 message.tool_calls 并按各自的 id 回答每个调用的原因。OpenAI 的函数调用指南描述了相同的行为:在默认的 tool_choice 为 auto 时,模型可以调用零个、一个或多个函数(OpenAI 文档,于 2026 年 10 月 22 日检查;该页面没有最后更新日期,描述的是较新的 Responses API 形式,而本指南使用的是 /v1/chat/completions 端点的 Chat Completions 形式)。

Agent 会重复这两个步骤,直到模型不再请求工具。把请求包在循环中,并始终设置步数上限:

python
for _ in range(5):  # safety limit against endless loops
    response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    message = response.choices[0].message
    if not message.tool_calls:
        print(message.content)
        break
    messages.append(message)
    for call in message.tool_calls:
        args = json.loads(call.function.arguments)
        result = FUNCTIONS[call.function.name](**args)
        messages.append({'role': 'tool', 'tool_call_id': call.id, 'content': json.dumps(result)})

如果你希望答案逐 token 出现,请将此与 Python 中的流式传输结合使用。

为什么 MiniMax-M2.7 适合工具循环#

工具循环会发送许多请求,且历史记录不断增长。针对这种模式,CheapTokens 提供 MiniMax-M2.7:180K 上下文,每次回答最多 8,192 个输出 token,全天任意时段统一价格、没有高峰时段,所有模型共用一个余额。将输入和输出合并计算,它比 OpenRouter 的参考价格更便宜(CheapTokens 价格比较,于 2026 年 10 月 31 日检查)。

当前每 1M token 的价格:输入 $0.05,输出 $0.15。作为一个示例工作负载(并非实测),上面那次两次请求的往返总共约 2,700 个输入 token 和 350 个输出 token,成本为 $0.00019。完整价格列表在价格页面上。

要尝试另一个模型,只需更改 id;id 列表在模型页面上。

如果它不工作#

  • message.tool_calls 是 None。 模型用文本回答了。让函数 description 更具体,并确保问题确实需要该函数。
  • json.JSONDecodeError 出现在 arguments 上。 模型生成了无效 JSON。将 json.loads 包在 try/except 中,并把错误文本作为工具结果发回,以便模型用修正后的参数重试。
  • 第二次请求失败或模型忽略结果。 检查每个调用是否恰好有一条 role: tool 消息,且其 tool_call_id 匹配,并检查带有 tool_calls 的 assistant 消息是否在它们之前的历史记录中。
  • KeyError 出现在 FUNCTIONS[...] 中。 模型命名了一个你未注册的函数。改为把简短的错误字符串作为工具结果返回,而不是让程序崩溃。
  • 答案被截断。 检查 finish_reason:值为 length 表示达到了输出 token 上限。参见为什么 AI 回答会在句子中间停止。
  • 401 或身份验证错误。 检查 key 是否在 CHEAPTOKENS_API_KEY 中,以及 base URL 是否以 /v1 结尾。

后续步骤#

用真实函数替换桩:数据库查询、搜索调用或计算器。向 tools 和 FUNCTIONS 添加更多条目,并让每个函数结果保持简短,因为它会在后续每次请求中回到历史记录里。要获取 key 并发起第一次调用,请打开 CheapTokens 文档。