13-大模型智能体开发工程师:工具使用(Tool Use)范式

系列文章导航:AI系列文章导航目录-持续更新中

第13课:工具使用(Tool Use)范式

📝 本文摘要:本文阐述Tool Use的完整概念层级(Tool Use→Function Calling/MCP/Skill等实现方式),详解工具描述规范(name/description/parameters JSON Schema)、工具选择策略(模型自动选择/强制调用/禁用)、工具结果处理(结构化返回→模型理解→下一步决策),对比不同模型工具调用能力差异,以及从Function Calling到MCP协议的标准化演进方向。
Function Calling是一种实现方式,Tool Use是更广泛的概念。理解Tool Use的完整范式,你才能理解MCP、Skill等上层技术的价值。


一、Tool Use到底是什么

1.1 从一个问题说起

大模型(LLM)本质上只能做一件事:根据输入的文字,生成输出的文字

这意味着它有很多事情做不了:

复制代码
LLM做不到的事情:
  ❌ 查询今天的天气(它不能上网)
  ❌ 查数据库里的订单(它没有数据库连接)
  ❌ 发一封邮件(它没有邮箱账号)
  ❌ 读取你电脑上的文件(它没有文件系统访问权限)
  ❌ 执行一段代码(它只能生成代码文本,不能运行)
  ❌ 调用任何外部API(它只是一个文本生成器)

LLM能做到的事情:
  ✅ 理解你的意图("我想查北京天气")
  ✅ 生成文本回复("北京今天可能是晴天"------但这是瞎猜的)
  ✅ 生成结构化数据(输出JSON格式的内容)

Tool Use(工具使用)就是解决这个问题的方案:让LLM能够"使用工具"来完成它本身做不到的事情。

1.2 一句话定义

复制代码
Tool Use = 让LLM能够调用外部工具(API、数据库、代码执行器等)来完成任务

类比:
  LLM本身 = 一个很聪明但没有手的大脑
  Tool Use = 给这个大脑装上手,让它能操作外部世界
  
  人类也是这样:
    大脑想知道天气 → 手拿起手机打开天气App → 眼睛看到结果 → 大脑理解
    LLM想知道天气 → 调用天气API → 获取返回数据 → LLM理解并回答用户

1.3 Tool Use的工作原理(核心!)

关键理解:LLM自己不执行工具,它只是"说"它想用什么工具。真正执行工具的是你的代码。

复制代码
完整流程(以查天气为例):

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  第1步: 你告诉LLM "你有一个查天气的工具可以用"               │
│         (通过API的tools参数传入工具描述)                    │
│                                                             │
│  第2步: 用户问 "北京今天天气怎么样?"                        │
│                                                             │
│  第3步: LLM思考后决定 → "我需要用查天气工具"                 │
│         LLM输出: {name: "get_weather", args: {city: "北京"}} │
│         注意: LLM只是输出了这段JSON,它并没有真的去查天气!   │
│                                                             │
│  第4步: 你的代码收到这个JSON                                 │
│         → 解析出: 要调get_weather,参数是city="北京"          │
│         → 你的代码真正去调用天气API                          │
│         → 得到结果: {temp: 28, condition: "晴"}              │
│                                                             │
│  第5步: 你把工具执行结果返回给LLM                            │
│         "天气API返回: 28度,晴"                              │
│                                                             │
│  第6步: LLM基于这个真实数据,生成最终回答                    │
│         "北京今天天气晴朗,气温28°C,适合出行。"              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

整个过程中的角色分工:
  LLM的职责: 理解意图 → 决定用哪个工具 → 生成参数 → 理解结果 → 生成回答
  你的代码的职责: 提供工具列表 → 执行工具 → 返回结果
  
  LLM = 大脑(决策者)
  你的代码 = 手(执行者)

1.4 Tool Use vs Function Calling:到底什么关系?

你会在各种文章中看到这两个词混用,这里彻底讲清楚:

复制代码
Tool Use(工具使用)= 一个广义的概念/范式
  含义: "让LLM能使用外部工具"
  不限定具体怎么实现

Function Calling = Tool Use的一种具体实现方式
  含义: "通过API的tools参数定义工具,LLM输出结构化的函数调用JSON"
  是OpenAI首先推出的具体API实现

关系:
  Tool Use 是"做什么"(让LLM用工具)
  Function Calling 是"怎么做"(通过API的tools参数+结构化JSON输出)

