第01章 工具调用的本质:LLM如何与外部世界交互

第01章 工具调用的本质:LLM如何与外部世界交互

“语言模型是大脑,工具是手脚。没有手脚的大脑,只能思考,无法行动。” —— AI系统设计师


在深入工具Schema设计、并行调用、安全防护之前,我们需要彻底理解一件事:工具调用在底层究竟是如何工作的?

很多开发者把工具调用当成"黑魔法"——传入一些函数定义,LLM自动就知道要调用哪个函数。理解背后的机制,是写好工具的前提。


1.1 LLM的根本限制:文本进,文本出

# LLM的本质:一个预测下一个token的函数

# 不使用工具时,LLM只能生成文本
conversation = [
    {"role": "user", "content": "今天北京的天气怎么样?"}
]

# LLM的回答:
# "我无法访问实时数据,所以我不知道今天北京的具体天气。
#  北京四月份通常...(一堆猜测)"

# 问题:LLM被训练于过去的数据,没有实时访问能力
# 解决:给LLM提供"工具",让它能调用外部系统

LLM有三个内置限制:

  1. 训练数据截止:不知道训练后发生的事
  2. 无法执行操作:不能发邮件、不能写文件
  3. 无状态:每次对话独立,不记得上次的结果

工具调用解决了所有三个限制。


1.2 工具调用的完整流程

# 工具调用的完整生命周期

import json

# ===== 第1步:开发者定义工具Schema =====

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的实时天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,使用中文,例如:北京、上海"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位,默认 celsius(摄氏度)"
                    }
                },
                "required": ["city"],
                "additionalProperties": False,  # 禁止未定义的额外参数
            }
        }
    }
]

# ===== 第2步:用户消息 + 工具定义发送给LLM =====

messages = [
    {"role": "user", "content": "北京今天天气如何?用摄氏度告诉我"}
]

# 发送给 OpenAI API:
# response = client.chat.completions.create(
#     model="gpt-4o-mini",
#     messages=messages,
#     tools=tools,
#     tool_choice="auto",  # 让LLM自己决定是否用工具
# )

# ===== 第3步:LLM返回工具调用请求(不是回答!)=====

# LLM 返回的内容(模拟):
mock_llm_response = {
    "role": "assistant",
    "content": None,  # 没有文字内容,只有工具调用
    "tool_calls": [
        {
            "id": "call_abc123",
            "type": "function",
            "function": {
                "name": "get_weather",
                "arguments": '{"city": "北京", "unit": "celsius"}'
                # 注意:arguments 是字符串!需要 json.loads() 解析
            }
        }
    ]
}

# ===== 第4步:开发者执行工具函数 =====

def get_weather(city: str, unit: str = "celsius") -> dict:
    """真实实现:调用天气API"""
    # 这里调用真实的天气API(如 OpenWeatherMap)
    return {
        "city": city,
        "temperature": 22,
        "unit": unit,
        "condition": "晴天",
        "humidity": "45%",
        "wind": "东南风 2级",
    }

tool_call = mock_llm_response["tool_calls"][0]
fn_name = tool_call["function"]["name"]
fn_args = json.loads(tool_call["function"]["arguments"])
result = get_weather(**fn_args)

# ===== 第5步:工具结果发回给LLM =====

messages.append(mock_llm_response)  # 添加LLM的工具调用请求
messages.append({
    "role": "tool",
    "tool_call_id": "call_abc123",  # 必须匹配工具调用的ID
    "content": json.dumps(result, ensure_ascii=False),
})

# ===== 第6步:LLM生成最终回答 =====

# LLM 现在知道天气数据,会生成自然语言回答:
# "北京今天天气晴朗,气温22°C,相对湿度45%,东南风2级。
#  今天非常适合户外活动!"

关键认知:LLM本身从不执行工具。工具调用是LLM告诉你"我想调用这个函数,参数是这些"——实际执行完全由你的代码负责。


1.3 Tool Choice:控制LLM的工具使用策略

# tool_choice 参数的三种模式

from openai import AsyncOpenAI

async def demo_tool_choice():
    client = AsyncOpenAI()
    
    # 模式1:auto(默认)
    # LLM自己决定是否用工具
    response = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": "你好"}],
        tools=tools,
        tool_choice="auto",  # LLM判断"你好"不需要天气工具,直接文字回答
    )
    
    # 模式2:none
    # 强制不使用工具(即使工具可用)
    response = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": "北京天气怎样"}],
        tools=tools,
        tool_choice="none",  # 强制文字回答,LLM只能说"我不知道实时数据"
    )
    
    # 模式3:required
    # 强制使用工具(LLM必须调用至少一个工具)
    response = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": "随便告诉我什么城市的天气"}],
        tools=tools,
        tool_choice="required",  # LLM必须调用工具
    )
    
    # 模式4:指定工具
    # 强制调用特定工具
    response = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": "上海天气"}],
        tools=tools,
        tool_choice={"type": "function", "function": {"name": "get_weather"}},
    )

# 实用建议:
# - 对话型Agent → auto(让LLM决定)
# - 工作流型Agent → required 或指定工具(确保执行路径)
# - 调试时 → required(强制LLM说明它要做什么)

1.4 LLM如何选择工具:内部机制

# 理解LLM的工具选择逻辑

