课程0基础Agent开发课 / 工具使用与Function-Calling / Function-Calling详解-LLM与外部世界的接口
— 20 min read

Function-Calling详解-LLM与外部世界的接口

LLM 本身是一个纯文本的输入输出系统,无法执行代码、查数据库、调 API。Function Calling(函数调用,也叫工具调用 Tool Use)是连接 LLM 和外部世界的机制:LLM 决定"需要调用哪个工具、传什么参数",开发者负责实际执行,然后把结果返回给 LLM。

Function Calling 详解:LLM 与外部世界的接口

LLM 本身是一个纯文本的输入输出系统,无法执行代码、查数据库、调 API。Function Calling(函数调用,也叫工具调用 Tool Use)是连接 LLM 和外部世界的机制:LLM 决定"需要调用哪个工具、传什么参数",开发者负责实际执行,然后把结果返回给 LLM。

1.1 为什么需要 Function Calling:一个具体场景

外部工具/函数Tool SchemaLLM用户外部工具/函数Tool SchemaLLM用户用户请求 + tools=[{name,description,parameters}]访问 Tool SchemaLLM 判断需调用函数返回 {function_call:{name:"get_weather",args:{city:"北京"}}}应用层执行实际函数get_weather(city="北京")工具返回 {temperature:22, weather:"晴"}LLM 整合工具结果自然语言回复

Function Calling 完整工作流程——用户请求经LLM判断后触发工具调用,结果整合后返回最终答案

核心要点:LLM不执行代码,只生成调用指令 | 应用层负责实际执行 | 结果回注LLM生成最终答案

假设你在构建一个客服机器人,用户问:"我的订单 ORD-20240315 现在到哪了?"

如果没有 Function Calling,LLM 只能回答:"您好,关于您的订单状态,建议您在订单详情页查询……"——它根本不知道这个订单的实际状态,只能说废话。

有了 Function Calling,流程变成:

  1. 用户问题发给 LLM,同时告诉它"你有一个查询订单状态的工具"
  2. LLM 判断:这个问题需要查实际数据,调用 get_order_status(order_id="ORD-20240315")
  3. 开发者代码收到这个请求,真正查数据库
  4. 把查询结果返回给 LLM
  5. LLM 根据真实数据回答:"您的订单 ORD-20240315 已于今天 10:30 发货,预计明天送达。"

这就是 Function Calling 的价值:它把 LLM 的语言理解能力和真实系统的数据/执行能力连接起来。

1.2 Function Calling 的本质设计

在有 Function Calling 之前,开发者也会用一种土办法:在 Prompt 里告诉 LLM"输出 JSON 格式,然后解析"。两者的核心差异在于:

  • Prompt 拼接方案:LLM 输出纯文本,开发者用正则或 JSON 解析,格式不稳定,模型容易在 JSON 前后加解释性文字导致解析失败
  • Function Calling:API 层面强制规范输出格式,专门经过训练,解析成功率接近 100%

这个分工很重要:LLM 只负责"决策",执行权始终在开发者手里。这意味着工具调用的安全边界是可控的——你可以在执行前加权限检查、参数校验,任何危险操作都可以拦截。

不需要工具

需要调工具

用户消息 + 工具定义

LLM API

LLM 决策

直接返回文字答案

返回 tool_calls
工具名 + 参数 JSON

开发者代码执行工具

工具返回结果

追加 tool result 消息

最终回答

整个流程是多轮的:工具调用不是一次性完成的,而是模型请求 → 执行 → 返回结果 → 模型继续生成的循环。

1.3 工具定义的 JSON Schema

每个工具用 JSON Schema(一种描述 JSON 数据结构的标准规范,规定有哪些字段、每个字段是什么类型、哪些是必填项)描述其名称、功能和参数。这个描述会被发给模型,模型根据它来决定何时调用以及传什么参数。

下面是一个天气查询工具和数据库查询工具的完整定义示例:

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            # description 是 LLM 的"使用说明书",写得越清晰,调用越准确
            "description": "查询指定城市的当前天气情况,包括温度、湿度、天气状况。"
                           "适用于用户询问天气、出行建议等场景。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,例如:北京、上海、广州。请使用中文城市名。",
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位,celsius=摄氏度(默认),fahrenheit=华氏度",
                    },
                },
                "required": ["city"],  # city 必填,unit 可选
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "query_order",
            "description": "查询订单状态和物流信息。用户提供订单号时调用此工具。",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "订单号,格式如 'ORD-20240315001'",
                    },
                    "include_logistics": {
                        "type": "boolean",
                        "description": "是否包含物流跟踪信息,默认 true",
                    },
                },
                "required": ["order_id"],
            },
        },
    },
]