类比:
  "出行" = Tool Use(广义概念)
  "开车" = Function Calling(具体方式之一)
  "骑车"、"坐地铁" = 其他实现方式
关于 "type": "function" 的常见误解

很多初学者看到OpenAI的工具定义格式后会产生一个误解:

python 复制代码
# OpenAI的工具定义
tools = [
    {
        "type": "function",   # ← 看到这个type,你可能会想:
        "function": { ... }   #    "是不是还有type为mcp、code_interpreter等的?"
    }
]

# 答案:不是!
# "type": "function" 目前是唯一的值!
# 不存在 "type": "mcp" 或 "type": "code_interpreter"

为什么只有 "function" 这一个type?

复制代码
因为对LLM来说,所有工具最终都是以"函数"的形式呈现的:

  不管工具来自哪里:
    ├── 你手动写的工具定义 → "type": "function"
    ├── MCP Server提供的工具 → 也被转换为 "type": "function"
    ├── Skill系统注册的工具 → 也被转换为 "type": "function"
    └── 框架自动生成的工具 → 也被转换为 "type": "function"

  LLM看到的永远是同一种格式!
  它根本不知道也不关心工具是从哪来的。

  类比:
    你去餐厅点菜,菜单上写着"宫保鸡丁"。
    你不关心这道菜的食材是从超市买的还是从农场直送的。
    对你来说,它就是菜单上的一道菜。
    
    LLM也一样,它只看到"这里有个get_weather工具可以用",
    不关心这个工具是开发者手写的还是MCP Server提供的。

图示:工具来源不同,但LLM看到的格式完全一样

复制代码
┌─────────────────────────────────────────────────────────┐
│                      LLM (大模型)                        │
│                                                         │
│  它只认一种格式:                                          │
│  tools = [{"type": "function", "function": {...}}]      │
│                                                         │
│  不管工具从哪来,对我来说都一样                            │
└────────────────────────┬────────────────────────────────┘
                         │
              传入的都是同一种格式
                         │
           ┌─────────────┼─────────────┐
           │             │             │
      ┌────▼────┐  ┌─────▼─────┐  ┌───▼────┐
      │ 手动定义 │  │ MCP Server │  │ Skill  │
      │ 的工具   │  │ 提供的工具  │  │ 的工具  │
      └─────────┘  └───────────┘  └────────┘
      
      来源不同,但最终格式完全一样!
      MCP解决的不是"LLM怎么调工具"的问题,
      而是"工具怎么被发现和管理"的问题。

为什么要区分这两个概念? 因为Tool Use不只有Function Calling这一种实现:

复制代码
Tool Use(工具使用)------ 广义概念
│
├── Function Calling / Tool Calling(最主流的实现方式)
│     各家LLM厂商的API都支持
│     OpenAI叫 Function Calling
│     Anthropic叫 Tool Use(注意:Anthropic用了同一个词!容易混淆)
│     本质一样: LLM输出结构化的工具调用JSON
│
├── Code Interpreter(代码解释器)
│     LLM自己写代码并执行
│     不是调用预定义的函数,而是动态生成代码
│     例: ChatGPT的Code Interpreter、Claude的Analysis Tool
│
├── Computer Use(电脑操作)
│     LLM直接操作电脑屏幕
│     点击、打字、截图、滚动
│     例: Claude的Computer Use、OpenAI的Operator
│
├── Web Browsing(网页浏览)
│     LLM自己上网搜索和浏览
│     例: ChatGPT的Browse with Bing
│
└── MCP(标准化协议)
      统一了工具暴露和调用的标准
      让任何工具都能被任何Agent使用

层级关系:
  Tool Use (概念层 - "LLM能用工具")
    └─ Function Calling (实现层 - 各家API的具体实现)
        └─ MCP (标准化层 - 统一协议,让工具可复用)
            └─ Skill (能力层 - 工具+知识+提示的组合包)

1.5 为什么Tool Use是Agent的核心能力

复制代码
没有Tool Use的LLM:
  用户: "帮我查一下订单ORD001的状态"
  LLM: "抱歉,我无法访问订单系统,请您自行查询。"
  → 只能聊天,不能做事

有Tool Use的LLM(= Agent):
  用户: "帮我查一下订单ORD001的状态"
  LLM: [调用query_order工具] → 获取结果
  LLM: "您的订单ORD001已发货,预计明天送达。"
  → 能理解意图 + 能执行操作 + 能返回结果 = 真正的智能助手

所以:
  LLM + Tool Use = Agent(能做事的智能体)
  Tool Use是从"聊天机器人"进化为"智能助手"的关键一步

