Function Calling 与 Python 实战完整指南

Function Calling 与 Python 实战完整指南

技术手册 | OpenAI Responses API、LangChain / LangGraph、PydanticAI、MCP

Function Calling 让模型负责"理解意图与生成参数",让应用程序负责"验证、授权、执行与记录"。它不是模型直接执行函数,而是一套由应用编排的结构化协议。

目录


概念与边界

工具调用适合把语言模型接到确定性能力上:查数据库、调 HTTP 接口、读取内部知识、创建工单、执行受控操作。模型给出调用建议,业务代码才是唯一的执行者。

三个职责

环节 职责 说明
模型选择 选择工具、生成 JSON 参数 根据用户问题和工具描述输出工具名称与参数,不应自己编造外部系统结果。
应用执行 验证、鉴权、限流、调用真实函数 服务器解析参数后决定是否执行;高风险动作必须要求用户确认。
结果回注 将工具结果交还模型 以协议消息回传结果,再由模型结合上下文组织最终回答。

核心原则: Function Calling 不是 RPC 的替代品,也不是"让大模型拥有数据库权限"。模型只提出意图;工具层必须决定该请求是否可执行、以谁的身份执行、可见哪些字段、能否产生副作用。


完整调用链路

一次成功的调用至少有两次模型交互:第一次产生 function_call,第二次消费 function_call_output 后回答用户。模型的 arguments 永远应当视作不可信输入。

text 复制代码
用户问题
  "查一下 A1001 订单"
        |
        v
模型返回调用建议
  get_order(order_id="A1001")
        |
        v
应用校验并执行
  解析 -> 鉴权 -> 查询 -> 审计
        |
        v
工具结果回传
  function_call_output (携带对应 call_id)
        |
        v
模型最终回答
  "订单已支付,预计明天送达"

完整循环的关键点:

  1. 应用把可用工具及其 JSON Schema 发送给模型。
  2. 模型返回一个或多个 function_call 项,其中包含 nameargumentscall_id
  3. 应用仅从白名单中选择工具,并对 arguments 进行结构、类型、权限和业务规则校验。
  4. 应用执行工具,把每个结果作为 function_call_output 回传给对应的 call_id
  5. 模型基于工具结果输出最后的自然语言回答;若需要,可继续请求工具。

Tool Schema:给模型的契约

一个工具定义通常含 typenamedescription 和 JSON Schema 参数。名称用动词开头;描述说清何时使用、何时不用;参数必须写清单位、枚举和限制。

好工具的共同点

  • 粒度恰当:get_order 比万能的 execute_sql 更可控。
  • 输入有界:用 enumminimumpattern 限制可选值。
  • 语义不重叠:不要同时提供 search_user 与含糊的 find
  • 返回结构稳定:工具输出 JSON,包含成功状态、数据和可给用户展示的错误码。

不要依赖模型保证的事项

  • 不要假设必填字段一定存在或类型正确。
  • 不要把参数直接拼进 SQL、Shell 或 URL。
  • 不要把账号、令牌、内部异常栈回传给模型。
  • 不要让删除、支付、发消息等操作无确认地执行。

一个受约束的工具定义

python 复制代码
TOOLS = [{
    "type": "function",
    "name": "get_order",
    "description": "查询单个订单的状态和物流。仅在用户提供订单号时使用;不能用于退款或修改订单。",
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "订单号,格式为一个大写字母后接 4 位数字,例如 A1001",
                "pattern": "^[A-Z][0-9]{4}$"
            }
        },
        "required": ["order_id"],
        "additionalProperties": False
    },
    "strict": True
}]

完整可运行样例:OpenAI Python SDK

这个样例实现订单查询与退款申请两种工具,包含多轮循环、Pydantic 参数校验、模拟鉴权、工具错误回传和高风险动作确认。需要 Python 3.10+、openaipydantic

安装依赖并设置密钥:

bash 复制代码
pip install openai pydantic
export OPENAI_API_KEY="你的密钥"

将以下代码保存为 function_call_demo.py,然后执行 python function_call_demo.py

python 复制代码
import json
import os
from typing import Literal

from openai import OpenAI
from pydantic import BaseModel, ConfigDict, Field, ValidationError

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

