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

26 阅读9分钟

手把手拆解 AI 工具调用的每一个字节,彻底搞懂底层原理!

为什么需要理解 Function Calling 的底层?

一个真实的困境

我们正在开发一个 AI 助手,已经用 LangChain 写好了 Agent,此时看起来一切正常:

const agent = new Agent()
const result = await agent.invoke("帮我查北京天气") // 输出:北京今天晴,22℃

但是突然有一天,AI 开始胡说八道了:

  • 用户问天气,它调用了send_email
  • 用户要计算,它返回了天气数据。
  • 明明工具定义正确,就是不调用。

我们打开调试日志,看到一堆 JSON 数据,但完全不知道问题出在哪!

Function Calling 的本质

用一句话总结 Function Calling 的本质:

Function Calling 是让 AI 输出结构化 JSON(函数调用请求),而非自然语言文本。

我们来看一组简单的对比:

普通对话输出

我无法查询实时天气,建议您打开天气应用!

Function Calling 输出

{
  "tool_calls": [{
    "function": {
      "name": "get_weather",
      "arguments": "{\"city\":\"北京\"}"
    }
  }]
}

Function Calling 让 AI 的角色从"回答者"变成了"决策者":它决定什么时候调用什么工具,而不是直接回答。

解剖请求:tools 和 tool_choice

一个完整的tools请求示例

{
  "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_weatherweather动词开头,明确是获取操作
search_databasedb避免缩写
send_notificationnotify完整表达意图
calculate_expressioncalc清晰表达功能

function.description:最关键字段

function.description 用于对工具进行描述,描述的质量直接决定AI调用工具的准确率,因此我认为它是整个 tools 参数中最关键的字段。

description 三要素
  • 工具能做什么(功能)
  • 工具需要输入什么(参数)
  • 工具什么时候被调用(触发条件)
差描述
"description": "查询天气"
好描述
"description": "获取指定城市的实时天气信息,返回温度、天气状况、湿度、风速。适用于用户询问任何城市的天气情况。如果用户没有指定城市,请先询问城市名称。"

function.parameters:JSON Schema 详解

JSON Schema 是参数定义的工业标准,下面是完整语法:

{
  "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自主决策(默认)

"tool_choice": "auto"

该模式下,AI 会根据用户输入自动判断是否需要调用工具,这是最常用的模式。

效果演示
用户:今天天气怎么样?  → AI调用天气工具
用户:帮我算一下数学   → AI调用计算工具
用户:你好啊           → AI不调用工具,直接聊天

none - 强制不调用工具

"tool_choice": "none"

该模式下,无论用户问什么,AI 都不会调用工具,只输出文本。

适用场景
  • 纯闲聊模式
  • 测试工具定义是否会影响对话
  • 节省Token(不输出tool_calls)

强制指定工具

"tool_choice": {
  "type": "function",
  "function": {
    "name": "get_weather"
  }
}

该模式下,强制 AI 必须调用指定的工具,即使用户的问题与此无关。

效果演示
用户:你好啊
AI:{ "tool_calls": [{ "function": { "name": "get_weather", ... } }] }
// AI会尝试从"你好啊"中提取城市参数,可能返回空或默认值
适用场景
  • 测试工具调用流程
  • 某些必须调用工具的特定场景
  • 多轮对话中,已知下一步必须调用某个工具

实战演示:三种模式的效果对比

// 测试代码
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的完整响应

{
  "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步:发送请求(用户消息 + 工具定义)

POST https://api.deepseek.com/v1/chat/completions
{ 
   "messages": [{"role":"user","content":"北京天气怎么样?"}],
   "tools": [{...天气工具定义...}] 
}      

第2步:AI返回 tool_calls

"tool_calls": [{ 
   "id": "call_123",
   "function": {
       "name": "get_weather",
       "arguments": "{\"city\":\"北京\"}"
   }
}]  

第3步:执行函数

const args = JSON.parse(toolCall.function.arguments) 
 // args = { city: "北京" }   
 const result = await getWeather(args.city)
 // result = { city: "北京", temperature: 22, condition: "晴" }

第4步:将结果作为 tool 角色消息返回

messages.push({
   role: "tool",
   tool_call_id: "call_123",
   content: JSON.stringify(result)
})  

第5步:AI生成最终答案

再次调用 API,带上完整的对话历史:

messages = [
    { role: "user", content: "北京天气怎么样?" },
    { role: "assistant", content: null, tool_calls: [...] },
    { role: "tool", content: "{...天气数据...}" }
]                          

AI 输出: "北京今天晴,温度22℃,湿度45%,适合户外活动!"

代码实现(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)

运行输出

============================================================
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%

今天北京天气晴朗,温度舒适,是个不错的好天气!适合外出活动。

多模型统一调用封装

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. 打印完整消息历史(带颜色)

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消耗

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. 工具调用追踪

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. 保存完整对话用于回放

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

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