函数调用详解:让大模型用上你的工具与数据库

函数调用如何让大模型查询数据库、使用工具?本文从完整的订单状态查询代码入手,讲清工具定义、参数校验、执行与结果回传,对照 OpenAI、Anthropic 和 Gemini 的消息格式,并说明结构化输出与 MCP 各自适合什么场景,以及上线前如何处理权限、工具误选、失败重试和重复执行,帮助你跑通首个只读查询。

发布于

函数调用详解:让大模型用上你的工具与数据库

函数调用(Function Calling)让大语言模型(LLM)能够请求你的软件执行操作。理解它,关键是分清 3 个环节:模型提出结构化的调用请求,应用代码负责校验和执行,再将结果交回模型。让模型用上数据库,靠的是搭好这条调用链路,而不是把数据库密码交给它。

大模型函数调用是怎么工作的?

模型提出请求,应用程序执行操作。 开发者要把 LLM 接入 API 或数据库,动手写代码前,最该弄清楚的就是这条分工边界。

可以把工具调用想成餐厅的点菜单。服务员可以下单,但菜单本身不会做菜;后厨会核对订单,制作能做的菜,再反馈结果。在应用里,工具调用就是点菜单,函数就是后厨。

你先向模型提供 工具 schema,说明有哪些操作可用,以及每项操作接受哪些输入字段。模型可以返回函数名和 参数值,告诉应用该往这些字段里填什么。应用检查请求、调用函数,再把输出交给模型,让它回答用户,或者继续请求其他操作。Google 的函数调用指南明确指出,执行操作是应用程序的职责。

工具既可以读,也可以写。查询订单、搜索文档库、检查是否可用,即使不修改任何数据,也属于工具操作。首次接入时,适合从范围明确的查询开始:整条调用链路都能看清,也不用先处理需要回滚的事务。

建筑模型展示工具调用经过应用校验、查询数据库,再将结果返回模型的过程
应用负责衔接数据库查询,并把结果返回给模型。

Function Calling 和 Tool Calling 有什么区别?

对于自定义函数,这两个名称往往指同一种机制。 OpenAI 两种说法都会用,Anthropic 也说明 tool use 又称 function calling。“工具”的范围更广,既可以是你实现的函数,也可以是模型服务商托管的能力。比名称更重要的是在哪里执行:如果函数访问的是你自己的数据库,就由你的应用负责运行。

下文对照三家厂商的文档,会更容易看出具体差异。各家的消息格式并不相同,同一家 API 的请求封装和结果封装要配套使用。

函数调用最小示例:从请求到返回结果

提供一个订单状态查询工具,再把数据库查到的状态如实返回。 下面的例子在本地 SQLite 数据库中放入了一个刻意虚构的订单 ORD-1042。这是教学用的测试数据,既不是真实客户记录,也不是实际采集的模型响应。

OpenAI 函数调用:跑通完整往返流程

OpenAI 的函数调用文档展示了 Responses API 的完整流程:声明函数,接收 function_call,执行应用代码,再返回带有相同 call_id 的 function_call_output。

OpenAI 函数调用文档
OpenAI:函数调用

用 python -m pip install openai 安装 Python SDK,在环境变量中设置 OPENAI_API_KEY,然后把以下代码保存为 order_tool.py。示例使用 gpt-6-astra,即当前文档中 Responses 示例采用的模型,并不意味着只能使用这个模型。运行脚本会发出真实的 API 请求,数据库本身则保留在内存中。

Python
import json
import re
import sqlite3

from openai import OpenAI

client = OpenAI()
model = "gpt-6-astra"

# Demo identity. In a web app, obtain this from your authenticated session.
tenant_id = "tenant_demo"
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE orders (tenant_id TEXT, order_id TEXT, status TEXT)")
db.execute(
    "INSERT INTO orders VALUES (?, ?, ?)",
    (tenant_id, "ORD-1042", "shipped"),
)

# The tool schema: a name, a purpose, and an input contract.
tool = {
    "type": "function",
    "name": "get_order_status",
    "description": (
        "Look up the current status of an existing order by order ID. "
        "Use for status questions, not refunds or address changes. "
        "Returns the order ID and status, or an error."
    ),
    "strict": True,
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "The order ID supplied by the user, such as ORD-1042.",
            }
        },
        "required": ["order_id"],
        "additionalProperties": False,
    },
}


