目录

一、什么是 Function Call

1.1 概念定义

1.2 为什么需要 Function Call

1.3 支持 Function Call 的模型

二、Function Call 解决了大模型的哪些问题

2.1 信息实时性

2.2 数据局限性

2.3 功能扩展性

2.4 结构化输出

三、Function Call 工作原理

3.1 无 Function Call 时的工作流

3.2 有 Function Call 时的工作流

四、Function Call 核心概念详解

4.1 工具定义(Tool Definition)

4.2 工具调用响应

4.3 工具执行结果回传

4.4 tool_choice 参数

五、实践一:基于 OpenAI SDK 原生实现

5.1 需求说明

5.2 环境准备

5.3 项目配置

5.4 完整实现代码

5.5 运行结果示例

5.6 代码流程解析

六、实践二:基于 LangChain 框架实现

6.1 LangChain 简介

6.2 环境准备

6.3 完整实现代码

6.4 与原生 SDK 实现的对比

七、综合实战:MySQL 数据库智能查询 Agent

7.1 架构设计

7.2 数据库准备

7.3 完整实现代码

7.4 运行效果示例

八、Function Call 的演进与最佳实践

8.1 Function Call 的版本演进

8.2 最佳实践

8.2.1 工具描述要清晰准确

8.2.2 参数描述要有示例

8.2.3 合理控制工具数量

8.2.4 做好错误处理

8.2.5 控制 Token 消耗

8.2.6 安全性考虑

8.3 并行函数调用示例

九、总结与展望

9.1 总结

9.2 展望


一、什么是 Function Call

1.1 概念定义

Function Call 是 OpenAI 在 2023 年 6 月 13 日随 API 更新一同发布的一项能力,允许开发者在调用 Chat Completions API 时,以 JSON Schema 的形式向模型描述一组可用的函数(工具)。模型会根据用户的输入内容,自主判断是否需要调用某个函数,并在需要时返回一个结构化的 JSON 对象,其中包含要调用的函数名称和参数。

一句话总结:Function Call 让大模型从"只会说话"变成"能干活"——它知道什么时候该调用什么工具,并且能正确地填写参数。

1.2 为什么需要 Function Call

在 Function Call 出现之前,开发者如果想让大模型使用外部工具,通常需要依赖提示词工程(Prompt Engineering),让模型在回复中以特定格式(如特定的 JSON 格式或标记语言)输出调用意图,然后再由程序解析。这种方式存在诸多问题:

问题 说明
格式不稳定 模型可能输出不符合预期格式的内容,导致解析失败
参数提取不可靠 模型可能遗漏必要参数或生成错误的参数值
维护成本高 每次增减工具都需要重新设计提示词和解析逻辑
多轮调用困难 需要多个工具协作时,流程编排非常复杂

Function Call 将这些"非正式"的约定提升为API 层面的标准化能力,由模型经过专门训练来识别函数调用意图并生成规范的 JSON 输出,从根本上提高了可靠性和可维护性。

1.3 支持 Function Call 的模型

目前,Function Call(或等价的工具调用能力)已经成为主流大模型的标配功能:

模型/平台 支持时间 备注
OpenAI GPT-4 / GPT-3.5-turbo 2023 年 6 月 首次发布 Function Call
OpenAI GPT-4o / GPT-4o-mini 2024 年 支持并行函数调用、更低成本
百度文心一言(ERNIE) 2023 年下半年 国内首批支持
智谱 ChatGLM3-6B 2023 年 10 月 开源模型中较早支持
讯飞星火 3.0 2023 年 支持 Function Call
阿里通义千问(Qwen) 2024 年 支持 Tool Calling
DeepSeek 2024 年 支持 Function Call,兼容 OpenAI 接口
Anthropic Claude 2024 年 称为 Tool Use
Google Gemini 2024 年 称为 Function Calling

二、Function Call 解决了大模型的哪些问题

大语言模型虽然强大,但受限于其工作原理,存在几个根本性的短板。Function Call 的出现正是为了弥补这些不足:

2.1 信息实时性

大模型的训练数据有时间截止点(knowledge cutoff),无法获取训练完成之后的信息。例如,模型不可能知道今天的天气、当前的股价或最新的新闻。

Function Call 的解决方案:通过调用外部 API(如天气 API、新闻 API、金融数据接口),模型可以实时获取最新数据,打破"信息茧房"。

2.2 数据局限性

模型的训练数据虽然庞大,但无法覆盖所有专业领域的详细知识。例如,特定企业的内部数据库、私有知识库、用户个人数据等,都不在训练集之中。

Function Call 的解决方案:模型可以调用企业内部的数据库查询工具、知识库检索系统、RAG 检索接口等,获取特定领域的精确信息。

2.3 功能扩展性

大模型本质上只是"文本生成器",无法直接执行操作——它不能发邮件、不能操作数据库、不能控制智能家居。

Function Call 的解决方案:通过为模型配备各种"工具函数",模型可以根据用户意图自动选择并调用合适的工具,实现从"理解意图"到"执行动作"的完整闭环。

2.4 结构化输出

直接让模型输出 JSON 等结构化数据时,格式往往不稳定,可能出现多余的说明文字、格式错误等问题。

