展开本页目录
开始前准备什么
- 模型和渠道明确支持函数调用
- Python 与 OpenAI SDK;演示工具只读本地数据
- 真实写入工具另设鉴权、人工确认与业务幂等
- 声明工具
- 验证模型参数
- 执行并回传
- 验收最终回答
2026-10-23 核验材料;实际执行范围、结果与限制见下方记录。
补充:原有练习资料与下载
只读 fixture、函数白名单、参数验证、四轮上限与 usage 记录。
Tool Calling:模型请求、参数校验与结果回传
保存每轮真实请求和响应,只有白名单工具参数通过才执行本地加法。
· 真实Ollama/api/chat两轮:模型发sum_items(17,25),程序校验白名单和整数边界后执行加法、回传tool42,真实最终回答The total number of items is 42,退出0。 网关分支未执行:独立真实探针确认当前测试Key的Gemini组429后停止新增请求,没有伪造网关tool_call。
已取得下列真实结果;仍有内容质量或功能范围需要按本页检查项复核。
- 运行环境
- Ollama0.40.9/Python3.12;独立练习目录;原创虚构输入;本页未使用apizt测试Key
1. 提交Schema与原创输入
向/api/chat提供sum_items的JSON Schema和原创17/25库存问题,stream:false/think:false。
2. 读取真实tool_calls
读取真实assistant.tool_calls,禁止把assistant自然语言当调用。
3. 白名单校验参数
校验name为白名单sum_items、两个参数为0–100整数,再执行本地加法。
4. 回传工具结果再生成
把assistant原消息和role:tool结果42完整回传,再请求最终答复。
5. 限制轮数并保留全部响应
最多3轮;原始每次请求响应全部留档,没有tool_calls必须报失败。
6. 保存本次原始结果
网关分支必须重新用有效账户、准确模型ID实跑两轮工具请求才能验证。本次当前测试Key的Gemini分组独立探针429,已停止新请求,本地工具循环成功不能证明公网工具协议可用。
这次实际得到什么
- 真实Ollama/api/chat两轮:模型发sum_items(17,25),程序校验白名单和整数边界后执行加法、回传tool42,真实最终回答The total number of items is 42,退出0。 网关分支未执行:独立真实探针确认当前测试Key的Gemini组429后停止新增请求,没有伪造网关tool_call。
- 模型为本次隔离Ollama原生导入的明确别名tutorial-qwen3-proof;权重和原始请求/响应留档。
- 网关分支未发起:当前测试Key的Gemini组独立探针429后停止新增请求。
使用这份结果时注意
- 只证明对应SDK和本地模型的本题任务,不证明apizt公网账户、Claude或所有模型均可用。
- 本次小样例通过不能推断复杂工具、多轮高风险任务也可靠。
- 仅本地工具回路有真实成功,未验证公网工具协议,不伪造tool_call或网关成功。
一次工具任务至少有两个模型阶段
第一次请求可能返回 tool_calls,而不是给用户的最终文字。应用执行工具后,把工具结果连同原助手消息加入历史,第二次请求才生成基于真实结果的答案。模型也可能连续请求多个工具,因此要设循环上限。
演示库存来自脚本内固定数据 QR-001=7、QR-002=0,属于本地样例,不是真实仓库连接。是否完成一次网关工具闭环,要以你的运行结果为准。
给模型的工具描述要短且明确
工具名表达单一动作,参数用 JSON Schema 约束。即便服务端接受 strict 或返回合法 JSON,本地仍要检查函数白名单、参数类型和业务权限。模型输入不具备执行授权。
json
{
"type": "function",
"function": {
"name": "get_stock",
"description": "查询演示库存;只读。",
"parameters": {
"type": "object",
"properties": {
"sku": {
"type": "string"
}
},
"required": [
"sku"
],
"additionalProperties": false
}
}
}完整 Python 库存工具循环
这个脚本不执行模型生成的代码或任意 shell。每轮处理所有 function tool calls,关联 tool_call_id;上限为四次模型请求。每次调用都可能计费,没有 SDK 自动重试。
developer-tool-calling.py
import json
import os
from openai import OpenAI
STOCK = {"QR-001": 7, "QR-002": 0}
TOOL = {"type": "function", "function": {
"name": "get_stock", "description": "查询演示库存;没有写入或下单操作。",
"parameters": {"type": "object", "properties": {"sku": {"type": "string"}},
"required": ["sku"], "additionalProperties": False}}}
def execute(name, arguments):
if name != "get_stock":
return {"error": "tool_not_allowed"}
try:
args = json.loads(arguments)
if not isinstance(args, dict) or set(args) != {"sku"} or not isinstance(args["sku"], str):
return {"error": "invalid_arguments"}
return {"sku": args["sku"], "stock": STOCK.get(args["sku"]), "source": "local_demo_fixture"}
except (json.JSONDecodeError, TypeError):
return {"error": "invalid_json"}
def main():
client = OpenAI(api_key=os.environ["tokenba_API_KEY"],
base_url="https://api.apizt.com/v1", timeout=60, max_retries=0)
messages = [{"role": "user", "content": "调用 get_stock 查询 QR-001 库存,再用中文回答。"}]
ledger = []
for turn in range(4):
result = client.chat.completions.create(model=os.environ["tokenba_MODEL"],
messages=messages, tools=[TOOL])
ledger.append({"turn": turn + 1, "usage": result.usage.model_dump() if result.usage else None})
print("usage turn:", json.dumps(ledger[-1]))
choice = result.choices[0]
message = choice.message
if message.tool_calls:
if choice.finish_reason != "tool_calls":
raise RuntimeError("Tool call output is incomplete")
messages.append(message.model_dump(exclude_none=True))
for call in message.tool_calls:
if call.type != "function":
raise RuntimeError("Unsupported tool type")
output = execute(call.function.name, call.function.arguments)
print("tool:", json.dumps(output, ensure_ascii=False))
messages.append({"role": "tool", "tool_call_id": call.id,
"content": json.dumps(output, ensure_ascii=False)})
else:
if choice.finish_reason != "stop" or not message.content:
raise RuntimeError("Final answer is incomplete")
print(message.content)
print("usage ledger:", json.dumps(ledger))
return
raise RuntimeError("Tool loop exceeded four model calls")
if __name__ == "__main__":
main()
运行后,检查工具事实而不是只看文案
期望先看到 tool 输出中 sku=QR-001、stock=7,随后最终回答使用 7。usage ledger 的条数应对应实际模型请求次数。如果模型没有调用工具却猜测库存,应判定任务未通过。
bash
python -m pip install -U openai
export tokenba_MODEL="REPLACE_WITH_SUPPORTED_MODEL_ID"
# tokenba_API_KEY 通过你的终端或部署环境设置
python developer-tool-calling.pyResponses 回传字段不同
Responses 使用扁平函数定义,并从 output 项中读取 function_call。回传是 type=function_call_output、call_id 与序列化后的 output。Chat 使用 role=tool、tool_call_id;两套结构不要混用。
在手动续传 Responses 上下文时,保存相关 output 项与工具结果。网关对推理项、多工具、并行调用或状态续接的支持都应分别验收。
写入型工具要在应用层控制
- 账户身份与访问范围由你的服务验证,不能根据模型传来的 user_id 扩大权限。
- 发送邮件、下单、转账、删除等动作在执行前展示参数,并按业务要求获得确认。
- 为真实写入使用业务侧唯一操作 ID 和执行记录;模型调用 ID 不能直接保证支付或数据库幂等。
- 工具输出过长时返回摘要与明确字段,不把整张数据库表加入下一轮上下文。
常见错误及恢复位置
| 现象 | 检查 | 恢复 |
|---|---|---|
| 400 或 tools 不支持 | 模型、渠道、工具定义 | 回到最小工具;确认接口能力 |
| 参数不是 JSON | 保留原参数但不执行 | 回传 invalid_json,让模型修正 |
| 工具结果被忽略 | 助手工具消息与调用 ID | 修复历史结构后重测 |
| 不断重复调用 | 工具失败或提示词循环 | 到上限停止,保存全部尝试 |
| 执行超时 | 真实业务执行记录 | 先确认已执行与否,再决定重试 |
- 现象
- 400 或 tools 不支持
- 检查
- 模型、渠道、工具定义
- 恢复
- 回到最小工具;确认接口能力
- 现象
- 参数不是 JSON
- 检查
- 保留原参数但不执行
- 恢复
- 回传 invalid_json,让模型修正
- 现象
- 工具结果被忽略
- 检查
- 助手工具消息与调用 ID
- 恢复
- 修复历史结构后重测
- 现象
- 不断重复调用
- 检查
- 工具失败或提示词循环
- 恢复
- 到上限停止,保存全部尝试
- 现象
- 执行超时
- 检查
- 真实业务执行记录
- 恢复
- 先确认已执行与否,再决定重试
本次补测:先完成搜索要解决的问题,再进入下一步
Schema 形状正确仍可能越权,需要在工具执行前控制可查范围。
新增记录是受限报表工具的本地执行与模型整理;原 Tool Calling 原生回路保留,不能用新JSON替代原生 tool_calls 证明。
- 把用户上下文放应用侧,不让模型决定当前客户身份。
- 只允许白名单报表工具和字段;未知工具名、额外字段、越权对象在执行前拒绝。
- 实际生成本地报表并核对行数、总额,回传结果仍需匹配原 tool_call_id。
新增补测结果;与原有原生工具记录分别保存
{
"reports": {
"R1": {
"period": "2026-10",
"total": "12.50"
},
"R2": {
"error": "not_allowed_or_missing"
}
},
"explanation": {
"summary": {
"R1": {
"period": "2026-10",
"total": "12.50"
}
}
}
}怎样确认已经完成
- 工具白名单拒绝未知函数名
- 非法 JSON 与多余参数不会执行
- QR-001 的最终结果来自工具返回值 7
- 循环到上限停止,全部轮次 usage 可查
常见问题
tool_calls 返回了,是不是工具已经运行?
没有。除明确的服务端托管工具外,应用仍需校验、执行自己的函数并回传结果。
所有模型都支持 strict 和并行工具吗?
不能假定。模型、接口和渠道需要分别确认;示例使用应用校验,不靠统一兼容承诺。

