logo蛋烘糕.

不写博客的工程师不是好的产品经理

AI 技术演进与核心算法实战 | 第十七篇:Function Calling 原理:从 Prompt 伪装到原生支持的技术演进与参数绑定机制

Cover Image for AI 技术演进与核心算法实战 | 第十七篇:Function Calling 原理:从 Prompt 伪装到原生支持的技术演进与参数绑定机制
蛋烘糕
蛋烘糕

如果说 CoT 和 ToT 是给模型装上了"思考的大脑",那么 Function Calling 就是给模型装上了"执行的双手"——让它从纸上谈兵的书生,变成能解决实际问题的实干家。

上一篇 中,我们探讨了 Graph RAG 如何通过知识图谱增强全局理解能力。但一个关键问题仍然存在:即使模型理解了复杂的知识网络,它如何将这些理解转化为实际行动?

想象这些真实场景:

场景 1:查询天气

用户:"帮我查一下北京明天的天气"

传统对话模型的局限:
- 可以生成"北京明天晴朗,气温 25°C"这样的文本
- ❌ 但无法真正调用天气 API 获取实时数据
- ❌ 无法触发后续的穿衣建议、出行提醒等动作

缺失的能力:工具调用(Tool Use)

场景 2:数据分析

用户:"分析一下上个月的销售数据,找出 Top 3 产品"

纯文本模型的局限:
- 可以解释"什么是销售分析"
- ❌ 但无法执行 SQL 查询数据库
- ❌ 无法运行 Python 代码进行统计分析

缺失的能力:代码解释器(Code Interpreter)

场景 3:多步骤任务

用户:"帮我订一张下周从北京到上海的机票,要靠窗的位置"

单轮对话的局限:
- 可以理解"订票"这个意图
- ❌ 但无法完成"搜索航班→选择座位→支付订单"的多步流程
- ❌ 无法与多个 API(航班搜索、选座、支付)交互

缺失的能力:工作流编排(Workflow Orchestration)

本篇是 《AI 技术演进与核心算法实战》第四模块的第一篇。我们将深入探讨 Function Calling(函数调用) 的核心原理,这是赋予模型"行动能力"的关键技术。


1. 从"坐而论道"到"起而行之"

1.1 一个思想实验:军师 vs 将军

让我们通过一个思想实验来理解 Function Calling 的必要性。

场景一:军师(纯对话模型)

君王:"敌军压境,该如何应对?"

军师的回答:
"臣以为,当务之急有三:
 其一,加固城防,囤积粮草;
 其二,遣使求和,缓兵待援;
 其三,招募勇士,训练精兵。"

军师的特点是:只能给出建议,无法亲自执行。他的输出停留在"语言层面"。

场景二:将军(支持 Function Calling 的模型)

君王:"敌军压境,该如何应对?"

将军的回答 + 行动:
"臣这就去办!"
→ 立即调用 send_messenger() 派遣信使
→ 调用 reinforce_city_walls() 加固城墙
→ 调用 recruit_soldiers() 招募士兵

将军的特点是:既能理解意图,又能付诸行动。他的输出连接到"执行层面"。

关键洞察:Function Calling 的本质,就是让模型从"军师"进化为"将军"——从单纯的"输出文本"升级为"输出行动"。

1.2 Function Calling 的定义与价值

定义:Function Calling(函数调用)是指大模型能够根据用户请求,自动选择并调用合适的函数或工具来完成特定任务的能力。

核心价值

维度 纯对话模型 Function Calling 模型
能力边界 受限于训练数据的截止时间 可通过 API 访问实时信息
准确性 可能产生幻觉(Hallucination) 基于真实工具返回的数据
行动力 只能"说说而已" 可以实际改变世界
专业性 通用知识,缺乏深度 可调用专业工具(如计算器、搜索引擎)

1.3 技术演进的三个阶段

Function Calling 的发展经历了三个关键阶段:

阶段一:Prompt 伪装(2022-2023 年初)

  • 代表方案:早期 LangChain、自定义 Prompt 模板
  • 特点:通过精心设计的提示词"骗"模型输出类似函数调用的格式
  • 问题:不稳定,依赖模型的"心情"

阶段二:JSON Schema 约束(2023 年中)

  • 代表方案:Guidance、lm-format-enforcer
  • 特点:使用语法约束强制模型输出合法的 JSON 格式
  • 进步:格式 100% 正确,但仍然需要解析

阶段三:原生支持(2023 年底至今)

  • 代表方案:OpenAI Function Calling、Anthropic Tool Use
  • 特点:模型在训练阶段就学习了函数调用的语义
  • 优势:最稳定、最准确、最易用