二、不同平台的Tool Use实现

不同的LLM厂商对Tool Use的API实现略有不同,但核心思路完全一致。下面分别讲解。

2.1 OpenAI: Function Calling

OpenAI是最早推出Function Calling的厂商(2023年6月),也是目前最成熟的实现。

2.1.1 工具定义格式
python 复制代码
from openai import OpenAI
import json

client = OpenAI()  # 需要设置OPENAI_API_KEY环境变量

# 工具定义:告诉LLM "你有哪些工具可以用"
# 每个工具需要: name(名称)、description(描述)、parameters(参数规范)
tools = [
    {
        "type": "function",  # 目前只有function这一种type
        "function": {
            "name": "get_weather",  # 工具名称(LLM用这个名字来调用)
            "description": "获取指定城市的当前天气信息,包括温度、天气状况和湿度",  # 描述越清晰,LLM选择越准确
            "parameters": {  # 参数定义,使用JSON Schema格式
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如'北京'、'上海'、'广州'"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],  # 枚举值,限定可选项
                        "description": "温度单位,celsius=摄氏度,fahrenheit=华氏度,默认摄氏度"
                    }
                },
                "required": ["city"],  # 必填参数
                "additionalProperties": False  # 不允许额外参数
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "query_order",
            "description": "根据订单编号查询订单详情,包括订单状态、商品列表和金额",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "订单编号,格式如ORD001、ORD002"
                    }
                },
                "required": ["order_id"],
                "additionalProperties": False
            }
        }
    }
]
2.1.2 完整的调用流程代码
python 复制代码
# 实际的工具实现(这些是你自己写的真实函数)
def get_weather(city: str, unit: str = "celsius") -> dict:
    """模拟天气API(实际项目中这里会调用真实的天气API)"""
    weather_data = {
        "北京": {"temp": 28, "condition": "晴", "humidity": 45},
        "上海": {"temp": 32, "condition": "多云", "humidity": 78},
        "深圳": {"temp": 35, "condition": "雷阵雨", "humidity": 85},
    }
    data = weather_data.get(city, {"temp": 25, "condition": "未知", "humidity": 50})
    if unit == "fahrenheit":
        data["temp"] = data["temp"] * 9/5 + 32
    return data

def query_order(order_id: str) -> dict:
    """模拟订单查询API"""
    orders = {
        "ORD001": {"status": "已发货", "items": ["手机壳", "充电器"], "total": 128.5},
        "ORD002": {"status": "待发货", "items": ["耳机"], "total": 299.0},
    }
    return orders.get(order_id, {"status": "未找到", "items": [], "total": 0})

# 工具名称 → 函数的映射(方便根据名称找到对应函数)
tool_map = {
    "get_weather": get_weather,
    "query_order": query_order,
}

# 完整的Agent循环
def agent_chat(user_message: str) -> str:
    messages = [
        {"role": "system", "content": "你是一个智能助手,可以查询天气和订单信息。"},
        {"role": "user", "content": user_message}
    ]
    
    max_rounds = 5  # 最多循环5次,防止死循环
    
    for _ in range(max_rounds):
        # 第1步: 调用LLM,传入消息历史 + 工具列表
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=tools,
            tool_choice="auto"  # auto=让模型自己决定是否调工具
        )
        
        msg = response.choices[0].message
        
        # 第2步: 判断LLM的响应类型
        
        # 情况A: LLM直接回复文本(不需要调工具)
        if msg.content and not msg.tool_calls:
            return msg.content
        
        # 情况B: LLM要求调用工具
        if msg.tool_calls:
            # 把LLM的tool_call消息加入历史
            messages.append(msg)
            
            # 逐个执行LLM要求的工具调用
            for tool_call in msg.tool_calls:
                func_name = tool_call.function.name
                func_args = json.loads(tool_call.function.arguments)
                
                print(f"  → LLM决定调用: {func_name}({func_args})")
                
                # 执行工具(你的代码负责真正执行)
                result = tool_map[func_name](**func_args)
                
                print(f"  ← 工具返回: {result}")
                
                # 把工具结果加入messages,告诉LLM执行结果
                messages.append({
                    "role": "tool",           # 角色是"tool"
                    "tool_call_id": tool_call.id,  # 对应哪个tool_call
                    "content": json.dumps(result, ensure_ascii=False)
                })
            
            # 继续循环,让LLM基于工具结果生成最终回答
    
    return "抱歉,处理过程中遇到了问题。"