Function Call 的解决方案:模型经过专门训练,能够输出严格符合函数参数定义的 JSON 对象,格式稳定可靠。


三、Function Call 工作原理

3.1 无 Function Call 时的工作流

在没有 Function Call 时,大模型应用的交互流程非常简单:

┌────────┐       ┌────────────┐       ┌──────────┐
│  用户   │──请求──▶│  应用服务   │──提示词──▶│  GPT 模型 │
│ Client │       │ Chat Server│       │          │
│        │◀─响应──│            │◀──文本──│          │
└────────┘       └────────────┘       └──────────┘

流程

  1. 1.用户向应用服务发送请求
  2. 2.应用服务将用户的请求组装成提示词,发送给 GPT 模型
  3. 3.模型生成文本响应,返回给应用服务
  4. 4.应用服务将响应返回给用户

这个流程中,模型只能基于自身知识回答,无法访问外部信息。

3.2 有 Function Call 时的工作流

引入 Function Call 后,交互流程变得更加丰富:

┌────────┐       ┌────────────┐       ┌──────────┐       ┌────────┐
│  用户   │──请求──▶│  应用服务   │──提示词+工具定义──▶│  GPT 模型 │       │ 外部工具 │
│ Client │       │ Chat Server│       │          │       │ (函数)  │
│        │       │            │◀──函数调用指令──│          │       │        │
│        │       │            │──执行函数,获取结果──▶│          │──────▶│        │
│        │       │            │◀──最终自然语言回答──│          │◀──────│        │
│        │◀─响应──│            │       │          │       │        │
└────────┘       └────────────┘       └──────────┘       └────────┘

详细步骤

  1. 1.用户发送请求,应用服务将用户的提示词以及可用的函数定义(JSON Schema 格式)一起发送给模型
  2. 2.模型分析用户意图,判断是否需要调用函数:
    • 如果不需要调用函数,直接返回自然语言回答
    • 如果需要调用函数,返回一个函数调用指令(包含函数名和参数的 JSON)
  3. 3.应用服务接收函数调用指令,在本地执行对应的函数,获取执行结果
  4. 4.应用服务将函数执行结果返回给模型
  5. 5.模型整合函数返回的数据,生成最终的自然语言回答

关键点:Function Call 不是模型直接执行函数,而是模型"告诉"应用层它想调用哪个函数、传什么参数,由应用层负责实际执行并将结果回传。


四、Function Call 核心概念详解

4.1 工具定义(Tool Definition)

向模型描述可用函数时,需要使用特定的 JSON Schema 格式。一个完整的工具定义包含以下关键字段:

{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "根据城市名称获取当前天气信息",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名称,例如:北京、上海"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "温度单位,默认为摄氏度"
                }
            },
            "required": ["city"]
        }
    }
}

字段说明

字段 类型 必填 说明
type string 固定为 "function"
function.name string 函数名称,需与代码中的函数名一致
function.description string 函数功能的自然语言描述,模型据此判断何时调用该函数
function.parameters object 函数参数的 JSON Schema 定义
function.parameters.type string 固定为 "object"
function.parameters.properties object 各参数的类型和描述
function.parameters.required array 必填参数列表

提示description 字段的质量直接影响模型的调用准确率。描述应该清晰、准确、包含使用场景和约束条件。

4.2 工具调用响应

当模型决定调用函数时,返回的消息结构如下:

{
    "role": "assistant",
    "content": null,
    "tool_calls": [
        {
            "id": "call_abc123",
            "type": "function",
            "function": {
                "name": "get_weather",
                "arguments": "{\"city\": \"北京\"}"
            }
        }
    ]
}

关键字段

  • contentnull,表示模型这次没有生成文本,而是发出了函数调用请求
  • tool_calls 是一个数组,支持一次返回多个函数调用请求(并行调用)
  • id 是调用的唯一标识,用于后续回传结果时关联
  • arguments 是一个 JSON 字符串(注意不是对象),需要 json.loads() 解析

4.3 工具执行结果回传

应用层执行完函数后,需要将结果以特定格式回传给模型:

{
    "role": "tool",
    "tool_call_id": "call_abc123",
    "name": "get_weather",
    "content": "晴,28°C"
}

关键字段

  • role 固定为 "tool"
  • tool_call_id 必须与模型返回的调用 id 一一对应
  • content 是函数执行的结果(字符串形式)

4.4 tool_choice 参数

调用 API 时,可以通过 tool_choice 参数控制模型的工具调用行为:

行为
"auto" 由模型自主决定是否调用工具(推荐)
"none" 强制模型不调用任何工具
"required" 强制模型必须调用某个工具
{"type": "function", "function": {"name": "xxx"}} 强制模型调用指定的函数

五、实践一:基于 OpenAI SDK 原生实现

5.1 需求说明

当用户向大模型提问时,大模型可以自动调用本地的两个方法:

  • get_weather:获取指定城市的天气信息
  • dress_advice:根据天气情况给出穿衣建议

并将工具的执行结果整合成最终的自然语言回答返回给用户。

5.2 环境准备

pip install openai python-dotenv 

5.3 项目配置

