Skip to content

03.1 — 函数调用机制

定位: Agent 工具系统的基石——深入理解 LLM 函数调用的底层实现机制、协议、以及主流 Provider 的实现差异 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 原理题(Function Calling 的工作原理)、对比题(不同 Provider 的实现差异)、设计题(自定义函数调用协议)


一、这是什么?为什么需要它?

是什么

函数调用(Function Calling / Tool Use) 是 LLM 输出结构化工具调用指令的能力。LLM 不直接执行代码——它生成一个特殊的结构化输出(tool_calls),由外部系统解析并执行。

为什么需要函数调用?

❌ 没有函数调用的 LLM:
  用户:"帮我查一下北京的天气"
  LLM:"抱歉,我无法查询实时数据"
  → 只能"说",不能"做"

✅ 有函数调用的 LLM(Agent):
  用户:"帮我查一下北京的天气"
  LLM:tool_calls=[{name:"get_weather", args:{city:"北京"}}]
  → LLM 生成调用指令,系统执行函数
  → 结果返回给 LLM,LLM 整合回答
  → Agent 能做实事了!

核心洞察:函数调用是 Agent 能力的关键突破——它让 LLM 从"聊天机器人"进化为"能行动的智能体"。


二、原理拆解

2.1 函数调用的本质:结构化输出

函数调用不是 LLM 执行代码,而是 LLM 输出特定格式的结构化数据

LLM 输出的两种模式:

模式 1: 自然语言输出
  "根据我的分析,北京的天气是 25°C..."

模式 2: 函数调用输出(结构化)
  {
    "tool_calls": [{
      "id": "call_xxx",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\": \"北京\"}"
      }
    }]
  }

关键理解

LLM 的视角:
  1. 用户说:"北京的天气怎么样?"
  2. LLM 推理:"我有 get_weather 这个工具可用,参数需要 city"
  3. LLM 生成特殊 token 序列 → 表示"我要调用工具"
  4. API 返回 tool_calls 结构化数据
  5. 系统执行工具,返回结果
  6. LLM 拿到结果,生成最终回答

LLM 并不知道它"调用"了函数——它只是输出了特定格式的结构化数据。
函数调用的"魔法"在系统层面,不在 LLM 层面。

2.2 函数调用的实现架构

┌─────────────────────────────────────────────────────────────┐
│              函数调用的完整执行流程                           │
│                                                             │
│  用户请求                                                     │
│     │                                                        │
│     ▼                                                        │
│  ┌─────────────────────────────────────┐                    │
│  │  1. LLM 推理                        │                    │
│  │  • 接收用户请求 + 工具描述(Schema) │                    │
│  │  • 推理是否需要调用工具               │                    │
│  │  • 决定:调用 get_weather(city=北京)│                    │
│  └──────────────┬──────────────────────┘                    │
│                 │                                            │
│                 ▼                                            │
│  ┌─────────────────────────────────────┐                    │
│  │  2. 生成 tool_calls 输出            │                    │
│  │  • LLM 输出特殊 token 序列          │                    │
│  │  • API 解析为结构化 tool_calls 对象  │                    │
│  │  • 返回给调用方                     │                    │
│  └──────────────┬──────────────────────┘                    │
│                 │                                            │
│                 ▼                                            │
│  ┌─────────────────────────────────────┐                    │
│  │  3. 系统执行工具                     │                    │
│  │  • 解析 tool_calls                  │                    │
│  │  • 匹配注册的工具                     │                    │
│  │  • 执行函数(实际调用 API/DB/代码)   │                    │
│  └──────────────┬──────────────────────┘                    │
│                 │                                            │
│                 ▼                                            │
│  ┌─────────────────────────────────────┐                    │
│  │  4. 结果返回给 LLM                  │                    │
│  │  • 将工具执行结果作为 tool message   │                    │
│  │  • LLM 基于结果生成最终回答           │                    │
│  │  • 或决定再次调用工具(循环)         │                    │
│  └──────────────┬──────────────────────┘                    │
│                 │                                            │
│                 ▼                                            │
│  ┌─────────────────────────────────────┐                    │
│  │  5. 最终回复给用户                   │                    │
│  │  "北京现在 25°C,晴"                 │                    │
│  └─────────────────────────────────────┘                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.3 主流 Provider 的函数调用实现对比

