Contents

Function Calling实战:让AI大模型学会使用工具

Function Calling实战:让AI大模型学会使用工具

大语言模型(LLM)的核心能力是理解和生成文本,但它本身并不能直接调用API、查询数据库或操作文件系统。Function Calling(函数调用)是连接大模型与外部世界的桥梁——它让模型能够"告诉"你的代码"我需要调用这个函数",然后由你的代码执行实际操作并把结果返回给模型。

本文将从原理到实战,带你构建一个完整的 Function Calling 工具调用系统。

目录

1. 为什么需要 Function Calling

1.1 LLM 的局限性

大语言模型本质上是一个文本输入→文本输出的系统。它无法:

  • 直接访问互联网获取实时数据
  • 执行代码或操作文件系统
  • 查询数据库
  • 调用第三方 API

传统做法是通过 Prompt Engineering 让模型"假装"拥有这些能力,但这种方式存在严重的幻觉问题——模型可能编造不存在的天气数据或股票价格。

1.2 Function Calling 的本质

Function Calling 的核心思想非常简单:

1
用户提问 → LLM 生成结构化的函数调用(JSON)→ 应用代码执行函数 → 结果返回给 LLM → LLM 生成最终回答

关键点在于:LLM 不执行任何函数,它只是生成一个结构化的 JSON,告诉你"我想调用什么函数、传什么参数"。实际的执行由你的应用代码完成。

1.3 与传统插件系统的区别

特性 传统插件 Function Calling
调用方 应用代码 LLM 生成调用指令
参数确定 代码硬编码 LLM 动态推断
适用场景 确定性流程 自然语言驱动
扩展性 需修改代码 添加工具描述即可

2. Function Calling 的工作流程

完整的 Function Calling 流程包含以下几个步骤:

 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
┌─────────────────────────────────────────────────────────┐
                      用户输入                            
           "北京今天天气怎么样?顺便看看苹果股价"              
└──────────────────────┬──────────────────────────────────┘
                       
                       
┌─────────────────────────────────────────────────────────┐
                   LLM 推理阶段                            
                                                         
  模型分析用户意图,决定需要调用两个工具:                     
  1. get_weather(city="北京")                             
  2. get_stock_price(symbol="AAPL")                      
                                                         
  返回结构化的 tool_calls JSON                             
└──────────────────────┬──────────────────────────────────┘
                       
                       
┌─────────────────────────────────────────────────────────┐
                  应用代码执行阶段                           
                                                         
  解析 tool_calls,执行对应的函数                           
  get_weather("北京")  {"temp": 28, "weather": "晴"}     
  get_stock_price("AAPL")  {"price": 198.50, ...}       
└──────────────────────┬──────────────────────────────────┘
                       
                       
┌─────────────────────────────────────────────────────────┐
               结果返回给 LLM                              
                                                         
  将工具执行结果作为 tool 角色的消息添加到对话中               
  LLM 基于这些真实数据生成最终回答                           
                                                         
  "北京今天晴天,气温28°C。苹果股价目前为$198.50,..."       
└─────────────────────────────────────────────────────────┘

3. OpenAI GPT-5.4 Function Calling 实战

3.1 API 费用参考(2026年6月)

在开始之前,先了解当前 GPT-5.4 系列的定价:

模型 输入价格 输出价格 上下文窗口
GPT-5.4 $2.50/百万tokens $15.00/百万tokens 270K tokens
GPT-5.4-mini $0.75/百万tokens $4.50/百万tokens 270K tokens

⚠️ 注:上述价格为标准处理模式(Standard)下上下文长度 ≤270K tokens 的定价。更长上下文会产生额外费用,请参考 OpenAI 官方定价页面

Function Calling 在工具较多时会增加 system prompt 的 token 消耗(工具描述会占用上下文),建议在非关键场景使用 gpt-5.4-mini 以降低成本。

3.2 基础用法:单工具调用

首先安装 OpenAI SDK:

1
pip install openai>=2.0

最简单的 Function Calling 示例:

 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
"""
基础 Function Calling 示例
使用 GPT-5.4 查询天气
"""
import json
import os
from openai import OpenAI

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# 第一步:定义工具(Tool)的 JSON Schema
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询指定城市的当前天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如 '北京'、'上海'、'New York'"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位,默认摄氏度"
                    }
                },
                "required": ["city"]
            }
        }
    }
]

# 第二步:模拟工具的实际执行(真实项目中这里会调用天气 API)
def get_weather(city: str, unit: str = "celsius") -> dict:
    """模拟天气查询函数"""
    # 这里用模拟数据,实际项目中应调用真实 API
    mock_data = {
        "北京": {"temp": 28, "weather": "晴", "humidity": 45, "wind": "北风3级"},
        "上海": {"temp": 30, "weather": "多云", "humidity": 70, "wind": "东南风2级"},
        "广州": {"temp": 33, "weather": "阵雨", "humidity": 80, "wind": "南风3级"},
    }
    data = mock_data.get(city, {"temp": 25, "weather": "未知", "humidity": 50, "wind": "微风"})
    if unit == "fahrenheit":
        data["temp"] = data["temp"] * 9/5 + 32
    return {"city": city, **data}

# 第三步:发送请求,让模型决定是否需要调用工具
messages = [
    {"role": "system", "content": "你是一个有用的天气助手。如果用户询问天气,请调用 get_weather 函数获取实时数据。"},
    {"role": "user", "content": "北京今天天气怎么样?"}
]

response = client.chat.completions.create(
    model="gpt-5.4",
    messages=messages,
    tools=tools,
    tool_choice="auto",  # auto: 模型自主决定 | required: 强制调用 | none: 不调用
)

# 第四步:检查模型是否要求调用工具
assistant_message = response.choices[0].message
print(f"模型回复内容: {assistant_message.content}")
print(f"工具调用请求: {assistant_message.tool_calls}")

if assistant_message.tool_calls:
    # 执行工具调用
    for tool_call in assistant_message.tool_calls:
        function_name = tool_call.function.name
        arguments = json.loads(tool_call.function.arguments)
        print(f"\n执行函数: {function_name}")
        print(f"参数: {arguments}")

        # 调用实际函数
        result = get_weather(**arguments)
        print(f"结果: {result}")

        # 将工具执行结果添加到消息历史
        messages.append(assistant_message.model_dump())
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result, ensure_ascii=False)
        })

    # 第五步:用工具结果继续对话
    final_response = client.chat.completions.create(
        model="gpt-5.4",
        messages=messages,
        tools=tools,
    )
    print(f"\n最终回答: {final_response.choices[0].message.content}")
else:
    print(f"\n模型直接回答: {assistant_message.content}")

运行输出示例:

1
2
3
4
5
6
7
8
模型回复内容: None
工具调用请求: [ChatCompletionMessageToolCall(id='call_abc123', function=Function(arguments='{"city": "北京", "unit": "celsius"}', name='get_weather'), type='function')]