# 真实项目中这些函数应访问受控的业务服务,不应把数据库账号交给模型。
ORDERS = {
    "A1001": {"status": "已支付", "item": "无线耳机", "delivery": "预计明天送达"},
    "B2002": {"status": "已签收", "item": "机械键盘", "delivery": "已于 7 月 24 日签收"},
}


class GetOrderArgs(BaseModel):
    model_config = ConfigDict(extra="forbid")
    order_id: str = Field(pattern=r"^[A-Z][0-9]{4}$")


class RequestRefundArgs(BaseModel):
    model_config = ConfigDict(extra="forbid")
    order_id: str = Field(pattern=r"^[A-Z][0-9]{4}$")
    reason: Literal["不想要了", "商品损坏", "发错商品"]
    # 必须由应用层确认,模型不能自行替用户确认。
    user_confirmed: bool


TOOLS = [
    {
        "type": "function",
        "name": "get_order",
        "description": "查询订单状态、商品和物流。用户给出订单号时使用。",
        "parameters": GetOrderArgs.model_json_schema(),
        "strict": True,
    },
    {
        "type": "function",
        "name": "request_refund",
        "description": "为已支付订单提交退款申请。只在用户明确提出退款且确认原因后使用。",
        "parameters": RequestRefundArgs.model_json_schema(),
        "strict": True,
    },
]


def get_order(args: GetOrderArgs, user_id: str) -> dict:
    # 示例鉴权:真实项目应使用会话/JWT 的订单归属校验。
    order = ORDERS.get(args.order_id)
    if not order:
        return {"ok": False, "code": "ORDER_NOT_FOUND", "message": "未找到该订单"}
    return {"ok": True, "order_id": args.order_id, **order}


def request_refund(args: RequestRefundArgs, user_id: str) -> dict:
    if not args.user_confirmed:
        return {"ok": False, "code": "CONFIRMATION_REQUIRED", "message": "退款需要用户明确确认"}
    order = ORDERS.get(args.order_id)
    if not order:
        return {"ok": False, "code": "ORDER_NOT_FOUND", "message": "未找到该订单"}
    if order["status"] != "已支付":
        return {"ok": False, "code": "REFUND_NOT_ALLOWED", "message": "当前订单状态不能申请退款"}
    # 真实项目:此处调用幂等的退款服务,并记录 request_id、用户和审计日志。
    return {"ok": True, "refund_request_id": "R-90001", "message": "退款申请已提交"}


TOOL_HANDLERS = {
    "get_order": (GetOrderArgs, get_order),
    "request_refund": (RequestRefundArgs, request_refund),
}


def execute_tool(name: str, raw_arguments: str, user_id: str) -> dict:
    """模型输出只是一份候选参数;在这里完成解析、白名单和校验。"""
    handler = TOOL_HANDLERS.get(name)
    if not handler:
        return {"ok": False, "code": "TOOL_NOT_ALLOWED", "message": "未授权的工具"}
    model, function = handler
    try:
        args = model.model_validate_json(raw_arguments)
    except ValidationError as exc:
        return {"ok": False, "code": "INVALID_ARGUMENTS", "message": str(exc.errors())}
    try:
        return function(args, user_id=user_id)
    except Exception:
        # 日志可保留完整异常;不要把敏感堆栈交给模型或用户。
        return {"ok": False, "code": "TOOL_EXECUTION_FAILED", "message": "服务暂时不可用"}


def answer(user_text: str, user_id: str = "demo-user") -> str:
    conversation = [{"role": "user", "content": user_text}]
    for _ in range(5):  # 防止模型/工具意外循环
        response = client.responses.create(
            model="gpt-4.1-mini",
            instructions="你是订单助手。仅根据工具结果回答;没有工具结果时不要编造订单状态。",
            input=conversation,
            tools=TOOLS,
        )
        calls = [item for item in response.output if item.type == "function_call"]
        if not calls:
            return response.output_text

        # 保留模型本轮输出,再把每个 call_id 对应的结果回传给下一轮。
        conversation.extend(response.output)
        for call in calls:
            result = execute_tool(call.name, call.arguments, user_id)
            print(f"[audit] tool={call.name} result={result}")
            conversation.append({
                "type": "function_call_output",
                "call_id": call.call_id,
                "output": json.dumps(result, ensure_ascii=False),
            })
    return "请求步骤超过限制,请稍后重试。"