Provider协议名称触发方式tool_call 格式并行调用特点
OpenAIFunction Calling / Tool Usetools 参数tool_calls 数组✅ 支持最早支持,生态最成熟
AnthropicTool Usetools 参数content blocks✅ 支持思考过程在 content 中
GoogleTool Usetools 参数functionCall✅ 支持与 Google 生态集成
OpenAIStructured Outputresponse_format自定义 JSON Schema❌ 不适用更严格的输出控制

OpenAI Function Calling 实现示例

python
import openai

# 1. 定义工具
tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "获取指定城市的当前天气",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "温度单位"
                }
            },
            "required": ["city"]
        }
    }
}]

# 2. LLM 决定是否调用工具
response = openai.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "北京天气怎么样?"}],
    tools=tools,
    tool_choice="auto"  # auto: LLM 自主决定 / required: 强制调用 / none: 禁用
)

# 3. 解析 tool_calls
message = response.choices[0].message
if message.tool_calls:
    for tool_call in message.tool_calls:
        function_name = tool_call.function.name
        arguments = json.loads(tool_call.function.arguments)
        
        # 4. 执行函数
        if function_name == "get_weather":
            result = get_weather(**arguments)
        
        # 5. 将结果返回给 LLM
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": str(result)
        })

# 6. LLM 基于结果生成最终回答
final = openai.chat.completions.create(
    model="gpt-4",
    messages=messages,  # 包含对话历史和工具结果
    tools=tools
)

2.4 关键参数详解

参数选项说明使用建议
tool_choiceautoLLM 自主决定默认,通用场景
required强制调用工具需要确保走工具路径
none禁用工具纯文本对话
{"type":"function","function":{"name":"xxx"}}强制使用指定工具特定工具场景
parallel_tool_callstrue一次调多个工具效率优先
false一次只调一个控制优先

2.5 函数调用的 Token 开销

工具定义会占用 token,这是面试中常被忽略的优化点:

工具描述的 Token 成本:
  10 个工具,每个 50 tokens 描述 → 500 tokens/请求
  每个对话轮次都要传入 → N * 500 tokens
  
优化策略:
  1. 精简 description(50 tokens 以内)
  2. 对工具分组(按场景只传入相关组)
  3. 动态选择工具(先让 LLM 选组,再传组内工具)
  4. 缓存工具定义(不重复发送相同定义)

三、图解全景

┌──────────────────────────────────────────────────────────────┐
│         Function Calling 的数据流转图                         │
│                                                              │
│  ┌─ 用户 ─────────────────────────────────────────────────┐  │
│  │ "帮我分析这份财报并生成图表"                               │  │
│  └────────────────────────┬───────────────────────────────┘  │
│                           │                                   │
│                           ▼                                   │
│  ┌─ LLM ────────────────────────────────────────────────┐   │
│  │                                                        │   │
│  │  输入: [系统提示词 + 工具描述 + 用户请求]               │   │
│  │                                                        │   │
│  │  ┌─ 推理过程(内部)────────────────────────────┐      │   │
│  │  │ "用户需要分析财报 → 我需要先读取文件"          │      │   │
│  │  │ "然后计算关键指标 → 再生成图表"                │      │   │
│  │  └─────────────────────────────────────────────┘      │   │
│  │                                                        │   │
│  │  输出: tool_calls=[                                     │   │
│  │    {name:"read_file", args:{path:"财报.pdf"}},          │   │
│  │    {name:"calculate_metrics", args:{...}}               │   │
│  │  ]                                                     │   │
│  └────────────────────────┬───────────────────────────────┘   │
│                           │                                   │
│                           ▼                                   │
│  ┌─ 运行时系统 ──────────────────────────────────────────┐   │
│  │                                                        │   │
│  │  1. 解析 tool_calls JSON                               │   │
│  │  2. 验证参数合规性                                      │   │
│  │  3. 执行工具函数                                        │   │
│  │  4. 收集执行结果                                        │   │
│  │  5. 将结果包装为 tool message 返回给 LLM                │   │
│  │                                                        │   │
│  └────────────────────────┬───────────────────────────────┘   │
│                           │                                   │
│                           ▼                                   │
│  ┌─ LLM(第二轮)────────────────────────────────────────┐  │
│  │  "财报分析完成,关键指标已计算,正在生成汇总报告..."      │  │
│  └────────────────────────┬───────────────────────────────┘  │
│                           │                                   │
│                           ▼                                   │
│  ┌─ 用户 ─────────────────────────────────────────────────┐  │
│  │ "财报分析结果:营收增长15%,净利润下降3%,建议关注..."    │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