执行函数: get_weather
参数: {'city': '北京', 'unit': 'celsius'}
结果: {'city': '北京', 'temp': 28, 'weather': '晴', 'humidity': 45, 'wind': '北风3级'}

最终回答: 北京今天天气晴朗,气温 28°C,湿度 45%,北风3级。适合外出活动,建议做好防晒。

3.3 关键参数说明

tool_choice 参数详解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. auto - 模型自主判断是否需要工具(最常用)
tool_choice="auto"

# 2. none - 强制模型不使用工具
tool_choice="none"

# 3. required - 强制模型必须使用至少一个工具
tool_choice="required"

# 4. 指定特定函数 - 强制调用特定函数
tool_choice={"type": "function", "function": {"name": "get_weather"}}

4. 构建天气+股票查询 Agent

下面构建一个能同时处理天气和股票查询的真实 Agent:

  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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
"""
多工具 Agent:天气查询 + 股票价格查询
使用 GPT-5.4 构建,支持多轮对话
"""
import json
import os
import datetime
from openai import OpenAI

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))


# ===================== 工具定义 =====================

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询指定城市的当前天气信息,包括温度、天气状况、湿度和风力",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称"
                    },
                    "date": {
                        "type": "string",
                        "description": "查询日期,格式 YYYY-MM-DD,默认今天"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_stock_price",
            "description": "查询股票的实时价格信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "symbol": {
                        "type": "string",
                        "description": "股票代码,如 AAPL(苹果)、GOOGL(谷歌)、000001.SS(上证指数)"
                    },
                    "market": {
                        "type": "string",
                        "enum": ["us", "cn", "hk"],
                        "description": "市场:us(美股)、cn(A股)、hk(港股)"
                    }
                },
                "required": ["symbol"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "执行数学计算,支持加减乘除、百分比、汇率换算等",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "数学表达式,如 '28 * 9/5 + 32' 或 '100 * (1 - 0.15)'"
                    }
                },
                "required": ["expression"]
            }
        }
    }
]


# ===================== 工具实现 =====================

def get_weather(city: str, date: str = None) -> dict:
    """查询天气(模拟实现)"""
    import random
    if date is None:
        date = datetime.date.today().isoformat()
    
    # 模拟不同城市的天气
    weather_patterns = {
        "北京": {"temp_range": (15, 35), "conditions": ["晴", "多云", "阴", "小雨"]},
        "上海": {"temp_range": (18, 33), "conditions": ["多云", "阴", "小雨", "晴"]},
        "深圳": {"temp_range": (22, 36), "conditions": ["晴", "多云", "雷阵雨", "阴"]},
        "东京": {"temp_range": (10, 28), "conditions": ["晴", "多云", "小雨"]},
        "New York": {"temp_range": (5, 30), "conditions": ["晴", "多云", "Rain", "Snow"]},
    }
    
    pattern = weather_patterns.get(city, {"temp_range": (15, 30), "conditions": ["晴", "多云"]})
    temp = random.randint(*pattern["temp_range"])
    condition = random.choice(pattern["conditions"])
    
    return {
        "city": city,
        "date": date,
        "temperature": f"{temp}°C",
        "condition": condition,
        "humidity": f"{random.randint(30, 90)}%",
        "wind": f"{random.choice(['微风', '轻风', '和风', '劲风'])} {random.randint(1, 6)}级"
    }


def get_stock_price(symbol: str, market: str = "us") -> dict:
    """查询股价(模拟实现)"""
    import random
    
    # 模拟股价数据
    mock_stocks = {
        "AAPL": {"name": "苹果公司", "base_price": 198.50},
        "GOOGL": {"name": "谷歌", "base_price": 175.20},
        "MSFT": {"name": "微软", "base_price": 420.80},
        "NVDA": {"name": "英伟达", "base_price": 880.50},
        "000001.SS": {"name": "上证指数", "base_price": 3250.00},
    }
    
    stock = mock_stocks.get(symbol, {"name": symbol, "base_price": 100.00})
    change_pct = random.uniform(-5, 5)
    current_price = round(stock["base_price"] * (1 + change_pct / 100), 2)
    
    return {
        "symbol": symbol,
        "name": stock["name"],
        "market": market,
        "current_price": f"${current_price}" if market == "us" else f{current_price}",
        "change_pct": f"{change_pct:+.2f}%",
        "volume": f"{random.randint(10, 500)}M",
        "timestamp": datetime.datetime.now().isoformat()
    }


def calculate(expression: str) -> dict:
    """安全执行数学计算"""
    import ast
    import operator as op
    
    # 安全的运算符映射
    safe_ops = {
        ast.Add: op.add,
        ast.Sub: op.sub,
        ast.Mult: op.mul,
        ast.Div: op.truediv,
        ast.Pow: op.pow,
        ast.Mod: op.mod,
    }
    
    def _eval(node):
        if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
            return node.value
        elif isinstance(node, ast.BinOp):
            left = _eval(node.left)
            right = _eval(node.right)
            return safe_ops[type(node.op)](left, right)
        elif isinstance(node, ast.UnaryOp):
            return -_eval(node.operand)
        else:
            raise ValueError(f"不支持的表达式: {ast.dump(node)}")
    
    try:
        tree = ast.parse(expression, mode='eval')
        result = _eval(tree.body)
        return {"expression": expression, "result": result, "success": True}
    except Exception as e:
        return {"expression": expression, "error": str(e), "success": False}


# ===================== 工具路由 =====================

TOOL_MAP = {
    "get_weather": get_weather,
    "get_stock_price": get_stock_price,
    "calculate": calculate,
}


def execute_tool(tool_call) -> str:
    """执行工具调用并返回结果"""
    func_name = tool_call.function.name
    arguments = json.loads(tool_call.function.arguments)
    
    func = TOOL_MAP.get(func_name)
    if func is None:
        return json.dumps({"error": f"未知工具: {func_name}"})
    
    try:
        result = func(**arguments)
        return json.dumps(result, ensure_ascii=False)
    except Exception as e:
        return json.dumps({"error": f"工具执行失败: {str(e)}"})


# ===================== Agent 循环 =====================