创建 .env 文件存放 API 配置(放在项目根目录):

MODEL_API_KEY=your_api_key_here
MODEL_BASE_URL=https://api.deepseek.com
MODEL_NAME=deepseek-chat

创建配置加载模块 config.py

import os
from dotenv import load_dotenv


def load_project_env() -> None:
    """
    加载项目根目录下的 .env 文件。
    """
    repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    env_path = os.path.join(repo_root, ".env")
    load_dotenv(env_path)

5.4 完整实现代码

"""
Function Call 基础示例 —— 基于 OpenAI SDK 原生实现
功能:根据用户出行计划,自动查询各城市天气并给出穿衣建议
"""
import json
import os
from openai import OpenAI
from config import load_project_env

load_project_env()


# ===================== 第一步:定义本地工具函数 =====================

def get_weather(city: str) -> str:
    """
    根据城市名称返回模拟天气情况。
    实际项目中应替换为真实的天气 API 调用。
    """
    dummy_weather = {
        "北京": "晴,28°C",
        "上海": "多云,22°C",
        "广州": "小雨,19°C",
        "长沙": "阴,25°C",
        "深圳": "多云转晴,26°C",
        "成都": "小雨,18°C",
    }
    return dummy_weather.get(city, f"未找到 {city} 的天气信息")


def dress_advice(weather: str) -> str:
    """
    根据天气情况给出穿衣建议。
    """
    if "雨" in weather:
        return "建议穿防水外套,记得带伞,鞋子选择防水款式。"
    elif "晴" in weather:
        # 判断温度
        try:
            temp = int(weather.split("°C")[0].split(",")[-1].strip())
            if temp >= 30:
                return "天气炎热,建议穿短袖短裤,注意防晒和补水。"
            elif temp >= 20:
                return "天气晴朗温度适宜,穿轻薄长袖或T恤即可。"
            else:
                return "虽然晴天但温度偏低,建议穿薄外套搭配长袖。"
        except (ValueError, IndexError):
            return "晴天建议适当防晒,根据体感增减衣物。"
    elif "阴" in weather:
        return "阴天温差较小,适合春秋装,穿薄外套即可。"
    elif "多云" in weather:
        return "多云天气,适合穿轻便舒适的衣物,可备一件薄外套。"
    else:
        return "根据体感温度适当增减衣物,建议随身携带一件外套。"


# ===================== 第二步:注册 Tools(JSON Schema 定义) =====================

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "根据城市名称返回该城市的当前天气情况,包括天气状况和温度。"
                           "适用于用户询问某个城市天气的场景。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,例如:北京、上海、广州、长沙"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "dress_advice",
            "description": "根据天气情况给出穿衣搭配建议。"
                           "适用于用户询问在某种天气下应该怎么穿衣服的场景。",
            "parameters": {
                "type": "object",
                "properties": {
                    "weather": {
                        "type": "string",
                        "description": "天气描述信息,例如:晴,28°C / 小雨,19°C"
                    }
                },
                "required": ["weather"]
            }
        }
    }
]


# ===================== 第三步:定义消息历史和工具映射 =====================

messages = [
    {
        "role": "system",
        "content": (
            "你是一个贴心的出行助手。当用户描述出行计划时,"
            "你需要先查询各地天气,再根据天气给出穿衣建议。"
            "回答要自然、亲切,像朋友一样给出建议。"
        )
    },
    {
        "role": "user",
        "content": "我今天要去上海,明天去长沙,后天去北京,大后天去广州,我该怎么准备衣服?"
    }
]

# 工具映射:函数名 -> 实际函数对象
tool_map = {
    "get_weather": get_weather,
    "dress_advice": dress_advice,
}


# ===================== 第四步:初始化客户端并执行调用循环 =====================

client = OpenAI(
    api_key=os.getenv("MODEL_API_KEY"),
    base_url=os.getenv("MODEL_BASE_URL"),
)

# Function Call 的核心循环
while True:
    # 向模型发起请求(携带工具定义)
    resp = client.chat.completions.create(
        model=os.getenv("MODEL_NAME", "deepseek-chat"),
        messages=messages,
        tools=tools,
        tool_choice="auto",  # 让模型自主决定是否调用工具
    )

    msg = resp.choices[0].message

    # 如果模型没有调用任何工具,说明已经生成了最终回答,退出循环
    if not msg.tool_calls:
        print("=" * 60)
        print("最终回答:")
        print(msg.content)
        print("=" * 60)
        break

    # 将模型的 assistant 消息(含 tool_calls)加入历史
    messages.append(msg.model_dump())

    # 依次处理每一个工具调用
    for call in msg.tool_calls:
        func_name = call.function.name
        func_args = json.loads(call.function.arguments or "{}")

        print(f"[调用工具] {func_name}({func_args})")

        # 执行对应的本地函数
        if func_name in tool_map:
            result = tool_map[func_name](**func_args)
        else:
            result = f"未知工具:{func_name}"

        print(f"[工具结果] {result}")

        # 将工具执行结果回传给模型
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "name": func_name,
            "content": str(result),
        })

5.5 运行结果示例