def execute(name, raw_arguments):
    if name != "get_order_status":
        return {"error": "unknown_tool"}
    try:
        args = json.loads(raw_arguments)
    except (json.JSONDecodeError, TypeError):
        return {"error": "invalid_arguments"}
    if not isinstance(args, dict) or set(args) != {"order_id"}:
        return {"error": "invalid_arguments"}
    order_id = args["order_id"]
    if not isinstance(order_id, str) or not re.fullmatch(r"ORD-[0-9]+", order_id):
        return {"error": "invalid_order_id"}
    row = db.execute(
        "SELECT status FROM orders WHERE tenant_id = ? AND order_id = ?",
        (tenant_id, order_id),
    ).fetchone()
    if row is None:
        return {"error": "order_not_found"}
    return {"order_id": order_id, "status": row[0]}


history = [{"role": "user", "content": "What is the status of order ORD-1042?"}]
response = client.responses.create(
    model=model,
    input=history,
    tools=[tool],
    tool_choice={"type": "function", "name": "get_order_status"},
    parallel_tool_calls=False,
)

# Keep every output item, including any reasoning items.
history.extend(response.output)
calls = [item for item in response.output if item.type == "function_call"]
if len(calls) != 1:
    raise RuntimeError("Expected one tool call; inspect the response before continuing.")
call = calls[0]
result = execute(call.name, call.arguments)
history.append({
    "type": "function_call_output",
    "call_id": call.call_id,
    "output": json.dumps(result),
})

answer = client.responses.create(
    model=model,
    input=history,
    tools=[tool],
    tool_choice="none",
    instructions="Answer from the tool result. If it contains an error, report the failure.",
)
print(answer.output_text)
db.close()

这个示例刻意限制了执行范围:成功执行时,会发出 2 次模型请求,完成 1 次数据库查询。首次请求强制调用已知的查询工具,第二次请求禁用后续工具调用,便于看清整个机制。通用助手还需要处理其他情况,比如没有合适的工具、信息不完整,或者需要再查一次。

工具 schema 中没有 tenant_id,因为应用已经知道当前登录者是谁。数据库查询会同时使用这个可信身份和请求中的订单 ID。如果把身份也改成由模型提供的参数,就会失去这个示例要说明的边界。

模型发出的调用,以及应用返回的结果

调用请求和执行结果是两条独立的消息,通过调用标识符关联。 下面是一条典型的调用示例。其中的标识符和取值仅用于说明,并非实际捕获的 API 响应。

JSON
{
  "type": "function_call",
  "id": "fc_demo",
  "call_id": "call_demo",
  "name": "get_order_status",
  "arguments": "{\"order_id\":\"ORD-1042\"}"
}

处理函数返回测试订单的状态后,应用追加以下结果。在真实流程中,应复制实际的 call.call_id,不要自行生成新的标识符。

JSON
{
  "type": "function_call_output",
  "call_id": "call_demo",
  "output": "{\"order_id\":\"ORD-1042\",\"status\":\"shipped\"}"
}

此时,模型就有依据回答“订单 ORD-1042 已发货”。状态来自数据库;模型负责把用户请求转换为参数,再把返回的数据组织成回答。

三家厂商的文档,如何描述同一套机制?

执行逻辑可以跨厂商沿用,通信格式不能直接照搬。 以下对照已于 2026 年 10 月 17 日 根据各厂商的在线文档核验,区分的是当前示例的具体写法,不能因为字段名称相近就混用。

厂商及文档中的名称模型如何请求执行操作应用返回什么
OpenAI:函数调用(function calling)Responses function_call,包含 name、arguments 和 call_id带有匹配 call_id 的 function_call_output
Anthropic:工具使用(tool use)assistant 消息中的 tool_use 块,包含 name、input 和 id引用 tool_use_id 的 tool_result 块
Google:Gemini 函数调用(function calling)当前 Interactions 指南中的 function_call 步骤引用 call_id 的 function_result

Anthropic:如何使用工具

Anthropic 的工具使用文档用 input_schema 定义函数输入。对于客户端工具,应用代码处理 assistant 消息中的 tool_use 块,再在下一条 user 消息中发送 tool_result,同时在对话历史中保留原来的 assistant 消息。

Anthropic 文档介绍客户端工具和工具结果
Anthropic:工具使用

文档明确区分了执行位置:客户端工具在你的应用中运行,服务端工具在 Anthropic 的基础设施上运行。因此,能使用托管搜索工具,并不代表 Claude 能直接执行访问你私有数据库的函数。这部分仍然需要你来实现。