def run_agent(user_input: str, conversation_history: list = None, max_rounds: int = 5) -> str:
    """
    运行 Agent,支持多轮工具调用
    
    Args:
        user_input: 用户输入
        conversation_history: 对话历史
        max_rounds: 最大工具调用轮数(防止无限循环)
    
    Returns:
        最终回复文本
    """
    if conversation_history is None:
        conversation_history = [
            {
                "role": "system",
                "content": (
                    "你是一个智能助手,可以查询天气、股票价格,并进行数学计算。"
                    "请用自然、友好的方式回答用户的问题。"
                    "如果需要获取实时数据,请调用相应的工具。"
                    "你可以同时调用多个工具来获取完整信息。"
                )
            }
        ]
    
    conversation_history.append({"role": "user", "content": user_input})
    
    for round_num in range(max_rounds):
        response = client.chat.completions.create(
            model="gpt-5.4",
            messages=conversation_history,
            tools=tools,
            tool_choice="auto",
        )
        
        message = response.choices[0].message
        conversation_history.append(message.model_dump())
        
        # 如果没有工具调用,返回最终回答
        if not message.tool_calls:
            return message.content
        
        # 执行所有工具调用
        print(f"[第{round_num + 1}轮] 模型请求调用 {len(message.tool_calls)} 个工具:")
        for tc in message.tool_calls:
            print(f"  - {tc.function.name}({tc.function.arguments})")
        
        for tool_call in message.tool_calls:
            result = execute_tool(tool_call)
            print(f"  → 结果: {result[:100]}...")
            
            conversation_history.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result
            })
    
    return "抱歉,处理过程中出现了问题,请稍后再试。"


# ===================== 运行示例 =====================

if __name__ == "__main__":
    # 示例1:单工具调用
    print("=" * 60)
    print("示例1:查询天气")
    print("=" * 60)
    answer = run_agent("北京今天天气怎么样?")
    print(f"回答: {answer}\n")
    
    # 示例2:多工具并行调用
    print("=" * 60)
    print("示例2:同时查询天气和股票")
    print("=" * 60)
    answer = run_agent("帮我看看北京天气,还有苹果和英伟达的股价")
    print(f"回答: {answer}\n")
    
    # 示例3:需要计算的调用
    print("=" * 60)
    print("示例3:带计算的查询")
    print("=" * 60)
    answer = run_agent("如果我在北京穿短袖出门,需要多少度以上才合适?另外帮我算算华氏温度")
    print(f"回答: {answer}\n")

5. 并行工具调用(Parallel Tool Calling)

GPT-5.4 原生支持并行工具调用——当用户的问题需要多个工具的数据时,模型会在一次响应中返回多个 tool_calls,你可以并行执行它们以减少延迟。

5.1 并行调用的实现

  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
"""
并行工具调用示例
同时获取多个城市的天气,使用 asyncio 并发执行
"""
import asyncio
import json
import time
from openai import OpenAI
from concurrent.futures import ThreadPoolExecutor

client = OpenAI()

# 同样的工具定义...
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询指定城市的当前天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称"}
                },
                "required": ["city"]
            }
        }
    }
]


def get_weather(city: str) -> dict:
    """模拟耗时的 API 调用"""
    import random
    time.sleep(0.5)  # 模拟网络延迟
    return {"city": city, "temp": random.randint(15, 35), "condition": "晴"}


# ===================== 方案1:串行执行 =====================
def run_serial(tool_calls) -> list:
    """逐个执行工具调用"""
    results = []
    for tc in tool_calls:
        args = json.loads(tc.function.arguments)
        result = get_weather(**args)
        results.append({"tool_call_id": tc.id, "content": json.dumps(result)})
    return results


# ===================== 方案2:并行执行(推荐) =====================
def run_parallel(tool_calls) -> list:
    """使用线程池并行执行工具调用"""
    results = []
    
    def execute(tc):
        args = json.loads(tc.function.arguments)
        result = get_weather(**args)
        return {"tool_call_id": tc.id, "content": json.dumps(result, ensure_ascii=False)}
    
    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = {executor.submit(execute, tc): tc for tc in tool_calls}
        for future in futures:
            results.append(future.result())
    
    return results


# ===================== 性能对比 =====================
def compare_performance():
    """对比串行和并行执行的性能"""
    # 模拟模型返回3个并行工具调用
    mock_tool_calls = [
        type('ToolCall', (), {
            'id': f'call_{i}',
            'function': type('Function', (), {
                'name': 'get_weather',
                'arguments': json.dumps({"city": city})
            })()
        })()
        for i, city in enumerate(["北京", "上海", "广州"])
    ]
    
    # 串行执行
    start = time.time()
    serial_results = run_serial(mock_tool_calls)
    serial_time = time.time() - start
    
    # 并行执行
    start = time.time()
    parallel_results = run_parallel(mock_tool_calls)
    parallel_time = time.time() - start
    
    print(f"串行执行耗时: {serial_time:.2f}s")
    print(f"并行执行耗时: {parallel_time:.2f}s")
    print(f"加速比: {serial_time/parallel_time:.1f}x")


if __name__ == "__main__":
    compare_performance()
    # 串行执行耗时: 1.50s
    # 并行执行耗时: 0.52s
    # 加速比: 2.9x

5.2 使用 asyncio 的异步版本

 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
"""
异步并行工具调用
适合 FastAPI / aiohttp 等异步框架
"""
import asyncio
import json
from openai import AsyncOpenAI

async_client = AsyncOpenAI()

async def async_get_weather(city: str) -> dict:
    """异步天气查询(实际中应使用 aiohttp 调用 API)"""
    import random
    await asyncio.sleep(0.5)  # 模拟异步网络请求
    return {"city": city, "temp": random.randint(15, 35), "condition": "晴"}


async def run_async_agent(user_input: str) -> str:
    """异步 Agent 主循环"""
    tools = [
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "查询指定城市的天气",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {"type": "string", "description": "城市名称"}
                    },
                    "required": ["city"]
                }
            }
        }
    ]
    
    messages = [
        {"role": "system", "content": "你是一个天气助手。"},
        {"role": "user", "content": user_input}
    ]
    
    while True:
        response = await async_client.chat.completions.create(
            model="gpt-5.4",
            messages=messages,
            tools=tools,
        )
        
        message = response.choices[0].message
        messages.append(message.model_dump())
        
        if not message.tool_calls:
            return message.content
        
        # 并行执行所有工具调用
        tasks = []
        for tc in message.tool_calls:
            args = json.loads(tc.function.arguments)
            tasks.append(async_get_weather(**args))
        
        results = await asyncio.gather(*tasks)
        
        for tc, result in zip(message.tool_calls, results):
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": json.dumps(result, ensure_ascii=False)
            })


# 使用示例
async def main():
    answer = await run_async_agent("北京、上海、广州天气分别怎么样?")
    print(answer)

# asyncio.run(main())

6. 流式响应中的工具调用

当使用流式输出(Streaming)时,工具调用的处理需要特别注意——tool_calls 的内容是增量拼接的,不是一次性完整返回。

6.1 流式处理工具调用

  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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""
