Function Calling深度解析:请求到响应完整流程

2026-06-19阅读 0热度 0
其他

为什么必须吃透 Function Calling 的底层原理

先看一个真实踩坑案例。我们正在搭建一个AI助手,用LangChain写好了Agent,表面一切正常:

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

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

然而某天AI开始乱套:用户问天气,它调用了send_email;用户要计算,它返回天气数据;工具定义明明正确,就是不肯调用。打开调试日志,满屏JSON,根本不知道根因在哪。这个时刻,吃透Function Calling的底层数据流就成了救命的技能。

Function Calling 的本质拆解

核心一句话:Function Calling把AI从“答案生成者”变成了“决策调度者”。看一组直观的对比:

普通对话输出

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

Function Calling 输出

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

看到了吗?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_weather weather 动词开头,明确执行获取操作
search_database db 避免缩写,保留完整语义
send_notification notify 完整表达调用意图
calculate_expression calc 清晰指明功能边界

function.description:最关键的字段

function.description 是整个 tools 参数中权重最高的字段,其质量直接决定AI调用工具的准确率。高质量描述必须包含三个要素:

  • 工具的能力边界(功能)
  • 工具需要哪些输入参数(参数说明)
  • 工具应该在什么场景下被触发(触发条件)

低质量描述:

"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也会尝试从“你好啊”中提取城市参数,很有可能返回空或默认值。

适用场景:测试工具调用流程的完整性、某些必须走工具的逻辑环节,以及多轮对话中已知下一步必须调用某个工具的情况。

三种模式效果对比(测试代码):

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
      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 {
  await new Promise(resolve => setTimeout(resolve, 100))
  const weatherDB: Record = {
    '北京': { 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 {
  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 {
  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 {
  // 步骤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

  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)
      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 {
  const endpoints: Record = {
    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'
  }

  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 sa veConversation(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)→ 开发者执行 → 结果回传。吃透了这条数据流,下次遇到AI胡言乱语时,打开调试日志,顺着JSON一层层排查,问题自然水落石出。

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策