[调用工具] get_weather({"city": "上海"})
[工具结果] 多云,22°C
[调用工具] get_weather({"city": "长沙"})
[工具结果] 阴,25°C
[调用工具] get_weather({"city": "北京"})
[工具结果] 晴,28°C
[调用工具] get_weather({"city": "广州"})
[工具结果] 小雨,19°C
[调用工具] dress_advice({"weather": "多云,22°C"})
[工具结果] 多云天气,适合穿轻便舒适的衣物,可备一件薄外套。
[调用工具] dress_advice({"weather": "阴,25°C"})
[工具结果] 阴天温差较小,适合春秋装,穿薄外套即可。
[调用工具] dress_advice({"weather": "晴,28°C"})
[工具结果] 天气晴朗温度适宜,穿轻薄长袖或T恤即可。
[调用工具] dress_advice({"weather": "小雨,19°C"})
[工具结果] 建议穿防水外套,记得带伞,鞋子选择防水款式。

============================================================
最终回答:
你好!根据你接下来几天的出行计划,我帮你查看了各地天气并整理了穿衣建议:

**今天 - 上海(多云,22°C)**:适合穿轻便舒适的衣物,建议备一件薄外套。

**明天 - 长沙(阴,25°C)**:阴天温差较小,穿春秋装搭配薄外套即可。

**后天 - 北京(晴,28°C)**:天气晴朗温度适宜,穿轻薄长袖或T恤即可。

**大后天 - 广州(小雨,19°C)**:记得带伞,穿防水外套,鞋子选择防水款式。

总体建议:带一件薄外套、一件防水外套,一把折叠伞是必备的。祝旅途愉快!
============================================================

5.6 代码流程解析

整个调用过程可以概括为以下循环:

        ┌──────────────────────────────────────────┐
        │            开始:发送请求+工具定义           │
        │                                          │
        ▼                                          │
   ┌─────────┐                                     │
   │ 模型响应  │                                    │
   └────┬────┘                                     │
        │                                          │
        ▼                                          │
   ┌──────────────┐    是     ┌─────────────┐      │
   │ 有 tool_calls?│─────────▶│ 执行本地函数  │      │
   └──────┬───────┘          └──────┬──────┘      │
         │ 否                       │              │
         ▼                          ▼              │
   ┌──────────┐          ┌──────────────┐         │
   │ 输出最终  │          │ 结果加入 messages│───────┘
   │  回答     │          └──────────────┘
   └──────────┘

六、实践二:基于 LangChain 框架实现

6.1 LangChain 简介

LangChain 是目前最流行的 LLM 应用开发框架之一,它将工具定义、Agent 编排、消息管理等进行了高度封装,让开发者可以用更少的代码实现复杂的 Agent 逻辑。

LangChain 对 Function Call 的封装主要体现在:

  • 使用 @tool 装饰器定义工具,函数的 docstring 自动作为工具描述
  • 使用 create_react_agent(基于 LangGraph)自动编排 Agent 循环
  • 自动管理消息历史和工具结果的回传

6.2 环境准备

pip install langchain-core langchain-openai langgraph python-dotenv 

6.3 完整实现代码

"""
Function Call 示例 —— 基于 LangChain + LangGraph 实现
功能:根据用户出行计划,自动查询各城市天气并给出穿衣建议
"""
import os
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from config import load_project_env

load_project_env()


# ===================== 第一步:定义工具(使用 @tool 装饰器) =====================
# 注意:函数的 docstring 非常重要!
# LangChain 会自动将 docstring 作为工具的 description 传给模型

@tool
def get_weather(city: str) -> str:
    """根据城市名称返回该城市的当前天气情况,包括天气状况和温度。

    Args:
        city: 城市名称,例如:北京、上海、广州
    """
    dummy_weather = {
        "北京": "晴,28°C",
        "上海": "多云,22°C",
        "广州": "小雨,19°C",
        "长沙": "阴,25°C",
        "深圳": "多云转晴,26°C",
        "成都": "小雨,18°C",
    }
    return dummy_weather.get(city, f"未找到 {city} 的天气信息")


@tool
def dress_advice(weather: str) -> str:
    """根据天气情况给出穿衣搭配建议。

    Args:
        weather: 天气描述信息,例如:晴,28°C / 小雨,19°C
    """
    if "雨" in weather:
        return "建议穿防水外套,记得带伞,鞋子选择防水款式。"
    elif "晴" in weather:
        try:
            temp = int(weather.split("°C")[0].split(",")[-1].strip())
            if temp >= 30:
                return "天气炎热,建议穿短袖短裤,注意防晒和补水。"
            elif temp >= 20:
                return "天气晴朗温度适宜,穿轻薄长袖或T恤即可。"
            else:
                return "虽然晴天但温度偏低,建议穿薄外套搭配长袖。"
        except (ValueError, IndexError):
            return "晴天建议适当防晒,根据体感增减衣物。"
    elif "阴" in weather:
        return "阴天温差较小,适合春秋装,穿薄外套即可。"
    elif "多云" in weather:
        return "多云天气,适合穿轻便舒适的衣物,可备一件薄外套。"
    else:
        return "根据体感温度适当增减衣物,建议随身携带一件外套。"


