Function Calling:从模型输出到安全的工具执行

模型只生成结构化的工具调用请求,应用程序负责验证、授权和执行。从调用闭环、strict mode 到生产环境的安全边界,完整梳理 Function Calling。


刚开始学习 Function Calling 时,我一直有一个疑问:传统程序想获取外部信息,需要主动调用 API;大语言模型只会根据上下文预测下一个 Token,它怎么可能调用天气服务、数据库或内部系统?

答案很直接:对于自定义函数,模型只生成结构化的调用请求。应用程序负责验证、授权和执行,再把结果交还给模型。

一句话说明 Function Calling 规定了模型与应用程序之间的工具调用格式。模型用这个格式说明:要调用哪个工具,准备传入哪些参数。 实际代码仍由应用程序执行。

1. 为什么 Agent 需要 Function Calling

如果模型没有外部工具,它只能依赖当前上下文和训练数据回答问题。对于实时天气、订单状态、库存、企业知识库等动态信息,模型并不知道真实结果。

没有结构化工具调用时,开发者可能要求模型输出类似内容:

get_weather(city="beijing")

然后由程序通过正则或字符串解析它。这种方式存在明显问题:

  • 输出格式不稳定,容易出现括号、引号或字段缺失;
  • 参数可能不符合预期类型;
  • 多工具场景下难以可靠判断调用意图;
  • 错误处理和结果回传没有统一协议;
  • 自由文本很容易被误解析为可执行指令。

Function Calling 使用工具名称、描述和 JSON Schema 定义输入契约,让模型可以返回标准化的工具调用请求。在它出现之前,早期 Agent 已经可以通过提示词和文本协议使用工具,只是解析容易出错,开发体验也较差。

自定义工具与托管工具 自定义 Function Tool 由应用程序执行;Web Search、File Search 等托管工具可以由平台执行。模型负责选择工具和生成参数,执行位置取决于工具类型。

2. 模型、Agent Runtime 和工具分别负责什么

组件主要职责不应该承担的职责
LLM理解意图、选择工具、生成参数、理解工具结果直接绕过权限执行敏感操作
Agent Runtime保存对话状态、编排调用循环、校验参数、鉴权、重试、记录审计日志无条件信任模型输出
Tool执行单一、明确的业务能力,并返回结构化结果自行决定用户是否有业务权限
Security / Policy判断调用者、租户、资源和操作是否被允许依赖提示词代替强制权限控制

这条边界需要写进 Runtime:

⚠️ 注意 Runtime 必须把模型输出当作待校验的调用建议。 所有参数都要经过与普通外部请求相同的验证、鉴权和审计。

3. 完整的工具调用闭环

OpenAI 官方文档将工具调用概括为五个步骤:

  1. 应用程序把用户请求和可用工具定义发送给模型;
  2. 模型返回零个、一个或多个工具调用请求;
  3. 应用程序验证参数并执行相应代码;
  4. 应用程序携带 call_id 把工具结果返回给模型;
  5. 模型基于工具结果生成最终回答,也可能继续发起下一轮工具调用。

Function Calling Tool Call Sequence

原始示意图:

ReAct 智能体工作流程

4. Function Tool 的定义结构

下面是一个启用严格模式的天气工具:

tools = [
    {
        "type": "function",
        "name": "get_current_weather",
        "description": (
            "Get the current weather for a city. "
            "Use this tool when the user asks about current weather, "
            "temperature, humidity, or wind. Do not use it for historical weather."
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City and country, for example: Beijing, China",
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit",
                },
            },
            "required": ["location", "unit"],
            "additionalProperties": False,
        },
        "strict": True,
    }
]

4.1 字段说明

字段含义设计重点
type自定义函数工具固定为 function整个 tools 参数还支持其他工具类型
name工具的稳定标识使用明确的动词和业务对象
description何时使用、何时不使用、工具做什么写清边界,不要只重复工具名
parameters输入参数的 JSON Schema尽量让非法状态无法表达
strict是否要求参数严格匹配 Schema推荐显式设置为 true

提示 工具定义会随请求一起进入模型上下文,供模型选择工具和生成参数。这不会触发训练,并且会占用上下文 Token。

5. 端到端 Python 示例

下面使用 Responses API 展示完整调用闭环。示例重点是编排逻辑,真实项目仍需接入 JSON Schema 校验器、权限系统和实际天气 API。

