Skip to content
在 Node.js 中实现第一个 Tool-Calling Agent
本篇通过一个完整的可运行示例,演示如何在 Node.js 中使用 OpenAI SDK 完成一次工具调用闭环:定义计算工具,让模型返回工具调用指令,在本地执行函数,再将执行结果回传模型,最终获得自然语言答案。读完这篇内容后可以拿到一段可以直接运行的最小实现,直观理解 Function Calling 在真实代码中的流转方式。阅读前需要掌握 async/await 编写 Node.js 异步代码,并对 Agent 中模型、工具、消息等概念有基本认识。
准备 OpenAI 客户端
安装 SDK 与配置环境变量
在项目根目录安装 OpenAI 官方 SDK:
bash
npm install openaiSDK 需要 API Key 才能访问模型。实际使用中,密钥应当通过环境变量注入,避免硬编码到源码里。在 macOS 或 Linux 终端执行:
bash
export OPENAI_API_KEY="your_api_key_here"Windows PowerShell 中对应的命令是:
powershell
setx OPENAI_API_KEY "your_api_key_here"设置完成后,Node.js 进程内可以直接通过 process.env.OPENAI_API_KEY 读取。
构造客户端实例
SDK 导出 OpenAI 类,实例化时如果已经配置了 OPENAI_API_KEY 环境变量,构造函数会自动读取,无需显式传入 apiKey 参数:
typescript
import OpenAI from "openai";
const client = new OpenAI();client 可以调用多种接口,包括 Responses API 和 Chat Completions API。本教程全程使用 client.chat.completions.create,也就是 Chat Completions API。这套接口广泛兼容各类模型,参数结构与工具调用流程契合度高,学习 Agent 实现时更容易把注意力集中在工具调用的消息序列上。
定义工具:加法函数与 JSON Schema
Function Calling 流程中,“工具”指的是模型无法直接执行的本地函数。为了让模型知道函数的存在以及如何生成调用指令,需要向请求中传入一份 JSON Schema 描述。每个工具由一个 type 字段和 function 字段构成,后者包含名称、描述以及符合 JSON Schema 的参数定义。
首先实现加法函数本身:
typescript
function addNumbers(a: number, b: number): number {
return a + b;
}接着给出对应的工具描述。参数 a 和 b 都声明为 number 类型,并且列入 required 数组:
typescript
const tools = [
{
type: "function" as const,
function: {
name: "addNumbers",
description: "计算两个数的和",
parameters: {
type: "object" as const,
properties: {
a: { type: "number", description: "第一个加数" },
b: { type: "number", description: "第二个加数" },
},
required: ["a", "b"],
},
},
},
];把这个 tools 数组随请求发给模型,模型就能在需要时在响应中“建议”调用 addNumbers,并填充具体的参数值。
第一次模型调用:让模型返回 tool_calls
构造消息并传入 tools
消息通过 messages 数组传递,每条消息都必须带 role 字段。这里先用一条 user 角色消息描述计算需求,同时把上面定义的 tools 一并传入:
typescript
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
const messages: ChatCompletionMessageParam[] = [
{ role: "user", content: "计算 3 和 5 的和" },
];
const response = await client.chat.completions.create({
model: "gpt-4o-mini",
messages,
tools,
});选择 gpt-4o-mini 是因为它支持 Function Calling 且响应快、成本低。如果换成其它模型,需要确认该模型同样支持工具调用。
理解返回的 assistant 消息
当模型判断需要调用工具时,response.choices[0].message 的 role 会是 "assistant" 并且 tool_calls 字段携带调用信息。每次调用都包含一个唯一 id、type(值为 "function")以及 function 对象,其中有函数名和 JSON 字符串形式的参数。
typescript
const assistantMessage = response.choices[0].message;
console.log(assistantMessage.role); // "assistant"
console.log(assistantMessage.tool_calls); // 工具调用数组如果模型认为问题不需要调用任何工具,tool_calls 会是 undefined,此时 content 字段直接包含文本回复。在实现中需要先检查 tool_calls 是否存在,再决定是继续执行工具分支,还是直接使用 content。
解析 tool_calls 并执行工具
拿到 tool_calls 数组后,对每一次调用,取出函数名、解析参数,然后根据名称路由到对应的本地函数。arguments 是 JSON 字符串,需要使用 JSON.parse 转换。
typescript
if (assistantMessage.tool_calls) {
for (const toolCall of assistantMessage.tool_calls) {
const fnName = toolCall.function.name;
if (fnName === "addNumbers") {
const args = JSON.parse(toolCall.function.arguments);
const result = addNumbers(args.a, args.b);
console.log(`执行 addNumbers(${args.a}, ${args.b}) = ${result}`);
}
}
}得到的 result 必须和对应的 tool_call_id 绑定,因为回传结果时,模型需要按 ID 把工具输出匹配到之前的调用。此时可以将结果暂时存在一个结构中,紧接着追加到消息历史。
回传工具结果并发起第二次调用
构造 tool 消息并追加到对话
对话历史需要完整保留每一次交互。目前 messages 中只有一条 user 消息,接下来要依次追加两条消息:一是模型上一次返回的 assistant 消息(携带 tool_calls),二是每条工具调用对应的执行结果,角色均为 "tool"。
typescript
// 将 assistant 消息加入对话
messages.push({
role: "assistant",
content: null,
tool_calls: assistantMessage.tool_calls,
});
// 为每一次 tool call 构造一条 tool 消息
const toolResults = [
{ tool_call_id: assistantMessage.tool_calls[0].id, result: 8 },
];
for (const tr of toolResults) {
messages.push({
role: "tool",
tool_call_id: tr.tool_call_id,
content: String(tr.result),
});
}现在 messages 包含了完整上下文:用户提问 → 模型要求调用工具 → 工具执行结果。再次调用 chat.completions.create,模型就会基于工具结果生成最终的自然语言回答。
typescript
const secondResponse = await client.chat.completions.create({
model: "gpt-4o-mini",
messages,
});
const finalText = secondResponse.choices[0].message.content;
console.log("模型最终回复:", finalText);这次响应里,message.role 仍为 "assistant",但 content 一般不为空,通常是类似“3 加 5 等于 8”这样的句子。
完整代码串联与运行说明
把以上步骤合并到一个异步函数中,就是一段可以直接运行的代码。这里对类型做了细化,messages 使用 SDK 导出的 ChatCompletionMessageParam 类型,避免 any 带来的歧义。
typescript
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";
async function main() {
const client = new OpenAI();
// 1. 定义工具
const tools = [
{
type: "function" as const,
function: {
name: "addNumbers",
description: "计算两个数的和",
parameters: {
type: "object" as const,
properties: {
a: { type: "number", description: "第一个加数" },
b: { type: "number", description: "第二个加数" },
},
required: ["a", "b"],
},
},
},
];
// 2. 第一次请求
const messages: ChatCompletionMessageParam[] = [
{ role: "user", content: "计算 3 和 5 的和" },
];
const response1 = await client.chat.completions.create({
model: "gpt-4o-mini",
messages,
tools,
});
const assistantMessage = response1.choices[0].message;
// 3. 如果模型要求调用工具,执行本地函数并回传结果
if (assistantMessage.tool_calls) {
const toolCall = assistantMessage.tool_calls[0];
const fnArgs = JSON.parse(toolCall.function.arguments);
const result = addNumbers(fnArgs.a, fnArgs.b);
messages.push({
role: "assistant",
content: null,
tool_calls: [toolCall],
});
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: String(result),
});
// 4. 第二次请求,获取最终答案
const response2 = await client.chat.completions.create({
model: "gpt-4o-mini",
messages,
});
console.log(response2.choices[0].message.content);
}
}
function addNumbers(a: number, b: number): number {
return a + b;
}
main();运行前,确保 OPENAI_API_KEY 环境变量已正确设置,且 Node.js 支持 ES Modules(可将文件保存为 .mjs 或在 package.json 中配置 "type": "module")。执行命令:
bash
node tool-calling-demo.mjs终端会输出类似:
3 加 5 等于 8。消息序列与关键字段
在这个工具调用闭环中,messages 数组按序演变,每个步骤承担不同角色:
| 步骤 | role | 说明 |
|---|---|---|
| 用户提问 | "user" | "计算 3 和 5 的和" |
| 模型返回工具调用 | "assistant" | content 为 null,tool_calls 包含 addNumbers 调用 |
| 工具执行结果 | "tool" | tool_call_id 对应上一步调用的 ID,content 为 "8" |
| 模型总结回复 | "assistant" | content 为自然语言答案 |
这些步骤的先后顺序不可改变。一旦缺少 tool 消息,模型无法获知工具执行的结果,就不会生成基于结果的回答,甚至可能直接忽略工具调用分支。
另外,tool_calls 是一个数组。虽然例子中只有一次调用,但实际响应可能包含多条调用,应该为每一条都生成对应的 tool 消息,确保一一匹配。
注意点
- 工具调用的判断:不要假设模型一定返回
tool_calls,需要先检查是否为undefined。若为undefined,说明模型认为当前问题无需工具,content中会直接给出回答。 - JSON 解析的健壮性:模型返回的
arguments字符串在极少数情况下可能不是合法 JSON。最好使用try/catch包裹JSON.parse,并规划降级处理方式,例如让模型重新生成或终止流程并提示错误。 - 密钥管理:API Key 不要硬编码,始终通过环境变量或密钥管理服务注入。
- 模型选择:必须使用支持 Function Calling 的模型。Chat Completions 接口中,
gpt-4o-mini、gpt-4o等系列都支持,其他模型需查阅文档确认。 - 工具数量限制:单次请求中
tools数组大小通常不超过 128 个,实际上限因模型而异。 - 多轮对话的上下文:每次请求都需要携带完整的
messages数组,否则模型会丢失之前的对话历史。在更复杂的 Agent 循环中,需要留意 token 消耗和上下文窗口大小。