# ===================== 第二步:初始化模型 =====================

llm = ChatOpenAI(
    api_key=os.getenv("MODEL_API_KEY"),
    base_url=os.getenv("MODEL_BASE_URL"),
    model=os.getenv("MODEL_NAME", "deepseek-chat"),
    temperature=0,  # 降低随机性,提高工具调用准确率
)


# ===================== 第三步:创建 Agent =====================

tools = [get_weather, dress_advice]

agent = create_react_agent(
    model=llm,
    tools=tools,
    prompt="你是一个贴心的出行助手。当用户描述出行计划时,"
           "你需要先查询各地天气,再根据天气给出穿衣建议。"
           "回答要自然、亲切,像朋友一样给出建议。",
)


# ===================== 第四步:运行交互循环 =====================

if __name__ == "__main__":
    print("出行穿衣助手已启动!输入 'exit' 退出。")
    print("-" * 50)

    while True:
        user_input = input("\n请输入你的问题:").strip()
        if user_input.lower() in ("exit", "quit", "q"):
            print("再见!")
            break
        if not user_input:
            continue

        # invoke 会自动处理整个 Agent 循环(调用工具 -> 获取结果 -> 再次推理)
        state = agent.invoke({"messages": [HumanMessage(content=user_input)]})

        # 输出最终回答
        messages = state.get("messages", [])
        if messages:
            final_message = messages[-1]
            print(f"\n助手回答:\n{final_message.content}")

6.4 与原生 SDK 实现的对比

维度 OpenAI SDK 原生 LangChain + LangGraph
工具定义 手写 JSON Schema @tool 装饰器 + docstring 自动提取
Agent 循环 手动 while 循环 create_react_agent 自动编排
消息管理 手动维护 messages 列表 框架自动管理
工具执行 手动解析参数并调用 框架自动解析和调用
灵活性 完全可控 封装度高,适合快速开发
调试难度 需自行添加日志 可开启 debug 模式查看详细流程
适用场景 需要精细控制流程的场景 快速原型开发、标准 Agent 场景

七、综合实战:MySQL 数据库智能查询 Agent

接下来,我们实现一个更复杂的案例:用自然语言查询 MySQL 数据库。用户只需用自然语言描述需求(如"用户最多的城市是哪个?"),Agent 就能自动获取数据库表结构、生成 SQL、执行查询、并以自然语言返回分析结果。

7.1 架构设计

┌──────────────┐
│   用户输入    │  "用户最多的城市是哪个?"
└──────┬───────┘
       ▼
┌──────────────┐
│  LLM Agent   │  分析意图,决定调用哪些工具
└──┬───┬───┬───┘
   │   │   │
   ▼   ▼   ▼
┌──────┐ ┌──────────┐ ┌──────────┐
│获取表 │ │执行SQL查询│ │查看表数据 │
│结构   │ │          │ │          │
└──┬───┘ └────┬─────┘ └────┬─────┘
   │          │             │
   ▼          ▼             ▼
┌──────────────────────────────┐
│       MySQL 数据库            │
└──────────────────────────────┘
   │
   ▼
┌──────────────┐
│  LLM 整合结果 │  生成自然语言分析报告
└──────┬───────┘
       ▼
┌──────────────┐
│   最终回答    │  "用户最多的城市是北京,共 15,230 人..."
└──────────────┘

7.2 数据库准备

首先创建示例数据库和表:

-- 创建数据库
CREATE DATABASE IF NOT EXISTS sales_analysis_db
DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE sales_analysis_db;