if __name__ == "__main__":
    print(answer("请查一下 A1001 订单的状态"))
    print(answer("我要退款 A1001,原因是商品损坏,已确认提交"))

运行时会发生什么

  1. 第一次 responses.create 返回一个或多个 function_call
  2. 程序用 call.name 查白名单,以 call.arguments 做 Pydantic 校验。
  3. 工具结果写回匹配的 call.call_id
  4. 下一次模型调用根据工具结果输出用户答案。

查询 A1001 时控制台会看到 get_order 的审计记录;退款例子会看到 request_refund 的审计记录。若模型未明确传入 user_confirmed=true,业务函数会拒绝执行。


Python 框架怎么选

不要为了"有 Agent"而上复杂编排。一个或两个工具的同步工作流,直接用模型官方 SDK 最透明;有状态图、人工介入或多步骤恢复需求,再引入框架。

方案 适用情况 工具定义方式 取舍
OpenAI Python SDK 最小闭环、需要完全掌控协议 JSON Schema / Pydantic Schema 推荐起步,依赖少,循环需自行维护。
PydanticAI Python 类型优先、依赖注入、快速业务 Agent @agent.tool + 类型注解 类型友好,框架抽象较多。
LangChain 已有 LangChain 生态、基础 tool binding @tool / Pydantic 生态广,需理解消息格式。
LangGraph 长流程、状态机、人工审批、恢复执行 复用 LangChain tools 适合生产编排,学习和运行复杂度更高。
MCP Python SDK 将工具独立为可复用服务,供多个客户端使用 @mcp.tool() 它是协议/服务层,不替代模型循环。
LlamaIndex 以数据检索、索引和 RAG 为主的应用 FunctionTool / QueryEngineTool RAG 集成强,不适合作为纯工具层首选。

PydanticAI:类型化工具

python 复制代码
from pydantic_ai import Agent

agent = Agent("openai:gpt-4.1-mini")

@agent.tool_plain
def get_weather(city: str) -> str:
    """查询城市天气;city 必须是城市名称。"""
    return f"{city}:晴,26°C"

result = agent.run_sync("北京天气如何?")
print(result.output)

LangChain:绑定工具

python 复制代码
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

@tool
def get_weather(city: str) -> str:
    """查询城市天气。"""
    return f"{city}:晴,26°C"

llm = ChatOpenAI(model="gpt-4.1-mini")
ai_msg = llm.bind_tools([get_weather]).invoke("北京天气?")
# ai_msg.tool_calls 是调用建议;应用仍需执行并回传 ToolMessage。

MCP:把能力发布成工具服务

python 复制代码
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("order-tools")

@mcp.tool()
def get_order(order_id: str) -> dict:
    """查询订单状态。"""
    return {"order_id": order_id, "status": "已支付"}

if __name__ == "__main__":
    mcp.run(transport="stdio")

LangGraph:显式状态编排

python 复制代码
# 核心思想:把模型、工具、审批设计成图中的节点。
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI

agent = create_react_agent(
    ChatOpenAI(model="gpt-4.1-mini"),
    tools=[get_weather],
)
state = agent.invoke({"messages": [("user", "北京天气?")]})

四种常见实现模式

模式 用法 注意事项
单轮查询 模型调用一个只读工具后直接回答;适合天气、订单、库存、指标查询。 简单、低延迟,应优先采用。
并行只读调用 模型一次输出多个独立调用,例如查航班与天气。 应用可并发执行,但每条结果必须回传原始 call_id
多步工具循环 前一个结果决定下一次调用,例如先检索客户,再查询其订单。 限制轮数和每轮工具数,防止循环与成本失控。
确认式副作用 创建退款、发邮件、删除资源等先生成待执行计划。 展示关键字段,收到用户确认后才调用写操作工具。

所有模式都应有最大轮数、超时和可观测日志。


安全、可靠性与成本控制

工具调用将语言输入连接到系统边界,安全控制不能只放在提示词中。

必须在工具层实现

  • 工具白名单和按用户/租户的授权校验。
  • JSON Schema 与服务端类型/业务规则的双重校验。
  • 数据库参数化查询、命令参数白名单、URL 域名允许列表。
  • 超时、重试、熔断、幂等键和最大调用轮数。
  • 调用审计:用户、工具、参数摘要、结果码、耗时、request_id