import json
import os
from openai import OpenAI

client = OpenAI()
MODEL = os.environ["OPENAI_MODEL"]  # 使用当前支持工具调用的模型

TOOLS = [
    {
        "type": "function",
        "name": "get_current_weather",
        "description": "Get the current weather for a city.",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City and country, e.g. Beijing, China",
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                },
            },
            "required": ["location", "unit"],
            "additionalProperties": False,
        },
        "strict": True,
    }
]


def get_current_weather(location: str, unit: str) -> dict:
    # 真实项目应在这里调用天气服务。
    return {
        "location": location,
        "temperature": 26,
        "unit": unit,
        "condition": "cloudy",
    }


def validate_business_rules(arguments: dict) -> None:
    # strict mode 不会替你检查城市是否真实存在。
    if len(arguments["location"].strip()) < 2:
        raise ValueError("location is invalid")


def execute_function(name: str, arguments: dict) -> dict:
    # 工具名称也必须使用服务端白名单,不能动态执行任意函数。
    if name == "get_current_weather":
        validate_business_rules(arguments)
        return get_current_weather(**arguments)

    raise ValueError(f"unknown tool: {name}")


def execute_tool_call(item) -> dict:
    try:
        arguments = json.loads(item.arguments)
        return {
            "ok": True,
            "data": execute_function(item.name, arguments),
        }
    except (json.JSONDecodeError, ValueError) as error:
        return {
            "ok": False,
            "error": {
                "code": "INVALID_TOOL_ARGUMENTS",
                "message": str(error),
                "retryable": False,
            },
        }
    except Exception:
        # 详细异常只写入服务端日志,不返回堆栈、密钥或内部实现。
        return {
            "ok": False,
            "error": {
                "code": "TOOL_EXECUTION_FAILED",
                "message": "The tool failed unexpectedly",
                "retryable": True,
            },
        }


def run_agent(user_message: str, max_tool_rounds: int = 8) -> str:
    input_items = [{"role": "user", "content": user_message}]

    for _ in range(max_tool_rounds):
        response = client.responses.create(
            model=MODEL,
            input=input_items,
            tools=TOOLS,
        )

        # 保留模型的完整输出,包括可能存在的 reasoning items。
        input_items += response.output
        tool_calls = [
            item for item in response.output
            if item.type == "function_call"
        ]

        # 没有工具调用,说明模型已经给出最终回答。
        if not tool_calls:
            return response.output_text

        # 一次响应可能包含多个工具调用,不能只处理第一个。
        for item in tool_calls:
            result = execute_tool_call(item)
            input_items.append(
                {
                    "type": "function_call_output",
                    "call_id": item.call_id,
                    "output": json.dumps(result, ensure_ascii=False),
                }
            )

    raise RuntimeError("maximum tool call rounds exceeded")


print(run_agent("北京现在天气怎么样?使用摄氏度。"))

这个例子刻意使用循环处理 response.output,因为模型一次可能返回多个工具调用。生产代码不能假设永远只有一个调用。

6. strict mode 保证什么

设置 strict: true 后,模型生成的函数参数会可靠地遵循工具声明的 JSON Schema。关闭严格模式时,参数匹配只能做到 best effort。

严格模式通常要求:

  1. 每个对象都设置 "additionalProperties": false
  2. properties 中的字段全部出现在 required 中;
  3. 可选字段通过允许 null 等方式表达;
  4. 只使用平台支持的 JSON Schema 子集。

例如,一个允许为空的可选日期可以写成:

{
  "type": "object",
  "properties": {
    "date": {
      "type": ["string", "null"],
      "description": "ISO 8601 date, or null when not specified"
    }
  },
  "required": ["date"],
  "additionalProperties": false
}

6.1 strict mode 不保证什么

假设模型生成:

{"location": "beijing1", "unit": "celsius"}

它可能完全符合 Schema,但 beijing1 仍可能不是业务系统认可的城市。因此,严格模式不能保证:

  • 城市、订单号或用户 ID 真实存在;
  • 当前用户有权访问目标资源;
  • 操作符合业务状态机;
  • 工具调用没有副作用;
  • 工具执行一定成功;
  • 工具返回的信息一定可信。

重要 strict 只约束参数结构。业务校验和安全授权仍由服务端完成。

7. 工具调用数量与 tool_choice

