Function Calling 实战:让大模型调用你的 API 与工具

Function Calling(函数调用)是大模型连接真实世界的关键能力:它让 LLM 在对话中自动决定”何时调用哪个外部 API、传什么参数”,把自然语言变成结构化请求。本文用 OpenAI 兼容接口和一段可运行的 Python 代码,带你从零实现 Function Calling,覆盖工具 Schema 定义、调用闭环、并行调用与生产避坑,帮你的 AI 应用真正”动手干活”。

一、Function Calling 解决了什么问题

大模型本身只能”说话”,不能”做事”。要让它查实时天气、查订单、改数据库,必须借助外部工具。传统做法是用人写死的 if-else 去解析用户意图,不仅脆弱,而且每新增一个能力就要改一堆分支,极难维护。Function Calling 把这件事交给模型:你声明一组工具(函数名、描述、参数 JSON Schema),模型根据用户问题自行选择工具并产出合法参数,再由你的代码执行,并把结果回灌给模型,最终生成自然语言回答。这样模型负责”决策”,代码负责”执行”,边界清晰、可扩展,新增工具只需追加一段声明。

更重要的是,它把”理解意图”和”执行动作”解耦。模型不需要知道天气接口怎么鉴权、订单库怎么连,它只产出结构化的 city 或 order_id;真正的权限、凭证、安全边界都握在你手里。这就是为什么 Function Calling 被认为是 Agent(智能体)的能力基石。

二、定义工具:写好 JSON Schema 是第一步

工具的本质是一段 JSON 描述。每个工具包含 name、description 和 parameters(JSON Schema)。description 写得越清楚,模型选得越准;required 字段决定哪些参数不可或缺;枚举用 enum 可以把取值锁死,避免模型自由发挥。

tools = [
  {
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "查询指定城市的当前天气,返回温度和天气状况",
      "parameters": {
        "type": "object",
        "properties": {
          "city": {"type": "string", "description": "城市名称,例如 北京"},
          "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius"}
        },
        "required": ["city"]
      }
    }
  }
]

要点:required 字段必须齐全;枚举用 enum 约束取值范围;description 用自然语言说明”什么时候该用这个工具”,能显著降低误调用。经验上,把”不要什么时候用”也写进 description,比只写正向描述更稳。

三、完整调用闭环(Python 实战)

下面用 OpenAI 兼容 SDK 演示一次标准闭环:发请求 → 解析 tool_calls → 本地执行 → 回传 → 拿到最终答案。

from openai import OpenAI
import json

client = OpenAI(base_url="https://your-proxy/v1", api_key="sk-xxx")

messages = [{"role": "user", "content": "北京今天天气怎么样?"}]
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)
msg = resp.choices[0].message

if msg.tool_calls:
    messages.append(msg)                      # 先把模型的工具调用存下来
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(**args)          # 本地执行真实函数
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result, ensure_ascii=False),
        })
    final = client.chat.completions.create(
        model="gpt-4o-mini", messages=messages
    )
    print(final.choices[0].message.content)

注意:必须把带有 tool_calls 的 assistant 消息原样 append 回 messages,否则模型会因缺少 tool_call_id 而报错;回传消息的 role 用 “tool” 且携带对应的 tool_call_id,这是闭环能否跑通的关键。

1. 让模型”只选工具不啰嗦”

设置 tool_choice=”required” 可强制模型先调用工具;设成具体函数名则锁定唯一工具。在调试阶段非常有用,能快速验证工具链路是否打通,也方便写单测。

2. 并行工具调用

新版模型支持一次返回多个 tool_calls(例如同时查天气和汇率)。代码里用 for 循环逐个执行即可,彼此顺序无关,必要时可并发执行以提升吞吐。注意并行时每个 tool_call 都要有独立的 tool_call_id 回传。

四、一个更真实的例子:订单查询工具

天气只是玩具例子。真实业务里,最常见的工具是”查订单””查库存””发起退款”。下面给一个订单查询工具的完整声明与执行示例,它演示了如何让模型在用户说”帮我看下订单 A2026 到哪了”时,自动调用你的内部接口。

def query_order(order_id: str) -> dict:
    # 这里换成你真实的订单服务调用,例如走内网 HTTP 或查数据库
    resp = requests.get(f"http://internal.order.svc/orders/{order_id}", timeout=3)
    resp.raise_for_status()
    data = resp.json()
    return {
        "order_id": data["id"],
        "status": data["status"],
        "eta": data["eta"],
        "tracking": data["tracking_no"],
    }

tools.append({
    "type": "function",
    "function": {
        "name": "query_order",
        "description": "根据用户提供的订单号查询物流状态与预计送达时间",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string", "description": "订单号,例如 A20260901"}
            },
            "required": ["order_id"]
        }
    }
})

关键点在于:工具函数内部走的是你完全可控的内部服务,模型拿不到任何凭证,只拿到你决定回传的字段。这就把”模型能做什么”牢牢框在你的业务边界内,安全性和可控性都更好。也正因如此,即便模型”想”做危险动作,也没有通道。

五、进阶:重试、流式与可观测

当工具链路变长,可观测性变得重要。建议在每次工具调用前后打日志:记录模型选了哪个工具、传了什么参数、执行耗时与结果摘要,并用 traceId 串联整轮对话,排障效率会高很多。若工具执行失败,不要直接把原始异常抛给模型,而是返回一段结构化错误(例如 {“error”:”timeout”}),让模型自行决定重试还是改用其它工具。这种”失败可协商”的设计,是 Agent 能稳定跑通多步任务的前提。

另外,工具返回内容要尽量精简。模型上下文有限,把几 KB 的原始 JSON 全塞回去既浪费 token 又容易让模型迷失在细节里。先抽取模型真正需要的字段,再回传,成本和稳定性都会更好。

六、生产环境避坑清单

把 Function Calling 真正上线前,这几条必须过一遍:

风险点现象应对
参数校验模型偶尔漏填 required 或类型错本地用 jsonschema 二次校验,失败返回友好错误而非直接崩溃
执行超时外部 API 慢导致整体卡死给工具函数加 timeout,配合重试与降级策略
成本失控模型狂调工具刷 token限制单轮最大调用次数,超阈值直接中断
安全风险工具能删库、能发短信危险操作加白名单与人工确认,绝不把写权限暴露给自动链路

七、在框架与私有化环境中落地

如果你用 Java 技术栈,可以直接用 Spring AI 接入大模型 的 ToolCallback 机制声明函数,无需手写 JSON Schema 拼接;想完全私有化、不把数据送出去,可用 Dify + Ollama 私有化部署 在本地跑带工具的大模型。当需要统一对接多家模型、做鉴权与限流时,LiteLLM 统一代理 能把不同厂商的 Function Calling 接口抹平成同一套。若你的工具涉及向量检索,先读 Embedding 模型选型 把召回质量打牢,再接工具链,效果会更稳。

八、小结

Function Calling 把大模型从”聊天机器人”升级成”能办事的 Agent”:你定义工具,模型决定何时调用,代码负责落地。写好 Schema、守住执行边界、做好超时与校验,就能在真实业务里稳定跑起来。下一步,把这些工具串成多步工作流,就是 Agent 的起点。

上一篇 《etcd 实战:Kubernetes 分布式键值存储与高可用运维》
下一篇 K8s 节点 NotReady 排障:kubelet 与网络故障定位
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]