2. Prompt 伪装阶段:用提示词"骗"出函数调用

2.1 基本思路:给模型一个"角色设定"

最早的 Function Calling 实现非常简单粗暴——通过 Prompt 告诉模型"你是一个函数调用器"

示例 Prompt

你是一个智能助手,负责调用 API 来完成任务。

可用的函数有:
1. weather_api(city: str, unit: str) -> 查询天气
2. calculator(expression: str) -> 计算数学表达式
3. search_web(query: str) -> 搜索互联网

当用户请求需要调用函数时,请按以下格式输出:
【FUNCTION_CALL】
函数名:weather_api
参数:{"city": "Beijing", "unit": "celsius"}
【/FUNCTION_CALL】

如果不需要调用函数,直接回答问题。

用户:"北京明天天气怎么样?"

模型的输出

【FUNCTION_CALL】
函数名:weather_api
参数:{"city": "Beijing", "unit": "celsius"}
【/FUNCTION_CALL】

然后程序解析这段文本,提取函数名和参数,真正执行调用。

2.2 核心挑战:格式不稳定性

这种方法的最大问题是:模型并不总是遵守格式

失败案例 1:格式错误

【FUNCTION_CALL】
weather_api(Beijing)  ← 忘记写参数键名
【/FUNCTION_CALL】

失败案例 2:忘记调用

北京明天天气晴朗,气温约 25 摄氏度。  ← 模型自己"编"了答案

失败案例 3:多余解释

我需要调用天气 API 来获取信息。

【FUNCTION_CALL】
函数名:weather_api
参数:{"city": "Beijing"}
【/FUNCTION_CALL】

现在让我来执行这个调用...  ← 多余的废话

2.3 改进策略:Few-shot 示例强化

为了提高稳定性,研究者发现 Few-shot Learning(少样本学习) 非常有效。

改进后的 Prompt

你是一个智能助手,负责调用 API 来完成任务。

可用函数:
- weather_api(city: str, unit: str)
- calculator(expression: str)

示例 1:
用户:"北京天气怎么样?"
助手:【FUNCTION_CALL】{"name":"weather_api","arguments":{"city":"Beijing","unit":"celsius"}}【/FUNCTION_CALL】

示例 2:
用户:"123 乘以 456 等于多少?"
助手:【FUNCTION_CALL】{"name":"calculator","arguments":{"expression":"123*456"}}【/FUNCTION_CALL】

示例 3:
用户:"你好"
助手:您好!有什么我可以帮助您的吗?← 不需要调用函数时正常回答

现在请回答:
用户:"上海的气温是多少?"

效果提升

  • 通过提供正例和反例,模型学会了"什么时候调用"和"怎么调用"
  • 格式稳定性从约 70% 提升到 85-90%

2.4 物理意义:条件反射的建立

从行为主义心理学的角度来看,Prompt 伪装的本质是建立条件反射

Prompt 伪装的心理学解释:条件反射建立 训练前:自由反应 刺激:"北京天气" 自由联想 → 任意回答 训练后:条件反射 刺激:"北京天气" 【FUNCTION_CALL】 weather_api(...) 巴甫洛夫的狗 vs Prompt 工程的模型 铃声 + 食物 → 流口水 | 天气问题 + Few-shot → FUNCTION_CALL 本质:通过重复的模式展示,让模型建立"问题类型→输出格式"的映射

图解说明:如上图所示,Prompt 伪装的原理与巴甫洛夫的经典条件反射实验如出一辙。通过反复展示"某类问题→某种输出格式"的配对,模型逐渐建立起模式关联。但这只是"表面功夫"——模型并没有真正理解函数的概念,只是在模仿格式。


3. JSON Schema 约束:给输出装上"护栏"

3.1 核心问题:为什么需要强制约束?

Prompt 伪装的根本问题是:它依赖于模型的"自觉性"

就像老师对学生说:“请用规范的格式答题”。好学生会照做,但总有学生会偷懒、粗心或者理解偏差。

JSON Schema 约束的思路:与其依赖自觉性,不如直接禁止错误答案的出现

JSON Schema 约束:从"建议"到"强制" 无约束:自由生成 模型输出的概率分布: 85% 正确格式 ✓ 10% 格式错误 ✗ 5% 完全跑题 ✗✗ 有约束:Mask 非法 token 约束解码过程: 1. 计算 logits 2. 检查 Schema 规则 3. Mask 掉所有非法 token 4. Softmax 得到新分布 100% 合法输出 ✓✓✓ 关键区别:事后检查 vs 事前预防 Prompt 伪装 = 考完后老师批改卷子(错了也晚了) JSON Schema = 只给你正确的答题卡(想错都难) 技术实现:基于 Earley 算法的增量式 CFG 解析器(详见第 9 篇)