-- 用户表
CREATE TABLE IF NOT EXISTS `users` (
    `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
    `name` VARCHAR(50) NOT NULL COMMENT '用户姓名',
    `city` VARCHAR(50) NOT NULL COMMENT '所在城市',
    `age` INT COMMENT '年龄',
    `gender` ENUM('男', '女') COMMENT '性别',
    `register_date` DATE COMMENT '注册日期',
    `vip_level` INT DEFAULT 0 COMMENT 'VIP等级(0-5)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';

-- 订单表
CREATE TABLE IF NOT EXISTS `orders` (
    `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID',
    `user_id` INT NOT NULL COMMENT '用户ID',
    `product_name` VARCHAR(100) NOT NULL COMMENT '商品名称',
    `category` VARCHAR(50) COMMENT '商品类别',
    `amount` DECIMAL(10, 2) NOT NULL COMMENT '订单金额',
    `quantity` INT NOT NULL DEFAULT 1 COMMENT '购买数量',
    `order_date` DATE NOT NULL COMMENT '订单日期',
    `status` ENUM('待付款', '已付款', '已发货', '已完成', '已取消') DEFAULT '待付款' COMMENT '订单状态',
    FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

-- 插入示例数据
INSERT INTO `users` (`name`, `city`, `age`, `gender`, `register_date`, `vip_level`) VALUES
('张三', '北京', 28, '男', '2023-01-15', 3),
('李四', '上海', 35, '女', '2023-02-20', 2),
('王五', '北京', 22, '男', '2023-03-10', 1),
('赵六', '广州', 30, '女', '2023-04-05', 4),
('钱七', '深圳', 45, '男', '2023-05-18', 5),
('孙八', '上海', 27, '女', '2023-06-22', 1),
('周九', '北京', 33, '男', '2023-07-08', 3),
('吴十', '成都', 29, '女', '2023-08-15', 2);

INSERT INTO `orders` (`user_id`, `product_name`, `category`, `amount`, `quantity`, `order_date`, `status`) VALUES
(1, 'MacBook Pro 14寸', '电脑', 14999.00, 1, '2024-01-10', '已完成'),
(1, 'AirPods Pro', '配件', 1899.00, 1, '2024-02-15', '已完成'),
(2, 'iPhone 15 Pro', '手机', 8999.00, 1, '2024-01-20', '已完成'),
(3, 'iPad Air', '平板', 4799.00, 1, '2024-03-05', '已发货'),
(3, 'Apple Watch', '穿戴', 2999.00, 1, '2024-03-05', '已发货'),
(4, 'MacBook Air', '电脑', 8999.00, 1, '2024-02-28', '已完成'),
(5, 'iPhone 15 Pro Max', '手机', 9999.00, 1, '2024-01-05', '已完成'),
(5, 'Mac Studio', '电脑', 29999.00, 1, '2024-04-01', '已完成'),
(6, 'AirPods 3', '配件', 1399.00, 2, '2024-03-15', '已完成'),
(7, 'iPad Pro', '平板', 8999.00, 1, '2024-04-10', '待付款'),
(8, 'iPhone 15', '手机', 6999.00, 1, '2024-02-20', '已完成');

7.3 完整实现代码

"""
MySQL 数据库智能查询 Agent
功能:用户通过自然语言提问,Agent 自动查询 MySQL 数据库并返回分析结果
"""
from collections import defaultdict
import os
import pymysql
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from config import load_project_env

load_project_env()


# ===================== 第一步:数据库基础操作 =====================

DB_NAME = "sales_analysis_db"


def get_connection():
    """创建数据库连接"""
    return pymysql.connect(
        host="localhost",
        user="root",
        password="123456",
        database=DB_NAME,
        charset="utf8mb4",
        cursorclass=pymysql.cursors.Cursor,
    )


def run_sql(sql: str) -> list:
    """
    执行只读 SQL 查询,返回结果集。
    注意:生产环境中应该增加 SQL 审核机制,仅允许 SELECT 语句。
    """
    # 安全检查:禁止危险操作
    forbidden_keywords = ["DROP", "DELETE", "TRUNCATE", "ALTER", "INSERT", "UPDATE", "CREATE"]
    sql_upper = sql.upper().strip()
    for keyword in forbidden_keywords:
        if sql_upper.startswith(keyword):
            return f"安全拒绝:不允许执行 {keyword} 操作"

    conn = get_connection()
    try:
        with conn.cursor() as cursor:
            cursor.execute(sql)
            result = cursor.fetchall()
            return result
    except Exception as e:
        return f"SQL 执行错误:{str(e)}"
    finally:
        conn.close()


# ===================== 第二步:数据库结构获取 =====================

def load_schema() -> dict:
    """获取数据库中所有表的结构信息"""
    sql = """
        SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_SCHEMA = %s
        ORDER BY TABLE_NAME, ORDINAL_POSITION
    """
    conn = get_connection()
    try:
        with conn.cursor() as cursor:
            cursor.execute(sql, (DB_NAME,))
            result = cursor.fetchall()
    finally:
        conn.close()

    schema_dict = defaultdict(list)
    for row in result:
        table_name = row[0]
        schema_dict[table_name].append(row)
    return schema_dict


def schema_to_markdown(schema_dict: dict) -> str:
    """将数据库结构转换为 Markdown 格式"""
    lines = [f"# 数据库 `{DB_NAME}` 结构信息\n"]

    for table, cols in schema_dict.items():
        lines.append(f"## 表:{table}\n")
        lines.append("| 列名 | 数据类型 | 可空 | 说明 |")
        lines.append("|------|---------|------|------|")

        for _, column_name, column_type, is_nullable, column_comment in cols:
            nullable = "是" if is_nullable == "YES" else "否"
            comment = column_comment or "-"
            lines.append(f"| `{column_name}` | {column_type} | {nullable} | {comment} |")

        lines.append("")  # 空行分隔

    return "\n".join(lines)


# 缓存 schema,避免重复查询
_schema_cache = None


def get_cached_schema() -> str:
    """获取缓存的数据库结构(Markdown 格式)"""
    global _schema_cache
    if _schema_cache is None:
        schema = load_schema()
        _schema_cache = schema_to_markdown(schema)
    return _schema_cache


# ===================== 第三步:定义工具 =====================

@tool
def get_mysql_schema() -> str:
    """获取数据库的完整结构信息,包括所有表名、字段名、字段类型和字段说明。
    在不确定表结构时应先调用此工具了解数据库架构。"""
    return get_cached_schema()


@tool
def run_mysql_sql_tool(sql: str) -> str:
    """执行 SQL 查询语句并返回查询结果。仅支持 SELECT 查询。

    Args:
        sql: 要执行的 SELECT SQL 查询语句。表名和字段名建议使用反引号包裹。
    """
    result = run_sql(sql)
    return str(result)


@tool
def peek_table(table: str, n: int = 5) -> str:
    """快速查看指定表的前 n 行数据,用于了解表中的实际数据内容。

    Args:
        table: 要查看的表名
        n: 要查看的行数,默认为 5
    """
    sql = f"SELECT * FROM `{table}` LIMIT {n}"
    return str(run_sql(sql))


# ===================== 第四步:定义系统提示词 =====================

SYSTEM_PROMPT = """
# 角色定位
你是一位资深的 MySQL 数据库分析师,擅长将用户的自然语言问题转化为精准的 SQL 查询,并用通俗易懂的语言解释查询结果。

# 工作流程
1. **理解需求**:仔细分析用户的问题,确定需要查询哪些表和字段
2. **了解结构**:如果不确定表结构,先调用 get_mysql_schema 工具获取数据库结构
3. **验证数据**:对不熟悉的表,可以用 peek_table 先查看前几行数据
4. **编写SQL**:编写准确的 SQL 查询语句
5. **执行查询**:调用 run_mysql_sql_tool 执行 SQL
6. **分析结果**:用自然语言解释查询结果,给出有价值的洞察

# SQL 编写规范
- 所有表名和字段名使用反引号(`)包裹
- 优先使用索引字段作为查询条件
- 预估数据量大的查询添加 LIMIT 子句
- 避免 SELECT *,明确指定需要的字段
- 多表连接必须明确指定连接条件
- 使用聚合函数时注意 GROUP BY 的搭配

# 回答要求
- 用中文回答,语言专业但易懂
- 先展示关键数据,再给出分析和建议
- 如果查询结果异常,主动分析可能的原因
"""


# ===================== 第五步:创建 Agent 并运行 =====================

def create_db_agent():
    """创建数据库查询 Agent"""
    llm = ChatOpenAI(
        api_key=os.getenv("MODEL_API_KEY"),
        base_url=os.getenv("MODEL_BASE_URL"),
        model=os.getenv("MODEL_NAME", "deepseek-chat"),
        temperature=0,
    )

    tools = [get_mysql_schema, run_mysql_sql_tool, peek_table]

    agent = create_react_agent(
        model=llm,
        tools=tools,
        prompt=SYSTEM_PROMPT,
    )

    return agent


def ask(question: str) -> str:
    """向 Agent 提问并获取回答"""
    agent = create_db_agent()
    state = agent.invoke({"messages": [HumanMessage(content=question)]})
    messages = state.get("messages", [])
    if messages:
        return messages[-1].content
    return "查询完成,但未获得预期结果。"


# ===================== 第六步:主程序入口 =====================

if __name__ == "__main__":
    print("MySQL 智能查询助手已启动!")
    print("示例问题:")
    print("  - 用户最多的城市是哪个?")
    print("  - 各商品类别的总销售额是多少?")
    print("  - VIP等级最高的用户有哪些?")
    print("  - 每个月的订单数量变化趋势如何?")
    print("输入 'exit' 退出。")
    print("-" * 60)

    while True:
        user_input = input("\n请输入你的问题:").strip()
        if user_input.lower() in ("exit", "quit", "q"):
            print("再见!")
            break
        if not user_input:
            continue

        try:
            result = ask(user_input)
            print(f"\n{result}")
        except Exception as e:
            print(f"\n查询出错:{str(e)}")

7.4 运行效果示例

请输入你的问题:用户最多的城市是哪个?

[Agent 内部调用流程]
→ 调用 get_mysql_schema() → 获取到 users 和 orders 表结构
→ 调用 run_mysql_sql_tool("SELECT `city`, COUNT(*) AS user_count FROM `users` GROUP BY `city` ORDER BY user_count DESC LIMIT 5")
→ 返回结果:(('北京', 3), ('上海', 2), ('广州', 1), ('深圳', 1), ('成都', 1))

最终回答:

根据查询结果,**用户最多的城市是北京**,共有 3 位用户。

各城市用户数量排名如下:
| 排名 | 城市 | 用户数 |
|------|------|--------|
| 1 | 北京 | 3 |
| 2 | 上海 | 2 |
| 3 | 广州 | 1 |
| 3 | 深圳 | 1 |
| 3 | 成都 | 1 |

可以看出,北京和上海的用户占比超过一半(5/8),建议将营销资源重点投放在这两个城市。

八、Function Call 的演进与最佳实践

8.1 Function Call 的版本演进

时间 版本/更新 主要变化
2023.06 初始版本 首次支持 Function Call,单次调用一个函数
2023.11 并行函数调用 支持在一次响应中返回多个 tool_calls,可并行执行
2024.01 Structured Outputs 引入 strict: true 模式,保证输出严格符合 JSON Schema
2024.05 GPT-4o 发布 原生支持函数调用,速度更快、成本更低
2024.08 Structured Outputs 全面开放 所有支持函数调用的模型均支持 Structured Outputs

8.2 最佳实践

8.2.1 工具描述要清晰准确

工具的 description 是模型判断何时调用的唯一依据。描述不清晰会导致模型误判或遗漏调用。

# ❌ 不好的描述
"description": "查询数据"

# ✅ 好的描述
"description": "根据 SQL 查询语句查询 MySQL 数据库并返回结果。"
               "仅支持 SELECT 查询,不支持 INSERT/UPDATE/DELETE 等写操作。"
               "适用于用户询问数据分析、统计、查询等场景。"
8.2.2 参数描述要有示例
# ❌ 不好的参数描述
"city": {"type": "string", "description": "城市"}

# ✅ 好的参数描述
"city": {
    "type": "string",
    "description": "城市名称,例如:北京、上海、广州、深圳"
}
8.2.3 合理控制工具数量

工具数量过多会增加模型的选择负担,降低准确率。一般建议:

  • 单次请求中的工具数量不超过 20 个
  • 如果工具确实很多,考虑使用工具分组动态加载策略
8.2.4 做好错误处理
@tool
def run_mysql_sql_tool(sql: str) -> str:
    """执行 SQL 查询"""
    try:
        result = run_sql(sql)
        if isinstance(result, str) and "安全拒绝" in result:
            return result
        if not result:
            return "查询结果为空,请检查查询条件是否正确。"
        return str(result)
    except Exception as e:
        return f"查询执行失败:{str(e)}。请检查 SQL 语法是否正确。"
8.2.5 控制 Token 消耗

当查询结果很大时,返回给模型的 token 会很多,既浪费成本又可能超出上下文窗口。建议:

@tool
def run_mysql_sql_tool(sql: str) -> str:
    """执行 SQL 查询"""
    result = run_sql(sql)

    # 限制返回行数
    MAX_ROWS = 50
    if isinstance(result, (list, tuple)) and len(result) > MAX_ROWS:
        truncated = result[:MAX_ROWS]
        return f"{str(truncated)}\n\n... 共 {len(result)} 条结果,已截取前 {MAX_ROWS} 条"
    return str(result)
8.2.6 安全性考虑

在生产环境中,使用 Function Call 操作数据库时必须注意安全:

def validate_sql(sql: str) -> tuple[bool, str]:
    """SQL 安全审核"""
    sql_upper = sql.upper().strip()

    # 1. 只允许 SELECT
    if not sql_upper.startswith("SELECT"):
        return False, "仅允许 SELECT 查询"

    # 2. 禁止子查询中包含写操作
    dangerous = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "TRUNCATE", "GRANT"]
    for keyword in dangerous:
        if keyword in sql_upper:
            return False, f"SQL 中不允许包含 {keyword}"

    # 3. 强制添加 LIMIT(防止全表扫描)
    if "LIMIT" not in sql_upper:
        sql = sql.rstrip(";") + " LIMIT 100"

    return True, sql