# 测试
print(agent_chat("北京今天天气怎么样?"))
# 输出:
#   → LLM决定调用: get_weather({'city': '北京'})
#   ← 工具返回: {'temp': 28, 'condition': '晴', 'humidity': 45}
#   "北京今天天气晴朗,温度28°C,湿度45%。适合户外活动!"

print(agent_chat("我的订单ORD001到哪了?"))
# 输出:
#   → LLM决定调用: query_order({'order_id': 'ORD001'})
#   ← 工具返回: {'status': '已发货', 'items': ['手机壳', '充电器'], 'total': 128.5}
#   "您的订单ORD001已发货,包含手机壳和充电器,总金额128.5元。"

print(agent_chat("你好"))
# 输出:
#   (不调用任何工具,直接回复)
#   "你好!有什么我可以帮你的吗?"
2.1.3 tool_choice参数详解
python 复制代码
# tool_choice 控制LLM是否/如何使用工具

# "auto"(默认): LLM自己判断要不要用工具
#   用户问"你好" → 不调工具,直接回复
#   用户问"北京天气" → 调get_weather工具
tool_choice="auto"

# "none": 禁止使用工具,强制纯文本回复
#   即使用户问"北京天气",也不会调工具
tool_choice="none"

# "required": 强制必须调用工具(至少调一个)
#   即使用户只是说"你好",也会被迫选一个工具调用
tool_choice="required"

# 指定工具: 强制调用某个特定工具
#   不管用户说什么,都会调get_weather
tool_choice={"type": "function", "function": {"name": "get_weather"}}
2.1.4 并行工具调用
python 复制代码
# 当用户的问题需要多个工具时,LLM可以一次性输出多个tool_calls

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "北京和上海今天天气怎么样?"}],
    tools=tools
)

msg = response.choices[0].message
# msg.tool_calls 会包含两个调用:
#   tool_calls[0]: get_weather({"city": "北京"})
#   tool_calls[1]: get_weather({"city": "上海"})

# 你需要都执行,然后把两个结果都返回给LLM
for tool_call in msg.tool_calls:
    # 执行每个工具调用...
    pass

2.2 Anthropic: Tool Use

Anthropic(Claude的开发商)对Tool Use的实现和OpenAI思路一样,但API格式有明显差异。下面给出完整的独立案例。

2.2.1 工具定义格式
python 复制代码
import anthropic
import json

client = anthropic.Anthropic()  # 需要设置ANTHROPIC_API_KEY环境变量

# Anthropic的工具定义格式
# 注意和OpenAI的区别:
#   1. 没有外层的 "type": "function" 包装
#   2. 参数定义的key叫 input_schema(不是parameters)
tools = [
    {
        "name": "get_weather",  # 工具名称
        "description": "获取指定城市的当前天气信息,包括温度、天气状况和湿度",  # 描述
        "input_schema": {  # ← 注意!叫input_schema,不是parameters
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名称,如'北京'、'上海'、'广州'"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "温度单位,默认摄氏度"
                }
            },
            "required": ["city"]
        }
    },
    {
        "name": "query_order",
        "description": "根据订单编号查询订单详情,包括订单状态、商品列表和金额",
        "input_schema": {
            "type": "object",
            "properties": {
                "order_id": {
                    "type": "string",
                    "description": "订单编号,格式如ORD001、ORD002"
                }
            },
            "required": ["order_id"]
        }
    }
]
2.2.2 完整的调用流程代码
python 复制代码
# 实际的工具实现(和OpenAI的例子一样)
def get_weather(city: str, unit: str = "celsius") -> dict:
    """模拟天气API"""
    weather_data = {
        "北京": {"temp": 28, "condition": "晴", "humidity": 45},
        "上海": {"temp": 32, "condition": "多云", "humidity": 78},
    }
    data = weather_data.get(city, {"temp": 25, "condition": "未知", "humidity": 50})
    if unit == "fahrenheit":
        data["temp"] = data["temp"] * 9/5 + 32
    return data

def query_order(order_id: str) -> dict:
    """模拟订单查询API"""
    orders = {
        "ORD001": {"status": "已发货", "items": ["手机壳", "充电器"], "total": 128.5},
        "ORD002": {"status": "待发货", "items": ["耳机"], "total": 299.0},
    }
    return orders.get(order_id, {"status": "未找到", "items": [], "total": 0})

tool_map = {"get_weather": get_weather, "query_order": query_order}