# LLM通过以下信号决定调用哪个工具:
TOOL_SELECTION_SIGNALS = {
    "工具名称(最重要)": {
        "好名称": "get_real_time_weather",   # 清楚说明是实时的
        "差名称": "tool_1",                  # 无意义
        "差名称2": "fetch",                  # 太通用
    },
    
    "工具描述(非常重要)": {
        "好描述": "获取指定城市的实时天气数据,包括温度、湿度、风速。适用于'今天/现在/当前'天气查询",
        "差描述": "天气工具",
        "关键点": "描述中包含触发场景(什么时候该用这个工具)",
    },
    
    "参数描述(重要)": {
        "好描述": "城市名称,使用完整中文城市名,如'北京'而非'BJ'",
        "差描述": "城市",
        "关键点": "告诉LLM参数的格式、范围、示例",
    },
    
    "参数的required设置": {
        "必须": "如果没有某个参数工具就无法工作,标记为required",
        "可选": "有合理默认值的参数标记为可选,并在描述中说明默认行为",
    },
}

# 实验:同一任务,不同描述,LLM的选择差异巨大
SAME_TOOLS_DIFFERENT_DESCRIPTIONS = {
    "差版本": {
        "name": "tool1",
        "description": "获取数据",
        "parameters": {"query": {"type": "string"}}
    },
    "好版本": {
        "name": "search_web_for_current_info",
        "description": "在互联网上搜索最新信息。适用于:最新新闻、实时数据、最新技术进展、当前事件。不适用于:数学计算、历史知识(使用其他工具)",
        "parameters": {
            "query": {
                "type": "string",
                "description": "搜索关键词。使用简洁精确的关键词,不要用完整句子"
            }
        }
    }
}

1.5 与Anthropic Claude的工具调用对比

# OpenAI vs Claude 的工具调用API差异

# OpenAI 格式(本书主要使用)
openai_request = {
    "model": "gpt-4o-mini",
    "messages": [{"role": "user", "content": "北京天气"}],
    "tools": [{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取天气",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"]
            }
        }
    }],
    "tool_choice": "auto",
}

# Claude 格式(本质相同,语法略有不同)
claude_request = {
    "model": "claude-3-5-sonnet-20241022",
    "messages": [{"role": "user", "content": "北京天气"}],
    "tools": [{
        "name": "get_weather",           # 没有 "type": "function" 包装
        "description": "获取天气",
        "input_schema": {               # 叫 input_schema,不叫 parameters
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"]
        }
    }],
}
# Claude的工具调用结果:
# response.content[0].type == "tool_use"
# response.content[0].name == "get_weather"
# response.content[0].input == {"city": "北京"}

# 建议:使用统一封装层,支持多模型
def create_tool_schema(name: str, description: str, parameters: dict, provider: str = "openai") -> dict:
    """创建跨模型兼容的工具Schema"""
    if provider == "openai":
        return {
            "type": "function",
            "function": {"name": name, "description": description, "parameters": parameters}
        }
    elif provider == "anthropic":
        return {
            "name": name,
            "description": description,
            "input_schema": parameters
        }
    raise ValueError(f"未知的provider: {provider}")

本章小结

五个核心认知:

  1. 工具调用是LLM与外部世界的唯一接口:LLM本身是纯文本系统,工具调用是在文本协议上建立的"外部能力接入层"

  2. LLM不执行工具,只描述调用意图tool_calls 是LLM的"指令",执行由你的代码完成——这个分离很重要,意味着你完全控制工具的执行逻辑

  3. 工具选择质量取决于Schema设计:工具名称、描述、参数说明是LLM选择工具的唯一依据;Schema写得好,工具调用准确率从70%提升到90%以上

  4. tool_call_id 是工具调用和结果的绑定键:多工具并行调用时,LLM通过这个ID知道哪个结果对应哪个调用——一定不能搞错

  5. OpenAI和Claude的工具调用语法不同,但概念相同:学好一种,理解原理后切换到另一种只是API语法问题;构建封装层让代码跨模型复用

核心行动

# 动手验证:用Python实现一个完整的工具调用循环
# 要求:
# 1. 定义两个工具(天气 + 计算器)
# 2. 打印出LLM返回的原始 tool_calls 内容
# 3. 手动解析 arguments(json.loads)
# 4. 执行工具函数
# 5. 把结果加回 messages 然后得到最终回答
#
# 不用框架,就用原始 OpenAI Python SDK
# 这是理解工具调用最好的方式

本章提示词模板

模板一:理解工具调用流程

我是一个Python开发者,了解基本的API调用,但是第一次接触LLM工具调用。

请用最简单的方式解释:
1. 为什么需要工具调用?直接让LLM回答不行吗?
2. LLM工具调用的完整数据流是什么?(用伪代码+流程说明)
3. "LLM不执行工具,只描述调用意图"这句话是什么意思?
4. tool_call_id 的作用是什么?
5. 给我一个最小的、能跑通的工具调用示例(Python,不超过50行)

模板二:从零设计第一个工具

我想给我的AI助手添加一个工具,用途是:[描述你要做的事]

请帮我:
1. 设计这个工具的JSON Schema(符合OpenAI格式)
2. 写出对应的Python函数
3. 分析这个工具的Schema有哪些可以改进的地方(让LLM更容易理解)
4. 这个工具可能被LLM错误调用的场景是什么?如何在描述中预防?
5. 如果这个工具可能失败(网络超时/参数错误),应该返回什么格式的错误信息?

→ 第02章:工具Schema设计:让LLM准确理解你的工具