让模型查询业务数据时,最重要的不是让它“记住”订单,而是把查询动作交给一个边界清晰的 Python 工具。本文把前面学过的 Tool Calling、结构化参数、历史记录和本地校验组合起来,做一个只读的订单查询助手:模型负责理解问题,程序负责查数据,最后由模型把工具结果解释给用户。
先划定模型和程序的职责
这个小项目只允许查询订单,不允许修改订单、退款或猜测不存在的数据。一次请求的路径如下:
- 用户输入自然语言问题。
- 模型判断是否需要调用
lookup_order。
- 程序读取工具参数,执行本地查询。
- 程序把查询结果作为
function_call_output 回传。
- 模型根据真实结果生成简短回答。
模型不能直接访问 Python 变量。工具定义是它能够使用的“接口说明”,真正执行工具的仍然是我们的程序。这种分工可以审计:日志中既能看到模型请求了什么,也能看到程序实际返回了什么。
准备环境和演示数据
示例使用 OpenAI Python SDK 的 Responses API。先创建虚拟环境并安装 SDK:
1 2 3
| python -m venv .venv source .venv/bin/activate pip install openai
|
密钥和模型名称只从环境变量读取,不要写进代码或提交到 Git:
1 2
| export OPENAI_API_KEY="你的 API 密钥" export OPENAI_MODEL="你的模型名称"
|
为了让示例可以独立运行,先用内存字典模拟订单库。真实项目中可以把 lookup_order 换成数据库查询,但不要把数据库连接、写操作或任意 SQL 交给模型。
编写最小可运行程序
把下面代码保存为 order_assistant.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| import json import os import sys from openai import OpenAI
ORDERS = { "A1001": {"status": "已发货", "amount": 129.0, "currency": "CNY"}, "A1002": {"status": "待支付", "amount": 59.0, "currency": "CNY"}, }
TOOLS = [ { "type": "function", "name": "lookup_order", "description": "只读查询演示订单。找不到订单时必须返回 found=false,不得猜测。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单编号,例如 A1001", } }, "required": ["order_id"], "additionalProperties": False, }, "strict": True, } ]
INSTRUCTIONS = """你是订单查询助手。 只能通过 lookup_order 查询订单,不能编造订单状态或金额。 如果工具返回 found=false,明确告诉用户没有查到,不要猜测。 回答使用中文,并说明这是查询结果。"""
def lookup_order(order_id: str) -> dict: normalized_id = order_id.strip().upper() order = ORDERS.get(normalized_id) if order is None: return {"found": False, "order_id": normalized_id} return {"found": True, "order_id": normalized_id, "order": order}
def main() -> None: api_key = os.getenv("OPENAI_API_KEY") model = os.getenv("OPENAI_MODEL") if not api_key or not model: raise SystemExit("请先设置 OPENAI_API_KEY 和 OPENAI_MODEL")
question = " ".join(sys.argv[1:]).strip() if not question: raise SystemExit("用法:python order_assistant.py 查询订单 A1001 的状态")
client = OpenAI(api_key=api_key) input_items = [{"role": "user", "content": question}] response = client.responses.create( model=model, instructions=INSTRUCTIONS, tools=TOOLS, tool_choice="auto", parallel_tool_calls=False, input=input_items, )
input_items.extend(response.output) calls = [item for item in response.output if item.type == "function_call"] if not calls: print(response.output_text) return
for call in calls: if call.name != "lookup_order": raise RuntimeError(f"未允许的工具:{call.name}") arguments = json.loads(call.arguments) result = lookup_order(arguments["order_id"]) input_items.append( { "type": "function_call_output", "call_id": call.call_id, "output": json.dumps(result, ensure_ascii=False), } )
final_response = client.responses.create( model=model, instructions=INSTRUCTIONS, tools=TOOLS, input=input_items, ) print(final_response.output_text)
if __name__ == "__main__": main()
|
执行方式如下:
1
| python order_assistant.py "查询订单 A1001 的状态和金额"
|
代码中有三个关键点。第一,工具参数使用 JSON Schema 描述,strict 和 additionalProperties 让参数边界更明确。第二,工具调用只是一条模型输出,SDK 不会替我们执行 lookup_order,必须由程序解析参数并调用函数。第三,回传结果时要带上原来的 call_id,这样模型才能把结果对应到刚才的请求。
还要注意,工具定义和工具实现是两回事。TOOLS 只是告诉模型“可以请求什么”,真正的安全边界在 lookup_order 内部:它只接受订单编号,只读取固定数据,并且对未知编号返回明确的 found=false。即使模型生成了一个看似合理的订单号,程序也不会把它当成真实订单。
真实项目可以把一次调用记录成一条审计事件,例如保存请求时间、用户标识、原始问题、工具名、规范化后的订单编号、工具结果摘要和最终响应。不要把完整密钥或不必要的个人信息写入日志。这样出现“模型说查到了,但数据库没有记录”的问题时,可以沿着调用链定位,而不是只依赖聊天窗口里的文字。
如果需要接入数据库,建议保持函数签名不变,把数据访问封装在函数内部,并给查询设置超时和最大返回字段。模型只应该看到完成任务所需的最少信息,例如状态和金额,不应获得整行订单记录。先让工具返回稳定的小 JSON,再考虑分页、权限和缓存,调试成本会更低。
常见问题
为什么模型有时不调用工具? 用户问题可能不需要订单查询,或者模型没有从问题中识别出订单编号。不要为了“保证调用”而把所有问题都强制路由到工具;程序还应在执行前检查订单编号格式和权限。
工具返回 JSON 后,为什么还要再次请求模型? JSON 是程序能读懂的结果,用户通常需要的是自然语言说明。第二次请求让模型基于工具的真实结果组织回答,而不是凭记忆回答。
只读工具真的安全吗? 只读能减少副作用,但不等于没有风险。真实项目仍需在工具函数内部做用户身份校验、字段脱敏、访问日志和超时控制。模型提示词只能辅助约束,不能替代程序权限。
为什么示例只执行一轮工具调用? 这是为了保持最小实现。生产代码应使用带上限的循环,处理模型连续请求多个工具的情况,并在达到最大步数、参数校验失败或工具异常时停止,避免无限调用和费用失控。
小结
Tool Calling 的核心不是让模型“拥有数据库权限”,而是把可调用能力收缩成明确的函数协议。模型负责选择和解释,Python 负责校验、执行与记录。先从一个只读工具开始,再逐步加入身份校验、错误处理和审计日志,通常比直接构建能够修改数据的 Agent 更容易验证和维护。