图解说明:如上图所示,JSON Schema 约束通过在生成过程中实时检查语法规则,将不符合 Schema 的 token 全部 mask 掉(即将其概率设为 0),从而保证输出 100% 合法。这比 Prompt 伪装的"软性建议"要可靠得多。

3.2 JSON Schema 基础:定义数据结构

JSON Schema 是一种用来描述 JSON 数据结构的规范语言。

简单示例

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "函数名称"
    },
    "arguments": {
      "type": "object",
      "description": "函数参数"
    }
  },
  "required": ["name", "arguments"]
}

这个 Schema 定义了:

  • 必须是一个对象(type: object
  • 必须包含 name 字段(字符串类型)
  • 必须包含 arguments 字段(对象类型)

3.3 实战:使用 lm-format-enforcer 实现约束

让我们通过一个实际例子来展示如何使用 JSON Schema 约束。

示例需求:让模型调用天气查询函数

第一步:定义 Schema

from pydantic import BaseModel, Field

class FunctionCall(BaseModel):
    """函数调用的标准格式"""
    
    name: str = Field(
        description="函数名称,必须是以下之一:weather_api, calculator, search_web"
    )
    
    arguments: dict = Field(
        description="函数的参数,键值对形式"
    )
    
    thought: str = Field(
        default="",
        description="可选的思考过程,用于解释为什么选择这个函数"
    )

# 转换为 JSON Schema
schema = FunctionCall.model_json_schema()
print(schema)

第二步:使用约束解码器

from lmformatenforcer import JsonSchemaOutputParser
from transformers import AutoTokenizer, AutoModelForCausalLM

# 加载模型和分词器
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf")

# 创建 JSON Schema 解析器
parser = JsonSchemaOutputParser(schema)

# 构建 Prompt
prompt = """
用户:"北京明天天气怎么样?"

请以 JSON 格式返回函数调用:
"""

# 约束生成
from transformers import GenerationConfig

inputs = tokenizer(prompt, return_tensors="pt")
outputs = model.generate(
    **inputs,
    max_new_tokens=100,
    prefix_allowed_tokens_fn=parser.get_prefix_allowed_tokens_fn(
        batch_id=0, 
        input_ids=inputs["input_ids"][0]
    )
)

result = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(result)

输出结果

{
  "name": "weather_api",
  "arguments": {
    "city": "Beijing",
    "unit": "celsius"
  },
  "thought": "用户询问天气,需要调用天气查询 API"
}

关键优势

  • 100% 格式正确:不可能输出非法 JSON
  • 类型安全:自动验证字段类型和必填项
  • 自文档化:Schema 本身就是文档

3.4 局限性:仍然需要后处理

虽然 JSON Schema 约束解决了格式问题,但它仍然存在一个根本性局限

模型输出的仍然是"文本",需要程序解析后才能执行。

模型输出 → JSON 文本 → 程序解析 → 提取函数名和参数 → 真正调用

这个链条中任何一个环节出错都会导致失败:

  • 模型可能选择不存在的函数
  • 参数类型可能不正确(如把数字传成字符串)
  • 缺少必要的参数

理想的情况:模型应该真正理解函数的含义,而不是机械地输出格式。这就是原生 Function Calling 要解决的问题。


4. 原生 Function Calling:模型真正"理解"了工具

4.1 什么是"原生支持"?

2023 年底,OpenAI 在 GPT-4 Turbo 中首次推出了原生 Function Calling功能。随后,Anthropic、Google 等公司纷纷跟进。

"原生"的含义

  • 训练阶段就学习了函数调用的语义
  • 内化了工具的用途,而不是表面的格式
  • 端到端优化,不需要中间的解析层
原生 Function Calling:从"模仿"到"理解" Prompt 工程:表面模仿 训练数据:通用文本 推理时:临时加 Prompt 模型认知:"这是一种格式" 输出格式正确 ≠ 理解函数 原生支持:深度理解 训练数据:包含函数调用样本 微调目标:学会选择合适工具 奖励信号:调用成功→正向反馈 真正理解:何时调用 + 为何调用 工具使用能力成为模型"本能" 类比:学外语 vs 母语思维 Prompt 伪装 = 背模板的英语学习者(说话前先想语法) 原生支持 = 英语母语者(不假思索脱口而出) 本质差异:是否将工具使用内化为"直觉"

图解说明:如上图所示,原生 Function Calling 通过在训练阶段就引入函数调用样本,并使用强化学习优化,让模型真正理解了工具的用途。这比 Prompt 伪装的"表面模仿"要深刻得多。

4.2 OpenAI Function Calling 实战

让我们通过 OpenAI 的官方 API 来体验原生 Function Calling。

第一步:定义函数描述

from openai import OpenAI

client = OpenAI(api_key="your-api-key")

# 定义可用工具列表
tools = [
    {
        "type": "function",
        "function": {
            "name": "weather_api",
            "description": "查询指定城市的当前天气情况",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如'Beijing'、'Shanghai'"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位"
                    }
                },
                "required": ["city", "unit"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "计算数学表达式的值",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "数学表达式,如'123 * 456'"
                    }
                },
                "required": ["expression"]
            }
        }
    }
]