# 完整的Agent循环
def agent_chat(user_message: str) -> str:
    messages = [{"role": "user", "content": user_message}]
    
    max_rounds = 5
    
    for _ in range(max_rounds):
        # 第1步: 调用Claude API
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            messages=messages,
            tools=tools,
            tool_choice={"type": "auto"}  # auto=模型自己决定
        )
        
        # 第2步: 判断响应类型
        # Anthropic的响应是一个content数组,可能包含多种block
        
        # 情况A: stop_reason为"end_turn",表示模型直接回复(不调工具)
        if response.stop_reason == "end_turn":
            # 从content中提取文本
            text_parts = [block.text for block in response.content if block.type == "text"]
            return "\n".join(text_parts)
        
        # 情况B: stop_reason为"tool_use",表示模型要调工具
        if response.stop_reason == "tool_use":
            # 把模型的响应加入消息历史(role为assistant)
            messages.append({"role": "assistant", "content": response.content})
            
            # 收集所有工具调用的结果
            tool_results = []
            
            for block in response.content:
                if block.type == "tool_use":
                    func_name = block.name
                    func_args = block.input  # 注意:Anthropic直接给dict,不需要json.loads
                    
                    print(f"  → Claude决定调用: {func_name}({func_args})")
                    
                    # 执行工具
                    result = tool_map[func_name](**func_args)
                    
                    print(f"  ← 工具返回: {result}")
                    
                    # 构造tool_result
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,  # 必须对应tool_use的id
                        "content": json.dumps(result, ensure_ascii=False)
                    })
            
            # 把工具结果以user消息的形式返回
            # 注意!这是和OpenAI最大的区别:
            #   OpenAI: 工具结果用 role="tool" 的独立消息
            #   Anthropic: 工具结果放在 role="user" 消息的content数组中
            messages.append({"role": "user", "content": tool_results})
            
            # 继续循环,让Claude基于工具结果生成最终回答
    
    return "处理超时"

# 测试
print(agent_chat("北京今天天气怎么样?"))
# 输出:
#   → Claude决定调用: get_weather({'city': '北京'})
#   ← 工具返回: {'temp': 28, 'condition': '晴', 'humidity': 45}
#   "北京今天天气晴朗,气温28°C,湿度45%。"

print(agent_chat("你好"))
# 输出:
#   (不调用任何工具,直接回复)
#   "你好!有什么我可以帮你的吗?"
2.2.3 tool_choice参数
python 复制代码
# Anthropic的tool_choice格式和OpenAI不同

# 自动模式:模型自己决定(默认)
tool_choice={"type": "auto"}

# 强制使用工具(必须调至少一个,类似OpenAI的"required")
tool_choice={"type": "any"}

# 强制调用指定工具
tool_choice={"type": "tool", "name": "get_weather"}

# 注意:Anthropic没有"none"选项
# 如果不想让模型调工具,直接不传tools参数即可

2.3 OpenAI vs Anthropic 完整对比

下面用同一个任务对比两家的实现差异,帮你一目了然:

2.3.1 工具定义对比
python 复制代码
# ═══════════════════════════════════════════════════
# OpenAI 的工具定义
# ═══════════════════════════════════════════════════
openai_tools = [
    {
        "type": "function",        # ← 有外层type包装
        "function": {              # ← 嵌套在function key下
            "name": "get_weather",
            "description": "获取城市天气",
            "parameters": {        # ← 叫parameters
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名"}
                },
                "required": ["city"]
            }
        }
    }
]

# ═══════════════════════════════════════════════════
# Anthropic 的工具定义
# ═══════════════════════════════════════════════════
anthropic_tools = [
    {
        # 没有外层type包装,直接就是工具定义
        "name": "get_weather",
        "description": "获取城市天气",
        "input_schema": {          # ← 叫input_schema
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "城市名"}
            },
            "required": ["city"]
        }
    }
]
2.3.2 LLM响应格式对比
python 复制代码
# ═══════════════════════════════════════════════════
# OpenAI 的响应(当LLM决定调工具时)
# ═══════════════════════════════════════════════════
# response.choices[0].message 的结构:
{
    "role": "assistant",
    "content": None,              # 调工具时content为空
    "tool_calls": [               # ← 工具调用在tool_calls数组中
        {
            "id": "call_abc123",
            "type": "function",
            "function": {
                "name": "get_weather",
                "arguments": "{\"city\": \"北京\"}"  # ← 是JSON字符串!需要json.loads
            }
        }
    ]
}

