手把手拆解 AI 工具调用的每一个字节,彻底搞懂底层原理!
为什么需要理解 Function Calling 的底层?
一个真实的困境
我们正在开发一个 AI 助手,已经用 LangChain 写好了 Agent,此时看起来一切正常:
javascript
const agent = new Agent()
const result = await agent.invoke("帮我查北京天气") // 输出:北京今天晴,22℃
但是突然有一天,AI 开始胡说八道了:
- 用户问天气,它调用了
send_email。 - 用户要计算,它返回了天气数据。
- 明明工具定义正确,就是不调用。
我们打开调试日志,看到一堆 JSON 数据,但完全不知道问题出在哪!
Function Calling 的本质
用一句话总结 Function Calling 的本质:
Function Calling 是让 AI 输出结构化 JSON(函数调用请求),而非自然语言文本。
我们来看一组简单的对比:
普通对话输出
text
我无法查询实时天气,建议您打开天气应用!
Function Calling 输出
text
{
"tool_calls": [{
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"北京\"}"
}
}]
}
Function Calling 让 AI 的角色从"回答者"变成了"决策者":它决定什么时候调用什么工具,而不是直接回答。
解剖请求:tools 和 tool_choice
一个完整的tools请求示例
javascript
{
"model": "deepseek-chat",
"messages": [
{
"role": "user",
"content": "北京今天天气怎么样?"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息,返回温度、天气状况、湿度",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如:北京、上海、深圳"
},
"unit": {
"type": "string",
"description": "温度单位",
"enum": ["celsius", "fahrenheit"],
"default": "celsius"
}
},
"required": ["city"]
}
}
}
],
"tool_choice": "auto"
}
请求参数 tools 详解
tools 数组结构
| 字段路径 | 示例值 | 说明 |
|---|---|---|
| type | "function" | 目前只有这一种类型,未来可能会支持"plugin"等其他类型 |
| function.name | "get_weather" | 函数名,建议动词开头、蛇形命名 |
| function.description | "获取指定城市的天气..." | 最关键字段,描述越详细,AI调用越准确 |
| function.parameters | {...} | JSON Schema格式的参数定义 |
function.name 命名规范
| ✅ 好命名 | ❌ 差命名 | 说明 |
|---|---|---|
| get_weather | weather | 动词开头,明确是获取操作 |
| search_database | db | 避免缩写 |
| send_notification | notify | 完整表达意图 |
| calculate_expression | calc | 清晰表达功能 |
function.description:最关键字段
function.description 用于对工具进行描述,描述的质量直接决定AI调用工具的准确率,因此我认为它是整个 tools 参数中最关键的字段。
description 三要素
- 工具能做什么(功能)
- 工具需要输入什么(参数)
- 工具什么时候被调用(触发条件)
差描述
javascript
"description": "查询天气"
好描述
javascript
"description": "获取指定城市的实时天气信息,返回温度、天气状况、湿度、风速。适用于用户询问任何城市的天气情况。如果用户没有指定城市,请先询问城市名称。"
function.parameters:JSON Schema 详解
JSON Schema 是参数定义的工业标准,下面是完整语法:
json
{
"type": "object", // 固定为object
"properties": { // 参数列表
"city": {
"type": "string", // 基础类型: string, number, boolean, array, object
"description": "城市名称", // 参数描述
"enum": ["北京", "上海", "深圳"], // 可选值限制
"default": "北京", // 默认值
"examples": ["北京", "上海"] // 示例值(部分模型支持)
},
"temperature": {
"type": "number",
"minimum": -50, // 数值最小值
"maximum": 50 // 数值最大值
},
"tags": {
"type": "array",
"items": {
"type": "string" // 数组元素类型
},
"minItems": 1, // 最少元素数
"maxItems": 10 // 最多元素数
},
"settings": {
"type": "object", // 嵌套对象
"properties": {
"theme": { "type": "string" }
}
}
},
"required": ["city"] // 必填参数列表
}
tool_choice 的三种模式
auto - AI自主决策(默认)
javascript
"tool_choice": "auto"
该模式下,AI 会根据用户输入自动判断是否需要调用工具,这是最常用的模式。
效果演示
text
用户:今天天气怎么样? → AI调用天气工具
用户:帮我算一下数学 → AI调用计算工具
用户:你好啊 → AI不调用工具,直接聊天
none - 强制不调用工具
javascript
"tool_choice": "none"
该模式下,无论用户问什么,AI 都不会调用工具,只输出文本。
适用场景
- 纯闲聊模式
- 测试工具定义是否会影响对话
- 节省Token(不输出tool_calls)
强制指定工具
javascript
"tool_choice": {
"type": "function",
"function": {
"name": "get_weather"
}
}
该模式下,强制 AI 必须调用指定的工具,即使用户的问题与此无关。
效果演示
text
用户:你好啊
AI:{ "tool_calls": [{ "function": { "name": "get_weather", ... } }] }
// AI会尝试从"你好啊"中提取城市参数,可能返回空或默认值
适用场景
- 测试工具调用流程
- 某些必须调用工具的特定场景
- 多轮对话中,已知下一步必须调用某个工具
实战演示:三种模式的效果对比
javascript
// 测试代码
async function testToolChoice() {
const tools = [weatherTool]
const messages = [{ role: 'user', content: '你好啊' }]
// 测试1: auto
const res1 = await callAPI({ tool_choice: 'auto' })
// 结果: 没有tool_calls,正常回复"你好!有什么可以帮助你的吗?"
// 测试2: none
const res2 = await callAPI({ tool_choice: 'none' })
// 结果: 没有tool_calls,正常回复
// 测试3: 强制指定
const res3 = await callAPI({ tool_choice: { function: { name: 'get_weather' } } })
// 结果: 返回tool_calls,arguments可能为空对象
// {"tool_calls":[{"function":{"name":"get_weather","arguments":"{}"}}]}
}
解剖响应:tool_calls 字段深度解析
带tool_calls的完整响应
json
{
"id": "chatcmpl-abc123def456",
"object": "chat.completion",
"created": 1710000000,
"model": "deepseek-chat",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123def456",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"北京\",\"unit\":\"celsius\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 120,
"completion_tokens": 25,
"total_tokens": 145
}
}
tool_calls 字段逐字段解析
| 字段 | 示例值 | 含义 | 代码中如何使用 |
|---|---|---|---|
| id | "call_abc123def456" | 本次调用的唯一标识 | 执行完函数后,用此id关联结果 |
| type | "function" | 工具类型 | 未来可能扩展,目前固定为function |
| function.name | "get_weather" | 要调用的函数名 | switch(name) 路由到对应函数 |
| function.arguments | "{"city":"北京"}" | JSON字符串格式的参数 | JSON.parse(arguments) 获取参数对象 |
关键点
- arguments 是字符串,不是对象,必须先
JSON.parse() - AI 可能一次返回多个 tool_calls,需要遍历处理
- content 在返回 tool_calls 时通常为 null
finish_reason 各个值含义
| finish_reason取值 | 含义 | 后续处理 |
|---|---|---|
| "tool_calls" | 返回了工具调用 | 执行工具,再次调用API |
| "stop" | 正常结束 | 直接返回content |
| "length" | 达到token限制 | 需要增加max_tokens或截断 |
| "content_filter" | 内容被过滤 | 提示用户修改输入 |
| "null" | 未完成(流式) | 继续接收 |
完整数据流:从请求到结果
流程五步法
第1步:发送请求(用户消息 + 工具定义)
javascript
POST https://api.deepseek.com/v1/chat/completions
{
"messages": [{"role":"user","content":"北京天气怎么样?"}],
"tools": [{...天气工具定义...}]
}
第2步:AI返回 tool_calls
json
"tool_calls": [{
"id": "call_123",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"北京\"}"
}
}]
第3步:执行函数
javascript
const args = JSON.parse(toolCall.function.arguments)
// args = { city: "北京" }
const result = await getWeather(args.city)
// result = { city: "北京", temperature: 22, condition: "晴" }
第4步:将结果作为 tool 角色消息返回
javascript
messages.push({
role: "tool",
tool_call_id: "call_123",
content: JSON.stringify(result)
})
第5步:AI生成最终答案
再次调用 API,带上完整的对话历史:
javascript
messages = [
{ role: "user", content: "北京天气怎么样?" },
{ role: "assistant", content: null, tool_calls: [...] },
{ role: "tool", content: "{...天气数据...}" }
]
AI 输出: "北京今天晴,温度22℃,湿度45%,适合户外活动!"
代码实现(TypeScript)
typescript
import axios from 'axios'
import dotenv from 'dotenv'
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[]
}
}
}
interface APIResponse {
choices: Array<{
message: {
role: string
content: string | null
tool_calls?: ToolCall[]
}
finish_reason: string
}>
usage: {
prompt_tokens: number
completion_tokens: number
total_tokens: number
}
}
// ==================== 工具定义 ====================
const weatherTool: Tool = {
type: 'function',
function: {
name: 'get_weather',
description: '获取指定城市的天气信息,返回温度、天气状况、湿度',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: '城市名称,如:北京、上海、深圳'
}
},
required: ['city']
}
}
}
// ==================== 工具实现 ====================
async function getWeather(city: string): Promise<any> {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 100))
const weatherDB: Record<string, any> = {
'北京': { temperature: 22, condition: '晴', humidity: 45 },
'上海': { temperature: 26, condition: '多云', humidity: 70 },
'深圳': { temperature: 28, condition: '晴', humidity: 65 }
}
return weatherDB[city] || {
temperature: 20 + Math.floor(Math.random() * 10),
condition: ['晴', '多云', '阴'][Math.floor(Math.random() * 3)],
humidity: 40 + Math.floor(Math.random() * 40)
}
}
// ==================== 工具调度器 ====================
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)
default:
throw new Error(`未知工具: ${name}`)
}
}
// ==================== API调用封装 ====================
async function callDeepSeekAPI(
messages: Message[],
tools?: Tool[]
): Promise<APIResponse> {
const response = await axios.post(
process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/v1/chat/completions',
{
model: 'deepseek-chat',
messages,
tools: tools || undefined,
tool_choice: 'auto',
temperature: 0.7
},
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.DEEPSEEK_API_KEY}`
}
}
)
return response.data
}
// ==================== 5步法核心实现 ====================
async function chatWithTools(userMessage: string): Promise<string> {
// 步骤1:初始化消息列表
const messages: Message[] = [
{ role: 'user', content: userMessage }
]
console.log('\n📤 步骤1: 发送请求')
console.log(` 用户: ${userMessage}`)
// 步骤2:第一次调用API
let response = await callDeepSeekAPI(messages, [weatherTool])
let assistantMessage = response.choices[0]?.message as Message
// 如果没有tool_calls,直接返回
if (!assistantMessage.tool_calls) {
console.log('✅ 无需工具调用,直接返回')
return assistantMessage.content || ''
}
console.log(`🔧 步骤2: AI决定调用工具`)
console.log(` 工具: ${assistantMessage.tool_calls.map(t => t.function.name).join(', ')}`)
// 步骤3:将assistant消息加入历史
messages.push({
role: 'assistant',
content: assistantMessage.content,
tool_calls: assistantMessage.tool_calls
})
// 步骤4:执行所有工具调用
for (const toolCall of assistantMessage.tool_calls) {
console.log(`\n⚙️ 步骤3: 执行工具 ${toolCall.function.name}`)
try {
const result = await executeToolCall(toolCall)
console.log(` ✅ 执行成功:`, result)
// 步骤4:将工具结果加入历史
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result)
})
} catch (error) {
console.log(` ❌ 执行失败:`, error)
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify({ error: (error as Error).message })
})
}
}
// 步骤5:再次调用API,生成最终答案
console.log(`\n🤖 步骤5: 携带工具结果,生成最终答案`)
response = await callDeepSeekAPI(messages)
const finalAnswer = response.choices[0]?.message.content || ''
console.log(` 最终答案: ${finalAnswer}`)
console.log(` Token消耗: ${response.usage.total_tokens}`)
return finalAnswer
}
// ==================== 运行示例 ====================
async function main() {
console.log('='.repeat(60))
console.log('Function Calling 完整数据流演示')
console.log('='.repeat(60))
const result = await chatWithTools('北京今天天气怎么样?')
console.log(`\n🎉 最终结果: ${result}`)
}
// 运行
main().catch(console.error)
运行输出
text
============================================================
Function Calling 完整数据流演示
============================================================
📤 步骤1: 发送请求
用户: 北京今天天气怎么样?
🔧 步骤2: AI决定调用工具
工具: get_weather
⚙️ 步骤3: 执行工具 get_weather
🔧 执行工具: get_weather { city: '北京' }
✅ 执行成功: { temperature: 22, condition: '晴', humidity: 45 }
🤖 步骤5: 携带工具结果,生成最终答案
最终答案: 根据查询结果,北京今天的天气情况如下:
**天气:** 晴 ☀️
**温度:** 22°C
**湿度:** 45%
今天北京天气晴朗,温度舒适,是个不错的好天气!适合外出活动。
Token消耗: 144
🎉 最终结果: 根据查询结果,北京今天的天气情况如下:
**天气:** 晴 ☀️
**温度:** 22°C
**湿度:** 45%
今天北京天气晴朗,温度舒适,是个不错的好天气!适合外出活动。
多模型统一调用封装
typescript
interface ModelConfig {
name: 'openai' | 'deepseek' | 'zhipu' | 'qwen'
baseURL: string
apiKey: string
model: string
}
async function callWithToolsMultiModel(
config: ModelConfig,
messages: Message[],
tools: Tool[]
): Promise<any> {
// 根据模型选择不同的endpoint和请求格式
const endpoints: Record<string, string> = {
openai: '/v1/chat/completions',
deepseek: '/v1/chat/completions',
zhipu: '/v1/chat/completions',
qwen: '/v1/chat/completions'
}
const requestBody = {
model: config.model,
messages,
tools,
tool_choice: 'auto'
}
// Claude的格式略有不同
if (config.name === 'claude') {
// Claude使用不同的工具格式
// ...
}
const response = await fetch(`${config.baseURL}${endpoints[config.name]}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`
},
body: JSON.stringify(requestBody)
})
const data = await response.json()
// 统一响应格式
return {
tool_calls: data.choices[0]?.message?.tool_calls || [],
content: data.choices[0]?.message?.content,
finish_reason: data.choices[0]?.finish_reason,
usage: data.usage
}
}
调试技巧与最佳实践
调试工具箱
1. 打印完整消息历史(带颜色)
typescript
function debugMessages(messages: Message[]) {
console.log('\n' + '='.repeat(60))
console.log('📋 MESSAGES HISTORY')
console.log('='.repeat(60))
messages.forEach((msg, i) => {
const role = msg.role.padEnd(10)
console.log(`[${i}] ${role} |`,
msg.content ? msg.content.slice(0, 80) : '',
msg.tool_calls ? `🔧 [${msg.tool_calls.map(t => t.function.name).join(', ')}]` : '',
msg.tool_call_id ? `🆔 ${msg.tool_call_id}` : ''
)
})
}
2. 记录Token消耗
typescript
function logUsage(usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number }) {
console.log(`
┌─────────────────────────────────────────┐
│ 📊 TOKEN USAGE │
├─────────────────────────────────────────┤
│ Prompt tokens: ${usage.prompt_tokens.toString().padStart(8)} │
│ Completion tokens: ${usage.completion_tokens.toString().padStart(8)} │
│ Total tokens: ${usage.total_tokens.toString().padStart(8)} │
└─────────────────────────────────────────┘
`)
}
3. 工具调用追踪
typescript
function traceToolCall(toolCall: ToolCall, result: any, duration: number) {
console.log(`
┌─────────────────────────────────────────┐
│ 🔧 TOOL CALL TRACE │
├─────────────────────────────────────────┤
│ ID: ${toolCall.id} │
│ Name: ${toolCall.function.name} │
│ Args: ${toolCall.function.arguments} │
│ Result: ${JSON.stringify(result).slice(0, 50)}... │
│ Duration: ${duration}ms │
└─────────────────────────────────────────┘
`)
}
4. 保存完整对话用于回放
typescript
function saveConversation(messages: Message[], filename: string) {
const fs = require('fs')
fs.writeFileSync(
`${filename}.json`,
JSON.stringify(messages, null, 2)
)
console.log(`💾 对话已保存到 ${filename}.json`)
}
最佳实践清单
| 实践项 | 说明 | 代码示例 |
|---|---|---|
| 工具描述要详细 | 包含使用场景和返回信息 | "适用于用户询问任何城市的天气情况" |
| 参数使用enum | 对于有限选项,用enum约束 | enum: ["celsius", "fahrenheit"] |
| 工具结果结构化 | 返回JSON而非纯文本 | JSON.stringify({ temp: 22 }) |
| 处理tool_calls数组 | 支持AI一次调用多个工具 | Promise.all(toolCalls.map(execute)) |
| 设置最大迭代次数 | 防止无限循环 | if (iterations > 5) break |
| 错误处理 | 工具失败时返回友好信息 | { error: "API调用失败" } |
常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| AI不调用工具 | 工具描述不清晰 | 优化description,添加"适用于..." |
| 参数传递错误 | 参数描述不准确 | 细化properties描述,使用enum |
| 工具结果未被理解 | 返回格式不符合预期 | 返回结构化JSON,添加说明字段 |
| 无限循环调用 | 工具结果未正确传递给AI | 检查tool角色消息的格式 |
| 多个tool_calls顺序 | 串行/并行选择 | 并行执行提高效率 |
| Token超限 | 工具定义太长 | 精简描述,按需传递工具 |
结语
Function Calling = 结构化输入(工具定义)+ 结构化输出(tool_calls)+ 开发者执行 + 结果回传
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!