返回 AI大模型从0到1——理论与实操

C3-05 Function Call工具调用


🎯 本章目标:理解 Function Calling(函数调用)机制的原理,学会用原生 API 构建生产级工具调用 Agent。对比手撕 ReAct,理解标准化的力量。

从手撕到 Function Calling

手撕 ReAct 让我们理解了 Agent 的原理,但 Prompt + 正则解析的方式有两个根本问题:

  • 不稳定:LLM 输出的 Action 格式不可控,常常格式错误
  • 不可移植:不同模型需要重新设计 Prompt 和解析逻辑

2023 年 6 月,OpenAI 推出 Function Calling 能力,让 LLM 原生支持结构化的工具调用:

sequenceDiagram participant U as 用户 participant A as Agent代码 participant L as LLM participant T as 工具函数 U->>A: "北京今天天气怎么样?" A->>L: messages + tools(Schema) L->>A: tool_calls=[{name:get_weather, args:{city:北京}}] A->>T: get_weather(city="北京") T->>A: {"temp": "28°C", "condition": "晴"} A->>L: messages(含tool结果) L->>A: "北京今天晴,28°C,适合出行" A->>U: 最终回答

三大优势:

对比项 手撕 ReAct Function Calling
格式稳定性 ❌ 依赖 Prompt 和正则 ✅ 原生 JSON Schema
跨模型兼容 ❌ 每个模型要重写 ✅ OpenAI 标准
参数验证 ❌ 手动校验 ✅ Schema 自动校验
并行调用 ❌ 一次一个 ✅ 一次可多个

JSON Schema 工具定义

Function Calling 用 JSON Schema 描述工具。LLM 看到这个 Schema 后,会自动决定是否调用、调哪个、传什么参数。

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
TOOLS_SCHEMA = [
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "计算数学表达式,支持加减乘除、括号",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "数学表达式,如 '(2 + 3) * 4'",
                    }
                },
                "required": ["expression"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取城市天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称"}
                },
                "required": ["city"],
            },
        },
    },
]

💡 类比:JSON Schema 就像工具的"产品说明书"。LLM 看到 description 字段来决定何时调用,看到 parameters 字段来决定传什么参数。 description 写得越清晰,LLM 调用越准确

完整可运行代码

Plain Text
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import json
import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()


def calculator(expression: str) -> str:
    try:
        allowed = set("0123456789+-*/().% ")
        if all(c in allowed for c in expression):
            return json.dumps({"result": eval(expression)}, ensure_ascii=False)
        return json.dumps({"error": "不支持的表达式"}, ensure_ascii=False)
    except Exception as e:
        return json.dumps({"error": str(e)}, ensure_ascii=False)


def get_weather(city: str) -> str:
    weather_data = {
        "北京": {"temp": "28°C", "condition": "晴"},
        "上海": {"temp": "32°C", "condition": "多云"},
        "深圳": {"temp": "34°C", "condition": "阵雨"},
    }
    data = weather_data.get(city, {"temp": "25°C", "condition": "晴"})
    return json.dumps({"city": city, **data}, ensure_ascii=False)


TOOLS_SCHEMA = [
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "计算数学表达式",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "数学表达式"}
                },
                "required": ["expression"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取城市天气",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string", "description": "城市名"}},
                "required": ["city"],
            },
        },
    },
]

TOOL_FUNCTIONS = {"calculator": calculator, "get_weather": get_weather}


class FunctionCallingAgent:
    def __init__(self):
        self.client = OpenAI(
            api_key=os.getenv("OPENAI_API_KEY"),
            base_url=os.getenv("OPENAI_BASE_URL"),
        )
        self.model = os.getenv("MODEL_NAME", "gpt-4o-mini")
        self.messages = []

    def run(self, user_input: str, max_iterations: int = 5) -> str:
        self.messages = [
            {"role": "system", "content": "你是一个AI助手,可以使用工具来回答问题。"},
            {"role": "user", "content": user_input},
        ]

        for i in range(max_iterations):
            response = self.client.chat.completions.create(
                model=self.model,
                messages=self.messages,
                tools=TOOLS_SCHEMA,
                tool_choice="auto",
            )

            msg = response.choices[0].message
            self.messages.append(msg)

            # 没有工具调用,直接返回文本
            if not msg.tool_calls:
                return msg.content

            # 处理工具调用
            for tool_call in msg.tool_calls:
                func_name = tool_call.function.name
                func_args = json.loads(tool_call.function.arguments)
                print(f"[工具调用] {func_name}({func_args})")

                if func_name in TOOL_FUNCTIONS:
                    result = TOOL_FUNCTIONS[func_name](**func_args)
                else:
                    result = json.dumps({"error": f"未知工具: {func_name}"})

                print(f"[工具结果] {result}")
                self.messages.append(
                    {
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "content": result,
                    }
                )

        return "达到最大迭代次数"


if __name__ == "__main__":
    agent = FunctionCallingAgent()
    questions = [
        "北京今天天气怎么样?",
        "帮我算一下 (100 + 200) * 0.85 是多少",
    ]
    for q in questions:
        print(f"\n用户: {q}")
        print(f"Agent: {agent.run(q)}")

Function Calling 核心机制

flowchart TD A[调用 LLM 传入 messages + tools] --> B{LLM 决定 是否调用工具} B -->|不调用| C[返回 content 直接给用户] B -->|调用| D[LLM 返回 tool_calls JSON 格式] D --> E[Agent 解析 函数名 + 参数] E --> F[执行工具函数] F --> G[工具结果 role=tool 回填] G --> A style A fill:#87CEEB style D fill:#FFE4B5 style F fill:#90EE90

三个关键 API 字段(模型训练时加入):

字段 位置 作用
tools 请求参数 告诉 LLM 有哪些工具可用(Schema 列表)
tool_calls LLM 返回 LLM 决定要调用的工具和参数
role: tool 消息列表 把工具执行结果回填给 LLM

Function Calling vs 手撕 ReAct

维度 手撕 ReAct Function Calling 胜出方
稳定性 依赖 Prompt 原生支持 FC ✅
参数校验 手动 Schema 自动 FC ✅
并行调用 不支持 支持 FC ✅
可读性 循环 + 字符串 messages 列表 FC ✅
理解原理 ✅ 直接 ❌ 抽象 ReAct ✅
学习价值 基础 生产 都要学

📝 实战建议:先学手撕 ReAct 理解原理,生产项目用 Function Calling。手撕版是"教学版",FC 是"工业版"。两个都要会,就像学开车先学手动挡再开自动挡。

💡 思考:大家有听过mcp server工具调用的吗?是不是有了mcp server工具就不需要function call了呢?