流式响应 + 工具调用
处理 chunk 增量拼接的正确方式
"""
import json
from openai import OpenAI

client = OpenAI()

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称"}
                },
                "required": ["city"]
            }
        }
    }
]


def get_weather(city: str) -> dict:
    import random
    return {"city": city, "temp": random.randint(15, 35), "condition": "晴"}


def process_streaming_tool_calls(user_input: str) -> str:
    """
    处理包含工具调用的流式响应
    
    关键点:
    - tool_calls 的 arguments 是增量发送的,需要拼接
    - tool_calls 通过 index 字段区分不同的调用
    """
    messages = [
        {"role": "system", "content": "你是一个有用的助手。"},
        {"role": "user", "content": user_input}
    ]
    
    # 存储所有工具调用的累积数据
    tool_calls_data = {}  # index -> {id, name, arguments_json}
    accumulated_content = ""
    
    stream = client.chat.completions.create(
        model="gpt-5.4",
        messages=messages,
        tools=tools,
        tool_choice="auto",
        stream=True,
    )
    
    for chunk in stream:
        if not chunk.choices:
            continue
        
        delta = chunk.choices[0].delta
        
        # 处理文本内容(正常回复)
        if delta.content:
            accumulated_content += delta.content
            print(delta.content, end="", flush=True)
        
        # 处理工具调用(增量拼接)
        if delta.tool_calls:
            for tc in delta.tool_calls:
                idx = tc.index
                
                if idx not in tool_calls_data:
                    tool_calls_data[idx] = {
                        "id": tc.id or "",
                        "name": "",
                        "arguments": ""
                    }
                
                # 拼接 ID
                if tc.id:
                    tool_calls_data[idx]["id"] = tc.id
                
                # 拼接函数名
                if tc.function and tc.function.name:
                    tool_calls_data[idx]["name"] = tc.function.name
                
                # 拼接参数(这是增量的!)
                if tc.function and tc.function.arguments:
                    tool_calls_data[idx]["arguments"] += tc.function.arguments
    
    print()  # 换行
    
    # 检查是否有工具调用
    if tool_calls_data:
        print(f"\n检测到 {len(tool_calls_data)} 个工具调用:")
        
        messages.append({
            "role": "assistant",
            "content": accumulated_content or None,
            "tool_calls": [
                {
                    "id": data["id"],
                    "type": "function",
                    "function": {
                        "name": data["name"],
                        "arguments": data["arguments"]
                    }
                }
                for data in tool_calls_data.values()
            ]
        })
        
        # 执行工具
        for idx, data in tool_calls_data.items():
            print(f"  调用: {data['name']}({data['arguments']})")
            args = json.loads(data["arguments"])
            result = get_weather(**args)
            print(f"  结果: {result}")
            
            messages.append({
                "role": "tool",
                "tool_call_id": data["id"],
                "content": json.dumps(result, ensure_ascii=False)
            })
        
        # 用工具结果继续
        print("\n最终回答: ", end="")
        final_stream = client.chat.completions.create(
            model="gpt-5.4",
            messages=messages,
            tools=tools,
            stream=True,
        )
        
        final_answer = ""
        for chunk in final_stream:
            if chunk.choices and chunk.choices[0].delta.content:
                content = chunk.choices[0].delta.content
                final_answer += content
                print(content, end="", flush=True)
        
        print()
        return final_answer
    
    return accumulated_content


# 使用
if __name__ == "__main__":
    answer = process_streaming_tool_calls("今天北京天气如何?")

6.2 流式处理的常见陷阱

⚠️ 重要提醒:流式模式下,tool_callsarguments 字段是逐 chunk 增量发送的,不是完整 JSON。必须在循环中进行字符串拼接,否则只能拿到最后一个 chunk 的片段。

7. 错误处理与重试策略

在生产环境中,工具调用可能因各种原因失败:网络超时、API 限流、参数错误等。健壮的错误处理是必不可少的。

7.1 完整的错误处理框架

  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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
"""
健壮的工具调用错误处理与重试
"""
import json
import time
import logging
from functools import wraps
from openai import OpenAI

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

client = OpenAI()


# ===================== 重试装饰器 =====================

def retry_with_backoff(max_retries=3, backoff_factor=2, retryable_exceptions=(Exception,)):
    """指数退避重试装饰器"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except retryable_exceptions as e:
                    last_exception = e
                    if attempt < max_retries:
                        wait_time = backoff_factor ** attempt
                        logger.warning(
                            f"工具调用失败 (尝试 {attempt + 1}/{max_retries + 1}): {e}. "
                            f"{wait_time}s 后重试..."
                        )
                        time.sleep(wait_time)
                    else:
                        logger.error(f"工具调用最终失败: {e}")
            raise last_exception
        return wrapper
    return decorator


# ===================== 带错误处理的工具 =====================

class ToolExecutionError(Exception):
    """工具执行错误"""
    def __init__(self, tool_name: str, message: str, retryable: bool = False):
        self.tool_name = tool_name
        self.message = message
        self.retryable = retryable
        super().__init__(f"{tool_name}: {message}")


@retry_with_backoff(max_retries=2, retryable_exceptions=(TimeoutError, ConnectionError))
def get_weather_with_retry(city: str) -> dict:
    """带重试的天气查询"""
    import random
    # 模拟偶发失败
    if random.random() < 0.3:  # 30% 失败率
        raise TimeoutError(f"天气 API 请求超时: {city}")
    
    return {"city": city, "temp": random.randint(15, 35), "condition": "晴"}


@retry_with_backoff(max_retries=2, retryable_exceptions=(TimeoutError,))
def get_stock_with_retry(symbol: str) -> dict:
    """带重试的股价查询"""
    import random
    if random.random() < 0.2:
        raise TimeoutError(f"股价 API 请求超时: {symbol}")
    
    return {"symbol": symbol, "price": round(random.uniform(50, 500), 2)}


# ===================== 参数验证 =====================

def validate_tool_arguments(tool_name: str, arguments: dict, schema: dict) -> dict:
    """
    验证工具参数
    
    Args:
        tool_name: 工具名称
        arguments: 实际参数
        schema: 参数 schema
    
    Returns:
        验证后的参数
    
    Raises:
        ValueError: 参数验证失败
    """
    properties = schema.get("parameters", {}).get("properties", {})
    required = schema.get("parameters", {}).get("required", [])
    
    # 检查必需参数
    for param in required:
        if param not in arguments:
            raise ValueError(f"工具 {tool_name} 缺少必需参数: {param}")
    
    # 检查参数类型
    for param, value in arguments.items():
        if param in properties:
            expected_type = properties[param].get("type")
            if expected_type == "string" and not isinstance(value, str):
                raise ValueError(f"参数 {param} 应为字符串类型")
            elif expected_type == "number" and not isinstance(value, (int, float)):
                raise ValueError(f"参数 {param} 应为数字类型")
    
    return arguments


# ===================== 完整的 Agent 循环(带错误处理) =====================

