Function Calling 入门:让模型请求你的 Python 工具
前面的文章里,模型只能返回文字;如果用户问“我所在城市今天的天气”,模型本身既不能访问你的数据库,也不能凭空调用天气服务。Function Calling(也叫 Tool Calling)解决的不是让模型直接执行 Python,而是让模型按照你声明的工具格式提出请求,由你的程序决定是否执行,再把结果交回模型。本篇只完成这个闭环,不讨论多工具编排或 Agent 循环。
模型为什么需要工具
一次普通调用的边界很清楚:程序发送消息,模型生成文本。工具调用则多了一个中间步骤:模型根据问题判断是否需要工具,并生成工具名和 JSON 参数;应用程序收到这个结构后,校验参数、执行真实函数;最后应用把函数结果作为新输入,让模型组织成用户能读懂的回答。
因此,模型不是 Python 函数的执行者,而是工具选择器和参数生成器。真正访问文件、数据库、网络或执行命令的始终是你的应用。这个边界很重要:如果工具具有删数据、转账等副作用,必须在程序侧做权限检查和人工确认,不能因为参数来自模型就直接信任。
声明一个最小工具
以查询天气为例,工具定义包含四部分:类型固定为 function,name 是程序识别的名称,description 帮助模型判断何时使用,parameters 用 JSON Schema 描述参数。严格模式下把参数列入 required,并关闭额外属性,可以减少模型传来未知字段的情况。
1 | import json |
这里的 get_weather 只是协议名称,函数还没有真正写出来。strict 约束的是参数形状,不代表城市一定存在,也不保证天气数据真实;业务校验仍然要由 Python 完成。
第一次请求:识别工具调用
Responses API 使用 client.responses.create(),输入放在 input,工具放在 tools。模型可能直接给文本,也可能在 response.output 中返回一个 type 为 function_call 的项目,所以程序不能只读取 output_text:
1 | def get_weather(city: str) -> dict: |
item.arguments 是 JSON 字符串,必须先用 json.loads 解析。不要使用 eval,因为模型输出是外部输入,eval 可能执行任意 Python 表达式。生产代码还应检查 city 是否为字符串、长度是否合理,以及是否在允许查询的城市范围内。
回传结果,完成闭环
模型第一次响应只是“请调用哪个工具、参数是什么”,还没有最终答案。把模型这次的输出项目追加到输入,再追加一条 function_call_output,其中 call_id 必须对应原来的调用,模型才能知道这份结果属于哪一次请求:
1 | # 保留模型生成的 function_call 项目,随后附上工具结果。 |
把两段代码合并后即可运行。完整顺序是:定义工具 → 第一次请求 → 找到调用 → 解析参数 → Python 执行 → 用 call_id 回传 → 第二次请求。实际天气服务只需替换 get_weather 的函数体,模型与回传协议不必改变。
需要注意,示例中的 item、weather 只在找到工具调用后才存在。更稳妥的完整程序应设置一个标志位;如果模型没有调用工具,就直接打印 response.output_text,避免访问未赋值变量:
1 | called = False |
常见问题
模型总是不调用工具。 工具描述要说明适用条件,用户问题也要明确;同时确认目标模型支持工具调用。不要把所有业务逻辑都塞进一个含糊的工具描述中。
工具被调用了多次。 一次响应可能包含多个 function_call,不能假设永远只有一个。初学时可以逐个执行并逐个回传;后续再学习并行工具调用。若只允许一次,应在程序侧设置上限。
参数合法但业务不合法。 JSON Schema 只能约束类型和字段。城市不存在、用户无权限、查询次数超限等情况都要在函数内部处理,并把可读的错误结果回传,而不是让异常直接终止整个对话。
把工具结果当成模型生成内容。 工具结果应来自你的程序或可信服务,但模型接下来仍会重新组织它。涉及金额、权限和写操作时,结果展示前要记录日志,执行前要二次确认。
接口返回普通文本。 这是允许的分支。工具只是模型可选的能力,不是每个问题都必须调用;应用必须同时处理 function_call 和普通文本两种响应。
小结
Tool Calling 的核心不是让模型直接运行代码,而是建立一套可追踪的请求协议:模型提出函数名和 JSON 参数,应用校验并执行,应用再用对应的 call_id 回传结果。掌握这个最小闭环后,才适合继续学习参数校验、工具错误处理和多个工具之间的选择。