【AI基础专题】工具调用

上一节我们通过不同的系统提示词,感受了提示词对模型行为的巨大影响,但到目前为止,模型还只停留在动嘴的阶段。这节我们来给模型则增加工具调用的能力,让模型能够动嘴又动手。

开始正题之前,我们新增一个新的agent人设,它的设定是你是一个有工具调用能力的助手。当需要查询信息或执行计算时,请使用提供的工具。如果不需要工具,直接回答即可。回答简洁。,本节内容将基于这个人设来演示工具调用的能力。

我们先来实现一个查询天气的工具,文件路径src/tools/weather.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export const weatherTool = {
name: 'get_weather',
description: '获取指定城市的当前天气信息',
parameters: {
type: 'object',
properties: {
city: { type: 'string', description: '城市名称,如"北京"、"上海"' },
},
required: ['city'],
},
execute: async (args) => {
// 模拟异步 API 调用
const weathers = ['晴', '多云', '小雨', '阴天'];
const picked = weathers[Math.floor(Math.random() * weathers.length)];
return `城市:${args.city},天气:${picked},温度:${Math.floor(Math.random() * 15 + 15)}°C`;
},
};

再注册一个计算器工具,文件路径src/tools/calculator.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export const calculatorTool: Tool = {
name: 'calculate',
description: '执行数学计算,支持加减乘除和括号',
parameters: {
type: 'object',
properties: {
expression: { type: 'string', description: '数学表达式,如"2+3*4"' },
},
required: ['expression'],
},
execute: async (args) => {
try {
// 安全警告:生产环境绝不可以用 eval
const result = eval(args.expression);
return `${args.expression} = ${result}`;
} catch (e: any) {
return `计算错误: ${e.message}`;
}
},
};

让后我们再定义一个工具管理工具,文件路径在src/tools/registry.ts

这个管理工具负责注册和查找所有可用的工具,并提供给模型调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { weatherTool } from './weather.js';
import { calculatorTool } from './calculator.js';
import { Tool } from '../types/index.js';

const tools: Tool[] = [weatherTool, calculatorTool];

export function getTools(): Tool[] {
return tools;
}

export function getOpenAITools() {
return tools.map((t) => ({
type: 'function' as const,
function: {
name: t.name,
description: t.description,
parameters: t.parameters,
},
}));
}

export function findTool(name: string): Tool | undefined {
return tools.find((t) => t.name === name);
}

接下来,我们需要在llm.ts中接收工具并附加到请求里:

1
2
3
4
5
6
7
8
9
10
11
12
export async function chat(
messages: { role: string; content: string }[],
tools?: any[] // OpenAI 格式的工具定义数组
) {
const response = await client.chat.completions.create({
model: 'deepseek-v4-pro',
messages: messages as any,
temperature: 0.7,
...(tools && { tools }),
});
return response.choices[0]!.message!;
}

然后在主入口中增加工具使用的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 省略引入部分和提示词部分

// 主对话循环
while (true) {
const userInput = await rl.question('我: ');
if (userInput.toLowerCase() === 'exit') break;

context.add({ role: 'user', content: userInput });

// 第一轮:带工具定义调用
const tools = getOpenAITools();
let reply = await chat(context.getMessages(), tools);

// 如果模型要求调用工具,执行并在上下文中构造 tool 消息
if (reply.tool_calls && reply.tool_calls.length > 0) {
// 先把 assistant 消息(含 tool_calls)加入上下文
context.add({ role: 'assistant', content: '', tool_calls: reply.tool_calls } as any);

// 处理需要同时调用多个工具的情况
for (const tc of reply.tool_calls) {
const tool = findTool(tc.function.name);
const args = JSON.parse(tc.function.arguments);
const result = await tool!.execute(args);
console.log(`[tools] ${tc.function.name}(${tc.function.arguments}) -> ${result}`);

// tool 消息:必须回传 tool_call_id
context.add({
role: 'tool',
tool_call_id: tc.id,
content: result,
} as any);
}

// 继续调用,看模型还需要不需要更多工具
reply = await chat(context.getMessages(), tools);
}

console.log('助手:', reply.content);
context.add({ role: 'assistant', content: reply.content! });
}
// 省略关闭 readline 部分

现在agent会处理模型的工具调用请求。每当模型请求调用工具时,我们就执行对应的工具函数,并把结果以 tool 消息的形式加入上下文,然后继续调用模型,模型会基于工具调用结果给出新的回答。

最后我们来看看实际效果:
/images/ai-based/05-01.png

发散点:

  • 模型返回的工具参数合法性检查
  • 工具执行的权限控制和安全性考虑
  • 工具之间的依赖性管理
  • 需要多轮次调用工具的情况