第二步:发送请求

response = client.chat.completions.create(
    model="gpt-4-turbo-preview",
    messages=[
        {"role": "user", "content": "北京明天天气怎么样?"}
    ],
    tools=tools,
    tool_choice="auto"  # 让模型自动决定是否调用工具
)

print(response.choices[0].message)

第三步:解析响应

message = response.choices[0].message

# 检查是否有工具调用
if message.tool_calls:
    for tool_call in message.tool_calls:
        print(f"函数名:{tool_call.function.name}")
        print(f"参数:{tool_call.function.arguments}")
        
        # 真正执行函数调用
        if tool_call.function.name == "weather_api":
            args = eval(tool_call.function.arguments)
            weather_result = call_weather_api(args['city'], args['unit'])
            
            # 将结果返回给模型,让它生成最终回答
            final_response = client.chat.completions.create(
                model="gpt-4-turbo-preview",
                messages=[
                    {"role": "user", "content": "北京明天天气怎么样?"},
                    {"role": "assistant", "content": None, "tool_calls": [tool_call]},
                    {"role": "tool", "content": weather_result, "tool_call_id": tool_call.id}
                ]
            )
            print(final_response.choices[0].message.content)
else:
    # 不需要调用工具,直接回答
    print(message.content)

典型输出

函数名:weather_api
参数:{"city": "Beijing", "unit": "celsius"}

最终回答:北京明天的天气晴朗,最高气温 25°C,最低气温 15°C,空气质量良好。

4.3 关键优势:为什么原生支持更好?

1. 更高的准确率

  • Prompt 伪装:约 85-90% 的格式正确率
  • JSON Schema 约束:100% 格式正确,但可能选错函数
  • 原生支持:95%+ 的函数选择正确率 + 100% 格式正确

2. 更好的泛化能力

  • 面对未见过的函数描述,原生模型能更好地理解并正确使用
  • Prompt 伪装的模型容易"死记硬背",遇到新函数就出错

3. 更自然的交互

  • 原生模型知道"什么时候不该调用工具"
  • 不会为了调用而调用,避免画蛇添足

4. 支持多轮对话

  • 可以在一轮对话中调用多个工具
  • 记住之前调用的结果,进行连贯的推理

5. 参数绑定机制:从自然语言到函数签名

5.1 核心挑战:自然语言的模糊性

Function Calling 的一个关键技术难点是参数绑定(Parameter Binding)——如何将用户的自然语言请求映射到函数的形式化参数。

示例问题

用户:"帮我查一下北京明天的天气,要摄氏度"

目标函数签名:
def weather_api(location: str, date: str, unit: str) -> WeatherResult

需要解决的子问题

  1. 意图识别:确定要调用 weather_api
  2. 槽位提取:从句子中提取"北京"、“明天”、“摄氏度”
  3. 参数映射:将提取的信息映射到 locationdateunit
  4. 默认值处理:如果用户没说日期,是否默认为"今天"?
  5. 类型转换:将"明天"转换为具体的日期格式如"2024-01-15"

5.2 槽位填充(Slot Filling)技术

槽位填充是任务型对话系统的核心技术,用于从用户话语中提取结构化信息。

传统方法:基于规则

def extract_slots_simple(utterance: str) -> dict:
    """简单的规则匹配"""
    slots = {}
    
    # 城市匹配
    cities = ["北京", "上海", "广州", "深圳"]
    for city in cities:
        if city in utterance:
            slots["location"] = city
            break
    
    # 时间匹配
    time_words = {"今天": "today", "明天": "tomorrow", "后天": "day_after_tomorrow"}
    for word, value in time_words.items():
        if word in utterance:
            slots["date"] = value
            break
    
    # 单位匹配
    if "摄氏度" in utterance or "摄氏" in utterance:
        slots["unit"] = "celsius"
    elif "华氏度" in utterance or "华氏" in utterance:
        slots["unit"] = "fahrenheit"
    
    return slots