def run_robust_agent(user_input: str) -> str:
    """带完整错误处理的 Agent"""
    
    tools = [
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "查询天气",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {"type": "string", "description": "城市名"}
                    },
                    "required": ["city"]
                }
            }
        },
        {
            "type": "function",
            "function": {
                "name": "get_stock_price",
                "description": "查询股价",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "symbol": {"type": "string", "description": "股票代码"}
                    },
                    "required": ["symbol"]
                }
            }
        }
    ]
    
    TOOL_MAP = {
        "get_weather": get_weather_with_retry,
        "get_stock_price": get_stock_with_retry,
    }
    
    messages = [
        {"role": "system", "content": "你是一个助手。如果工具调用失败,请告知用户并建议重试。"},
        {"role": "user", "content": user_input}
    ]
    
    MAX_ROUNDS = 5
    MAX_RETRIES_PER_TOOL = 2
    
    for round_num in range(MAX_ROUNDS):
        try:
            response = client.chat.completions.create(
                model="gpt-5.4",
                messages=messages,
                tools=tools,
                tool_choice="auto",
            )
        except Exception as e:
            logger.error(f"LLM API 调用失败: {e}")
            return "抱歉,AI 服务暂时不可用,请稍后再试。"
        
        message = response.choices[0].message
        messages.append(message.model_dump())
        
        if not message.tool_calls:
            return message.content
        
        # 执行工具调用
        for tool_call in message.tool_calls:
            func_name = tool_call.function.name
            try:
                arguments = json.loads(tool_call.function.arguments)
            except json.JSONDecodeError as e:
                # JSON 解析失败 - 返回错误给模型
                logger.error(f"参数 JSON 解析失败: {e}")
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": json.dumps({
                        "error": f"参数格式错误: {tool_call.function.arguments}",
                        "suggestion": "请重新生成正确的参数"
                    })
                })
                continue
            
            tool_func = TOOL_MAP.get(func_name)
            if tool_func is None:
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": json.dumps({"error": f"未知工具: {func_name}"})
                })
                continue
            
            # 带本地重试的执行
            last_error = None
            for attempt in range(MAX_RETRIES_PER_TOOL):
                try:
                    result = tool_func(**arguments)
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "content": json.dumps(result, ensure_ascii=False)
                    })
                    last_error = None
                    break
                except Exception as e:
                    last_error = e
                    logger.warning(
                        f"工具 {func_name} 执行失败 (尝试 {attempt + 1}): {e}"
                    )
            
            # 如果所有重试都失败
            if last_error is not None:
                error_result = {
                    "error": str(last_error),
                    "tool": func_name,
                    "retries_exhausted": True
                }
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": json.dumps(error_result, ensure_ascii=False)
                })
    
    return "处理超时,请简化您的问题后重试。"


# 运行测试
if __name__ == "__main__":
    print(run_robust_agent("北京天气怎么样?苹果股价多少?"))

7.2 错误处理最佳实践

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
┌──────────────────────────────────────────────────────┐
│               错误处理决策树                            │
│                                                      │
│  工具调用失败?                                        │
│  ├─ JSON 解析错误 → 返回错误信息,让模型修正参数          │
│  ├─ 参数验证失败 → 返回具体错误原因                      │
│  ├─ 网络/超时错误 → 重试(指数退避)                      │
│  ├─ 4xx 客户端错误 → 不重试,返回错误信息                 │
│  ├─ 5xx 服务端错误 → 重试 2-3 次                        │
│  └─ 重试用尽 → 返回最终错误,让模型告知用户               │
└──────────────────────────────────────────────────────┘

8. Anthropic Claude 工具调用对比

Claude(Anthropic)也支持类似的工具调用功能,但 API 格式有所不同:

8.1 格式对比

 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
# ===================== OpenAI GPT-5.4 格式 =====================
openai_tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名"}
                },
                "required": ["city"]
            }
        }
    }
]


# ===================== Anthropic Claude 格式 =====================
claude_tools = [
    {
        "name": "get_weather",
        "description": "查询天气",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "城市名"}
            },
            "required": ["city"]
        }
    }
]

8.2 调用方式对比

 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
"""
OpenAI vs Anthropic 工具调用对比
"""
import os

# ---------- OpenAI 方式 ----------
from openai import OpenAI

openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# OpenAI: 工具在 tools 参数中,结果通过 role="tool" 消息返回
openai_response = openai_client.chat.completions.create(
    model="gpt-5.4",
    messages=[{"role": "user", "content": "北京天气?"}],
    tools=openai_tools,
)
# tool_call 结果: response.choices[0].message.tool_calls


# ---------- Anthropic 方式 ----------
import anthropic

claude_client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

# Anthropic: 工具在 tools 参数中,结果通过 user 角色的 tool_result 返回
claude_response = claude_client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[{"role": "user", "content": "北京天气?"}],
    tools=claude_tools,
)

# Claude 工具调用的返回格式不同
for content_block in claude_response.content:
    if content_block.type == "tool_use":
        print(f"工具: {content_block.name}")
        print(f"参数: {content_block.input}")
        print(f"ID: {content_block.id}")

8.3 关键差异总结

特性 OpenAI GPT-5.4 Anthropic Claude
工具定义字段 function.parameters input_schema
工具角色 role="tool" 消息 user 消息中的 tool_result
并行调用 ✅ 原生支持 ✅ 支持
流式工具调用 ✅ 增量拼接 ✅ 增量拼接
强制调用 tool_choice="required" tool_choice={"type": "any"}
指定函数 tool_choice={"type":"function",...} tool_choice={"type":"tool","name":...}
错误处理 返回 tool 消息 返回 tool_result with is_error

9. 构建通用工具注册中心

在实际项目中,我们通常需要管理大量工具。一个好的工具注册中心模式能极大提升可维护性。

9.1 工具注册中心实现

  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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
"""
通用工具注册中心 (Tool Registry)
支持自动注册、自动生成 schema、工具发现
"""
import json
import inspect
import functools
from typing import Any, Callable, Optional
from openai import OpenAI

client = OpenAI()