# ═══════════════════════════════════════════════════
# Anthropic 的响应(当LLM决定调工具时)
# ═══════════════════════════════════════════════════
# response.content 的结构:
[
    {
        "type": "text",            # 可能先有一段思考文本
        "text": "让我查一下北京的天气。"
    },
    {
        "type": "tool_use",        # ← 工具调用是content中的一个block
        "id": "toolu_abc123",
        "name": "get_weather",
        "input": {"city": "北京"}   # ← 直接是dict!不需要json.loads
    }
]
2.3.3 返回工具结果的方式对比
python 复制代码
# ═══════════════════════════════════════════════════
# OpenAI: 用独立的 role="tool" 消息返回结果
# ═══════════════════════════════════════════════════
messages.append({
    "role": "tool",                # ← 专门的tool角色
    "tool_call_id": "call_abc123", # 对应哪个tool_call
    "content": "{\"temp\": 28, \"condition\": \"晴\"}"  # 结果
})

# ═══════════════════════════════════════════════════
# Anthropic: 放在 role="user" 消息的content数组中
# ═══════════════════════════════════════════════════
messages.append({
    "role": "user",                # ← 注意!是user角色
    "content": [                   # content是数组
        {
            "type": "tool_result",  # 类型是tool_result
            "tool_use_id": "toolu_abc123",  # 对应哪个tool_use
            "content": "{\"temp\": 28, \"condition\": \"晴\"}"  # 结果
        }
    ]
})
2.3.4 总结对比表
复制代码
┌──────────────────┬─────────────────────────┬─────────────────────────────┐
│ 维度             │ OpenAI                  │ Anthropic                   │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 工具定义外层     │ {"type":"function",     │ 直接定义,无外层包装          │
│                  │  "function":{...}}      │ {"name":..., ...}           │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 参数定义key      │ parameters              │ input_schema                │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 工具调用输出位置 │ message.tool_calls数组  │ content中的tool_use block   │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 参数格式         │ JSON字符串              │ 直接是dict对象               │
│                  │ (需要json.loads解析)    │ (不需要额外解析)             │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 工具结果角色     │ role: "tool"            │ role: "user"                │
│                  │ (专门的tool角色)        │ (放在user消息的content中)    │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ tool_choice选项  │ auto / none /           │ auto / any /                │
│                  │ required / 指定工具     │ 指定工具(无none)           │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 并行调用         │ 多个tool_calls          │ 多个tool_use blocks         │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 判断是否调工具   │ msg.tool_calls存在      │ stop_reason=="tool_use"     │
└──────────────────┴─────────────────────────┴─────────────────────────────┘
2.3.5 核心结论
复制代码
虽然格式不同,但本质完全一样:

  1. 你定义工具列表 → 传给LLM
  2. LLM决定调哪个工具 → 输出结构化的调用信息
  3. 你执行工具 → 把结果返回给LLM
  4. LLM基于结果 → 生成最终回答

这个流程不管是OpenAI还是Anthropic都一样。
差异只在API格式的细节上,不影响你理解Tool Use的核心思想。

如果你用框架(如LangChain、LlamaIndex),
框架会帮你屏蔽这些格式差异,你写一份代码就能同时支持两家。

三、高级Tool Use模式

3.1 动态工具注册

python 复制代码
class ToolRegistry:
    """动态工具注册器------运行时增减可用工具"""
    
    def __init__(self):
        self.tools = {}
        self.schemas = []
    
    def register(self, func, schema):
        """注册工具"""
        self.tools[schema["function"]["name"]] = func
        self.schemas.append(schema)
    
    def unregister(self, name):
        """注销工具"""
        self.tools.pop(name, None)
        self.schemas = [s for s in self.schemas if s["function"]["name"] != name]
    
    def get_schemas(self):
        return self.schemas
    
    def execute(self, name, args):
        return self.tools[name](**args)

# 使用
registry = ToolRegistry()

registry.register(
    func=get_weather,
    schema={
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取城市天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称"}
                },
                "required": ["city"],
                "additionalProperties": False
            }
        }
    }
)

# 根据用户权限动态加载工具
if user.has_permission("order_query"):
    registry.register(query_order_func, query_order_schema)
if user.has_permission("refund"):
    registry.register(create_refund_func, refund_schema)

3.2 工具组合(Tool Composition)

python 复制代码
# 一个工具的输出作为另一个工具的输入
# 让Agent自己决定调用顺序

