GPT-6 Astra 的工具调用(Function Calling)已经是 AI Agent 的基础能力:让模型决定调用哪个工具、传什么参数,运行时负责执行副作用,再把结果送回给模型继续推理。这条链路写对了,Astra 能自主查询 API、读数据库、执行自定义逻辑;写错了,要么工具循环死锁,要么工具权限失控。本文不重复「Function Calling 是什么」——那篇见 协议对照;也不重复「怎么拿 Key」——那篇见 API 调用教程。本文只把从「能跑」到「能安全上线」的三个核心工具类型拆开讲清楚。
第一步:定义工具 schema
工具 schema 是一段 JSON 对象,告诉模型「这个工具叫什么、做什么、接受什么参数」。Astra 支持 type: function,参数用标准 JSON Schema 描述。以一个调用天气 HTTP API 的工具为例:
import json
from openai import OpenAI
client = OpenAI()
tools = [
{
"type": "function",
"name": "get_weather",
"description": "Get current weather for a city via HTTP API.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. Tokyo, London, Shanghai",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Default celsius.",
},
},
"required": ["city"],
"additionalProperties": False,
},
}
]
几条写法建议:description 要说「做什么」而不是「叫什么名字」,模型靠这里决定要不要调用这个工具。required 列出必填字段,可选字段加在 properties 里但不进 required。additionalProperties: false 可以降低模型传入意外字段的概率。多个工具放在同一个 list 里,模型会根据对话决定调用哪个(或同时调用多个,需要你的循环处理并行 tool call)。
第二步:完整工具循环
工具循环的核心是:发请求 → 检查 output 里是否有 function_call → 执行 → 把结果塞回 input → 再次请求,直到没有新的 function_call 为止。循环必须是 while-true,不能假设模型只会调用一次工具。
import json
import requests
from openai import OpenAI
client = OpenAI()
def call_weather_api(city: str, unit: str = "celsius") -> dict:
"""Replace with your real HTTP endpoint."""
url = "https://api.example.com/weather"
r = requests.get(url, params={"city": city, "unit": unit}, timeout=5)
r.raise_for_status()
return r.json() # e.g. {"city": "Tokyo", "temp": 22, "condition": "sunny"}
# Initial request — model may call tools
response = client.responses.create(
model="gpt-6-astra",
input="What is the weather in Tokyo right now?",
tools=tools,
reasoning={"effort": "low"},
)
# Tool loop: keep going until model stops calling tools
while True:
tool_calls = [o for o in response.output if o.type == "function_call"]
if not tool_calls:
break # model is done — final answer is in response.output_text
tool_results = []
for call in tool_calls:
args = json.loads(call.arguments)
if call.name == "get_weather":
result = call_weather_api(**args)
else:
result = {"error": f"Unknown tool: {call.name}"}
tool_results.append({
"type": "function_call_output",
"call_id": call.call_id,
"output": json.dumps(result, ensure_ascii=False),
})
# Append history + results and continue
response = client.responses.create(
model="gpt-6-astra",
input=response.output + tool_results,
tools=tools,
reasoning={"effort": "low"},
)
print(response.output_text)
注意几个细节:第一,response.output 是一个列表,其中可能同时存在 function_call 和 message 类型的事件;只过滤 type == "function_call" 的项。第二,提交结果时,input 要传 response.output + tool_results,带上历史,模型才知道自己问了什么、得到了什么。第三,call_id 必须和工具结果的 call_id 对应,平台用这个字段做配对校验。第四,工具返回值用 json.dumps 序列化为字符串,不要直接传 dict 对象。
name 字段上做白名单过滤;参数用类型校验;对 HTTP 工具加超时和域名白名单;对数据库工具只执行预设 SQL,不接受用户或模型拼的 SQL 字符串。会写磁盘或跑命令的工具,放到远程 Mac 上隔离执行。
第三步:数据库只读工具
数据库工具的安全底线是三件事:只读(不给 INSERT / UPDATE / DELETE 权限)、参数化查询(防注入)、SQL 白名单(防模型构造恶意查询)。下面示例用 SQLite,但模式对 PostgreSQL / MySQL 同样适用:把允许的查询名和 SQL 存在 dict 里,工具只执行 dict 里有的语句,参数由调用方传入。
import sqlite3
from typing import Any
# Only these parameterized queries can be executed — no raw user SQL
ALLOWED_QUERIES: dict[str, str] = {
"get_order": "SELECT id, status, total, created_at FROM orders WHERE id = ?",
"list_orders": "SELECT id, status, total FROM orders WHERE user_id = ? LIMIT 20",
}
def query_db(query_name: str, params: list[Any]) -> list[dict]:
"""Execute an allowlisted, parameterized read-only query.
Never accepts raw SQL strings from the model.
"""
sql = ALLOWED_QUERIES.get(query_name)
if sql is None:
raise ValueError(f"Query '{query_name}' not in allowlist")
con = sqlite3.connect("orders.db", check_same_thread=False)
con.row_factory = sqlite3.Row
try:
rows = con.execute(sql, params).fetchall()
return [dict(r) for r in rows]
finally:
con.close()
# Schema exposed to the model
db_tool = {
"type": "function",
"name": "query_db",
"description": "Query the orders DB. Only read-only allowlisted queries.",
"parameters": {
"type": "object",
"properties": {
"query_name": {
"type": "string",
"enum": list(ALLOWED_QUERIES.keys()),
"description": "Name of the allowlisted query",
},
"params": {
"type": "array",
"items": {"type": ["string", "number"]},
"description": "Positional parameters for the query",
},
},
"required": ["query_name", "params"],
},
}
实际生产中还需要:连接池(不要每次 tool call 都 connect/close)、查询超时(timeout 参数或数据库层配置)、结果行数硬限制(LIMIT N 写进 SQL,不让模型传 999999)、敏感字段脱敏(手机号、邮箱在返回前 mask)。工具返回给模型的应该是摘要,不是整张表的 raw dump。
函数工具 vs 托管工具
Astra 同时支持两类工具,选择逻辑不同,不能混淆。
| 维度 | 函数工具(type: function) | 托管工具(OpenAI 运营) |
|---|---|---|
| 执行位置 | 你的运行时(本机 / 远程 Mac) | OpenAI 基础设施 |
| 适用场景 | 私有 API、内部数据库、自定义业务逻辑 | 网页搜索、文件搜索、代码解释器、shell、Computer Use |
| 开发成本 | 需要自己写 schema + 执行逻辑 + 工具循环 | 直接在 tools 里写工具名,OpenAI 负责执行 |
| 计费 | 仅 token(输入 + 输出) | token + 工具调用附加费(如搜索按次、Computer Use 按时) |
| 隔离要求 | 写操作需要隔离执行环境 | OpenAI 负责隔离,但仍需考虑数据安全 |
| 能力边界 | 任意自定义;受运行时权限约束 | 受 OpenAI 策略约束;不支持调用私有内网 |
两类工具可以混用:同一次请求既可以传函数工具又可以开启托管工具(如 {"type": "web_search_preview"})。模型会根据 description 和对话上下文决定调用哪个。混用时注意:托管工具的返回会自动进入对话历史,不需要你手动提交结果;函数工具的结果必须手动通过 function_call_output 提交。
决策矩阵
| 如果你需要 | 用这个 | 理由 |
|---|---|---|
| 调用公司内网 REST API | 函数工具 | 内网不对 OpenAI 基础设施开放 |
| 查询内部数据库(只读) | 函数工具 + allowlist SQL | 私有数据,需自己控制访问权限 |
| 搜索公开网页最新信息 | 托管 web_search_preview | OpenAI 负责爬取和排名,无需维护 |
| 让模型运行 Python 代码 | 托管 code_interpreter | 在 OpenAI 沙箱里执行,不消耗本机资源 |
| 让模型操作桌面 / 浏览器 | 托管 computer_use + 隔离 Mac | Computer Use 需要可见桌面;隔离执行避免误操作 |
| 执行任意 shell 命令(写磁盘) | 函数工具 + 远程 Mac 隔离 | 写权限必须在可销毁环境里,不上日常桌面 |
| 既要搜索又要调私有 API | 混用:托管 web_search + 函数工具 | 同一次请求可以组合两类工具 |
推荐组合
A — 个人开发者 / 原型: 本机 + 函数工具(HTTP API,不写磁盘)。工具 schema 写好,循环跑通,日志打出 call_id 和工具返回。托管工具按需开启(web_search 最常用)。先把单工具循环跑稳,再接多工具或并行 tool call。
B — 小团队 / 产品: 函数工具挂内部服务,接口用 token 鉴权;数据库工具只读,加连接池和查询超时;托管工具(web_search、code_interpreter)按任务需要开启。工具执行逻辑单测覆盖,工具返回值做 schema 校验再送模型。会写磁盘的场景放远程 Mac。
C — 企业 / 高安全: 所有函数工具经 API 网关代理,工具名和参数做审计日志;数据库只读 replica,工具层再加 Row-Level Security;Computer Use 放到可销毁的隔离 Mac 节点,会话结束自动销毁。托管工具的数据安全合规(零数据保留)按账户约定。账户和交付边界见 帮助中心,节点月费见 Mac mini 定价。
常见陷阱
- 工具循环只跑一轮: 用 if 而不是 while,模型第一次调工具后就退出,拿不到最终文本回答。
- 提交结果时不带历史:
input只传tool_results而不传response.output + tool_results,模型不知道上下文,大概率报错或答非所问。 - 把模型输出直接拼进 SQL / shell: 即使模型通常不会恶意构造,数据质量或 prompt injection 攻击仍然可以触发危险语句。始终用 allowlist + 参数化查询。
- 工具超时没设: HTTP 工具没有超时,下游服务卡住后整个 Agent 会话挂死。所有外部调用加
timeout,工具循环加最大轮数上限。 - 工具返回整张表 / 大文件: 工具结果会进模型上下文,返回 10,000 行 CSV 会直接触发 272K 升档费率。工具层做聚合和截断,只返回模型需要的摘要。
落地步骤:7 步
- 确认文本通路已经成功(见 API 调用教程);工具基于文本通路,不要跳步。
- 写第一个函数工具 schema,description 清晰,parameters 有 required,additionalProperties false。
- 实现工具执行函数,HTTP 工具加超时,DB 工具用 allowlist + 参数化查询。
- 用 Responses API 发带 tools 参数的请求,打印 response.output,确认出现 function_call 事件。
- 实现 while 循环,正确提交 function_call_output,call_id 一一对应,循环直到无新 tool call。
- 按工具类型决定是否引入托管工具(web_search、code_interpreter),混用时注意托管工具结果无需手动提交。
- 写磁盘、跑命令、开浏览器的工具移到远程 Mac 隔离执行,会话结束销毁节点。
FAQ
Function Calling 必须用 Responses API 吗?
是的。工具调用、托管工具和异步 tool 必须走 Responses API。Chat Completions 在 Astra 上不是函数调用的主路径,一接托管工具就会 400。
一次调用可以定义多少个工具?
没有硬性小数字上限,但工具越多,提示 token 越贵。建议按任务只传入相关工具,避免把整个工具库塞进每次请求。
数据库工具怎么防止 SQL 注入?
使用参数化查询(? 占位符),配合预设 SQL 白名单:工具只能执行白名单里的语句,参数由调用方传入。绝不把模型输出直接拼接成 SQL。
函数工具和托管工具怎么选?
需要调外部私有 API 或内部数据库,用函数工具;需要网页搜索、代码执行、Computer Use 这类 OpenAI 运营的能力,走托管工具。两类可以在同一次请求里混用。
工具调用结果可以流式输出吗?
工具调用本身是结构化事件,不需要流式;工具结果提交后的最终回答可以加 stream=True。建议先验证非流式工具循环成功,再叠加流式。
总结
GPT-6 Astra Function Calling 的三个核心:工具 schema 写清楚 description 和 parameters;工具循环用 while,提交结果时带上历史;数据库工具加 allowlist 和参数化查询,写权限的工具放到远程 Mac 隔离。选对了工具类型(函数工具 vs 托管工具),Agent 能力就能从「跑通」快速推进到「安全上线」。远程 Mac 节点从 租用页 和 定价页 查看,账户问题走 帮助中心。