class ToolRegistry:
    """
    工具注册中心
    
    用法:
        registry = ToolRegistry()
        
        @registry.register(
            description="查询天气",
            tags=["weather", "utility"]
        )
        def get_weather(city: str, unit: str = "celsius") -> dict:
            ...
        
        tools = registry.to_openai_tools()
        result = registry.execute("get_weather", {"city": "北京"})
    """
    
    def __init__(self):
        self._tools: dict[str, dict] = {}
    
    def register(
        self,
        description: str = None,
        tags: list[str] = None,
    ):
        """
        注册工具的装饰器
        
        Args:
            description: 工具描述(如果为 None,使用函数 docstring)
            tags: 工具标签,用于分类和筛选
        """
        def decorator(func: Callable):
            name = func.__name__
            
            # 自动生成 JSON Schema
            schema = self._generate_schema(func, description)
            
            self._tools[name] = {
                "function": func,
                "schema": schema,
                "tags": tags or [],
                "name": name,
            }
            
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)
            
            return wrapper
        
        return decorator
    
    def _generate_schema(self, func: Callable, description: str = None) -> dict:
        """从函数签名自动生成 JSON Schema"""
        sig = inspect.signature(func)
        hints = func.__annotations__
        
        properties = {}
        required = []
        
        for param_name, param in sig.parameters.items():
            if param_name in ("self", "cls"):
                continue
            
            param_info = {"type": "string"}  # 默认类型
            
            # 从类型注解推断类型
            if param_name in hints:
                hint = hints[param_name]
                if hint == int:
                    param_info["type"] = "integer"
                elif hint == float:
                    param_info["type"] = "number"
                elif hint == bool:
                    param_info["type"] = "boolean"
                elif hint == list:
                    param_info["type"] = "array"
                elif hint == dict:
                    param_info["type"] = "object"
                else:
                    param_info["type"] = "string"
            
            # 从默认值判断是否必需
            if param.default is inspect.Parameter.empty:
                required.append(param_name)
            else:
                if param.default is not None:
                    param_info["default"] = param.default
            
            # 使用参数名作为描述(可以改进)
            param_info["description"] = param_name
            
            properties[param_name] = param_info
        
        # 获取描述
        desc = description or func.__doc__ or f"执行 {func.__name__}"
        
        return {
            "type": "function",
            "function": {
                "name": func.__name__,
                "description": desc,
                "parameters": {
                    "type": "object",
                    "properties": properties,
                    "required": required,
                }
            }
        }
    
    def execute(self, name: str, arguments: dict) -> Any:
        """执行指定工具"""
        if name not in self._tools:
            return {"error": f"未知工具: {name}"}
        
        tool = self._tools[name]
        try:
            return tool["function"](**arguments)
        except Exception as e:
            return {"error": f"执行失败: {str(e)}"}
    
    def to_openai_tools(self, tags: list[str] = None) -> list[dict]:
        """转换为 OpenAI API 格式的 tools 列表"""
        tools = []
        for tool_info in self._tools.values():
            if tags and not any(t in tool_info["tags"] for t in tags):
                continue
            tools.append(tool_info["schema"])
        return tools
    
    def to_anthropic_tools(self, tags: list[str] = None) -> list[dict]:
        """转换为 Anthropic API 格式的 tools 列表"""
        tools = []
        for tool_info in self._tools.values():
            if tags and not any(t in tool_info["tags"] for t in tags):
                continue
            schema = tool_info["schema"]["function"].copy()
            # 转换格式
            anthropic_tool = {
                "name": schema["name"],
                "description": schema["description"],
                "input_schema": schema["parameters"],
            }
            tools.append(anthropic_tool)
        return tools
    
    def list_tools(self) -> list[dict]:
        """列出所有已注册的工具"""
        return [
            {
                "name": info["name"],
                "description": info["schema"]["function"]["description"],
                "tags": info["tags"],
                "parameters": list(info["schema"]["function"]["parameters"]["properties"].keys()),
            }
            for info in self._tools.values()
        ]


# ===================== 使用示例 =====================

registry = ToolRegistry()


@registry.register(description="查询指定城市的天气信息", tags=["weather"])
def get_weather(city: str, unit: str = "celsius") -> dict:
    """查询城市天气"""
    import random
    return {
        "city": city,
        "temperature": f"{random.randint(15, 35)}°{'C' if unit == 'celsius' else 'F'}",
        "condition": random.choice(["晴", "多云", "雨"]),
    }


@registry.register(description="查询股票实时价格", tags=["finance"])
def get_stock_price(symbol: str, market: str = "us") -> dict:
    """查询股价"""
    import random
    return {
        "symbol": symbol,
        "price": f"${round(random.uniform(50, 500), 2)}",
        "change": f"{random.uniform(-5, 5):+.2f}%",
    }


@registry.register(description="执行数学计算", tags=["utility", "math"])
def calculate(expression: str) -> dict:
    """安全数学计算"""
    # 简化的实现
    try:
        result = eval(expression, {"__builtins__": {}}, {})
        return {"expression": expression, "result": result}
    except Exception as e:
        return {"error": str(e)}


@registry.register(description="发送邮件", tags=["communication"])
def send_email(to: str, subject: str, body: str) -> dict:
    """发送邮件(模拟)"""
    return {"status": "sent", "to": to, "subject": subject}


# ===================== 完整的 Agent =====================

class ToolAgent:
    """基于工具注册中心的 Agent"""
    
    def __init__(self, registry: ToolRegistry, model: str = "gpt-5.4"):
        self.registry = registry
        self.model = model
        self.client = OpenAI()
    
    def chat(self, user_input: str, context: list = None) -> str:
        """执行对话"""
        if context is None:
            context = [
                {"role": "system", "content": "你是一个智能助手,可以使用各种工具帮助用户。"}
            ]
        
        context.append({"role": "user", "content": user_input})
        
        for _ in range(5):  # 最多5轮工具调用
            response = self.client.chat.completions.create(
                model=self.model,
                messages=context,
                tools=self.registry.to_openai_tools(),
                tool_choice="auto",
            )
            
            message = response.choices[0].message
            context.append(message.model_dump())
            
            if not message.tool_calls:
                return message.content
            
            for tc in message.tool_calls:
                args = json.loads(tc.function.arguments)
                result = self.registry.execute(tc.function.name, args)
                context.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": json.dumps(result, ensure_ascii=False)
                })
        
        return "处理轮次超限。"
    
    def chat_with_tags(self, user_input: str, tags: list[str]) -> str:
        """仅使用特定标签的工具"""
        # 临时过滤工具
        original_to_openai = self.registry.to_openai_tools
        self.registry.to_openai_tools = lambda t=None: original_to_openai(tags)
        result = self.chat(user_input)
        self.registry.to_openai_tools = original_to_openai
        return result


# 使用
if __name__ == "__main__":
    # 查看所有工具
    print("已注册工具:")
    for tool in registry.list_tools():
        print(f"  - {tool['name']}: {tool['description']} {tool['tags']}")
    
    # 使用 Agent
    agent = ToolAgent(registry)
    # answer = agent.chat("北京天气怎么样?帮我算算 32 摄氏度等于多少华氏度")
    # print(answer)

9.2 工具注册中心的优势

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
传统方式:                    注册中心方式:
                          
def tool_a(): ...           @registry.register(description="...")
def tool_b(): ...           def tool_a(): ...
                            @registry.register(description="...")
tools = [                   def tool_b(): ...
  define_a(),                    ...
  define_b(),               tools = registry.to_openai_tools()
]                           result = registry.execute(name, args)
                            

 手动维护 schema            自动生成 schema
 工具散落各处                统一管理
 难以扩展                    装饰器注册,即插即用

10. 安全性考量

Function Calling 引入了安全风险——LLM 生成的参数可能被恶意利用。以下是关键的安全实践:

10.1 输入验证

  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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