# 测试
utterance = "帮我查一下北京明天的天气,要摄氏度"
slots = extract_slots_simple(utterance)
print(slots)
# 输出:{'location': '北京', 'date': 'tomorrow', 'unit': 'celsius'}

优点:简单直接,可控性强 缺点:难以处理复杂表达,泛化能力弱

现代方法:使用大模型

from pydantic import BaseModel, Field
from openai import OpenAI

client = OpenAI()

class WeatherSlots(BaseModel):
    location: str = Field(description="城市名称")
    date: str = Field(description="日期,格式 YYYY-MM-DD")
    unit: str = Field(default="celsius", description="温度单位")

def extract_slots_llm(utterance: str) -> WeatherSlots:
    prompt = f"""
    从以下用户话语中提取天气查询的参数:
    
    用户:"{utterance}"
    
    请以 JSON 格式返回提取的结果。
    """
    
    response = client.beta.chat.completions.parse(
        model="gpt-4-turbo-preview",
        messages=[{"role": "user", "content": prompt}],
        response_format=WeatherSlots
    )
    
    return response.choices[0].message.parsed

# 测试
slots = extract_slots_llm("北京明天天气,摄氏度")
print(slots)
# 输出:WeatherSlots(location='北京', date='2024-01-15', unit='celsius')

优点:强大的泛化能力,能处理各种表达方式 缺点:依赖外部 API,成本较高

5.3 歧义消解:一词多义的处理

自然语言充满了歧义,参数绑定时需要消解。

歧义类型 1:指代不明

用户:"上海和北京的天气怎么样?"

歧义:
- 理解 1:分别查询两个城市 → 调用两次 weather_api
- 理解 2:对比两个城市 → 一次调用返回对比结果

解决方案:多值参数
class WeatherQuery(BaseModel):
    locations: list[str] = Field(
        description="城市列表,可以查询多个城市",
        min_length=1,
        max_length=5  # 限制最多 5 个城市
    )
    date: str
    unit: str

# 模型输出
{
  "locations": ["上海", "北京"],
  "date": "2024-01-15",
  "unit": "celsius"
}

# 后端处理:循环调用
for city in locations:
    weather = call_weather_api(city, date, unit)

歧义类型 2:时间表达的相对性

用户:"下周三下午的天气"

挑战:
- "下周三"是相对于哪一天?
- "下午"具体指几点?

解决方案:上下文感知的时间解析
from datetime import datetime, timedelta

def parse_relative_date(expr: str, reference_date: datetime = None) -> str:
    """
    解析相对时间表达
    """
    if reference_date is None:
        reference_date = datetime.now()
    
    if expr == "今天":
        return reference_date.strftime("%Y-%m-%d")
    elif expr == "明天":
        tomorrow = reference_date + timedelta(days=1)
        return tomorrow.strftime("%Y-%m-%d")
    elif expr.startswith("下"):
        # 简化处理:假设"下周 X"
        weekday_map = {"一": 0, "二": 1, "三": 2, "四": 3, "五": 4, "六": 5, "日": 6}
        target_weekday = weekday_map[expr[1]]
        days_ahead = target_weekday - reference_date.weekday()
        if days_ahead <= 0:  # 已经过了
            days_ahead += 7
        next_weekday = reference_date + timedelta(days=days_ahead)
        return next_weekday.strftime("%Y-%m-%d")
    
    return expr  # 原样返回(可能是绝对日期)

# 测试
print(parse_relative_date("明天"))  # 输出:2024-01-15(假设今天是 14 号)

5.4 类型转换与验证

从自然语言提取的参数通常是字符串,需要转换为函数期望的类型。

类型转换矩阵

from typing import Any, Type

def convert_parameter_type(value: str, target_type: Type) -> Any:
    """
    将字符串值转换为目标类型
    """
    if target_type == int:
        try:
            return int(value)
        except ValueError:
            raise TypeError(f"无法将 '{value}' 转换为整数")
    
    elif target_type == float:
        try:
            return float(value)
        except ValueError:
            raise TypeError(f"无法将 '{value}' 转换为浮点数")
    
    elif target_type == bool:
        true_values = {"是", "true", "yes", "1"}
        false_values = {"否", "false", "no", "0"}
        if value.lower() in true_values:
            return True
        elif value.lower() in false_values:
            return False
        else:
            raise TypeError(f"无法将 '{value}' 转换为布尔值")
    
    elif target_type == str:
        return value
    
    else:
        # 复杂类型尝试用 JSON 解析
        import json
        try:
            return json.loads(value)
        except:
            raise TypeError(f"无法将 '{value}' 转换为 {target_type}")

# 测试
print(convert_parameter_type("42", int))        # 输出:42
print(convert_parameter_type("是", bool))       # 输出:True
print(convert_parameter_type("3.14", float))    # 输出:3.14