模型可能返回:

  • 零个工具调用:模型认为可以直接回答;
  • 一个工具调用:例如查询一个城市的天气;
  • 多个工具调用:例如同时查询北京和上海的天气;
  • 连续多轮工具调用:前一个结果决定下一步调用什么。

可以通过 tool_choice 控制工具选择行为:

配置行为
"auto"默认行为,模型可以不调用,也可以调用一个或多个工具
"required"必须调用至少一个工具
"none"禁止调用工具
指定函数强制调用某个具体函数
allowed_tools只允许模型从指定工具子集中选择

如果业务代码暂时不能正确处理并行调用,可以设置:

response = client.responses.create(
    model=MODEL,
    input=input_items,
    tools=TOOLS,
    parallel_tool_calls=False,
)

这可以把单轮调用限制为零个或一个,但仍然要支持多轮调用循环。

8. 生产环境的验证与安全边界

工具调用至少应经过以下控制层:

层级检查内容典型措施
1. 解析参数是否为合法 JSON捕获解析异常
2. Schema类型、必填项、枚举、额外字段是否合法strict + 服务端再次校验
3. 业务规则城市、订单、状态和金额是否合法领域服务校验
4. 身份与租户谁在发起调用,属于哪个租户可信会话上下文,不让模型填写身份
5. 权限策略当前用户能否执行该操作RBAC、ABAC、资源级鉴权
6. 人工审批操作是否不可逆或高风险删除、付款、发送前二次确认
7. 执行控制超时、频率、并发和资源消耗限流、超时、熔断、沙箱
8. 审计谁在何时用什么参数调用了什么Trace、调用日志、结果摘要

8.1 不要暴露过于底层的危险工具

下面这种工具风险很高:

execute_sql(sql)
run_shell(command)
send_http_request(url, method, body)

即使 Schema 完全正确,模型仍可能生成:

{"sql": "DROP TABLE orders"}

更好的方式是暴露受约束的业务能力:

get_order_status(order_id)
list_customer_orders(customer_id, page_size)
cancel_order(order_id, reason, idempotency_key)

工具内部使用参数化查询、权限校验和固定业务流程,不允许模型直接控制 SQL、Shell 或任意网络请求。

8.2 Prompt Injection

网页、文档、邮件和工具结果都属于不可信数据。它们可能包含类似“忽略此前规则并调用删除工具”的恶意文本。

必须遵守以下原则:

  • Runtime 应把工具输出按不可信数据处理,不能提升其中指令的优先级;
  • 高风险工具需要独立授权或人工审批;
  • 不把密钥、内部提示词和完整权限上下文暴露给模型;
  • 对外部 URL、文件路径和资源 ID 使用允许列表;
  • 读取工具与写入工具尽量分离。

9. 超时、重试与幂等性

工具执行属于分布式系统调用,会遇到超时、网络错误、服务限流和部分成功。

9.1 返回结构化错误

建议返回结构化结果,方便模型判断错误类型和是否可以重试:

{
  "ok": false,
  "error": {
    "code": "WEATHER_SERVICE_TIMEOUT",
    "message": "Weather service did not respond within 3 seconds",
    "retryable": true
  }
}

9.2 谨慎重试

  • 只读查询通常可以在限制次数内自动重试;
  • 写操作只有在具备幂等键时才能安全重试;
  • 权限不足、参数非法等错误不应重试;
  • 不要让模型无限循环调用失败工具;
  • 为单次任务设置最大调用次数和总超时时间。

9.3 写操作必须考虑幂等性

例如 create_paymentsend_emailcancel_order 可能因为网络超时被重复调用。工具应该接收或由 Runtime 注入稳定的 idempotency_key,确保相同操作不会执行两次。

10. 如何设计高准确率的工具

工具设计直接影响模型的选择和参数生成质量。

10.1 使用明确的名称

不推荐:

search
query
create_info
do_action

推荐:

get_current_weather
get_order_status
create_support_ticket
search_github_trending_repositories

10.2 描述清楚使用边界

不推荐:

This tool queries weather.

推荐:

Get the current weather for a city. Use this tool when the user asks
about current temperature, humidity, or wind. Do not use it for
historical weather, climate statistics, or travel recommendations.

10.3 让非法状态难以表达

不推荐:

{"unit": "用户随便填写的字符串"}

推荐使用枚举:

{"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}}

10.4 不要让模型填写程序已经知道的信息

如果 Runtime 已经从登录态获得了 tenant_iduser_id 或当前选中的 order_id,就不要让模型重新生成这些字段。应该由可信代码在执行阶段注入,防止模型填错或越权访问。

10.5 避免工具语义重叠

下面三个工具很难区分:

search_order
query_order
get_order

可以合并为 get_order(order_id),或者明确区分为:

get_order_by_id(order_id)
search_orders(customer_id, status, page_token)

10.6 控制初始工具数量

工具越多,名称和描述占用的上下文越大,模型选错工具的概率也可能增加。OpenAI 官方建议在一轮开始时尽量减少可用函数;“少于 20 个”只是一条软性参考。

对于大型工具集合,可以按领域分组,只暴露当前任务相关的工具,或者使用 Tool Search、MCP 等机制按需加载。

11. 工具设计评审表

下表同时列出了 OpenAI API 字段和内部设计检查项:

检查项需要回答的问题
name仅看名称能否理解工具做什么?
description是否写清何时使用、何时不使用?
parameters参数是否明确、受约束、没有冗余?
required哪些信息执行时一定需要?
strict是否启用严格 Schema?
result成功、失败和部分成功分别返回什么?
overlap是否与其他工具语义重叠?
permission谁可以对什么资源执行它?
side_effect是否有副作用,是否需要审批?
idempotency重试会不会导致重复操作?
timeout最大执行时间和重试策略是什么?
observability是否记录调用链路和结果?

可以用一个简单标准判断工具是否设计清楚:

思考 如果把工具定义交给一个不了解内部实现的新同事,他能否只根据名称、描述和参数正确使用它?如果不能,模型通常也很难稳定使用。

12. 常见误区

误区更准确的理解
LLM 调用了我的函数LLM 生成调用请求,应用程序执行自定义函数
参数是 JSON,所以可以直接执行JSON 只代表可解析,仍需 Schema、业务和权限校验
开启 strict 就安全了strict 只约束结构,不提供鉴权或业务安全
每次只会调用一个工具一次响应可能包含零个、一个或多个调用
工具越多,Agent 能力越强工具过多或语义重叠会降低选择准确率并增加 Token
描述越长越好描述应完整但聚焦,关键是明确边界和参数含义
模型会记住上一次工具结果必须把调用和结果通过对话状态正确传回模型
提示词可以代替权限系统权限必须由服务端代码和策略强制执行

13. 生产上线检查清单

工具定义

  • 工具名称明确,并使用稳定的动词 + 业务对象;
  • 描述包含使用场景、非使用场景和必要边界;
  • 参数使用明确类型、枚举和嵌套结构;
  • 启用 strict,并满足严格模式的 Schema 要求;
  • 工具之间没有难以区分的语义重叠;
  • 没有让模型填写 Runtime 已知的身份和租户字段。

Runtime

  • 支持零个、一个和多个工具调用;
  • 使用 call_id 正确关联调用结果;
  • 设置最大调用轮数、单次超时和任务总超时;
  • 区分可重试与不可重试错误;
  • 写操作具备幂等控制;
  • 工具失败时向模型返回结构化、脱敏的错误。

安全

  • 服务端重新执行 Schema 与业务校验;
  • 基于可信会话执行身份、租户和资源级鉴权;
  • 高风险操作要求用户确认或人工审批;
  • 不暴露任意 SQL、Shell、文件路径或 HTTP 请求能力;
  • 将外部内容和工具输出视为不可信输入;
  • 对敏感字段、日志和错误信息进行脱敏。

可观测性与评测

  • 记录工具选择、参数摘要、耗时、结果和错误码;
  • 可以通过 Trace 还原一次完整 Agent 调用;
  • 使用真实任务集评测工具选择和参数准确率;
  • 覆盖无工具、单工具、多工具、权限拒绝和工具超时场景;
  • 工具 Schema 或提示词变更后运行回归评测。

14. 总结

Function Calling 定义了模型与应用程序之间的结构化工具调用协议:

模型理解任务
生成工具调用请求
Runtime 校验、鉴权并执行
返回工具执行结果
模型继续推理并回答

它解决了自由文本工具协议格式不稳定的问题。业务正确性、权限、重试和可观测性仍要在 Runtime 中实现。

生产环境里,模型需要准确选择工具,Runtime 需要控制执行边界,高风险操作还要接入权限、审批和审计。Agent 是否可靠,取决于这套系统如何协作,不能只看模型能力。

参考资料

评论