"""
工具调用安全最佳实践
"""
import re
import hashlib
import logging
from typing import Any
from functools import wraps

logger = logging.getLogger(__name__)


# ===================== 1. 输入净化 =====================

def sanitize_input(value: str, max_length: int = 500) -> str:
    """净化用户输入"""
    if not isinstance(value, str):
        return str(value)
    
    # 截断过长输入
    value = value[:max_length]
    
    # 移除潜在危险字符
    value = re.sub(r'[<>"\';]', '', value)
    
    # 移除控制字符
    value = re.sub(r'[\x00-\x1f\x7f]', '', value)
    
    return value.strip()


# ===================== 2. SQL 注入防护 =====================

def safe_sql_param(value: str) -> str:
    """确保 SQL 参数安全"""
    # 如果包含 SQL 关键字,拒绝
    sql_keywords = ['DROP', 'DELETE', 'INSERT', 'UPDATE', 'UNION', 'EXEC']
    for keyword in sql_keywords:
        if keyword.lower() in value.lower():
            raise ValueError(f"输入包含不允许的 SQL 关键字: {keyword}")
    return sanitize_input(value)


# ===================== 3. 文件路径安全 =====================

def safe_file_path(path: str, allowed_dirs: list[str] = None) -> str:
    """验证文件路径安全"""
    import os
    
    # 规范化路径
    normalized = os.path.normpath(path)
    
    # 防止路径遍历攻击
    if '..' in normalized or normalized.startswith('/'):
        raise ValueError(f"不允许的文件路径: {path}")
    
    # 检查是否在允许的目录中
    if allowed_dirs:
        is_allowed = any(normalized.startswith(d) for d in allowed_dirs)
        if not is_allowed:
            raise ValueError(f"路径不在允许范围内: {path}")
    
    return normalized


# ===================== 4. 速率限制 =====================

class RateLimiter:
    """简单的速率限制器"""
    
    def __init__(self, max_calls: int = 10, window_seconds: int = 60):
        self.max_calls = max_calls
        self.window_seconds = window_seconds
        self._calls = {}  # tool_name -> [(timestamp, count)]
    
    def check(self, tool_name: str) -> bool:
        """检查是否允许执行"""
        import time
        now = time.time()
        cutoff = now - self.window_seconds
        
        if tool_name not in self._calls:
            self._calls[tool_name] = []
        
        # 清理过期记录
        self._calls[tool_name] = [
            ts for ts in self._calls[tool_name] if ts > cutoff
        ]
        
        if len(self._calls[tool_name]) >= self.max_calls:
            logger.warning(f"工具 {tool_name} 触发速率限制")
            return False
        
        self._calls[tool_name].append(now)
        return True


# ===================== 5. 沙箱执行 =====================

def sandboxed_execution(func):
    """沙箱化工具执行(简化版)"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        import signal
        
        def timeout_handler(signum, frame):
            raise TimeoutError("工具执行超时")
        
        # 设置超时(5秒)
        old_handler = signal.signal(signal.SIGALRM, timeout_handler)
        signal.alarm(5)
        
        try:
            result = func(*args, **kwargs)
            return result
        except TimeoutError:
            logger.error(f"工具 {func.__name__} 执行超时")
            return {"error": "执行超时"}
        except Exception as e:
            logger.error(f"工具 {func.__name__} 执行异常: {e}")
            return {"error": f"执行异常: {str(e)}"}
        finally:
            signal.alarm(0)
            signal.signal(signal.SIGALRM, old_handler)
    
    return wrapper


# ===================== 6. 敏感操作确认 =====================

def require_confirmation(tool_name: str, func):
    """对敏感工具操作添加确认日志"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 记录操作审计日志
        logger.info(
            f"[AUDIT] 工具调用: {tool_name}, "
            f"参数: {sanitize_input(str(args))}, {sanitize_input(str(kwargs))}"
        )
        
        # 执行
        result = func(*args, **kwargs)
        
        # 记录结果摘要
        logger.info(f"[AUDIT] 工具 {tool_name} 执行完成")
        return result
    
    return wrapper


# ===================== 安全工具示例 =====================

rate_limiter = RateLimiter(max_calls=10, window_seconds=60)


@sandboxed_execution
def safe_database_query(sql: str) -> dict:
    """安全的数据库查询工具"""
    # 参数验证
    sql = safe_sql_param(sql)
    
    # 速率限制
    if not rate_limiter.check("database_query"):
        return {"error": "查询频率过高,请稍后再试"}
    
    # 执行查询(模拟)
    return {"result": "查询结果", "sql": sql}


@sandboxed_execution
def safe_web_request(url: str) -> dict:
    """安全的网页请求工具"""
    import re
    
    # URL 验证
    url_pattern = re.compile(
        r'^https?://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(/.*)?$'
    )
    if not url_pattern.match(url):
        return {"error": "无效的 URL 格式"}
    
    # 禁止内网地址
    blocked = ['localhost', '127.0.0.1', '10.', '192.168.', '172.']
    for b in blocked:
        if b in url:
            return {"error": "不允许访问内网地址"}
    
    # 速率限制
    if not rate_limiter.check("web_request"):
        return {"error": "请求频率过高"}
    
    # 执行请求(模拟)
    return {"status": 200, "url": url}


# ===================== 安全审计日志 =====================

class SecurityAuditor:
    """安全审计器"""
    
    def __init__(self):
        self.log = []
    
    def record(self, tool_name: str, arguments: dict, result: Any, user: str = "system"):
        """记录工具调用"""
        import time
        entry = {
            "timestamp": time.time(),
            "tool": tool_name,
            "arguments": {k: sanitize_input(str(v))[:100] for k, v in arguments.items()},
            "success": "error" not in str(result),
            "user": user,
        }
        self.log.append(entry)
        logger.info(f"[SECURITY] {entry}")
    
    def check_anomalies(self) -> list[dict]:
        """检查异常调用模式"""
        anomalies = []
        
        # 检查频率异常
        from collections import Counter
        tool_counts = Counter(e["tool"] for e in self.log[-100:])
        for tool, count in tool_counts.most_common():
            if count > 50:
                anomalies.append({
                    "type": "high_frequency",
                    "tool": tool,
                    "count": count,
                })
        
        return anomalies

10.2 安全检查清单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
✅ 安全检查清单:
                                
□ 所有工具输入都经过验证和净化
□ 文件路径工具限制了访问目录
□ 数据库工具只允许 SELECT 查询
□ 每个工具有独立的速率限制
□ 敏感操作有审计日志
□ 工具执行有超时限制(防止无限循环)
□ 禁止访问内网地址
□ 参数长度有上限
□ 工具结果在返回给模型前脱敏
□ 定期检查异常调用模式

11. 实战踩坑指南

以下是 Function Calling 开发中最常见的坑和解决方案:

11.1 坑1:工具描述写得太差

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ❌ 错误:描述太简短,模型经常调错
tools = [{
    "type": "function",
    "function": {
        "name": "search",
        "description": "搜索",
        "parameters": { ... }
    }
}]

# ✅ 正确:描述要具体,说明何时使用、何时不使用
tools = [{
    "type": "function",
    "function": {
        "name": "search_products",
        "description": (
            "在商品数据库中搜索产品。当用户询问某个产品的价格、"
            "库存、规格或想要比较不同产品时使用此工具。"
            "注意:此工具不支持搜索已下架的商品。"
            "如果用户只是在闲聊或问一般性问题,不要调用此工具。"
        ),
        "parameters": { ... }
    }
}]

11.2 坑2:JSON 参数解析失败

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 模型返回的 arguments 有时不是合法 JSON
# 特别是在函数名/字符串中有中文或特殊字符时

# ❌ 错误:直接解析,不处理异常
args = json.loads(tool_call.function.arguments)

# ✅ 正确:处理所有可能的解析错误
try:
    args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError as e:
    logger.error(f"JSON 解析失败: {e}")
    logger.error(f"原始内容: {tool_call.function.arguments}")
    # 返回错误信息给模型,让它重试
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps({
            "error": f"参数格式错误,请使用合法的 JSON 格式: {str(e)}",
            "received": tool_call.function.arguments[:200]
        })
    })

11.3 坑3:无限循环

 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
# 模型可能反复调用同一个工具,进入死循环

# ❌ 错误:没有限制循环次数
while True:
    response = client.chat.completions.create(...)
    # 可能永远循环

# ✅ 正确:设置最大循环次数和重复检测
def run_agent_safe(user_input, max_rounds=5):
    seen_tools = {}  # 记录工具调用历史
    
    for round_num in range(max_rounds):
        response = client.chat.completions.create(...)
        message = response.choices[0].message
        
        if not message.tool_calls:
            return message.content
        
        for tc in message.tool_calls:
            tool_key = f"{tc.function.name}:{tc.function.arguments}"
            
            # 检测重复调用
            if tool_key in seen_tools:
                seen_tools[tool_key] += 1
                if seen_tools[tool_key] > 2:
                    # 超过2次相同调用,强制结束
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tc.id,
                        "content": json.dumps({
                            "error": "此工具已被调用多次且返回相同结果,请直接使用已有数据回答用户。"
                        })
                    })
                    continue
            else:
                seen_tools[tool_key] = 1
            
            # 正常执行...
    
    return "处理轮次超限,请简化问题。"

11.4 坑4:工具结果太大

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 工具返回大量数据会消耗大量 tokens,且可能超过模型上下文

# ❌ 错误:返回完整的数据库查询结果
result = db.query("SELECT * FROM products LIMIT 1000")

# ✅ 正确:截断和摘要
def get_search_results(query: str, max_results: int = 5) -> dict:
    """限制返回结果数量"""
    results = db.query(query, limit=max_results)
    
    return {
        "count": len(results),
        "results": results[:max_results],
        "summary": f"共找到 {len(results)} 条结果,显示前 {max_results} 条"
    }

11.5 坑5:忽略 tool_choice 的影响

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 不同的 tool_choice 对行为影响很大

# "auto" - 最灵活,但模型可能不调用需要的工具
tool_choice="auto"

# "required" - 强制调用,但每次都会调用(浪费 token)
tool_choice="required"

# 指定特定工具 - 精确控制
tool_choice={"type": "function", "function": {"name": "get_weather"}}

# 实际项目中应该根据场景动态选择
def smart_tool_choice(user_input: str, context: list) -> str:
    """根据用户意图智能选择 tool_choice"""
    # 如果用户明确要求使用工具
    if any(kw in user_input for kw in ["查询", "查一下", "帮我查", "search", "lookup"]):
        return "required"
    
    # 如果已经有多轮对话,避免重复调用
    if len(context) > 5:
        return "auto"
    
    return "auto"

11.6 坑6:不处理并发请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# ❌ 错误:使用全局共享的状态
class Agent:
    def __init__(self):
        self.messages = []  # 多个并发请求共享同一个消息列表!

# ✅ 正确:每个请求独立的状态
class Agent:
    def __init__(self):
        pass  # 不存储状态
    
    def chat(self, user_input: str, session_id: str) -> str:
        # 使用 session_id 管理独立对话
        messages = get_session_messages(session_id)
        messages.append({"role": "user", "content": user_input})
        # ... 处理
        save_session_messages(session_id, messages)
        return result

11.7 坑7:忘记处理模型的 content 和 tool_calls 同时存在

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# GPT-5.4 有时会同时返回 content 和 tool_calls
# 例如:content="让我查一下..." + tool_calls=[...]

# ❌ 错误:只处理 tool_calls
if message.tool_calls:
    for tc in message.tool_calls:
        execute_tool(tc)

# ✅ 正确:同时处理两者
# 先把 assistant 的完整回复加入历史
messages.append(message.model_dump())

# 然后执行工具
if message.tool_calls:
    for tc in message.tool_calls:
        result = execute_tool(tc)
        messages.append({
            "role": "tool",
            "tool_call_id": tc.id,
            "content": json.dumps(result)
        })

11.8 完整踩坑速查表

症状 解决方案
描述太差 模型调错工具 写详细的 description
JSON 解析失败 工具调用中断 try/except 包裹
无限循环 消耗大量 token 设置 max_rounds + 重复检测
结果太大 超出上下文 限制返回数据量
tool_choice 不当 不调用/多调用 根据场景动态选择
并发问题 状态混乱 每个请求独立状态
content+tool_calls 丢失信息 完整保存 assistant 消息

12. 总结

Function Calling 是构建 AI Agent 的核心能力。通过本文,你已经掌握了:

核心要点回顾

  1. Function Calling 本质:LLM 生成结构化 JSON,由应用代码执行——模型不直接调用任何 API
  2. 多工具并行:GPT-5.4 支持一次返回多个 tool_calls,配合 ThreadPoolExecutor 可并行执行
  3. 流式处理:tool_calls 的 arguments 是增量发送的,必须在循环中拼接
  4. 错误处理:用重试+指数退避处理网络错误,用 JSON 解析保护处理格式错误
  5. 安全性:输入验证、速率限制、路径沙箱缺一不可
  6. 工具注册中心:装饰器+自动 schema 生成是管理大量工具的最佳实践

成本优化建议

场景 推荐模型 原因
复杂推理+多工具 GPT-5.4 推理能力强
简单单工具调用 GPT-5.4-mini 性价比高
高频调用 GPT-5.4-mini 成本低 3x
需要1M上下文 GPT-5.4 mini 也支持但大上下文更适合强模型

下一步


本文基于 2026 年 6 月的 GPT-5.4 API 编写。如 API 有更新,请以官方文档为准。