Gemini:如何调用函数

Google 的 Gemini 函数调用文档同样把模型请求和应用执行分开。当前指南使用 Interactions API,以 function_call 和 function_result 步骤表示调用与结果,并通过 previous_interaction_id 延续交互。

Google Gemini 函数调用文档
Gemini:函数调用

应以你实际使用的 API 所对应的示例为准。从一套 API 复制函数声明,再从另一套 API 复制结果消息,并不能实现跨平台复用。可以保留 get_order_status 这样的通用操作,但它的声明和结果封装需要一起适配。

为什么工具调用构成了智能体的工作循环?

使用工具的智能体会反复选择操作、接收结果,再决定下一步。 函数调用把模型的决策交接给软件能够执行的具体操作。

例如,订单助手可以先查询发货记录,查看结果中的承运商单号,再查询物流轨迹,最后回答用户。后一次操作依赖前一次的结果。这种反馈循环比让模型事先编出所有步骤更有用。

Anthropic 的智能体架构指南区分了固定工作流与自主决定如何使用工具的智能体,也把工具反馈和停止条件列为循环的组成部分。

把函数调用称为“所有智能体的基础”,只能算有一定适用范围的概括。文档并没有证明,所有被称为智能体的系统都使用某一种函数调用 API。真正可沿用的核心思路是“模型决策—执行操作”的循环。一次简单查询完全可以保留为固定工作流,不必配上自主规划器。

上线后常见的三类问题

schema 质量、工具选择、参数校验,要分别处理。 修好其中一项,不代表另外两项也已解决。

schema 写得含糊,格式对了,含义却错了

字段只叫 id,模型无法从名称判断你要的是订单号、客户标识,还是运单号。把 action 定义成没有约束的字符串,也容易让模型填入处理函数不认识的值。与其继续补提示词,不如先把输入约定写清楚。

参数命名要准确,含义要说明;只接受固定几种值时,就使用枚举。第一个工具的职责应足够集中,用一句普通的话就能解释清楚。示例中的 order_id 含义明确,描述也排除了退款和地址变更。

OpenAI 的严格模式要求对象设置 additionalProperties: false,所有属性都列入 required;允许缺省的值可以通过接受 null 来表示。指南列出了这些严格模式要求。Google 也规定了 schema 限制,因此,本地校验器接受的 schema,不代表所有模型 API 都会接受。

能力边界: schema 能约束数据的表示形式,但格式合法的订单 ID 仍可能指向错误的订单。这要由应用检查。

工具选错了:参数合法,操作却不对

客户问“我的退款到哪一步了?”,需要的是查询状态,而不是再发起一笔退款。如果两个工具的描述都只是“处理退款”,模型就很难分清该选哪个。

写明每个工具应该和不应该在哪些情况下使用,并且只开放适合当前任务和用户的操作。Anthropic 的工具定义指南明确建议说明工具用途、参数含义、使用边界和限制。

如果应用已经确定必须执行哪项操作,可以使用 API 支持的工具选择控制,也可以直接执行该操作。示例强制查询,是为了演示机制,不能因此认为“每条用户消息都必须触发工具”。面对一句问候,或者信息不完整的订单问题,正常对话可能才是合适的回应。

能力边界: 参数格式正确,无法说明模型选中了正确的业务操作。应使用有代表性的用户请求评估工具选择,也要覆盖不应发生任何调用的情况。

参数没校验:结构正确,仍然可能越权

处理函数就是输入边界。先对照明确的允许列表检查函数,再解析参数、校验结构和含义,并根据应用中已认证的身份执行访问控制。

对于订单示例,一个值得测试的失败场景是查询其他租户,也就是其他客户账户的订单。即使订单标识符是完全合法的字符串,查询也不能返回那条记录。还应测试缺少订单 ID、出现预期之外的字段,以及请求处理函数并未实现的操作。

Anthropic 的概述文档指出,信息缺失时,模型可能自行推断参数值。字段是必填的,不等于用户实际提供了这个值。执行影响较大的操作前,应把请求中的操作和输入,与用户实际授权的内容逐一核对。

执行失败时,应返回可识别的错误。超时并不能证明操作从未发生:重试写入前,应使用应用层的幂等机制,识别是否为同一次预期操作,避免重复产生效果。这些属于实现建议,并非模型 API 自带的保证。

什么时候该用结构化输出?

