工具调用

定义函数、执行模型请求的工具,并回填结果生成回答。

模型返回工具名称和参数,由你的程序执行工具,再把结果交回模型。收到工具调用还不是完成回答。

本例查询本地演示数据:Paris 的温度固定为 22°C,不访问天气服务。deepseek-flash 在 GET /openai/v1/models 的 supported_parameters 中列有 tools 和 tool_choice;下面的两轮调用于 2026-10-12 验证通过。

1. 准备终端

将 Praxis 云 API Key 放入环境变量 PRAXIS_API_KEY;获取方式见 Key 与鉴权。不要把 Key 写入代码。准备 Python 和 uv;TypeScript 示例使用 Bun。

test -n "$PRAXIS_API_KEY"

期望:退出码为 0。示例默认使用 https://api.deepraxis.ai。切换环境时,将 PRAXIS_API_BASE 设为相应 API 域名,不带末尾斜杠或 API 路径。

将每段 Python 保存为标明的文件名,以 uv run -q --no-project --with openai --with anthropic python <文件名> 执行。各段代码独立运行。

2. 定义工具并完成两轮调用

保存为 tool-calling.py,运行 uv run -q --no-project --with openai --with anthropic python tool-calling.py。

代码先声明 get_temperature 的名称、描述和参数,再要求模型调用它;验证参数后执行本地查询,将结果和原始 tool_call_id 一起回填,最后请求文字回答。两轮都设置 reasoning_effort="none"。

import json
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.ai') + '/openai/v1')
tools = [{'type':'function','function':{
    'name':'get_temperature', 'description':'Read the demo temperature for Paris.',
    'parameters':{'type':'object','properties':{'city':{'type':'string','enum':['Paris']}},
        'required':['city'],'additionalProperties':False}}}]
messages = [{'role':'user','content':'Use get_temperature for Paris, then tell me the temperature in Celsius.'}]
first = client.chat.completions.create(model='deepseek-flash', messages=messages,
    tools=tools, reasoning_effort='none', tool_choice={'type':'function','function':{'name':'get_temperature'}}, max_tokens=1024)
print('X-Request-Id:', first._request_id)
message = first.choices[0].message
assert first.choices[0].finish_reason == 'tool_calls' and message.tool_calls
messages.append(message.model_dump(exclude_none=True))
for call in message.tool_calls:
    assert call.function.name == 'get_temperature'
    args = json.loads(call.function.arguments)
    assert args == {'city':'Paris'}, args
    result = {'city':args['city'], 'temperature_c':22}
    messages.append({'role':'tool','tool_call_id':call.id,'content':json.dumps(result)})
second = client.chat.completions.create(model='deepseek-flash', messages=messages,
    tools=tools, reasoning_effort='none', tool_choice='none', max_tokens=1024)
print('X-Request-Id:', second._request_id)
answer = second.choices[0].message.content or ''
assert second.choices[0].finish_reason == 'stop' and '22' in answer, answer
print(answer)

期望:第一轮 finish_reason=tool_calls,函数参数是 {"city":"Paris"};第二轮回答包含 22,finish_reason=stop;退出码为 0。

保留第一轮完整 assistant 消息,为每个工具调用返回一条 role="tool" 消息。不要重造调用 ID,也不要只把结果作为普通用户文字发回。参数是 JSON 字符串,需要解析和校验;不要直接执行模型提供的任意函数名。这里用 tool_choice="none" 明确结束工具阶段;使用 auto 的实际应用应允许多轮工具调用,并设置轮数上限。

3. 用 TypeScript 执行同一流程

保存为 tool-calling.ts,运行 bun tool-calling.ts;使用内置 fetch,无需安装 SDK。

import assert from 'node:assert/strict';
const base = process.env.PRAXIS_API_BASE ?? 'https://api.deepraxis.ai';
async function chat(body: object) {
  const response = await fetch(`${base}/openai/v1/chat/completions`, {
    method: 'POST', headers: {'Authorization': `Bearer ${process.env.PRAXIS_API_KEY}`, 'Content-Type': 'application/json'},
    body: JSON.stringify({model: 'deepseek-flash', max_tokens: 1024, reasoning_effort: 'none', ...body}),
  });
  console.log('X-Request-Id:', response.headers.get('x-request-id'));
  const data = await response.json();
  assert(response.ok, JSON.stringify(data));
  return data;
}
const tools = [{type: 'function', function: {name: 'get_temperature',
  description: 'Read the demo temperature for Paris.',
  parameters: {type: 'object', properties: {city: {type: 'string', enum: ['Paris']}},
    required: ['city'], additionalProperties: false}}}];