# 例: 查询订单 → 根据结果决定是否退款
tools = [
    {
        "type": "function",
        "function": {
            "name": "query_order",
            "description": "查询订单详情,包括状态、金额、商品等",
            "parameters": {...}
        }
    },
    {
        "type": "function", 
        "function": {
            "name": "create_refund",
            "description": "为指定订单创建退款申请。只有状态为'已签收'的订单才能退款",
            "parameters": {...}
        }
    }
]

# Agent会自动: 先query_order → 检查状态 → 再create_refund(如果可以)

3.3 人工确认(Human-in-the-loop,人工介入循环)

python 复制代码
# 危险操作需要人工确认
DANGEROUS_TOOLS = {"create_refund", "delete_account", "send_email"}

def agent_with_confirmation(user_message: str) -> str:
    messages = [{"role": "user", "content": user_message}]
    
    for _ in range(5):
        response = client.chat.completions.create(
            model="gpt-4o-mini", messages=messages, tools=tools
        )
        msg = response.choices[0].message
        
        if not msg.tool_calls:
            return msg.content
        
        messages.append(msg)
        
        for tc in msg.tool_calls:
            if tc.function.name in DANGEROUS_TOOLS:
                # 请求人工确认
                args = json.loads(tc.function.arguments)
                confirm = input(f"⚠️ 即将执行 {tc.function.name}({args}),确认?(y/n): ")
                if confirm.lower() != 'y':
                    result = {"status": "cancelled", "message": "用户取消操作"}
                else:
                    result = tool_map[tc.function.name](**args)
            else:
                result = tool_map[tc.function.name](**args)
            
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": json.dumps(result, ensure_ascii=False)
            })
    
    return "处理超时"

四、Code Interpreter(代码解释器)------特殊的Tool Use

4.1 什么是Code Interpreter

复制代码
让Agent能执行代码,是最强大的工具之一

普通工具: 预定义的API,模型只能选择调用
Code Interpreter: 模型自己写代码来解决问题

适用场景:
  - 数学计算(避免模型算错)
  - 数据分析(处理CSV/Excel)
  - 图表生成
  - 文件格式转换

4.2 实现一个简易Code Interpreter

python 复制代码
import subprocess
import tempfile
import os

def execute_python(code: str) -> dict:
    """安全地执行Python代码"""
    try:
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(code)
            f.flush()
            temp_path = f.name
        
        result = subprocess.run(
            ["python3", temp_path],
            capture_output=True,
            text=True,
            timeout=30,  # 30秒超时
        )
        
        os.unlink(temp_path)
        
        return {
            "stdout": result.stdout[:5000],   # 限制输出长度
            "stderr": result.stderr[:5000],
            "returncode": result.returncode
        }
    except subprocess.TimeoutExpired:
        return {"error": "执行超时(30秒)"}
    except Exception as e:
        return {"error": str(e)}

# 作为工具注册
code_interpreter_tool = {
    "type": "function",
    "function": {
        "name": "execute_python",
        "description": "执行Python代码并返回结果。用于数学计算、数据分析等需要精确计算的场景。",
        "parameters": {
            "type": "object",
            "properties": {
                "code": {
                    "type": "string",
                    "description": "要执行的Python代码"
                }
            },
            "required": ["code"],
            "additionalProperties": False
        }
    }
}

五、Tool Use的设计哲学

5.1 给模型合适的工具

复制代码
❌ 给模型一个"do_everything"工具
✅ 给模型多个专一的工具

原因: 工具粒度越细,模型选择越准确
类比: 给员工一个"万能遥控器"不如给他每个设备的遥控器

5.2 工具描述即API文档

复制代码
工具的description就是给模型看的API文档
写好工具描述 = 写好API文档

好的工具描述要素:
1. 功能说明: 这个工具做什么
2. 使用时机: 什么时候应该用
3. 参数说明: 每个参数的含义和格式
4. 返回说明: 返回什么格式的数据
5. 注意事项: 使用限制和边界条件

5.3 工具数量与质量的权衡

复制代码
工具数量:
  < 5个: 模型选择很准
  5-20个: 偶尔选错
  > 20个: 经常选错,需要分组或检索

优化策略:
  1. 按场景分组: 只加载当前场景需要的工具
  2. 两步选择: 先选类别,再选具体工具
  3. 工具检索: 用嵌入相似度选择最相关的工具

📝 作业

作业1:实现一个带人工确认的订单管理Agent

要求:

  1. 有3个工具:查询订单、创建退款、发送通知
  2. 退款操作需要人工确认
  3. 完整的Agent循环