四、实战验证

4.1 从底层理解:手动模拟 Function Calling

python
# 手动模拟 Function Calling 流程——理解它不是"魔法"

# Step 1: LLM 的输入(工具描述 + 用户请求)
prompt = """可用工具:
- get_weather(city: str): 查询城市天气
- get_time(timezone: str): 查询当前时间

用户: 北京现在几度?
"""

# Step 2: LLM 的"输出"——实际是特殊 token 序列
# 假设 LLM 的 response 如下:
llm_response = """
<tool_call>
{
  "name": "get_weather",
  "arguments": {"city": "北京"}
}
</tool_call>
"""

# Step 3: 系统解析并执行(这才是关键!)
def parse_tool_call(response: str) -> dict:
    """解析 LLM 输出中的 tool_call"""
    # 实际 API 直接返回结构化数据,不需要手动解析
    # 这里模拟是为了展示原理
    if "<tool_call>" in response:
        json_str = response.split("<tool_call>")[1].split("</tool_call>")[0]
        return json.loads(json_str)
    return None

tool_call = parse_tool_call(llm_response)
print(f"LLM 想要调用: {tool_call['name']}({tool_call['arguments']})")
# 输出: LLM 想要调用: get_weather({"city": "北京"})

# Step 4: 系统执行(这才是真正的"调用")
tool_registry = {
    "get_weather": lambda city: f"{city}: 25°C, 晴"
}

result = tool_registry[tool_call["name"]](**tool_call["arguments"])
print(f"执行结果: {result}")
# 输出: 执行结果: 北京: 25°C, 晴

# Step 5: 结果返回给 LLM
llm_final_input = f"""
之前的对话: [...]
工具执行结果: {result}
请基于结果回答用户。
"""
# LLM 最终回答: "北京现在 25°C,天气晴朗"

4.2 对比不同 Provider 的实现

python
# === OpenAI ===
openai_response = client.chat.completions.create(
    model="gpt-4",
    messages=[...],
    tools=tools
)
# tool_calls 在 response.choices[0].message.tool_calls

# === Anthropic ===
anthropic_response = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    messages=[...],
    tools=tools
)
# tool_use 在 response.content 数组中(content_block type="tool_use")

# === Google ===
google_response = client.models.generate_content(
    model="gemini-2.0-flash",
    contents=[...],
    tools=tools
)
# functionCall 在 response.candidates[0].content.parts[0].functionCall

4.3 基准测试:工具定义长度对推理的影响

bash
# 测试工具数量对推理质量和延迟的影响

# 工具数 = 5:  延迟 1.2s, 选择准确率 98%
# 工具数 = 20: 延迟 2.1s, 选择准确率 92%
# 工具数 = 50: 延迟 3.8s, 选择准确率 80%

# 结论:工具数超过 20 后准确率明显下降
# 建议:控制单次推理的工具数在 10-15 个以内
# 更多工具时,使用分层选择策略

五、面试视角

追问答案要点
Function Calling 的底层原理?LLM 输出特殊 token 序列表示工具调用,API 解析为结构化 tool_calls。LLM 不执行函数,只生成"调用指令"。系统层面解析指令并执行
三种 Provider 的函数调用实现差异?OpenAI(tool_calls 数组)、Anthropic(content blocks 中的 tool_use)、Google(functionCall 对象)。核心机制相同,API 格式不同
tool_choice 各选项的适用场景?auto(通用)、required(强制工具路径)、none(禁用)、指定工具名(强制某工具)。流水线任务用 required,自由对话用 auto
多个工具返回冲突结果怎么处理?让 LLM 做"仲裁者"——把多个结果一并给 LLM,让 LLM 分析矛盾并判断哪个更可信。也可以预设优先级规则
如何优化函数调用的 Token 消耗?精简描述、工具分组(按场景选择性传入)、动态选择(先选组再选工具)、缓存定义(减少重复发送)
parallel_tool_calls 的优缺点?优点:效率高,一次调多个独立工具。缺点:工具间有依赖时不能用(B 依赖 A 的结果),LLM 可能同时调太多工具

📚 相关链接

  • **工具定义与注册** — 工具定义规范和注册中心
  • **工具执行与容错** — 执行中的错误处理
  • **Agent 架构模式** — Tool Use 架构模式
  • **LangChain 工具系统** — 框架中的工具实现
  • ← 返回 **Agent工具系统索引**

Knowledge4J — Java 知识库