工具与函数调用
在提示词中使用工具
工具调用(也称为函数调用)让 LLM 能够使用外部工具。LLM 不会直接调用工具。相反,它建议要调用的工具。随后由用户单独调用该工具,并将结果提供回 LLM。最后,LLM 将响应整理成对用户原始问题的回答。
OpenRouter 在模型和模型服务提供商之间标准化了工具调用接口,从而可以轻松地将外部工具与任何受支持的模型集成。
支持的模型:你可以在 openrouter.ai/models?supported_parameters=tools 上按筛选条件查找支持工具调用的模型。
如果你更喜欢通过完整的端到端示例学习,请继续阅读。
请求体示例
使用 OpenRouter 进行工具调用包含三个关键步骤。以下是每个步骤的核心请求体格式:
{
"model": "google/gemini-3-flash-preview",
"messages": [
{
"role": "user",
"content": "詹姆斯·乔伊斯有哪些书的书名?"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "search_gutenberg_books",
"description": "在古腾堡计划图书馆中搜索书籍",
"parameters": {
"type": "object",
"properties": {
"search_terms": {
"type": "array",
"items": {"type": "string"},
"description": "用于查找书籍的搜索词列表"
}
},
"required": ["search_terms"]
}
}
}
]
}
收到带有 tool_calls 的模型响应后,在本地执行所请求的工具并准备结果:
// 模型以 tool_calls 响应,你在本地执行该工具
const toolResult = await searchGutenbergBooks(["James", "Joyce"]);
{
"model": "google/gemini-3-flash-preview",
"messages": [
{
"role": "user",
"content": "詹姆斯·乔伊斯有哪些书的书名?"
},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "search_gutenberg_books",
"arguments": "{\"search_terms\": [\"James\", \"Joyce\"]}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "[{\"id\": 4300, \"title\": \"Ulysses\", \"authors\": [{\"name\": \"Joyce, James\"}]}]"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "search_gutenberg_books",
"description": "在古腾堡计划图书馆中搜索书籍",
"parameters": {
"type": "object",
"properties": {
"search_terms": {
"type": "array",
"items": {"type": "string"},
"description": "用于查找书籍的搜索词列表"
}
},
"required": ["search_terms"]
}
}
}
]
}
注意:必须在每次请求(步骤 1 和步骤 3)中都包含 tools 参数,以便路由器在每次调用时校验工具 schema。
下面的 Python 代码让 LLM 能够调用外部 API——这里是古腾堡计划,用于搜索书籍。
首先做一些基础设置:
import { OpenRouter } from '@openrouter/sdk';
const OPENROUTER_API_KEY = "<OPENROUTER_API_KEY>";
// 可以使用任何支持工具调用的模型
const MODEL = "google/gemini-3-flash-preview";
const openRouter = new OpenRouter({
apiKey: OPENROUTER_API_KEY,
});
const task = "詹姆斯·乔伊斯有哪些书的书名?";
const messages = [
{
role: "system",
content: "你是一名乐于助人的助手。"
},
{
role: "user",
content: task,
}
];
接下来,定义我们要调用的工具。请记住,工具将由 LLM 请求,但我们在此编写的代码最终负责执行调用,并将结果返回给 LLM。
async function searchGutenbergBooks(searchTerms: string[]): Promise<Book[]> {
const searchQuery = searchTerms.join(' ');
const url = 'https://gutendex.com/books';
const response = await fetch(`${url}?search=${searchQuery}`);
const data = await response.json();
return data.results.map((book: any) => ({
id: book.id,
title: book.title,
authors: book.authors,
}));
}
const tools = [
{
type: 'function',
function: {
name: 'searchGutenbergBooks',
description:
'根据指定搜索词在古腾堡计划图书馆中搜索书籍',
parameters: {
type: 'object',
properties: {
search_terms: {
type: 'array',
items: {
type: 'string',
},
description:
"在古腾堡图书馆中查找书籍的搜索词列表(例如 ['dickens', 'great'] 可搜索狄更斯所著、标题中含 'great' 的书籍)",
},
},
required: ['search_terms'],
},
},
},
];
const TOOL_MAPPING = {
searchGutenbergBooks,
};
注意,「工具」只是一个普通函数。然后我们编写与 OpenAI 函数调用参数兼容的 JSON「规范」。我们会把该规范传给 LLM,让它知道此工具可用以及如何使用。需要时它会请求该工具以及任何参数。随后我们在本地整理工具调用、执行函数,并将结果返回给 LLM。
让我们向模型发出第一次 OpenRouter API 调用:
const result = await openRouter.chat.send({
model: 'google/gemini-3-flash-preview',
tools,
messages,
stream: false,
});
const response_1 = result.choices[0].message;
LLM 以结束原因 tool_calls 和一个 tool_calls 数组响应。在通用的 LLM 响应处理程序中,你应在处理工具调用之前检查 finish_reason,但这里我们假定情况就是如此。让我们继续处理工具调用:
// 将响应追加到 messages 数组,以便 LLM 拥有完整上下文
// 很容易忘记这一步!
messages.push(response_1);
// 现在处理所请求的工具调用,并使用我们的书籍查找工具
for (const toolCall of response_1.tool_calls) {
const toolName = toolCall.function.name;
const { search_params } = JSON.parse(toolCall.function.arguments);
const toolResponse = await TOOL_MAPPING[toolName](search_params);
messages.push({
role: 'tool',
toolCallId: toolCall.id,
name: toolName,
content: JSON.stringify(toolResponse),
});
}
此时 messages 数组包含:
- 我们的原始请求
- LLM 的响应(包含一次工具调用请求)
- 工具调用的结果(从古腾堡计划 API 返回的 json 对象)
现在,我们可以发出第二次 OpenRouter API 调用,并有望得到结果!
const response_2 = await openRouter.chat.send({
model: 'google/gemini-3-flash-preview',
messages,
tools,
stream: false,
});
console.log(response_2.choices[0].message.content);
输出会类似:
以下是詹姆斯·乔伊斯的一些作品:
* *Ulysses*
* *Dubliners*
* *A Portrait of the Artist as a Young Man*
* *Chamber Music*
* *Exiles: A Play in Three Acts*
成功了!我们已经在提示词中成功使用了工具。
交错思考
交错思考允许模型在工具调用之间进行推理,从而在收到工具结果后做出更复杂的决策。此功能帮助模型在工具调用之间插入推理步骤来串联多次调用,并根据中间结果做出更细致的判断。
重要:交错思考会增加 Token 用量和响应延迟。启用此功能时请考虑预算和性能要求。
交错思考如何工作
借助交错思考,模型可以:
- 在决定下一步之前,对工具调用的结果进行推理
- 在工具调用之间插入推理步骤,串联多次调用
- 根据中间结果做出更细致的决策
- 为其工具选择过程提供透明的推理
示例:带推理的多步研究
下面的示例展示模型如何使用交错思考,跨多个来源研究一个主题:
初始请求:
{
"model": "anthropic/claude-sonnet-4.5",
"messages": [
{
"role": "user",
"content": "研究电动汽车的环境影响,并提供一份全面分析。"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "search_academic_papers",
"description": "按给定主题搜索学术论文",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"field": {"type": "string"}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "get_latest_statistics",
"description": "获取某主题的最新统计数据",
"parameters": {
"type": "object",
"properties": {
"topic": {"type": "string"},
"year": {"type": "integer"}
},
"required": ["topic"]
}
}
}
]
}
模型的推理与工具调用:
-
初始思考:「我需要研究电动汽车的环境影响。先从学术论文入手,获取经过同行评审的研究。」
-
第一次工具调用:search_academic_papers({"query": "electric vehicle lifecycle environmental impact", "field": "environmental science"})
-
第一次工具结果之后:「论文对制造环节影响的结论不一。我需要当前统计数据来补充这项学术研究。」
-
第二次工具调用:get_latest_statistics({"topic": "electric vehicle carbon footprint", "year": 2024})
-
第二次工具结果之后:「现在我同时有了学术研究和当前数据。接着搜索制造相关的专项研究,以填补发现的缺口。」
-
第三次工具调用:search_academic_papers({"query": "electric vehicle battery manufacturing environmental cost", "field": "materials science"})
-
最终分析:将收集到的全部信息综合成一份全面回应。
交错思考的最佳实践
- 清晰的工具描述:提供详细描述,以便模型推理何时使用每个工具
- 结构化参数:使用定义良好的参数 schema,帮助模型进行精确的工具调用
- 保留上下文:在多次工具交互中维持对话上下文
- 错误处理:设计工具以提供有意义的错误消息,帮助模型调整方法
实现注意事项
实现交错思考时:
- 由于额外的推理步骤,模型响应可能更慢
- 由于推理过程,Token 用量会更高
- 推理质量取决于模型能力
- 某些模型可能比其他模型更适合这种方法
简单的智能体循环
在上面的示例中,调用是显式、顺序进行的。要处理各种各样的用户输入和工具调用,可以使用智能体循环。
下面是一个简单智能体循环的示例(使用与上面相同的 tools 和初始 messages):
async function callLLM(messages: Message[]): Promise<ChatResponse> {
const result = await openRouter.chat.send({
model: 'google/gemini-3-flash-preview',
tools,
messages,
stream: false,
});
messages.push(result.choices[0].message);
return result;
}
async function getToolResponse(response: ChatResponse): Promise<Message> {
const toolCall = response.choices[0].message.toolCalls[0];
const toolName = toolCall.function.name;
const toolArgs = JSON.parse(toolCall.function.arguments);
// 在本地查找正确的工具,并用提供的参数调用它
// 可以添加其他工具而无需更改智能体循环
const toolResult = await TOOL_MAPPING[toolName](toolArgs);
return {
role: 'tool',
toolCallId: toolCall.id,
content: toolResult,
};
}
const maxIterations = 10;
let iterationCount = 0;
while (iterationCount < maxIterations) {
iterationCount++;
const response = await callLLM(messages);
if (response.choices[0].message.toolCalls) {
messages.push(await getToolResponse(response));
} else {
break;
}
}
if (iterationCount >= maxIterations) {
console.warn("Warning: Maximum iterations reached");
}
console.log(messages[messages.length - 1].content);
最佳实践与高级模式
函数定义指南
为 LLM 定义工具时,请遵循这些最佳实践:
清晰且具描述性的名称:使用能清楚表明工具用途的描述性函数名。
// 好:清晰且具体
{ "name": "get_weather_forecast" }
// 避免:过于含糊
{ "name": "weather" }
全面的描述:提供详细描述,帮助模型理解何时以及如何使用该工具。
{
"description": "获取特定地点的当前天气状况和 5 日预报。支持城市、邮政编码和坐标。",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名、邮政编码或坐标 (lat,lng)。示例:'New York'、'10001'、'40.7128,-74.0060'"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位偏好",
"default": "celsius"
}
},
"required": ["location"]
}
}
对流式响应使用工具调用时,请适当处理不同的内容类型:
const stream = await fetch('/api/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'anthropic/claude-sonnet-4.5',
messages: messages,
tools: tools,
stream: true
})
});
const reader = stream.body.getReader();
let toolCalls = [];
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
const chunk = new TextDecoder().decode(value);
const lines = chunk.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
if (data.choices[0].delta.tool_calls) {
toolCalls.push(...data.choices[0].delta.tool_calls);
}
if (data.choices[0].delta.finish_reason === 'tool_calls') {
await handleToolCalls(toolCalls);
} else if (data.choices[0].delta.finish_reason === 'stop') {
// 没有工具调用的常规完成
break;
}
}
}
}
使用 tool_choice 参数控制工具使用:
// 由模型决定(默认)
{ "tool_choice": "auto" }
// 禁用工具使用
{ "tool_choice": "none" }
// 强制使用特定工具
{
"tool_choice": {
"type": "function",
"function": {"name": "search_database"}
}
}
使用 parallel_tool_calls 参数控制是否可以同时调用多个工具(对大多数模型默认为 true):
// 禁用并行工具调用——工具将按顺序调用
{ "parallel_tool_calls": false }
当 parallel_tool_calls 为 false 时,模型每次只会请求一次工具调用,而不是可能并行请求多次调用。
设计能够良好配合的工具:
{
"tools": [
{
"type": "function",
"function": {
"name": "search_products",
"description": "在目录中搜索产品"
}
},
{
"type": "function",
"function": {
"name": "get_product_details",
"description": "获取特定产品的详细信息"
}
},
{
"type": "function",
"function": {
"name": "check_inventory",
"description": "检查产品的当前库存水平"
}
}
]
}
这允许模型自然地串联操作:搜索 → 获取详情 → 检查库存。
可靠性跟踪
OpenRouter 会跟踪每个模型服务提供商完成工具调用的可靠程度,并在每个模型页面的性能选项卡上以 工具调用错误率(Tool Call Error Rate) 展示。同一信号也会驱动工具调用请求上的 Auto Exacto 模型服务提供商排序。关于精确校验器、JSON Schema 草案、正则语义以及按工具调用的分类,参见 工具调用成功率如何衡量。
关于 OpenRouter 的消息格式和工具参数的更多细节,参见 API 参考。