工具描述质量直接影响调用准确率description 字段要说清楚:这个工具能做什么、什么时候该用它、参数的含义和格式要求。参数的 description 写得越具体,模型传错参数的概率越低。

1.4 完整代码示例:使用 DeepSeek API

DeepSeek 兼容 OpenAI 的接口格式,可以直接用 openai 库调用,只需修改 base_url

python
import json
import os
from openai import OpenAI

# DeepSeek API 使用 OpenAI 兼容格式
client = OpenAI(
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com",
)

# ---- 模拟的工具实现 ----
# 实际项目中,这里替换为真实的 API 调用、数据库查询等

def get_weather(city: str, unit: str = "celsius") -> dict:
    """模拟天气查询,实际项目调用真实天气 API"""
    weather_data = {
        "北京":  {"temp": 22, "humidity": 45, "condition": "晴天"},
        "上海":  {"temp": 28, "humidity": 80, "condition": "多云"},
        "广州":  {"temp": 32, "humidity": 90, "condition": "阵雨"},
    }
    data = weather_data.get(city, {"temp": 20, "humidity": 60, "condition": "未知"})
    temp = data["temp"] if unit == "celsius" else round(data["temp"] * 9 / 5 + 32, 1)
    return {
        "city":      city,
        "temp":      temp,
        "unit":      "摄氏度" if unit == "celsius" else "华氏度",
        "humidity":  data["humidity"],
        "condition": data["condition"],
    }

def query_order(order_id: str, include_logistics: bool = True) -> dict:
    """模拟订单查询,实际项目查数据库"""
    orders = {
        "ORD-20240315001": {
            "status": "已发货",
            "product": "Python 进阶指南",
            "shipped_at": "2024-03-15 10:30",
            "logistics": "顺丰快递 SF1234567890,预计明天送达"
        },
    }
    order = orders.get(order_id)
    if not order:
        return {"error": f"未找到订单 {order_id}"}
    result = {"order_id": order_id, "status": order["status"], "product": order["product"]}
    if include_logistics:
        result["logistics"] = order.get("logistics", "暂无物流信息")
    return result

# ---- 工具执行分发函数 ----

def execute_tool(tool_name: str, tool_args: dict) -> str:
    """
    根据工具名调用对应函数,统一返回 JSON 字符串
    这是 Function Calling 的执行层,实际工程中在此加权限检查、参数验证、日志记录
    """
    if tool_name == "get_weather":
        result = get_weather(**tool_args)
    elif tool_name == "query_order":
        result = query_order(**tool_args)
    else:
        result = {"error": f"未知工具:{tool_name}"}
    return json.dumps(result, ensure_ascii=False)

# ---- 工具定义 ----
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询指定城市的当前天气,包括温度、湿度和天气状况。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称,如北京、上海"},
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位,默认 celsius"
                    },
                },
                "required": ["city"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "query_order",
            "description": "查询订单状态和物流信息,需要用户提供订单号。",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "description": "订单号,格式如 ORD-YYYYMMDDXXX"},
                    "include_logistics": {"type": "boolean", "description": "是否包含物流信息,默认 true"},
                },
                "required": ["order_id"],
            },
        },
    },
]

# ---- 主对话循环 ----

def chat_with_tools(user_message: str) -> str:
    """
    支持工具调用的对话函数
    处理多轮工具调用:一次对话中模型可能连续调用多个工具
    """
    messages = [{"role": "user", "content": user_message}]

    print(f"\n用户:{user_message}")

    while True:
        response = client.chat.completions.create(
            model="deepseek-chat",
            messages=messages,
            tools=tools,
            tool_choice="auto",  # auto=模型自己决定是否调用工具
        )

        message = response.choices[0].message

        # 如果模型没有调用工具,说明它已经有了最终答案
        if not message.tool_calls:
            print(f"\nAI:{message.content}")
            return message.content

        # 模型请求调用工具,把工具调用请求记录到消息历史
        messages.append(message)

        # 执行模型请求的所有工具调用
        for tool_call in message.tool_calls:
            tool_name = tool_call.function.name
            tool_args = json.loads(tool_call.function.arguments)

            print(f"\n[调用工具] {tool_name}({tool_args})")
            result = execute_tool(tool_name, tool_args)
            print(f"[工具结果] {result}")

            # 把工具执行结果加入消息历史(必须与 tool_call.id 对应)
            messages.append({
                "role":         "tool",
                "tool_call_id": tool_call.id,  # 这个 ID 将调用请求和结果关联起来
                "content":      result,
            })
        # 循环继续:把工具结果发回给模型,让它继续生成

