大模型 Function Calling 与工具使用:从对话到行动的工程实践

cover

一、大模型的"知行鸿沟":能说不能做的困境

大语言模型拥有丰富的知识储备,但本质上只是一个文本生成器——它无法执行操作、无法查询实时数据、无法调用外部 API。用户问"北京今天天气如何",模型只能基于训练数据猜测,无法获取实时天气信息。用户问"帮我查一下订单 #12345 的状态",模型无法连接数据库。

Function Calling(函数调用)是打通"知"与"行"的桥梁:模型不再直接回答问题,而是生成结构化的函数调用指令,由外部执行器调用真实 API 获取结果,再将结果返回模型生成最终回答。这一机制让大模型从"百科全书"进化为"智能助手"。

二、Function Calling 的执行流程

Function Calling 的核心流程是:用户提问 → 模型生成函数调用 → 外部执行 → 结果回注 → 模型生成回答。

sequenceDiagram
    participant User as 用户
    participant LLM as 大模型
    participant Executor as 函数执行器
    participant API as 外部 API

    User->>LLM: 北京今天天气如何?
    LLM->>LLM: 判断需要调用 get_weather
    LLM-->>Executor: get_weather(city="北京")
    Executor->>API: HTTP GET /weather?city=北京
    API-->>Executor: {"temp": 28, "condition": "晴"}
    Executor-->>LLM: 函数返回结果
    LLM->>LLM: 基于结果生成自然语言回答
    LLM-->>User: 北京今天晴天,气温 28°C

关键机制:模型不直接执行函数,只生成调用意图和参数。执行器负责实际调用和错误处理。这种分离确保了安全性——模型无法绕过权限限制直接访问外部系统。

三、工程化实现

3.1 工具定义与注册

# tool_registry.py
from dataclasses import dataclass
from typing import Callable, Any
import json

@dataclass
class ToolDefinition:
    name: str
    description: str
    parameters: dict  # JSON Schema
    function: Callable

class ToolRegistry:
    def __init__(self):
        self.tools: dict[str, ToolDefinition] = {}

    def register(self, name: str, description: str, parameters: dict):
        """装饰器:注册工具函数"""
        def decorator(func: Callable):
            self.tools[name] = ToolDefinition(
                name=name,
                description=description,
                parameters=parameters,
                function=func,
            )
            return func
        return decorator

    def get_tool_definitions(self) -> list[dict]:
        """生成 OpenAI Function Calling 格式的工具定义"""
        return [
            {
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.parameters,
                }
            }
            for tool in self.tools.values()
        ]

    def execute(self, name: str, arguments: dict) -> Any:
        """执行工具函数"""
        tool = self.tools.get(name)
        if not tool:
            raise ValueError(f"未知工具:{name}")
        return tool.function(**arguments)


# 注册具体工具
registry = ToolRegistry()

@registry.register(
    name="get_weather",
    description="获取指定城市的当前天气信息",
    parameters={
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "城市名称,如'北京'、'上海'",
            },
        },
        "required": ["city"],
    },
)
def get_weather(city: str) -> dict:
    import requests
    # 调用天气 API
    response = requests.get(
        f"https://api.weather.com/current",
        params={"city": city},
        timeout=10,
    )
    if response.status_code != 200:
        return {"error": f"天气 API 请求失败:HTTP {response.status_code}"}
    return response.json()

@registry.register(
    name="search_database",
    description="查询数据库中的订单信息",
    parameters={
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "订单编号",
            },
            "fields": {
                "type": "array",
                "items": {"type": "string"},
                "description": "需要查询的字段列表",
            },
        },
        "required": ["order_id"],
    },
)
def search_database(order_id: str, fields: list[str] = None) -> dict:
    # 模拟数据库查询
    return {
        "order_id": order_id,
        "status": "已发货",
        "amount": 299.00,
        "tracking_number": "SF1234567890",
    }

3.2 Function Calling 执行器

# function_calling_executor.py
import json
from openai import OpenAI