根据已有信息生成规定格式的答案,用结构化输出;需要执行外部操作,用工具。 例如,将客服消息归类并返回类别和紧急程度,可以使用结构化输出。助手如果需要订单的当前状态,就必须通过查询获取。

OpenAI 的结构化输出指南也区分了“连接应用功能”和“规定模型回答格式”这两种需求,并说明严格的函数 schema 底层使用了 Structured Outputs。两项能力可以配合使用,并不是互斥的产品。

建筑式分岔示意图:需要实时数据的问题进入工具调用,信息已知的回答进入结构化输出
回答需要实时数据时,先通过工具获取,再组织输出格式。

假设你已经提供了订单记录,界面只需要 {order_id, status, explanation},结构化回答就够了。如果缺少记录,或者记录需要更新,就先执行查询,之后仍然可以让最终答案按指定结构输出。

一个声称“邮件已发送”的 JSON 对象,本身仍然只是输出数据。真正发送邮件,需要执行操作,并拿到能够核查的结果。

函数调用和 MCP 有什么区别?

函数调用负责表达模型要执行什么操作;Model Context Protocol(MCP)提供共享接口,让兼容的应用发现和使用工具及其他上下文。 在引入独立的工具服务器之前,完全可以先把函数直接接入一个应用。

MCP 与工具调用:什么时候需要共享工具?

如果多个应用都需要同一个订单查询能力,可以通过 MCP 服务器开放工具,供它们发现和调用。MCP 架构文档介绍了用于发现工具的 tools/list,以及用于发起执行请求的 tools/call。服务器仍然需要实现具体功能,并检查访问权限。

下一步可以阅读 MCP 是什么,以及 host、client 和 server 如何配合。直接调用跑通后,再参考 MCP 服务器实现指南,把能力封装为可复用的工具。引入 MCP,并不会免去对请求操作的校验。

哪些说法被夸大了?

通过 schema 校验,只能证明数据符合格式约束,不能证明任务已经完成。 操作可能不合适,记录可能无权访问,底层服务也可能不可用。最终回答再流畅,也不能解决这些问题。

有一份工具列表,也不等于有了智能体。你仍然要规定哪些工具可用、结果如何进入对话、何时停止,以及出错后怎么办。框架可以封装这些工作,但执行策略仍然需要自己设计。

第一个有价值的里程碑其实很具体:请求到达预期的处理函数,只返回有权访问的数据,回答准确反映执行结果。先让这条链路清晰可查,再扩大系统的权限。

第一次上线,先做什么?

从一个只读操作开始,解决用户经常提出的问题。 订单状态查询适合作为起点,因为预期结果和访问边界都容易检查。

对 开发者 而言,如果回答依赖应用能够可靠获取的数据,就可以开始接入。当下一步操作已经确定时,保留固定工作流即可。等明确了权限、失败处理方式和防止重复执行的机制,再考虑让模型选择写操作。

对 运维人员 而言,要能看清执行了什么、返回了什么。记录选中的工具、校验结果、执行状态和调用之间的关联,同时避免把敏感参数写入普通日志。根据任务设置超时和工具调用轮次上限,不要让对话循环无限继续。

对 采购方 而言,除了成功回答,也要让实施团队演示一次被拒绝的查询和一次执行失败。如果产品只是归纳已经提供的信息,可能并不需要函数调用,结构化输出或普通文本生成就已足够。

下一步就从这里动手:实现一个范围明确的查询,分别尝试有权访问的记录、无权访问的记录和信息不完整的请求,检查每种情况下交回模型的结果。只有当每个请求为何被接受或拒绝都能解释清楚时,再扩充工具列表。

常见问题

什么是函数调用?

函数调用是一种让模型通过指定函数名和参数来请求执行操作的机制。对于你自己定义的函数,由应用程序负责执行,再把结果送回对话。

大模型的工具调用和函数调用有什么区别?

对于自定义函数,厂商经常混用这两个术语。工具使用还可能包含服务商托管的能力,因此应确认操作在哪里执行,不能仅凭名称判断。

函数调用的具体例子是什么?

模型请求调用 get_order_status,将 order_id 设为 ORD-1042。应用代码校验请求、检查权限、查询订单,再返回状态。本文中的订单和状态均为虚构的示例数据。

如何在 GPT-5 中使用函数调用?

选择项目中可用且支持该功能的模型,按照文档中的 Responses 流程操作:发送工具定义,处理函数调用输出项,再用匹配的调用 ID 返回结果。延续对话时应保留推理项。上面的可运行示例采用当前指南所用的模型,没有假定你的账户已启用某个特定的 GPT-5 模型。