# 测试
chat_with_tools("北京今天天气怎么样?")
print("-" * 50)
chat_with_tools("我的订单 ORD-20240315001 现在到哪了?")
print("-" * 50)
chat_with_tools("上海天气如何?另外查一下订单 ORD-20240315001 的状态。")

预期输出:

code
用户:北京今天天气怎么样?

[调用工具] get_weather({'city': '北京'})
[工具结果] {"city": "北京", "temp": 22, "unit": "摄氏度", "humidity": 45, "condition": "晴天"}

AI:北京今天天气晴朗,气温 22°C,湿度 45%,非常适合户外活动。

--------------------------------------------------
用户:我的订单 ORD-20240315001 现在到哪了?

[调用工具] query_order({'order_id': 'ORD-20240315001'})
[工具结果] {"order_id": "ORD-20240315001", "status": "已发货", "product": "Python 进阶指南", "logistics": "顺丰快递 SF1234567890,预计明天送达"}

AI:您的订单 ORD-20240315001(Python 进阶指南)已发货,正通过顺丰快递(SF1234567890)配送,预计明天送达。

1.5 消息格式详解

理解消息格式是掌握 Function Calling 的关键。一次完整的工具调用对话,消息历史是这样的:

python
# 完整的消息历史,展示工具调用的消息结构

messages = [
    # 1. 用户提问
    {
        "role": "user",
        "content": "北京今天天气如何?"
    },

    # 2. 模型返回工具调用请求(注意:content 可能为 None)
    {
        "role": "assistant",
        "content": None,
        "tool_calls": [
            {
                "id": "call_abc123",        # 唯一 ID,用于关联工具结果
                "type": "function",
                "function": {
                    "name": "get_weather",
                    "arguments": '{"city": "北京"}'  # 注意:这是 JSON 字符串,不是 dict
                }
            }
        ]
    },

    # 3. 工具执行结果(role 必须是 "tool")
    {
        "role": "tool",
        "tool_call_id": "call_abc123",     # 必须和上面的 id 对应
        "content": '{"city": "北京", "temp": 22, "condition": "晴天"}'
    },

    # 4. 模型根据工具结果生成最终回答
    {
        "role": "assistant",
        "content": "北京今天天气晴朗,气温 22°C..."
    }
]

有几个细节容易出错:

  • message.tool_calls 存在时,message.content 通常为 None
  • tool_call.function.argumentsJSON 字符串,不是 dict,需要 json.loads() 解析
  • 工具结果消息的 tool_call_id 必须和对应的工具调用请求 id 完全匹配

1.6 并行工具调用

当用户的问题涉及多个独立工具时,模型可能在一次响应中返回多个 tool_calls,可以并行执行,大幅节省总耗时:

python
import asyncio

async def execute_tool_async(tool_call) -> dict:
    """异步执行单个工具调用"""
    tool_name = tool_call.function.name
    tool_args  = json.loads(tool_call.function.arguments)

    # 实际场景中,这里是真正的 async IO 操作(HTTP 请求、数据库查询)
    result = execute_tool(tool_name, tool_args)

    return {
        "role":         "tool",
        "tool_call_id": tool_call.id,
        "content":      result,
    }

async def chat_with_parallel_tools(user_message: str) -> str:
    """并行执行多个工具调用,而不是串行等待"""
    messages = [{"role": "user", "content": user_message}]

    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=messages,
        tools=tools,
    )
    message = response.choices[0].message

    if not message.tool_calls:
        return message.content

    messages.append(message)

    # 并行执行所有工具调用(比串行快 N 倍)
    tool_results = await asyncio.gather(
        *[execute_tool_async(tc) for tc in message.tool_calls]
    )
    messages.extend(tool_results)

    final_response = client.chat.completions.create(
        model="deepseek-chat",
        messages=messages,
        tools=tools,
    )
    return final_response.choices[0].message.content

