从"只会聊天"到"能干实事",一文搞懂AI工具调用机制
什么是Function Calling?为什么它如此重要?
一个让所有开发者都头疼的场景
想象一下,我们正在开发一个AI助手,此时用户提问:
text
用户:"帮我查一下北京今天的天气怎么样?"
如果没有 Function Calling,AI只能这样回答:
AI:"抱歉,目前我无法直接获取实时天气信息。建议您打开天气应用或访问天气网站。"
或者更糟糕的情况:
bash
用户:"帮我把当前目录的文件列表列出来"
AI:"我无法直接访问您本地的文件系统,您可以使用以下命令:ls -la (在Linux/Mac)或 dir (在Windows)"
看到了吧,这种情况下,AI只能告诉我们 该怎么做 ,而不是 帮我们做。这就像我们问助手"帮我拿杯水",他回答"你应该走到饮水机旁,按下出水按钮"一样------理论上没错,但毫无用处。
Function Calling:让AI真正"动起来"
Function Calling的核心思想很简单:让AI输出结构化的函数调用请求,而不是普通文本。
sql
┌─────────────────────────────────────────────────────────────────┐
│ 没有Function Calling │
├─────────────────────────────────────────────────────────────────┤
│ 用户:北京天气怎么样? │
│ AI:抱歉,我无法查询实时天气...(死胡同) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 有Function Calling │
├─────────────────────────────────────────────────────────────────┤
│ 用户:北京天气怎么样? │
│ AI:返回 { "tool": "get_weather", "args": { "city": "北京" } } │
│ 开发者:执行get_weather("北京") → { temp: 22, condition: "晴" } │
│ AI:北京今天晴,温度22℃,适合户外活动! │
└─────────────────────────────────────────────────────────────────┘
一句话总结
Function Calling让AI从"只会说话"进化到"能干实事"。它可以:
- ✅ 查询实时数据(天气、股票、新闻)
- ✅ 操作本地系统(读写文件、执行命令)
- ✅ 调用第三方API(发送邮件、创建日历)
- ✅ 执行复杂计算(数学运算、数据分析)
Function Calling的核心机制
请求格式:告诉AI,我们有哪些工具
调用 API 时,除了常规的messages,还可以传入 tools 参数,描述可用的工具:
json
{
"model": "deepseek-chat",
"messages": [
{
"role": "user",
"content": "北京今天天气怎么样?"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息,返回温度、天气状况、湿度",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如:北京、上海、深圳"
}
},
"required": ["city"]
}
}
}
],
"tool_choice": "auto"
}
响应格式:AI决定调用哪个工具
当 AI 认为需要调用工具时,返回的就不在是普通文本,而是 tool_calls:
json
{
"choices": [
{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"北京\"}"
}
}
]
}
}
]
}
关键字段解析:
| 字段 | 说明 |
|---|---|
id |
工具调用的唯一标识,用于追踪和调试 |
function.name |
要调用的函数名称 |
function.arguments |
JSON字符串格式的参数 |
完整的5步调用流程
步骤1:发送用户消息 + 工具定义
javascript
messages: [{ role: "user", content: "北京天气" }]
tools: [{ function: { name: "get_weather", ... } }]
步骤2:AI返回tool_calls
javascript
tool_calls: [
{
function: { name: "get_weather",
arguments: "{\"city\":\"北京\"}"
}
]
步骤3:执行对应函数
javascript
const result = await getWeather("北京")
步骤4:将结果作为tool角色消息返回给AI
javascript
messages.push({
role: "tool",
tool_call_id: "call_abc123",
content: JSON.stringify(result)
})
步骤5:AI基于工具结果生成最终答案
text
AI: "北京今天晴,温度22℃,湿度65%,适合户外活动!"
完整代码示例(5步流程)
typescript
// 步骤1:发送消息 + 工具定义
async function chatWithTools(userMessage: string) {
const messages = [{ role: 'user', content: userMessage }]
// 第一次调用
const response = await callDeepSeek(messages, tools)
const assistantMessage = response.choices[0].message
// 检查是否有tool_calls
if (assistantMessage.tool_calls) {
// 步骤2:有工具调用,执行工具
messages.push(assistantMessage) // 先把AI的消息加入历史
for (const toolCall of assistantMessage.tool_calls) {
// 步骤3:执行工具
const result = await executeToolCall(toolCall)
// 步骤4:将工具结果加入历史
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result)
})
}
// 步骤5:再次调用AI,带上工具结果
const finalResponse = await callDeepSeek(messages, tools)
return finalResponse.choices[0].message.content
}
// 没有工具调用,直接返回
return assistantMessage.content
}
Tool Schema设计指南
工具描述的质量,直接决定了AI调用工具的准确率。一个好的Schema能让AI准确理解工具的用途和参数。
命名规范
使用动词开头的蛇形命名,清晰表达功能:
| ✅ 好的命名 | ❌ 差的命名 | 说明 |
|---|---|---|
get_weather |
weather |
动词开头,明确是获取操作 |
search_files |
files |
明确是搜索行为 |
send_email |
email |
明确是发送操作 |
calculate_expression |
calc |
避免缩写 |
描述的艺术 - 至关重要
描述越详细,AI选择工具的准确率越高。描述应该包含:做什么、输入什么、输出什么、何时使用。
错误示范:
json
{
"name": "get_weather",
"description": "查询天气" // ❌ 太简单了!
}
正确示范:
json
{
"name": "get_weather",
"description": "获取指定城市的实时天气信息,返回温度、天气状况、湿度、风速。适用于用户询问任何城市的天气情况。如果用户没有指定城市,请先询问城市名称。"
}
参数定义技巧
| 技巧 | 说明 | 示例 |
|---|---|---|
| 详细描述 | 说明参数含义和格式 | "城市名称,如:北京、上海、深圳" |
| 使用enum | 限定可选值 | enum: ["user", "admin", "editor"] |
| 设置required | 明确必填参数 | required: ["city"] |
| 提供示例 | 部分模型支持examples | examples: ["北京", "上海"] |
复杂参数的Schema设计
json
{
"name": "create_event",
"description": "在日历中创建新事件",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "事件标题,简洁明了"
},
"start_time": {
"type": "string",
"description": "开始时间,ISO 8601格式,如:2026-01-01T14:00:00+08:00"
},
"end_time": {
"type": "string",
"description": "结束时间,ISO 8601格式"
},
"attendees": {
"type": "array",
"description": "参与者邮箱列表",
"items": {
"type": "string",
"format": "email"
}
},
"reminder_minutes": {
"type": "integer",
"description": "提前多少分钟提醒",
"minimum": 0,
"maximum": 1440,
"default": 15
}
},
"required": ["title", "start_time"]
}
}
好/坏描述对比表
| 方面 | ❌ 坏描述 | ✅ 好描述 |
|---|---|---|
| 工具名 | process |
send_email |
| 工具描述 | 处理邮件 | 发送邮件,收件人、主题、正文为必填,支持HTML格式 |
| 参数city | 城市 | 城市名称,如北京、上海,支持直辖市和省会城市 |
| 参数date | 日期 | 日期,格式YYYY-MM-DD,如2026-01-01 |
| 参数status | 状态 | 状态,可选值:pending/done/cancelled |
实战:从0到1实现工具调用
项目准备
首先,确保我们有DeepSeek API Key(或者相关大模型 API Key)。如果没有,可以去DeepSeek官网注册获取。
bash
# 创建项目目录
mkdir ai-function-calling
cd ai-function-calling
# 初始化项目
npm init -y
# 安装依赖
npm install typescript @types/node axios dotenv
npm install -D ts-node
# 创建TypeScript配置
npx tsc --init
创建 .env 文件:
text
DEEPSEEK_API_KEY=sk-your-api-key-here
DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions
完整代码实现
创建 index.ts:
typescript
import axios from 'axios'
import dotenv from 'dotenv'
import readline from 'readline'
dotenv.config()
// ==================== 类型定义 ====================
interface Message {
role: 'system' | 'user' | 'assistant' | 'tool'
content: string | null
tool_calls?: ToolCall[]
tool_call_id?: string
}
interface ToolCall {
id: string
type: 'function'
function: {
name: string
arguments: string
}
}
interface Tool {
type: 'function'
function: {
name: string
description: string
parameters: {
type: 'object'
properties: Record<string, any>
required?: string[]
}
}
}
// ==================== 工具定义 ====================
const tools: Tool[] = [
{
type: 'function',
function: {
name: 'get_weather',
description: '获取指定城市的天气信息,返回温度、天气状况、湿度。适用于用户询问任何城市的天气情况。',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: '城市名称,如:北京、上海、深圳、广州'
}
},
required: ['city']
}
}
},
{
type: 'function',
function: {
name: 'calculate',
description: '执行数学计算,支持加减乘除和幂运算。当用户需要计算数学表达式时使用。',
parameters: {
type: 'object',
properties: {
expression: {
type: 'string',
description: '数学表达式,如:2+2、10*5、2^10'
}
},
required: ['expression']
}
}
},
{
type: 'function',
function: {
name: 'get_current_time',
description: '获取当前日期和时间。当用户询问现在几点、今天几号、当前时间等信息时使用。',
parameters: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: '时区,如:Asia/Shanghai、America/New_York,默认为本地时区',
enum: ['Asia/Shanghai', 'America/New_York', 'Europe/London', 'Asia/Tokyo']
}
}
}
}
}
]
// ==================== 工具实现 ====================
/**
* 获取天气(模拟数据,实际可替换为真实API)
*/
async function getWeather(city: string): Promise<any> {
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 模拟天气数据
const weatherData: Record<string, any> = {
'北京': { temperature: 22, condition: '晴', humidity: 45, wind_speed: 12 },
'上海': { temperature: 26, condition: '多云', humidity: 70, wind_speed: 8 },
'深圳': { temperature: 28, condition: '晴', humidity: 65, wind_speed: 10 },
'广州': { temperature: 30, condition: '雷阵雨', humidity: 85, wind_speed: 15 }
}
const data = weatherData[city] || {
temperature: 20 + Math.floor(Math.random() * 10),
condition: ['晴', '多云', '阴', '小雨'][Math.floor(Math.random() * 4)],
humidity: 40 + Math.floor(Math.random() * 40),
wind_speed: 5 + Math.floor(Math.random() * 15)
}
return {
city,
...data,
update_time: new Date().toISOString()
}
}
/**
* 执行数学计算
*/
async function calculate(expression: string): Promise<number> {
// 安全起见,只允许数字和基本运算符
const sanitized = expression.replace(/[^0-9+\-*/^().]/g, '')
if (!sanitized) {
throw new Error('无效的表达式')
}
// 处理幂运算
const withPow = sanitized.replace(/\^/g, '**')
// 使用Function构造器执行计算(注意:生产环境需要更严格的安全检查)
const result = new Function(`return (${withPow})`)()
if (typeof result !== 'number' || isNaN(result)) {
throw new Error('计算失败')
}
return result
}
/**
* 获取当前时间
*/
async function getCurrentTime(timezone?: string): Promise<string> {
const date = new Date()
if (timezone === 'Asia/Shanghai') {
return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
} else if (timezone === 'America/New_York') {
return date.toLocaleString('zh-CN', { timeZone: 'America/New_York' })
} else if (timezone === 'Europe/London') {
return date.toLocaleString('zh-CN', { timeZone: 'Europe/London' })
} else if (timezone === 'Asia/Tokyo') {
return date.toLocaleString('zh-CN', { timeZone: 'Asia/Tokyo' })
}
return date.toLocaleString('zh-CN')
}
// ==================== 工具调度器 ====================
async function executeToolCall(toolCall: ToolCall): Promise<any> {
const { name, arguments: argsStr } = toolCall.function
const args = JSON.parse(argsStr)
console.log(`🔧 执行工具: ${name}`, args)
switch (name) {
case 'get_weather':
return await getWeather(args.city)
case 'calculate':
const result = await calculate(args.expression)
return { expression: args.expression, result }
case 'get_current_time':
const time = await getCurrentTime(args.timezone)
return { time, timezone: args.timezone || 'local' }
default:
throw new Error(`未知工具: ${name}`)
}
}
// ==================== DeepSeek API调用 ====================
const API_URL = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/v1/chat/completions'
const API_KEY = process.env.DEEPSEEK_API_KEY
async function callDeepSeek(messages: Message[]): Promise<any> {
const response = await axios.post(
API_URL,
{
model: 'deepseek-chat',
messages,
tools,
tool_choice: 'auto',
temperature: 0.7
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
}
}
)
return response.data
}
// ==================== 主对话循环 ====================
async function chat(userInput: string): Promise<string> {
const messages: Message[] = [
{ role: 'user', content: userInput }
]
// 第一次调用
console.log(`\n📝 用户: ${userInput}`)
const response = await callDeepSeek(messages)
const assistantMessage = response.choices[0].message
// 如果没有tool_calls,直接返回
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
console.log(`🤖 AI: ${assistantMessage.content}`)
return assistantMessage.content
}
// 有工具调用,处理
messages.push({
role: 'assistant',
content: null,
tool_calls: assistantMessage.tool_calls
})
// 执行所有工具调用
for (const toolCall of assistantMessage.tool_calls) {
try {
const result = await executeToolCall(toolCall)
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result)
})
} catch (error) {
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify({ error: (error as Error).message })
})
}
}
// 第二次调用,带上工具结果
console.log(`🔄 携带工具结果,再次调用...`)
const finalResponse = await callDeepSeek(messages)
const finalContent = finalResponse.choices[0].message.content
console.log(`🤖 AI: ${finalContent}`)
return finalContent
}
// ==================== 交互式运行 ====================
async function runInteractive() {
// 创建 readline 接口
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
// 封装 ask 函数
const ask = (question: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, resolve)
})
}
console.log('='.repeat(50))
console.log('🤖 AI助手已启动(支持工具调用)')
console.log('可用工具: 天气查询、数学计算、获取时间')
console.log('输入 "exit" 退出')
console.log('='.repeat(50))
while (true) {
const input = await ask('\n> ')
if (input === 'exit') break
if (!input.trim()) continue
try {
await chat(input)
} catch (error) {
console.error('❌ 错误:', error)
}
console.log('-'.repeat(50))
}
rl.close()
console.log('👋 再见!')
}
// ==================== 启动应用 ====================
// 检查环境变量
if (!API_KEY) {
console.error('❌ 错误: 请在 .env 文件中设置 DEEPSEEK_API_KEY')
process.exit(1)
}
runInteractive().catch(console.error)
运行效果演示
diff
==================================================
🤖 AI助手已启动(支持工具调用)
可用工具: 天气查询、数学计算、获取时间
输入 "exit" 退出
==================================================
> 北京今天天气怎么样?
📝 用户: 北京今天天气怎么样?
🔧 执行工具: get_weather { city: '北京' }
🔄 携带工具结果,再次调用...
🤖 AI: 根据最新的天气信息,北京今天的天气情况如下:
- **温度**:22°C
- **天气状况**:晴
- **湿度**:45%
- **风速**:12 km/h
今天北京天气晴朗,温度适中,是个不错的天气!
--------------------------------------------------
> 帮我算一下 123 * 456
📝 用户: 帮我算一下 123 * 456
🔧 执行工具: calculate { expression: '123*456' }
🔄 携带工具结果,再次调用...
🤖 AI: 123 × 456 = 56,088
--------------------------------------------------
> 现在几点?
📝 用户: 现在几点?
🔧 执行工具: get_current_time {}
🔄 携带工具结果,再次调用...
🤖 AI: 现在是2026年3月23日 12:26:24。
--------------------------------------------------
多工具协同实战
场景:AI需要连续调用多个工具
用户问:"北京天气怎么样?如果温度低于20度,提醒我多穿衣服"
这个场景需要AI:
- 先调用
get_weather获取北京天气 - 分析温度,判断是否需要调用
compare_temperature - 生成最终建议
实现:串行调用链
typescript
// 添加温度比较工具
const additionalTools: Tool[] = [
{
type: 'function',
function: {
name: 'compare_temperature',
description: '比较温度与阈值,返回是否需要提醒。用于根据天气温度生成生活建议。',
parameters: {
type: 'object',
properties: {
temperature: {
type: 'number',
description: '当前温度(摄氏度)'
},
threshold: {
type: 'number',
description: '比较阈值,默认20度',
default: 20
}
},
required: ['temperature']
}
}
}
]
// 实现比较函数
async function compareTemperature(temperature: number, threshold: number = 20): Promise<any> {
return {
temperature,
threshold,
need_reminder: temperature < threshold,
suggestion: temperature < threshold
? `温度${temperature}℃低于${threshold}℃,建议多穿衣服`
: `温度${temperature}℃适宜,无需特殊提醒`
}
}
// 在调度器中添加case
async function executeToolCall(toolCall: ToolCall): Promise<any> {
const { name, arguments: argsStr } = toolCall.function
const args = JSON.parse(argsStr)
switch (name) {
case 'get_weather':
return await getWeather(args.city)
case 'calculate':
return await calculate(args.expression)
case 'get_current_time':
return await getCurrentTime(args.timezone)
case 'compare_temperature':
return await compareTemperature(args.temperature, args.threshold)
default:
throw new Error(`未知工具: ${name}`)
}
}
多工具并行调用
当用户问"北京、上海、深圳的天气",AI可能会一次性返回多个 tool_calls:
json
{
"tool_calls": [
{ "function": { "name": "get_weather", "arguments": "{\"city\":\"北京\"}" } },
{ "function": { "name": "get_weather", "arguments": "{\"city\":\"上海\"}" } },
{ "function": { "name": "get_weather", "arguments": "{\"city\":\"深圳\"}" } }
]
}
并行执行:
typescript
// 并行执行所有工具调用
const results = await Promise.all(
assistantMessage.tool_calls.map(async (toolCall) => {
const result = await executeToolCall(toolCall)
return { toolCall, result }
})
)
// 将结果添加到messages
for (const { toolCall, result } of results) {
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result)
})
}
调试与优化
常见问题及解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| AI不调用工具 | 工具描述不够清晰 | 优化description,增加使用场景说明 |
| 参数传递错误 | 参数描述不准确 | 使用enum限制,提供examples |
| 工具结果未被使用 | 返回格式不符合预期 | 确保返回的是JSON可序列化对象 |
| 无限循环调用 | 工具结果没有正确传递给AI | 检查tool_call_id是否正确匹配 |
| Token超限 | 工具描述占用过多Token | 精简描述,按需传递工具 |
调试技巧
1. 打印完整的messages数组
typescript
function debugMessages(messages: Message[]) {
console.log('='.repeat(50))
messages.forEach((msg, i) => {
console.log(`[${i}] role: ${msg.role}`)
if (msg.content) console.log(` content: ${msg.content.slice(0, 100)}`)
if (msg.tool_calls) console.log(` tool_calls: ${msg.tool_calls.map(t => t.function.name)}`)
if (msg.tool_call_id) console.log(` tool_call_id: ${msg.tool_call_id}`)
})
console.log('='.repeat(50))
}
2. 强制调用工具(测试用)
typescript
// 设置 tool_choice 为 required 强制调用工具
const response = await axios.post(API_URL, {
model: 'deepseek-chat',
messages,
tools,
tool_choice: 'required' // 强制返回tool_calls
})
3. 记录每次调用的Token消耗
typescript
console.log(`📊 Token使用: prompt=${usage.prompt_tokens}, completion=${usage.completion_tokens}, total=${usage.total_tokens}`)
Token消耗优化
工具描述会消耗Token,以下是优化策略:
| 策略 | 说明 | 示例 |
|---|---|---|
| 按需传递工具 | 根据对话场景动态选择工具 | 天气场景只传天气工具 |
| 精简描述 | 去掉冗余描述 | "获取天气" → "获取城市天气" |
| 复用工具 | 设计通用工具减少数量 | 一个execute_command替代多个小工具 |
| 缓存工具定义 | 避免重复传递相同的工具 | 在对话循环中复用tools |
进阶:从Function Calling到Agent
Function Calling是Agent的基础
从工具调用到完整Agent的层次等级:
Level 1: 单次工具调用
用户问天气 → AI返回tool_calls → 执行 → 返回结果
Level 2: 多轮工具调用
用户问复杂问题 → AI调用工具A → 分析结果 → 调用工具B → 最终答案
Level 3: Agent(ReAct模式)
循环: 思考(Thought) → 行动(Action) → 观察(Observation) → ... → 直到任务完成
Level 4: 高级Agent
- 记忆(短期/长期)
- 规划(任务分解)
- 自我反思
Agent的核心循环
typescript
async function agentLoop(userGoal: string, maxSteps: number = 5) {
let messages: Message[] = [{ role: 'user', content: userGoal }]
let step = 0
while (step < maxSteps) {
step++
console.log(`\n🔄 Step ${step}`)
const response = await callDeepSeek(messages)
const assistantMessage = response.choices[0].message
// 没有工具调用,任务完成
if (!assistantMessage.tool_calls) {
return assistantMessage.content
}
// 执行工具
messages.push(assistantMessage)
for (const toolCall of assistantMessage.tool_calls) {
const result = await executeToolCall(toolCall)
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result)
})
}
}
return "达到最大步数限制"
}
总结与实践建议
核心要点回顾
| 要点 | 说明 |
|---|---|
| 机制 | AI输出tool_calls,开发者执行,结果返回,AI生成答案 |
| Schema设计 | 命名规范、描述详细、参数精确是准确率的关键 |
| 流程管理 | 需要正确维护messages数组,处理多轮调用 |
| 调试技巧 | 打印messages、强制调用、记录Token消耗 |
实践建议
- 从小工具开始:先实现1-2个简单工具,理解完整流程
- 写好描述:工具描述直接决定AI的调用准确率,值得花时间打磨
- 记录日志:保存每次调用的messages,便于调试和优化
- 处理异常:工具可能失败,需要优雅的错误处理
- 考虑安全:涉及文件操作、命令执行等敏感工具时,需要用户确认
结语
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!