"""练习 2:多工具选择 —— 测试模型在多个工具之间的“选择/边界”能力
练习 1 只有一个工具,模型只需判断“调 or 不调”。
现实里 agent 手上往往有一排工具,难点变成:
· 选对:在多个工具里挑出最匹配的那个;
· 忍住:遇到压根不需要工具的问题,一个都别调(这就是“边界识别”)。
而模型选得准不准,直接取决于每个工具的 description 写得清不清楚——
所以本练习完全复用练习 6 的“黄金规则”:description 写【什么时候用】。
这里放 3 个领域明显不同的工具:查天气 / 货币换算 / 文本翻译。
"""
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"]
# ── 我们的“手”:3 个真实函数(演示用假数据/简化实现) ──────────────────
def get_weather(city: str) -> dict:
fake = {"北京": (26, "晴"), "上海": (24, "多云"), "广州": (30, "雷阵雨")}
temp, desc = fake.get(city, (None, "未知"))
return {"city": city, "temperature": temp, "weather": desc}
def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
# 演示用固定汇率(相对 USD)
rate = {"USD": 1.0, "CNY": 7.25, "EUR": 0.92, "JPY": 156.0}
usd = amount / rate[from_currency]
result = round(usd * rate[to_currency], 2)
return {"result": result, "to_currency": to_currency}
def translate_text(text: str, target_language: str) -> dict:
# 真翻译交给模型,这里只回传参数,证明“工具被正确选中并传对了参数”
return {"text": text, "target_language": target_language, "note": "已收到翻译请求"}
# 工具名 -> 真实函数 的调度表,模型选了谁就执行谁
DISPATCH = {
"get_weather": get_weather,
"convert_currency": convert_currency,
"translate_text": translate_text,
}
# ── 3 份工具 schema:每个 description 都写清【什么时候用】 ────────────────
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "当用户询问某个城市当前的天气或气温时调用。",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称,如 北京"}
},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "convert_currency",
"description": "当用户想把一笔具体金额从一种货币换算成另一种货币时调用。仅用于金额换算。",
"parameters": {
"type": "object",
"properties": {
"amount": {"type": "number", "description": "要换算的金额,纯数字"},
"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"],
},
},
},
{
"type": "function",
"function": {
"name": "translate_text",
"description": "当用户明确要求把一段文字翻译成另一种语言时调用。",
"parameters": {
"type": "object",
"properties": {
"text": {"type": "string", "description": "要翻译的原文"},
"target_language": {
"type": "string",
"enum": ["EN", "ZH", "JA", "FR"],
"description": "目标语言代码",
},
},
"required": ["text", "target_language"],
},
},
},
]
def run(question: str) -> None:
"""跑一个问题:打印模型选了哪个工具、传了什么参数,再执行并给出最终回答。"""
print("=" * 60)
print(f"问题:{question}")
messages = [{"role": "user", "content": question}]
first = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS)
msg = first.choices[0].message
# 边界识别:模型判断不需要任何工具,直接回答
if not msg.tool_calls:
print(" 🛑 未选用任何工具(判断为无需工具)")
print(f" 💬 直接回答:{msg.content}")
return
# 选择能力:模型从 3 个里挑了哪个、传了什么
call = msg.tool_calls[0]
args = json.loads(call.function.arguments)
print(f" 🔧 选中工具:{call.function.name}")
print(f" 📦 传入参数:{args}")
# 我们执行被选中的工具
result = DISPATCH[call.function.name](**args)
print(f" ⚙️ 执行结果:{result}")
# 把结果发回模型,让它说人话
messages.append(msg)
messages.append(
{
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result, ensure_ascii=False),
}
)
second = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS)
print(f" 💬 最终回答:{second.choices[0].message.content}")
# 五个问题,分别考查不同的选择/边界场景
QUESTIONS = [
"上海现在多少度?", # → 应选 get_weather
"把 200 欧元换成日元", # → 应选 convert_currency
"把“今天天气真好”翻译成英文", # → 应选 translate_text(注意:含“天气”,干扰项!)
"你觉得人生的意义是什么?", # → 边界:哪个都不该选
]
if __name__ == "__main__":
for q in QUESTIONS:
run(q)
print("=" * 60)
print(
"\n观察重点:\n"
" · 第 3 题句子里有“天气”二字,但意图是翻译——好的 description 让模型\n"
" 按【意图】选 translate_text,而不是被关键词“天气”骗去选 get_weather。\n"
" · 第 4 题是闲聊,模型应一个工具都不调(边界识别)。\n"
" · 选错工具/选不出,先回去检查各工具 description 是否互相清晰、没有歧义。"
)