想获取更多 AI 开发实用指南,欢迎订阅邮件通讯。

发布日期
分类
Build
MCP是什么?与 API 有何区别,产品该如何选择

MCP是什么?与 API 有何区别,产品该如何选择

MCP是什么,已有 API,还需要它吗?本文从工具发现、认证授权、部署和版本兼容入手,比较 MCP 与 REST API,并用创建工单流程说明两套接口如何共享业务规则。结合 Claude、ChatGPT、Cursor 和 GitHub Copilot 的文档,判断何时保留 API、增加 MCP 或同时提供两者。2026年10月17日Build
Hermes Agent 评测:记忆、技能与成本,是否值得部署?

Hermes Agent 评测:记忆、技能与成本,是否值得部署?

Hermes Agent 值得安装吗?这篇评测从持久记忆、自动创建技能、消息网关和定时任务出发,梳理桌面与终端安装、模型选择、Nous Portal 价格及云端托管费用。以每周客户反馈简报为例,说明如何核实记忆写入、审核技能变更、限制权限,并按每份合格报告的总成本判断是否适合企业试点。2026年10月17日Build
Ollama vs llama.cpp:本地大模型部署,怎么选才合适?

Ollama vs llama.cpp:本地大模型部署,怎么选才合适?

Ollama vs llama.cpp 怎么选?本文从模型管理、GGUF 量化、API 兼容性、并发配置与 GPU 支持逐项比较,说明何时选择 Ollama 的便捷流程,何时需要 llama.cpp 的直接控制,并梳理迁移前要核对的模型文件、上下文与请求行为,帮助团队根据实际负载决定是否更换运行时。2026年10月16日Build
Devin 价格与 Desktop 评测:迁移、功能和选型

Devin 价格与 Desktop 评测:迁移、功能和选型

Devin 价格是否划算,取决于工作流程能否受益。本文梳理 Devin Desktop 的 Agent Command Center、Spaces、Devin Local 与 ACP 接入,解释 Windsurf 迁移缺口、各套餐费用及 Teams 计费差异,说明何时更适合 Cursor 或 Claude Code。2026年10月16日Build
上下文窗口有多大才够用?读懂 token 限制与长上下文

上下文窗口有多大才够用?读懂 token 限制与长上下文

上下文窗口有多大,决定了模型一次能容纳多少输入与输出,却不保证它能准确利用所有信息。本文解释 token 限制与上下文长度的区别,对照 Claude、GPT-6 和 Gemini 4 的窗口及输出规则,并用预算示例说明为何提示词放得下、回答却可能超限,再介绍检索、摘要、压缩和缓存各自能解决什么问题。2026年10月16日Build
RAG 与微调怎么选?企业知识、模型行为与成本对比

RAG 与微调怎么选?企业知识、模型行为与成本对比

RAG 与微调该怎么选?先分清模型缺少企业知识,还是反复出现行为问题。本文通过客服问答、品牌文案和工单分类三个案例,拆解搭建方式、训练与推理费用,并说明提示词工程、长上下文和缓存如何改变成本比较。结合模型支持、部署端点、文档权限与评估要求,帮助团队判断何时检索、何时微调,以及何时值得将两者结合。2026年10月16日Build
Aider AI 评测:安装教程、模型配置与 API 费用

Aider AI 评测:安装教程、模型配置与 API 费用

Aider AI 适合谁?本文梳理终端编程、Git 自动提交、仓库映射与模型配置,提供安装和首个任务教程,对比 Claude Code、Codex CLI 与 Cursor 的工作流,并按明确的 token 预算计算 API 费用,说明测试、上下文和兼容性限制,帮你判断是否值得为这套灵活但需要自己把关的工具投入时间。2026年10月16日Build
Claude Code GitHub Actions 实战:配置、费用与权限控制

Claude Code GitHub Actions 实战:配置、费用与权限控制

用 Claude Code GitHub Actions 在 GitHub 中按需审查 PR、修复 Issue。本文讲清 API 与订阅计费、安装配置、工具权限、轮数和超时限制,提供可复制的工作流示例,并解释 fork 安全边界、常见故障与自动审查的启用时机,帮助小团队逐步落地,保留人工合并决策。2026年10月15日Build
订阅通讯

每周日,一封信。写运转中的系统,不写热评。

每周一期。无垃圾邮件。随时退订。