const messages: any[] = [{role: 'user', content: 'Use get_temperature for Paris, then tell me the temperature in Celsius.'}];
const first = await chat({messages, tools, tool_choice: {type: 'function', function: {name: 'get_temperature'}}});
const choice = first.choices[0];
assert.equal(choice.finish_reason, 'tool_calls');
assert(choice.message.tool_calls?.length);
messages.push(choice.message);
for (const call of choice.message.tool_calls) {
  assert.equal(call.function.name, 'get_temperature');
  const args = JSON.parse(call.function.arguments);
  assert.deepEqual(args, {city: 'Paris'});
  messages.push({role: 'tool', tool_call_id: call.id,
    content: JSON.stringify({city: args.city, temperature_c: 22})});
}
const second = await chat({messages, tools, tool_choice: 'none'});
assert.equal(second.choices[0].finish_reason, 'stop');
assert(second.choices[0].message.content.includes('22'));
console.log(second.choices[0].message.content);

期望:完成相同的工具调用与回填,最终回答包含 22,退出码为 0。

4. 使用 Anthropic Messages

Messages 用 input_schema 定义参数,返回 tool_use 内容块和 stop_reason="tool_use"。将完整 assistant 内容带回后,在下一条 role="user" 消息中放入 tool_result,用 tool_use_id 对应原调用。本例的 claude-opus-5 已实测通过。

保存为 tool-calling-anthropic.py,运行 uv run -q --no-project --with openai --with anthropic python tool-calling-anthropic.py。

import json
import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.ai') + '/anthropic')
tools = [{'name':'get_temperature','description':'Read the demo temperature for Paris.',
    'input_schema':{'type':'object','properties':{'city':{'type':'string','enum':['Paris']}},
        'required':['city'],'additionalProperties':False}}]
messages = [{'role':'user','content':'Use get_temperature for Paris, then tell me the temperature in Celsius.'}]
raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=1024,
    tools=tools, tool_choice={'type':'tool','name':'get_temperature'}, messages=messages)
print('X-Request-Id:', raw.headers.get('x-request-id'))
first = raw.parse()
assert first.stop_reason == 'tool_use'
messages.append({'role':'assistant','content':[block.model_dump(exclude_none=True) for block in first.content]})
results = []
for block in first.content:
    if block.type == 'tool_use':
        assert block.name == 'get_temperature' and block.input == {'city':'Paris'}
        results.append({'type':'tool_result','tool_use_id':block.id,
            'content':json.dumps({'city':'Paris','temperature_c':22})})
assert results
messages.append({'role':'user','content':results})
raw = client.messages.with_raw_response.create(model='claude-opus-5', max_tokens=1024,
    tools=tools, tool_choice={'type':'none'}, messages=messages)
print('X-Request-Id:', raw.headers.get('x-request-id'))
second = raw.parse()
answer = ''.join(block.text for block in second.content if block.type == 'text')
assert second.stop_reason == 'end_turn' and '22' in answer, answer
print(answer)

期望:第一轮请求工具,第二轮返回包含 22 的文本,stop_reason=end_turn,退出码为 0。

实测失败与处理

强制指定函数、但没有关闭思考时,deepseek-flash 返回 HTTP 400:

tool_choice a named function is not supported while thinking is enabled on this model. Set reasoning effort to "none" or use tool_choice "auto".

本页采用已验证的解决办法:显式设置 reasoning_effort="none"。仅第一轮设置、第二轮省略时,还曾收到 HTTP 400:

The selected thinking setting is not supported on this model route.

两轮都保留 reasoning_effort="none" 后通过。不要在重试时丢掉该参数。

协议细节:OpenAI · Anthropic。

API 参考 → · Cookbook →

导出

本页内容