参数验证器

from pydantic import BaseModel, validator, Field

class ValidatedWeatherCall(BaseModel):
    location: str = Field(..., min_length=1, max_length=100)
    date: str
    unit: str
    
    @validator('location')
    def validate_location(cls, v):
        # 检查是否是已知城市
        known_cities = {"北京", "上海", "广州", "深圳"}
        if v not in known_cities:
            raise ValueError(f"未知城市:{v}。已知城市:{known_cities}")
        return v
    
    @validator('date')
    def validate_date(cls, v):
        # 检查日期格式
        from datetime import datetime
        try:
            datetime.strptime(v, "%Y-%m-%d")
            return v
        except ValueError:
            raise ValueError(f"日期格式错误:{v}。应为 YYYY-MM-DD")
    
    @validator('unit')
    def validate_unit(cls, v):
        if v not in {"celsius", "fahrenheit"}:
            raise ValueError(f"未知温度单位:{v}。只能是 celsius 或 fahrenheit")
        return v

# 测试:正确输入
try:
    call = ValidatedWeatherCall(
        location="北京",
        date="2024-01-15",
        unit="celsius"
    )
    print("验证通过:", call)
except Exception as e:
    print("验证失败:", e)

# 测试:错误输入
try:
    call = ValidatedWeatherCall(
        location="未知城市",
        date="昨天",  # 格式错误
        unit="kelvin"  # 不支持的单位
    )
except Exception as e:
    print("验证失败:", e)

6. 高级话题:多工具协作与错误恢复

6.1 多工具顺序调用

实际应用中,经常需要按顺序调用多个工具。

示例场景:旅行规划

用户:"帮我规划下周从北京到上海的旅行"

需要的工具链:
1. flight_search(origin, destination, date) → 查询航班
2. hotel_search(city, check_in, check_out) → 查询酒店
3. weather_api(city, date) → 查询天气
4. currency_convert(amount, from_currency, to_currency) → 货币转换(如果需要)

实现代码

from typing import List, Dict
import json

class TravelPlanner:
    """旅行规划 Agent"""
    
    def __init__(self):
        self.tools = {
            "flight_search": self.flight_search,
            "hotel_search": self.hotel_search,
            "weather_api": self.weather_api,
        }
    
    def plan_trip(self, user_request: str) -> str:
        # 第 1 步:解析用户意图
        intent = self.parse_intent(user_request)
        
        # 第 2 步:生成工具调用计划
        plan = self.generate_tool_call_plan(intent)
        
        # 第 3 步:执行计划
        results = []
        for tool_call in plan:
            tool_name = tool_call["name"]
            args = tool_call["arguments"]
            
            try:
                result = self.tools[tool_name](**args)
                results.append({"tool": tool_name, "result": result, "status": "success"})
            except Exception as e:
                results.append({"tool": tool_name, "error": str(e), "status": "failed"})
                
                # 错误恢复策略
                recovery = self.handle_error(tool_name, e, results)
                if recovery:
                    # 重试或替代方案
                    result = self.execute_recovery(recovery)
                    results.append({"tool": recovery["name"], "result": result, "status": "recovered"})
        
        # 第 4 步:综合所有结果生成回答
        final_answer = self.synthesize_answer(results)
        return final_answer
    
    def flight_search(self, origin: str, destination: str, date: str) -> dict:
        """模拟航班搜索"""
        return {
            "flights": [
                {"airline": "CA1501", "departure": "08:00", "arrival": "10:30", "price": 1200},
                {"airline": "MU5102", "departure": "14:00", "arrival": "16:30", "price": 980},
            ]
        }
    
    def hotel_search(self, city: str, check_in: str, check_out: str) -> dict:
        """模拟酒店搜索"""
        return {
            "hotels": [
                {"name": "上海大酒店", "stars": 5, "price": 800},
                {"name": "上海商务宾馆", "stars": 3, "price": 350},
            ]
        }
    
    def weather_api(self, city: str, date: str) -> dict:
        """模拟天气查询"""
        return {
            "condition": "晴朗",
            "high_temp": 25,
            "low_temp": 15,
        }
    
    def parse_intent(self, request: str) -> dict:
        """解析用户意图(简化版)"""
        # 实际应使用 LLM
        return {
            "origin": "北京",
            "destination": "上海",
            "date": "2024-01-22",
            "duration": 3
        }
    
    def generate_tool_call_plan(self, intent: dict) -> List[dict]:
        """生成工具调用计划"""
        return [
            {
                "name": "flight_search",
                "arguments": {
                    "origin": intent["origin"],
                    "destination": intent["destination"],
                    "date": intent["date"]
                }
            },
            {
                "name": "hotel_search",
                "arguments": {
                    "city": intent["destination"],
                    "check_in": intent["date"],
                    "check_out": intent["date"]  # 简化:同一天
                }
            },
            {
                "name": "weather_api",
                "arguments": {
                    "city": intent["destination"],
                    "date": intent["date"]
                }
            }
        ]
    
    def handle_error(self, tool_name: str, error: Exception, context: List[dict]) -> dict:
        """错误处理策略"""
        # 策略 1:重试
        # 策略 2:使用备用工具
        # 策略 3:跳过并继续
        # 这里简化处理
        return None
    
    def synthesize_answer(self, results: List[dict]) -> str:
        """综合所有工具的结果生成最终回答"""
        answer_parts = []
        
        for r in results:
            if r["status"] == "success":
                if r["tool"] == "flight_search":
                    flights = r["result"]["flights"]
                    answer_parts.append(f"找到{len(flights)}个航班,最便宜的是{min(f['price'] for f in flights)}元")
                elif r["tool"] == "hotel_search":
                    hotels = r["result"]["hotels"]
                    answer_parts.append(f"找到{len(hotels)}家酒店,价格从{min(h['price'] for h in hotels)}元起")
                elif r["tool"] == "weather_api":
                    weather = r["result"]
                    answer_parts.append(f"当天天气:{weather['condition']}{weather['low_temp']}-{weather['high_temp']}°C")
        
        return "\n".join(answer_parts)