参考答案

python 复制代码
from openai import OpenAI
import json

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

# 模拟数据
ORDERS = {
    "ORD001": {"status": "已签收", "amount": 299.0, "item": "蓝牙耳机"},
    "ORD002": {"status": "配送中", "amount": 89.5, "item": "手机壳"},
    "ORD003": {"status": "已签收", "amount": 1299.0, "item": "机械键盘"},
}

def query_order(order_id: str) -> dict:
    return ORDERS.get(order_id, {"status": "未找到", "amount": 0, "item": ""})

def create_refund(order_id: str, reason: str) -> dict:
    order = ORDERS.get(order_id)
    if not order:
        return {"success": False, "message": "订单不存在"}
    if order["status"] != "已签收":
        return {"success": False, "message": f"订单状态为{order['status']},无法退款"}
    return {"success": True, "message": f"退款申请已创建,金额{order['amount']}元"}

def send_notification(order_id: str, message: str) -> dict:
    return {"success": True, "message": f"已发送通知到订单{order_id}关联的用户"}

tool_map = {"query_order": query_order, "create_refund": create_refund, "send_notification": send_notification}
DANGEROUS = {"create_refund", "send_notification"}

tools = [
    {
        "type": "function",
        "function": {
            "name": "query_order",
            "description": "查询订单详情",
            "parameters": {
                "type": "object",
                "properties": {"order_id": {"type": "string", "description": "订单编号"}},
                "required": ["order_id"],
                "additionalProperties": False
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "create_refund",
            "description": "为已签收的订单创建退款申请",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "description": "订单编号"},
                    "reason": {"type": "string", "description": "退款原因"}
                },
                "required": ["order_id", "reason"],
                "additionalProperties": False
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "send_notification",
            "description": "向用户发送通知",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "description": "关联订单编号"},
                    "message": {"type": "string", "description": "通知内容"}
                },
                "required": ["order_id", "message"],
                "additionalProperties": False
            }
        }
    }
]

def order_agent(user_message: str) -> str:
    messages = [
        {"role": "system", "content": "你是订单管理助手。查询订单不需要确认,但退款和发通知需要用户确认。"},
        {"role": "user", "content": user_message}
    ]
    
    for _ in range(5):
        response = client.chat.completions.create(
            model="qwen2.5:7b", messages=messages, tools=tools, tool_choice="auto"
        )
        msg = response.choices[0].message
        
        if msg.content and not msg.tool_calls:
            return msg.content
        
        if msg.tool_calls:
            messages.append(msg)
            for tc in msg.tool_calls:
                args = json.loads(tc.function.arguments)
                
                if tc.function.name in DANGEROUS:
                    print(f"⚠️ Agent请求执行: {tc.function.name}({args})")
                    confirm = input("确认执行?(y/n): ")
                    if confirm.lower() != 'y':
                        result = {"status": "cancelled", "message": "用户取消了操作"}
                    else:
                        result = tool_map[tc.function.name](**args)
                else:
                    result = tool_map[tc.function.name](**args)
                
                messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": json.dumps(result, ensure_ascii=False)
                })
    
    return "处理超时"

下一篇文章见:AI系列文章导航目录-持续更新中

相关推荐
canonical_entropy1 小时前
为什么 Attractor Guided Engineering 不能被降级为 AI Agent Skill
架构·agent·ai编程
weixin_468466851 小时前
图像处理特征提取新手实战指南
图像处理·人工智能·算法·ai·机器视觉·特征提取
weixin_509138341 小时前
[特殊字符] 【硬核深度/万字解析】大模型“炼金术”时代的终结?带你读懂AGI范式转移!
人工智能·智能体·认知动力学·智能体认知
weixin_468466851 小时前
图像处理之形态学处理新手实战指南
图像处理·人工智能·算法·ai·机器视觉·形态学
DreamWear1 小时前
用本地 LLM 写 commit,不消耗云端 token:git-courer 是怎么做到的
agent·ai编程
RockHopper20251 小时前
智能体的《目的论》模型
人工智能·llm·智能体
weiwin1232 小时前
MAF入门(3 下):多轮对话进阶——清除历史、注入 System、截断策略
人工智能·agent
XLYcmy2 小时前
面向Agent权限系统的快速审计工具
python·网络安全·ai·llm·飞书·agent·字节跳动
guyoung2 小时前
BoxAgnts 运行时(1)——运行时工程决定 Agent 未来
agent·ai编程