"""练习 6:Tool Schema 设计 —— 坏 schema vs 好 schema 对照实验
为什么提前做这个练习?
因为练习 2(多工具选择)依赖它。模型选不选对工具、传不传对参数,
90% 取决于 schema 写得好不好,而不是模型笨。
本练习用【同一个货币换算工具】,写两份 schema:
- BAD_TOOLS :常见的新手写法(毛病全集)
- GOOD_TOOLS:规范写法
然后把【同样的问题】分别喂给模型,打印它吐出的参数,差异一眼可见。
四条要对比的 schema 设计规范:
1) description 写【什么时候用】,不是【做什么】 ← 黄金规则,最重要
2) 参数用对类型(金额是 number,不是 string)
3) 明确列出 required 必需字段
4) 用 enum 锁死可选值(货币代码只能是固定几种)
"""
import json
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url=os.environ["DEEPSEEK_BASE_URL"],
)
MODEL = os.environ["MODEL"]
# ── 坏 schema:把新手常犯的错全踩一遍 ──────────────────────────────────
BAD_TOOLS = [
{
"type": "function",
"function": {
"name": "convert",
# 毛病①:description 只说“做什么”,没说“什么时候用”。
# 模型据此很难判断一个问题到底该不该调它。
"description": "货币转换",
"parameters": {
"type": "object",
"properties": {
# 毛病②:金额本该是数字,这里写成 string,模型可能回 "100" 甚至 "一百"。
"amount": {"type": "string"},
# 毛病③:没有 enum、没有 description,
# 模型会随手填“美元/人民币/USD/dollar”各种花样,下游没法用。
"from": {"type": "string"},
"to": {"type": "string"},
},
# 毛病④:没有 required,模型可能漏传参数。
},
},
}
]
# ── 好 schema:四条规范全部做到 ───────────────────────────────────────
GOOD_TOOLS = [
{
"type": "function",
"function": {
"name": "convert_currency",
# 规范①(黄金规则):description 说清【什么时候用】+【边界】,
# 让模型自己就能判断该不该调、什么情况不该调。
"description": (
"当用户想把一笔【具体金额】从一种货币换算成另一种货币时调用,"
"例如“100美元是多少人民币”。"
"仅用于金额换算;如果用户只是闲聊或问汇率以外的问题,不要调用。"
),
"parameters": {
"type": "object",
"properties": {
# 规范②:用对类型——金额是 number。
"amount": {
"type": "number",
"description": "要换算的金额,纯数字",
},
# 规范④:用 enum 锁死取值,逼模型输出标准货币代码而非随意文字。
"from_currency": {
"type": "string",
"enum": ["USD", "CNY", "EUR", "JPY"],
"description": "源货币代码",
},
"to_currency": {
"type": "string",
"enum": ["USD", "CNY", "EUR", "JPY"],
"description": "目标货币代码",
},
},
# 规范③:明确必需字段,缺一不可。
"required": ["amount", "from_currency", "to_currency"],
},
},
}
]
def ask(tools: list, question: str) -> str:
"""把一个问题 + 一份 schema 发给模型,返回它的“决定”(调用了什么/传了什么)。
注意:这里【故意不真正执行函数】,因为本练习只关心
“模型在这份 schema 下做出了什么决定、吐出了什么参数”。
"""
resp = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": question}],
tools=tools,
)
msg = resp.choices[0].message
if not msg.tool_calls:
return "(未调用工具)直接回答:" + (msg.content or "")
call = msg.tool_calls[0]
# 原样打印参数字符串,方便观察类型/格式问题("100" 还是 100、USD 还是 美元)
return f"调用 {call.function.name},参数 = {call.function.arguments}"
# 三个测试问题:第 1 个正常换算,第 2 个是边界(只问汇率不换算),第 3 个完全无关。
QUESTIONS = [
"100美元是多少人民币?",
"美元和人民币的汇率大概是多少?",
"你好,帮我写一首关于春天的诗。",
]
if __name__ == "__main__":
for q in QUESTIONS:
print("=" * 60)
print(f"问题:{q}")
print(f" ❌ 坏 schema → {ask(BAD_TOOLS, q)}")
print(f" ✅ 好 schema → {ask(GOOD_TOOLS, q)}")
print("=" * 60)
print(
"\n观察重点:\n"
" · 参数类型:坏 schema 的 amount 常是字符串 \"100\",好 schema 是数字 100。\n"
" · 货币格式:坏 schema 可能填“美元/人民币”,好 schema 受 enum 约束输出 USD/CNY。\n"
" · 何时调用:好 schema 的 description 写清了边界,边界/无关问题更不容易误触发。"
)