8.3 并行函数调用示例

当模型需要同时获取多个独立数据时,可以一次返回多个 tool_calls:

{
    "tool_calls": [
        {
            "id": "call_001",
            "function": {"name": "get_weather", "arguments": "{\"city\": \"北京\"}"}
        },
        {
            "id": "call_002",
            "function": {"name": "get_weather", "arguments": "{\"city\": \"上海\"}"}
        },
        {
            "id": "call_003",
            "function": {"name": "get_weather", "arguments": "{\"city\": \"广州\"}"}
        }
    ]
}

这样应用层可以并行执行这三个查询(例如使用 asyncio 或多线程),显著提高响应速度。

import asyncio
from concurrent.futures import ThreadPoolExecutor

# 并行执行多个工具调用
async def execute_tools_parallel(tool_calls, tool_map):
    """并行执行多个工具调用"""
    loop = asyncio.get_event_loop()
    results = []

    with ThreadPoolExecutor() as executor:
        tasks = []
        for call in tool_calls:
            name = call.function.name
            args = json.loads(call.function.arguments or "{}")
            func = tool_map.get(name)
            if func:
                tasks.append(loop.run_in_executor(executor, lambda f=func, a=args: f(**a)))

        results = await asyncio.gather(*tasks)

    return results

九、总结与展望

9.1 总结

本文从概念到实践,系统介绍了 Function Call 的方方面面:

层次 内容
概念层 Function Call 的定义、意义、解决的问题
原理层 工作流程、请求-响应结构、核心字段含义
实践层 OpenAI SDK 原生实现、LangChain 框架实现
进阶层 MySQL 智能查询 Agent、并行调用、安全机制、最佳实践

Function Call 的本质是为大模型提供了结构化的外部交互接口,使得大模型从一个"聊天机器人"进化为一个真正有能力执行操作的 AI Agent

9.2 展望

Function Call 正在成为 AI Agent 生态的基础设施。随着技术的演进,以下趋势值得关注:

  1. 1.MCP(Model Context Protocol):Anthropic 提出的标准化协议,旨在统一模型与外部工具的交互方式
  2. 2.Multi-Agent 系统:多个 Agent 协作完成复杂任务,每个 Agent 拥有不同的工具集
  3. 3.代码生成与执行:模型不再只是调用预定义函数,而是动态生成代码并执行
  4. 4.工具自动发现:模型能够自动发现和学习使用新的工具,无需预先定义

掌握 Function Call,就是掌握了构建 AI Agent 应用的核心能力。

Logo

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

更多推荐