03-多工具选择:模型的工具选择与边界识别能力

在天气查询、货币换算、文本翻译三个工具并列场景下,测试模型的「选对」与「忍住」能力,尤其是含干扰项的边界问题。派生自练习1,深化对 description 黄金法则的理解。

2026/6/14
12 分钟阅读
目录

直接看代码注释

"""练习 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 是否互相清晰、没有歧义。"
    )