# 这个查询会触发两个工具并行调用
result = asyncio.run(
    chat_with_parallel_tools("上海天气怎样?同时查一下订单 ORD-20240315001 的状态。")
)

并行调用的前提:工具之间没有依赖关系。如果工具 B 的输入依赖工具 A 的输出(比如先查询天气,再根据天气决定是否启动某个流程),则必须串行执行。

1.7 强制工具调用(tool_choice)

有时候需要强制模型使用特定工具,而不是让它自由选择:

python
# 强制调用特定工具
# 适用场景:用 Function Calling 做结构化数据提取,比 JSON mode 更精确
response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "分析这段文本的情感:今天天气真好!"}],
    tools=[sentiment_tool],
    # 强制调用 analyze_sentiment,模型不能选择直接回答
    tool_choice={
        "type": "function",
        "function": {"name": "analyze_sentiment"}
    },
)

# tool_choice 的三个值:
# "auto"    - 默认,模型自己决定是否调用工具(推荐)
# "none"    - 禁止调用任何工具,强制模型直接回答
# {"type": "function", "function": {"name": "xxx"}} - 强制调用指定工具

强制工具调用最有价值的场景:用 Function Calling 做结构化数据提取。定义一个"结构化提取"工具,强制模型调用它,就能稳定地从非结构化文本中提取结构化字段,比 JSON 模式或 Prompt 拼接更可靠。

1.8 多轮工具调用:解决复杂问题

某些复杂问题需要多轮工具调用才能解决。比如:

code
用户:帮我计算一下,如果我把 1000 美元换成人民币,能买几本售价 89.9 元的书?

这个问题需要三步:

  1. 查询美元兑人民币汇率
  2. 计算 1000 美元等于多少人民币(1000 × 汇率)
  3. 计算能买几本(总金额 ÷ 书价)
python
@tool
def get_exchange_rate(from_currency: str, to_currency: str) -> str:
    """查询实时汇率"""
    # 实际调用汇率 API
    return "1 USD = 7.23 CNY(2024-03-15)"

@tool
def calculate(expression: str) -> str:
    """安全地计算数学表达式"""
    allowed_chars = set("0123456789+-*/.() ")
    if not all(c in allowed_chars for c in expression):
        return "不支持的表达式"
    try:
        return str(eval(expression))
    except Exception as e:
        return f"计算失败:{str(e)}"

# 模型会自动规划:
# 第一轮:调用 get_exchange_rate(from_currency="USD", to_currency="CNY")
# 第二轮:调用 calculate(expression="1000 * 7.23")
# 第三轮:调用 calculate(expression="7230 / 89.9")
# 最终:整合所有结果,给出自然语言回答

这就是为什么工具调用的代码要写成 while True 循环——直到模型不再请求工具调用,才返回最终答案。

1.9 Function Calling 的适用场景总结

场景 是否适合 Function Calling
查询实时数据(天气、股价、汇率) 非常适合
访问私有数据(内部数据库、文件系统) 非常适合
执行操作(发邮件、创建工单、调用 API) 非常适合
结构化数据提取(从非结构文本提取字段) 非常适合
复杂计算(金融模型、数据分析) 非常适合
纯文字问答(不需要外部数据) 不需要

1.10 工具设计的注意事项

工具定义的质量直接决定模型调用的准确率:

描述要精准description 告诉模型这个工具干什么,什么情况下用。描述模糊会导致模型乱用或不知道该用哪个。

参数说明要具体:每个参数的 description 要说清楚格式要求、可选值、默认值。模型会严格按照 schema 填参数。

必填参数要标清楚required 数组里的参数必填,否则模型可能传 null 导致工具报错。

做好错误处理:工具执行失败时,返回有意义的错误信息字符串,让模型知道发生了什么,能够调整策略,而不是直接抛异常。

最小权限原则:查询工具只给只读权限,不能有写入操作;写入工具只能改特定的资源,不能访问其他数据。工具的权限边界要严格控制。

下一篇介绍 LangChain 的工具调用封装——@tool 装饰器、bind_tools、以及如何用更少的代码实现更完整的工具调用循环。

本页目录