class FunctionCallingExecutor:
    def __init__(self, registry: ToolRegistry, model: str = "gpt-4o"):
        self.client = OpenAI()
        self.registry = registry
        self.model = model

    def run(self, user_message: str, max_rounds: int = 5) -> str:
        """执行 Function Calling 循环"""
        messages = [
            {"role": "system", "content": "你是一个智能助手,可以调用工具帮助用户。"},
            {"role": "user", "content": user_message},
        ]

        tools = self.registry.get_tool_definitions()

        for round_num in range(max_rounds):
            # 调用 LLM
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
                tools=tools if tools else None,
                tool_choice="auto",
            )

            choice = response.choices[0]
            message = choice.message

            # 如果没有工具调用,直接返回回答
            if not message.tool_calls:
                return message.content

            # 将助手消息(含工具调用)加入历史
            messages.append(message)

            # 执行每个工具调用
            for tool_call in message.tool_calls:
                function_name = tool_call.function.name
                try:
                    arguments = json.loads(tool_call.function.arguments)
                except json.JSONDecodeError:
                    arguments = {}

                try:
                    # 执行工具函数
                    result = self.registry.execute(function_name, arguments)
                    result_str = json.dumps(result, ensure_ascii=False)
                except Exception as e:
                    result_str = json.dumps(
                        {"error": f"工具执行失败:{str(e)}"},
                        ensure_ascii=False,
                    )

                # 将工具结果加入消息历史
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result_str,
                })

        return "抱歉,工具调用轮次已达上限,请简化您的问题。"

3.3 多轮工具调用与错误恢复

# advanced_executor.py
class AdvancedExecutor(FunctionCallingExecutor):
    def run_with_retry(self, user_message: str, max_rounds: int = 10) -> str:
        """支持重试和错误恢复的执行器"""
        messages = [
            {"role": "system", "content": (
                "你是一个智能助手。调用工具时请注意:\n"
                "1. 参数必须符合 Schema 定义\n"
                "2. 如果工具返回错误,分析原因并重试\n"
                "3. 不要编造工具返回的数据"
            )},
            {"role": "user", "content": user_message},
        ]

        tools = self.registry.get_tool_definitions()
        failed_calls = set()  # 记录失败的调用,避免无限重试

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

            choice = response.choices[0]
            message = choice.message

            if not message.tool_calls:
                return message.content

            messages.append(message)

            for tool_call in message.tool_calls:
                call_key = f"{tool_call.function.name}:{tool_call.function.arguments}"

                # 避免重复调用同一个失败的工具
                if call_key in failed_calls:
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "content": json.dumps({
                            "error": "此调用之前已失败,请尝试不同的参数或工具"
                        }),
                    })
                    continue

                try:
                    arguments = json.loads(tool_call.function.arguments)
                    result = self.registry.execute(
                        tool_call.function.name, arguments
                    )
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "content": json.dumps(result, ensure_ascii=False),
                    })
                except Exception as e:
                    failed_calls.add(call_key)
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "content": json.dumps({
                            "error": str(e),
                            "hint": "请检查参数格式是否正确",
                        }),
                    })

        return "工具调用轮次已达上限。"

四、Function Calling 的 Trade-offs

参数生成的准确性:模型可能生成不符合 Schema 的参数(如字符串传给了数字字段、缺少必填参数)。建议在执行器中加入参数校验层,对不符合 Schema 的参数返回错误信息,让模型重新生成。

工具选择的准确性:模型可能选择错误的工具(如用 search_database 查天气)。优化工具描述(description)是提高选择准确性的关键——描述必须清晰区分工具的适用场景。

延迟的累积效应:每轮 Function Calling 包含一次 LLM 调用 + 一次工具执行,总延迟 = LLM 延迟 × 轮数 + 工具延迟。3 轮调用可能需要 5-10 秒。建议对简单查询(无需工具)跳过工具调用,对复杂查询设置最大轮数限制。

安全性风险:工具函数可能执行危险操作(如删除数据、发送邮件)。建议对写操作工具设置确认机制——执行前向用户展示操作意图,获得确认后再执行。

五、总结

Function Calling 是大模型从"对话"走向"行动"的关键机制,通过模型生成调用意图、外部执行器实际调用的分离架构,兼顾了能力和安全性。落地路线上,建议先定义核心工具集(查询类),再逐步扩展到操作类工具(写入类需加确认)。关键原则:工具描述决定选择准确性,参数校验防止格式错误,错误恢复提升鲁棒性,安全确认守住底线。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