敏感信息处理

  • 密钥仅从环境变量或密钥服务读取,不写入 prompt、代码或工具输出。
  • 对工具输出做字段级脱敏,只返回回答必需的数据。
  • 工具异常对模型返回稳定错误码,完整堆栈仅写内部日志。
  • 高风险动作添加二次确认、额度限制或人工审批节点。
  • 将外部网页、邮件、检索内容视作不可信数据,防范提示注入。

反例:run_sql(sql: str)shell(command: str) 或带管理员凭据的 HTTP 通道直接提供给模型,等于把模型生成文本升级为系统执行权限。应改为领域工具,例如 get_monthly_sales(region, month),并在服务端固定查询模板。


排错与测试方法

先记录结构化调用事件,后调模型描述。大多数问题可归为 Schema、工具实现、消息回传或授权四类。

现象 常见原因 检查与修复
模型从不调用工具 描述不清、工具与问题不匹配、工具选择策略受限 在 description 写清触发条件;打印实际发送的 tools;用明确问题做最小验证。
参数经常错误 Schema 太宽、缺少枚举/格式/示例 缩小参数空间;增加 requiredpatternenum;服务器仍要校验。
模型忽略工具结果 没有把结果按 call_id 以正确消息类型回传 记录 response.outputcall_idfunction_call_output;保证结果 JSON 可解析。
重复调用或死循环 工具输出不明确、模型不断尝试、没有最大步数 返回稳定错误码;设置循环上限;对相同调用做去重或幂等。
生产数据泄露 工具返回过量字段或日志记录原始敏感参数 最小化 DTO;日志脱敏;添加租户过滤和审计回放测试。

建议的单元测试

  • 无效 JSON、缺少字段、非法枚举、越界长度。
  • 无权限用户不能查询他人资源。
  • 写操作未确认、重复请求、下游超时。
  • 未知工具名、工具抛异常时返回稳定错误码。

建议记录的字段

  • request_id、模型、token 用量、轮次、耗时。
  • 工具名、已脱敏参数摘要、用户/租户、结果码。
  • 模型的 call_id 与下游服务 trace id,便于串联追踪。

上线前清单

协议与功能

  • 每个工具有唯一名字、明确描述和收紧的 Schema。
  • 工具调用与结果通过正确的 call_id 配对。
  • 有最大循环次数、单请求超时和明确失败回复。
  • 至少覆盖成功、参数错误、权限拒绝、下游失败四类测试。

生产治理

  • 权限、租户隔离、限流和审计在服务端生效。
  • 写操作需要确认、幂等键或人工审批。
  • 敏感字段已脱敏,密钥不进入模型上下文。
  • 具备指标、告警和可回放的调用日志。

推荐落地顺序: 先用官方 SDK 写一个只读单工具闭环,完善参数校验和日志;再引入多个工具;当流程出现条件分支、人工审批、长时间任务或恢复需求时,使用 LangGraph 等状态编排框架。这样能始终看清模型、工具和业务权限之间的边界。

相关推荐
Web4Browser1 小时前
指纹浏览器 API 自动化怎么接:启动 Profile、获取 CDP 端点并连接自动化框架
前端·网络·typescript·自动化
c_lb72881 小时前
2026年不同基础做量化,先找AI能参与的位置
人工智能·python
小小晓.1 小时前
C++:语句和作用域
开发语言·c++
wanderist.2 小时前
Lambda表达式在算法竞赛中的应用
java·开发语言·算法
chenment3 小时前
ComfyUI 自定义节点开发:从零扩展你的图像生成工作流
python·stable diffusion
海天鹰3 小时前
PHP上传文件
android·开发语言·php
互联网中的一颗神经元4 小时前
小白python入门 - 25. SQL 与表设计入门
数据库·python·sql
KaMeidebaby4 小时前
卡梅德生物技术快报 | 核酸适配体文库测序:核酸适配体文库测序的技术原理、实验流程与数据解析
前端·网络·数据库·人工智能·算法
上海云盾商务经理杨杨5 小时前
2026 生产级 Linux 内核抗 DDoS 调优!全套 sysctl 配置直接套用
网络·安全·ddos