Function Calling解剖:从请求到响应的完整数据流

手把手拆解 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)+ 开发者执行 + 结果回传

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

相关推荐
宝桥南山2 小时前
GitHub Copilot - 可以使用Local, Copilot CLI, Cloud等不同方式来运行agent tasks
microsoft·微软·github·aigc·copilot·ai编程
喝咖啡的女孩2 小时前
多智能体任务可视化界面
前端
whisper2 小时前
#新手必看!Map.size 和 Object.keys().length 的区别,看完再也不混淆
前端
秋天的一阵风2 小时前
【LeetCode 刷题系列|第 3 篇】详解大数相加:从模拟竖式到简洁写法的优化之路🔢
前端·算法·面试
假面骑士阿猫2 小时前
TRAE配置OpenSpec实现SDD规范驱动开发
前端·人工智能·代码规范
哈哈哈哈哈哈哈哈8532 小时前
WSL + Tailscale 导致 apt update 卡 0% 的解决方案
前端
JYeontu2 小时前
程序员都是这样剪视频的?
前端
AI扑社2 小时前
AI+GEO 驱动的全新数字营销解决方案
大数据·人工智能·geo·ai搜索
wx_xkq12882 小时前
营销智脑V3 产品迭代更新全景图:从V6.0到V6.2,AI营销平台的成长之路
人工智能