# 使用示例
planner = TravelPlanner()
result = planner.plan_trip("下周从北京到上海旅行")
print(result)

6.2 错误恢复策略

工具调用可能失败,需要设计容错机制。

常见错误类型

  1. API 不可用:网络超时、服务宕机
  2. 参数错误:缺少必填参数、类型不匹配
  3. 业务错误:查询无结果、权限不足

恢复策略

class ErrorRecoveryStrategy:
    """错误恢复策略基类"""
    
    def recover(self, failed_call: dict, error: Exception, context: dict) -> dict:
        raise NotImplementedError

class RetryStrategy(ErrorRecoveryStrategy):
    """重试策略"""
    
    def __init__(self, max_retries: int = 3, backoff_factor: float = 1.0):
        self.max_retries = max_retries
        self.backoff_factor = backoff_factor
    
    def recover(self, failed_call: dict, error: Exception, context: dict) -> dict:
        import time
        
        for attempt in range(self.max_retries):
            try:
                print(f"重试第{attempt + 1}次...")
                result = execute_tool_call(failed_call)
                return {"status": "success", "result": result}
            except Exception as e:
                if attempt == self.max_retries - 1:
                    return {"status": "failed", "error": str(e)}
                wait_time = self.backoff_factor * (2 ** attempt)
                time.sleep(wait_time)
        
        return {"status": "failed", "error": "达到最大重试次数"}

class FallbackStrategy(ErrorRecoveryStrategy):
    """降级策略:使用备用工具"""
    
    def __init__(self, fallback_map: Dict[str, str]):
        self.fallback_map = fallback_map  # {主工具:备用工具}
    
    def recover(self, failed_call: dict, error: Exception, context: dict) -> dict:
        primary_tool = failed_call["name"]
        fallback_tool = self.fallback_map.get(primary_tool)
        
        if not fallback_tool:
            return {"status": "failed", "error": "无备用工具"}
        
        print(f"{primary_tool}失败,降级使用{fallback_tool}")
        
        # 调整参数以适配备用工具
        adjusted_args = self.adapt_arguments(
            from_tool=primary_tool,
            to_tool=fallback_tool,
            original_args=failed_call["arguments"]
        )
        
        try:
            result = execute_tool_call({
                "name": fallback_tool,
                "arguments": adjusted_args
            })
            return {"status": "recovered", "result": result, "used_fallback": fallback_tool}
        except Exception as e:
            return {"status": "failed", "error": f"备用工具也失败:{str(e)}"}
    
    def adapt_arguments(self, from_tool: str, to_tool: str, original_args: dict) -> dict:
        """适配参数(简化版)"""
        # 实际需要根据具体工具的参数差异来实现
        return original_args

# 使用示例
recovery_system = {
    "weather_api": RetryStrategy(max_retries=3),
    "flight_search": FallbackStrategy({
        "flight_search": "train_search"  # 航班查询失败→查火车
    })
}

7. 总结与展望

7.1 技术演进的启示

回顾 Function Calling 的三个发展阶段,我们可以看到一条清晰的演进路径:

Function Calling 技术演进路线图 阶段 1: Prompt 伪装 • 表面格式模仿 • 依赖模型"自觉" • 正确率~85% • 无需训练 ✗ 不稳定 阶段 2: JSON Schema • 语法强制约束 • 100% 格式正确 • 需要解析层 • 仍需后处理 ⚠ 形似神不似 阶段 3: 原生支持 • 训练时学习 • 真正理解工具 • 端到端优化 • 直觉式调用 ✓ 最佳实践 2022-2023 初 2023 中 2023 底至今 演进趋势:从"外挂"到"内生",从"形式"到"本质" 下一个阶段:自主 Agent——模型不仅能调用工具,还能自主规划多步任务

图解说明:如上图所示,Function Calling 的技术演进呈现出一条从表面到本质、从外挂到内生的清晰路径。每个阶段都解决了前一阶段的核心痛点,同时也带来了新的挑战。

7.2 核心知识点总结

1. Prompt 伪装阶段

  • 通过 Few-shot 示例建立条件反射
  • 心理学原理:经典条件反射
  • 局限性:依赖模型"自觉性"

2. JSON Schema 约束

  • 基于 CFG 和下推自动机的语法约束
  • 核心优势:100% 格式正确
  • 局限性:仍然需要解析层

3. 原生 Function Calling

  • 训练阶段学习工具语义
  • 端到端优化,直觉式调用
  • 当前最佳实践

4. 参数绑定机制

  • 槽位填充:从自然语言提取结构化信息
  • 歧义消解:处理一词多义、指代不明
  • 类型转换:字符串→形式化参数

7.3 未来展望:从 Function Calling 到自主 Agent

Function Calling 只是"赋予模型行动能力"的第一步。接下来的演进方向是:

短期(1-2 年):工具使用能力的深化

  • 更复杂的工具组合(多模态工具、GUI 操作)
  • 更强的错误恢复和异常处理能力
  • 更好的长期记忆和上下文理解

中期(3-5 年:自主 Agent 系统

  • 模型能够自主规划多步任务
  • 跨工具、跨应用的协作
  • 人类只需给出高层目标,Agent 自主拆解和执行

长期(5-10 年):通用人工智能的雏形

  • 具备真正的"理解"和"推理"能力
  • 能够在陌生环境中学习和适应
  • 从"工具使用者"进化为"问题解决者"

在下一篇中,我们将深入探讨 ReAct 框架——如何结合推理(Reasoning)和行动(Acting),让模型具备自主解决问题的能力。敬请期待!


参考文献与延伸阅读

  1. Function Calling with Large Language Models: A Survey (Zhang et al., 2024):全面综述了大模型函数调用技术的发展历程、主要方法和应用场景,是了解该领域的入门必读。

  2. Tool Learning with Foundation Models (Qin et al., 2023):系统性地介绍了基础模型如何学习使用工具,包括分类体系、训练方法和评估指标。

  3. HuggingGPT: Solving AI Tasks with ChatGPT and its Friends in Hugging Face (Shen et al., 2023):展示了如何利用 LLM 作为控制器来调度多个 AI 模型完成复杂任务,是工具调用的典型案例。

  4. ToolLLM: Facilitating Large Language Models to Master 16000+ Real-world APIs (Qin et al., 2023):训练 LLM 掌握超过 16000 个真实世界 API 的研究,展示了工具学习的规模化应用。

  5. Gorilla: Large Language Model Connected with Massive APIs (Patil et al., 2023):专门针对 API 调用优化的 LLM,在准确性和效率上都有出色表现。

  6. React: Synergizing Reasoning and Acting in Language Models (Yao et al., 2022):提出了 ReAct 框架,将推理和行动结合,是下一代 Agent 系统的基础。

  7. ChatDev: Communicative Agents for Software Development (Chen et al., 2023):展示了多个 Agent 如何协作完成软件开发任务,是多工具协作的典型案例。

  8. OpenAI Function Calling 官方文档:OpenAI 官方的 Function Calling 使用指南和最佳实践。

  9. LangChain 框架文档:流行的 LLM 应用开发框架,提供了丰富的工具集成和工作流编排能力。

  10. LlamaIndex Tools 文档:专注于 RAG 应用的工具调用框架,特别适合知识库问答场景。

  11. Task Planning in Large Language Models: A Survey (Huang et al., 2024):关于大模型任务规划能力的全面综述,涵盖了从单步调用到多步协作的演进。

  12. Reflexion: Language Agents with Verbal Reinforcement Learning (Shinn et al., 2023):通过自我反思来提升工具使用和任务规划能力的研究。

博客日历
2026年04月
SuMoTuWeThFrSa
29
30
31
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
01
02
03
04
05
06
07
08
09
更多
--
--
--
--