---
title: "Agent_07_Tool设计"
source: https://www.zbhgis.com/blog/blog-20260221-1
author: zbhgis 浩瀚地学
created: 2026-02-20
tags: [Agent, agent, Python]
---

> 本文来自浩瀚地学，规范页面（含代码高亮与图片灯箱）：https://www.zbhgis.com/blog/blog-20260221-1
# Agent_07_Tool设计

前面的专题讲了 Agent 架构、执行模式、上下文管理。这些都围绕一个问题：LLM 怎么和外部世界交互？答案是工具。

大多数 Agent 项目的问题不是"没给工具"，而是"工具设计得不好"。Anthropic 总结过一句话——**Agent 工具的质量，比 Agent 架构的质量更重要**。一个设计精良的工具配合简单循环，胜过复杂架构配上设计粗糙的工具。

工具设计不是"写函数"，而是"写 LLM 能理解和正确使用的函数"。人类调用函数时可以看文档、试参数、调试错误，LLM 只能靠工具名、描述和 schema 来决定怎么用。工具对 LLM 来说必须是自解释的。

---

## 一、好工具 vs 坏工具

先看同一个功能的三种写法。场景是查询天气：

```python
# 写法一：坏工具

def get_weather(city: str):
    """查天气。"""
    import requests
    resp = requests.get(f"https://api.weather.com/{city}")
    return resp.json()

# 问题：
# 1. 描述太短——"查天气"没有告诉 LLM 返回格式、单位、什么时候用
# 2. 没有参数描述——LLM 不知道 city 应该是中文名还是拼音
# 3. 没有错误处理——API 失败时抛异常，LLM 不知道发生了什么
# 4. 输出格式不固定——LLM 无法可靠解析返回结果


# 写法二：普通工具

def get_weather_v2(city: str, unit: str = "celsius") -> dict:
    """
    查询指定城市的当前天气。

    :param city: 城市名称（中文，如"北京"）
    :param unit: 温度单位，"celsius"（摄氏度）或"fahrenheit"（华氏度），默认 celsius
    :return: 包含天气信息的字典：{"city": str, "temperature": float,
             "condition": str, "humidity": int, "unit": str}
    """
    import requests
    try:
        resp = requests.get(
            f"https://api.weather.com/{city}",
            params={"unit": unit},
            timeout=10,
        )
        resp.raise_for_status()
        data = resp.json()
        return {
            "city": data["name"],
            "temperature": data["temp"],
            "condition": data["condition"],
            "humidity": data["humidity"],
            "unit": unit,
        }
    except requests.RequestException as e:
        return {"error": f"天气查询失败: {str(e)}"}

# 改进：描述变长了，有参数描述，有错误处理，输出格式固定
# 还差什么？使用场景说明


# 写法三：生产级工具

def get_weather_v3(city: str, unit: str = "celsius") -> dict:
    """
    查询指定城市的当前天气。

    使用场景：
    - 用户询问某个城市的天气
    - 用户需要对比多个城市的天气
    - 用户计划出行，需要了解目的地天气

    不使用场景：
    - 用户问天气预报/未来几天的天气（使用 forecast_weather 工具）
    - 用户问历史天气数据（使用 historical_weather 工具）

    :param city: 城市名称。支持中文名（如"北京"）、拼音（如"beijing"）、
                 英文（如"New York"）。如果不确定，使用中文名。
    :param unit: 温度单位。"celsius"（摄氏度，默认）或"fahrenheit"（华氏度）。
                 中国大陆用户默认 celsius。
    :return: 成功时返回 {"city": str, "temperature": float,
             "condition": str, "humidity": int, "wind_speed": float,
             "unit": str}
             失败时返回 {"error": str, "city": str, "suggestion": str}

    示例：
    >>> get_weather_v3("北京")
    {"city": "北京", "temperature": 22.0, "condition": "晴",
     "humidity": 45, "wind_speed": 3.2, "unit": "celsius"}

    >>> get_weather_v3("unknown_city_xyz")
    {"error": "未找到城市 unknown_city_xyz", "city": "unknown_city_xyz",
     "suggestion": "请检查城市名称是否正确"}
    """
    import requests

    # 参数验证
    if not city or len(city.strip()) < 2:
        return {
            "error": f"城市名称无效: '{city}'",
            "city": city,
            "suggestion": "请输入有效的城市名称（至少 2 个字符）",
        }

    if unit not in ("celsius", "fahrenheit"):
        return {
            "error": f"温度单位无效: '{unit}'",
            "city": city,
            "suggestion": "请使用 'celsius' 或 'fahrenheit'",
        }

    try:
        resp = requests.get(
            f"https://api.weather.com/{city}",
            params={"unit": unit},
            timeout=10,
        )
        resp.raise_for_status()
        data = resp.json()
        return {
            "city": data["name"],
            "temperature": data["temp"],
            "condition": data["condition"],
            "humidity": data["humidity"],
            "wind_speed": data.get("wind_speed", 0),
            "unit": unit,
        }
    except requests.Timeout:
        return {
            "error": "天气查询超时",
            "city": city,
            "suggestion": "请稍后重试",
        }
    except requests.ConnectionError:
        return {
            "error": "无法连接天气服务",
            "city": city,
            "suggestion": "请检查网络连接",
        }
    except requests.HTTPError as e:
        if e.response.status_code == 404:
            return {
                "error": f"未找到城市: {city}",
                "city": city,
                "suggestion": "请检查城市名称是否正确",
            }
        return {
            "error": f"天气服务返回错误 {e.response.status_code}",
            "city": city,
            "suggestion": None,  # 服务端错误，LLM 无法处理
        }
```

生产级工具的关键在于 docstring 不只是给人类看的，更是给 LLM 看的。使用场景和不使用场景告诉 LLM 什么时候该用、什么时候不该用。参数描述告诉 LLM 参数的格式和约束。返回示例让 LLM 知道结果的格式。错误信息加建议让 LLM 在失败时能指导下一步行动。

---

## 二、Tool Schema 自动生成

手动写工具 schema 容易和实际代码不同步。用 Python 的 inspect 模块从函数签名和 docstring 自动提取，保证 schema 始终与函数定义一致：

```python
import inspect
import json
import re
from typing import get_type_hints, Any


def extract_tool_schema(func) -> dict:
    """
    从 Python 函数自动提取工具 schema

    提取内容：
    1. 函数名 -> tool name
    2. docstring -> description + parameters description
    3. 函数签名 -> parameters + types + defaults + required
    """
    sig = inspect.signature(func)
    docstring = inspect.getdoc(func) or ""
    hints = get_type_hints(func)

    description = _extract_description(docstring)

    properties = {}
    required = []

    for name, param in sig.parameters.items():
        if name in ("self", "cls"):
            continue

        param_info = _extract_param_info(name, param, hints, docstring)
        properties[name] = param_info["schema"]

        if param.default is inspect.Parameter.empty:
            required.append(name)

    return {
        "type": "function",
        "function": {
            "name": func.__name__,
            "description": description,
            "parameters": {
                "type": "object",
                "properties": properties,
                "required": required,
            },
        },
    }


def _extract_description(docstring: str) -> str:
    """从 docstring 提取主描述（第一行，不含参数部分）"""
    param_idx = docstring.find(":param")
    if param_idx > 0:
        return docstring[:param_idx].strip()
    return docstring.split("\n")[0].strip()


def _extract_param_info(name: str, param, hints: dict, docstring: str) -> dict:
    """提取单个参数的完整信息"""
    type_mapping = {
        str: "string",
        int: "integer",
        float: "number",
        bool: "boolean",
        list: "array",
        dict: "object",
    }
    param_type = hints.get(name, Any)
    json_type = type_mapping.get(param_type, "string")

    param_desc = _extract_param_description(docstring, name)

    schema = {"type": json_type}
    if param_desc:
        schema["description"] = param_desc

    if param.default is not inspect.Parameter.empty:
        schema["default"] = param.default

    return {"schema": schema, "required": param.default is inspect.Parameter.empty}


def _extract_param_description(docstring: str, param_name: str) -> str:
    """从 docstring 提取某个参数的描述"""
    pattern = rf":param\s+{param_name}:\s*(.+?)(?=\n:param|\n:return|$)"
    match = re.search(pattern, docstring, re.DOTALL)
    if match:
        return match.group(1).strip()
    return ""


# 使用示例

def analyze_data(
    data_source: str,
    metrics: list = None,
    group_by: str = None,
    output_format: str = "json",
) -> dict:
    """
    对数据源执行统计分析。

    使用场景：用户要求分析数据、计算统计指标、对比不同维度的数据。

    :param data_source: 数据源路径或标识符（如 "sales.csv" 或 "db:sales_table"）
    :param metrics: 要计算的指标列表，如 ["mean", "sum", "count"]。默认计算所有常用指标
    :param group_by: 分组字段，如 "region" 或 "month"。不指定则不分组
    :param output_format: 输出格式，"json"（默认）或 "csv"
    """
    pass


schema = extract_tool_schema(analyze_data)
print(json.dumps(schema, indent=2, ensure_ascii=False))
# 输出：
# {
#   "type": "function",
#   "function": {
#     "name": "analyze_data",
#     "description": "对数据源执行统计分析。",
#     "parameters": {
#       "type": "object",
#       "properties": {
#         "data_source": {
#           "type": "string",
#           "description": "数据源路径或标识符（如 \"sales.csv\" 或 \"db:sales_table\"）"
#         },
#         "metrics": {
#           "type": "array",
#           "description": "要计算的指标列表，如 [\"mean\", \"sum\", \"count\"]。默认计算所有常用指标"
#         },
#         "group_by": {
#           "type": "string",
#           "description": "分组字段，如 \"region\" 或 \"month\"。不指定则不分组"
#         },
#         "output_format": {
#           "type": "string",
#           "description": "输出格式，\"json\"（默认）或 \"csv\"",
#           "default": "json"
#         }
#       },
#       "required": ["data_source"]
#     }
#   }
# }
```

改了函数参数，schema 自动更新。改了 docstring，描述自动更新。工具有 10+ 个时这尤其重要——手动维护的 schema 一定会和实际代码不同步。

---

## 三、工具错误处理

工具一定会失败——API 超时、文件不存在、数据格式不对。重点不是"避免失败"，而是让 LLM 能从失败信息中指导下一步行动。

```python
from dataclasses import dataclass
from typing import Any


@dataclass
class ToolResponse:
    """工具响应——统一的成功/失败格式

    无论成功还是失败，都返回同样的结构。
    LLM 只需要检查 success 字段，就能判断是否需要采取行动。
    """
    success: bool
    data: Any = None
    error: str = ""
    suggestion: str = ""  # 关键：告诉 LLM 下一步该做什么

    def to_dict(self) -> dict:
        result = {"success": self.success}
        if self.success:
            result["data"] = self.data
        else:
            result["error"] = self.error
            if self.suggestion:
                result["suggestion"] = self.suggestion
        return result


class ErrorClassifier:
    """错误分类器——根据错误类型生成不同的 suggestion

    不同错误类型需要不同的修复建议。
    "文件不存在"和"权限不足"的修复方式完全不同。
    """

    @staticmethod
    def classify(error_type: str, context: dict = None) -> str:
        """
        根据错误类型生成建议

        error_type: 错误类型标识符
        context: 额外上下文（如文件路径、API 地址等）
        """
        suggestions = {
            "file_not_found": (
                f"请检查文件路径是否正确。"
                f"{'当前路径: ' + context['path'] if context and 'path' in context else ''}"
            ),
            "permission_denied": (
                "权限不足。请使用正确的凭证或联系管理员。"
            ),
            "timeout": (
                "操作超时。可能是网络问题或服务响应慢。"
                "建议：1) 重试一次 2) 检查网络连接 3) 尝试其他方法"
            ),
            "invalid_format": (
                f"数据格式不正确。期望格式: {context.get('expected', '未知')}。"
                f"请检查输入数据是否符合格式要求。"
            ),
            "api_error": (
                f"API 调用失败，状态码: {context.get('status_code', '未知')}。"
                f"请检查 API 凭证和请求参数。"
            ),
            "validation_error": (
                f"参数验证失败: {context.get('details', '未知')}。"
                f"请检查参数的格式和取值范围。"
            ),
            "unknown": (
                "发生了未知错误。请尝试其他方法或联系技术支持。"
            ),
        }
        return suggestions.get(error_type, suggestions["unknown"])


def read_file(path: str, encoding: str = "utf-8") -> dict:
    """
    读取文件内容。

    :param path: 文件路径
    :param encoding: 文件编码，默认 utf-8
    """
    import os

    if not path:
        return ToolResponse(
            success=False,
            error="文件路径不能为空",
            suggestion=ErrorClassifier.classify("validation_error", {"details": "path 为空"}),
        ).to_dict()

    if not os.path.exists(path):
        return ToolResponse(
            success=False,
            error=f"文件不存在: {path}",
            suggestion=ErrorClassifier.classify("file_not_found", {"path": path}),
        ).to_dict()

    try:
        with open(path, "r", encoding=encoding) as f:
            content = f.read()
        return ToolResponse(success=True, data={"content": content, "size": len(content)}).to_dict()
    except PermissionError:
        return ToolResponse(
            success=False,
            error=f"没有读取权限: {path}",
            suggestion=ErrorClassifier.classify("permission_denied"),
        ).to_dict()
    except UnicodeDecodeError:
        return ToolResponse(
            success=False,
            error=f"文件编码不匹配: 期望 {encoding}",
            suggestion=ErrorClassifier.classify(
                "invalid_format", {"expected": f"编码为 {encoding} 的文本文件"}
            ),
        ).to_dict()
    except Exception as e:
        return ToolResponse(
            success=False,
            error=f"读取文件时发生错误: {str(e)}",
            suggestion=ErrorClassifier.classify("unknown"),
        ).to_dict()
```

LLM 收到错误后，需要知道下一步该怎么做。如果错误信息只说"出错了"，LLM 只能盲目重试或放弃。如果错误信息说"文件不存在，请检查路径是否正确"，LLM 可以请求用户确认路径。suggestion 字段是错误处理里最重要的部分。

---

## 四、工具测试

工具写好了，怎么知道 LLM 能不能正确使用？需要专门的测试框架：

```python
"""
工具测试 —— 评估 LLM 能否正确使用工具

工具测试不是"函数单元测试"，而是"LLM 理解测试"。
你需要测试的不是工具本身能不能跑，而是 LLM 能不能：
1. 根据描述选对工具
2. 根据参数 schema 传对参数
3. 根据错误信息采取正确的修复行动
"""

import json


class ToolEvalSuite:
    """工具评估套件

    三类测试：
    1. 工具选择测试——给 LLM 一个场景，看它选不选得对工具
    2. 参数传递测试——给 LLM 一个工具，看它传不传得对参数
    3. 错误恢复测试——给 LLM 一个错误，看它能不能修复
    """

    def __init__(self, llm, tools: dict):
        self.llm = llm
        self.tools = tools

    def test_tool_selection(self, test_cases: list[dict]) -> dict:
        """测试一：工具选择

        每个用例：给 LLM 一个场景，看它选不选得对工具。

        test_cases: [
            {"scenario": "用户想知道北京今天的天气", "expected_tool": "get_weather"},
            {"scenario": "用户想读取 sales.csv 的内容", "expected_tool": "read_file"},
            ...
        ]

        返回：{"total": N, "correct": M, "accuracy": 0.X, "wrong_cases: [...]}
        """
        results = {"total": len(test_cases), "correct": 0, "wrong_cases": []}

        for case in test_cases:
            tool_list = "\n".join(
                f"- {name}: {tool.__doc__.split(chr(10))[0] if tool.__doc__ else '无描述'}"
                for name, tool in self.tools.items()
            )

            prompt = f"""
用户场景: {case['scenario']}

可用工具:
{tool_list}

请选择合适的工具。只返回工具名称，不要解释。
"""
            response = self.llm.generate(prompt).strip().lower()
            expected = case["expected_tool"].lower()

            if response == expected or expected in response:
                results["correct"] += 1
            else:
                results["wrong_cases"].append({
                    "scenario": case["scenario"],
                    "expected": case["expected_tool"],
                    "got": response,
                })

        results["accuracy"] = results["correct"] / results["total"] if results["total"] else 0
        return results

    def test_parameter_passing(self, test_cases: list[dict]) -> dict:
        """测试二：参数传递

        每个用例：给 LLM 一个工具和场景，看它传不传得对参数。

        test_cases: [
            {
                "tool": "get_weather",
                "scenario": "查询上海的天气，用华氏度",
                "expected_params": {"city": "上海", "unit": "fahrenheit"},
            },
            ...
        ]
        """
        results = {"total": len(test_cases), "correct": 0, "wrong_cases": []}

        for case in test_cases:
            tool = self.tools[case["tool"]]
            schema = extract_tool_schema(tool)

            prompt = f"""
用户场景: {case['scenario']}

工具定义: {json.dumps(schema, ensure_ascii=False, indent=2)}

请调用合适的工具并传入参数。返回 JSON 格式:
{{"tool": "工具名", "parameters": {{参数}}}}
"""
            response = self.llm.generate(prompt)
            try:
                data = json.loads(response)
                params = data.get("parameters", {})

                all_correct = True
                for key, expected_val in case["expected_params"].items():
                    if params.get(key) != expected_val:
                        all_correct = False
                        results["wrong_cases"].append({
                            "scenario": case["scenario"],
                            "param": key,
                            "expected": expected_val,
                            "got": params.get(key),
                        })
                        break

                if all_correct:
                    results["correct"] += 1
            except (json.JSONDecodeError, KeyError):
                results["wrong_cases"].append({
                    "scenario": case["scenario"],
                    "error": "LLM 返回格式不正确",
                    "response": response[:200],
                })

        results["accuracy"] = results["correct"] / results["total"] if results["total"] else 0
        return results

    def test_error_recovery(self, test_cases: list[dict]) -> dict:
        """测试三：错误恢复

        每个用例：给 LLM 一个工具调用结果（错误），看它能不能采取正确的修复行动。

        test_cases: [
            {
                "tool_call": {"tool": "read_file", "params": {"path": "missing.csv"}},
                "error_response": {"success": false, "error": "文件不存在: missing.csv",
                                   "suggestion": "请检查文件路径是否正确。"},
                "expected_action": "询问用户确认文件路径",
            },
            ...
        ]
        """
        results = {"total": len(test_cases), "correct": 0, "wrong_cases": []}

        for case in test_cases:
            prompt = f"""
你调用了工具：
工具: {case['tool_call']['tool']}
参数: {case['tool_call']['params']}

工具返回：
{json.dumps(case['error_response'], ensure_ascii=False)}

你应该采取什么行动？请简要描述。
"""
            response = self.llm.generate(prompt).lower()
            expected = case["expected_action"].lower()

            if any(kw in response for kw in expected.split()):
                results["correct"] += 1
            else:
                results["wrong_cases"].append({
                    "tool_call": case["tool_call"],
                    "expected_action": case["expected_action"],
                    "got_action": response[:200],
                })

        results["accuracy"] = results["correct"] / results["total"] if results["total"] else 0
        return results


def evaluate_tools(llm, tools: dict) -> dict:
    """运行完整评估，返回报告"""
    suite = ToolEvalSuite(llm, tools)

    selection_results = suite.test_tool_selection([
        {"scenario": "用户想知道北京今天天气", "expected_tool": "get_weather"},
        {"scenario": "用户想读取 sales.csv", "expected_tool": "read_file"},
        {"scenario": "用户想分析销售数据", "expected_tool": "analyze_data"},
    ])

    param_results = suite.test_parameter_passing([
        {
            "tool": "get_weather",
            "scenario": "查询上海天气，用华氏度",
            "expected_params": {"city": "上海", "unit": "fahrenheit"},
        },
    ])

    return {
        "tool_selection": selection_results,
        "parameter_passing": param_results,
    }
```

工具测试测的不只是函数能不能跑，而是 LLM 能不能理解和使用这个函数。一个在人类看来"描述清楚"的工具，在 LLM 看来可能"完全不知道什么时候用"。这是 Agent 工程里最容易被忽视但最重要的质量保障环节。

---

## 五、工具组合模式

原子工具是好的起点，但实际场景中 LLM 经常需要组合多个工具来完成复杂任务。三种常见模式：

```python
# 模式一：管道模式

class ToolPipeline:
    """管道模式——多个工具串联执行

    适用场景：步骤固定、顺序固定的操作链。
    优点：LLM 只需要调用一次，不需要管理中间步骤。
    缺点：灵活性低，中间步骤失败整个管道终止。
    """

    def __init__(self, steps: list[dict], name: str = "", description: str = ""):
        """
        steps: [
            {"tool": "read_csv", "args_mapper": lambda x: {"path": x}},
            {"tool": "clean_data", "args_mapper": lambda x: {"data": x}},
            {"tool": "analyze", "args_mapper": lambda x: {"data": x}},
        ]
        """
        self.steps = steps
        self.name = name
        self.description = description
        self._docstring = description

    def execute(self, tool_registry, initial_input) -> dict:
        current = initial_input
        history = []

        for step in self.steps:
            args = step["args_mapper"](current)
            result = tool_registry.execute(step["tool"], **args)

            history.append({"step": step["tool"], "success": result.success, "data": result.data})

            if not result.success:
                return {
                    "success": False,
                    "error": f"管道在 '{step['tool']}' 步骤失败",
                    "history": history,
                    "suggestion": f"检查 '{step['tool']}' 的输入数据是否正确",
                }

            current = result.data

        return {"success": True, "data": current, "history": history}


# 模式二：分支模式

class ToolBranch:
    """分支模式——根据输入条件选择不同的工具

    适用场景：不同类型的输入需要不同的处理工具。
    优点：LLM 只需要调用一个入口工具，自动路由到正确的处理工具。
    缺点：路由逻辑内嵌在工具中，LLM 不了解内部决策过程。
    """

    def __init__(self, name: str, description: str, routes: dict):
        """
        routes: {
            "csv": {"tool": "read_csv", "condition": lambda x: x.endswith('.csv')},
            "excel": {"tool": "read_excel", "condition": lambda x: x.endswith(('.xlsx', '.xls'))},
            "json": {"tool": "read_json", "condition": lambda x: x.endswith('.json')},
        }
        """
        self.name = name
        self.description = description
        self.routes = routes

    def execute(self, tool_registry, path: str) -> dict:
        for route_name, route_info in self.routes.items():
            if route_info["condition"](path):
                return tool_registry.execute(route_info["tool"], path=path)

        return {
            "success": False,
            "error": f"不支持的文件类型: {path}",
            "suggestion": f"支持的文件类型: {', '.join(self.routes.keys())}",
        }


# 模式三：聚合模式

class ToolAggregator:
    """聚合模式——并行调用多个工具，合并结果

    适用场景：需要多角度获取信息的场景。
    优点：一次调用获取多方面信息。
    缺点：所有工具都成功才算成功，一个失败可能影响整体质量。
    """

    def __init__(self, name: str, description: str, tools: list[dict]):
        """
        tools: [
            {"tool": "get_current_weather", "args": {"city": "{{city}}"}},
            {"tool": "get_weather_forecast", "args": {"city": "{{city}}"}},
            {"tool": "get_air_quality", "args": {"city": "{{city}}"}},
        ]
        """
        self.name = name
        self.description = description
        self.tools = tools

    def execute(self, tool_registry, **kwargs) -> dict:
        results = {}
        errors = []

        for tool_info in self.tools:
            args = {}
            for key, value in tool_info["args"].items():
                args[key] = value.replace("{{", "").replace("}}", "")
                if args[key] in kwargs:
                    args[key] = kwargs[args[key]]

            result = tool_registry.execute(tool_info["tool"], **args)
            if result.success:
                results[tool_info["tool"]] = result.data
            else:
                errors.append({"tool": tool_info["tool"], "error": result.error})

        return {
            "success": len(errors) == 0,
            "data": results,
            "partial_errors": errors if errors else None,
            "suggestion": (
                f"部分工具执行失败: {', '.join(e['tool'] for e in errors)}"
                if errors else ""
            ),
        }
```

什么时候用原子工具，什么时候用复合工具？看使用频率和复杂度。如果某个工具组合（"读取 -> 清洗 -> 分析"）被调用的频率很高，封装成复合工具可以减少 LLM 的调用次数和出错概率。但如果组合内部逻辑很复杂（比如需要根据中间结果分支），保留原子工具让 LLM 自己组合更灵活。

---

## 六、实践中的几个问题

### 工具描述到底应该多长？

不是越长越好，也不是越短越好——看信息密度。

```
# 太短（信息不足）：
"分析数据"——LLM 不知道分析什么、怎么分析、返回什么

# 刚好（信息密度高）：
"对数值型数据执行描述性统计分析，返回均值、标准差、最大最小值、四分位数。
 不适用于文本数据或时间序列。"

# 太长（信息稀释）：
"分析数据。这个工具可以对数据进行各种各样的统计分析，
 包括但不限于均值、中位数、众数、标准差、方差、偏度、峰度、
 四分位数、百分位数等。还可以进行分组分析、时间序列分析、
 相关性分析、回归分析等等。使用时需要注意数据的质量……"
（后面还有 500 字——LLM 的注意力已经散了）
```

经验法则：工具描述 1-3 句话，包含"做什么"+"适用场景"+"不适用场景"+"返回格式"。

### 一个 Agent 应该有多少个工具？

| 工具数量 | 效果 | 建议 |
|---------|------|------|
| 1-3 个 | LLM 选择准确 | 适合简单任务 |
| 4-8 个 | LLM 选择良好 | 大多数场景的甜点 |
| 9-15 个 | LLM 选择开始下降 | 需要工具分组或按需加载 |
| 16+ 个 | LLM 选择准确率明显下降 | 需要分层注入、工具搜索、或复合工具 |

优先把工具控制在 10 个以内。如果超过 10 个，考虑用复合工具减少数量，或用条件注入按需加载。

### 工具设计和 API 设计有什么区别？

| 维度 | API 设计 | Agent 工具设计 |
|------|---------|---------------|
| 调用者 | 人类开发者（可以看文档、试参数、调试） | LLM（只能看描述和 schema） |
| 错误处理 | 抛异常、HTTP 状态码 | 返回结构化错误 + suggestion |
| 文档 | OpenAPI/Swagger（人类可读） | docstring + schema（LLM 可读） |
| 参数 | 可以省略、可以有默认值 | 必填参数必须明确，默认值要合理 |
| 测试 | 单元测试（输入->输出） | LLM 理解测试（LLM 是否能选对、传对、处理错误） |

什么时候算过度设计？工具只有 1-2 个时不需要自动生成 schema 和评估套件；工具只给人类开发者用时不需要 LLM 理解导向的设计；工具是内部使用不需要错误 suggestion。

---

## 七、框架中的实际应用

| 框架/协议 | 工具设计实践 | 说明 |
|-----------|-------------|------|
| **MCP (Model Context Protocol)** | 标准化协议——定义工具接口、参数、错误格式 | Anthropic 推动的开放协议，统一工具描述和调用方式 |
| **OpenAI Function Calling** | JSON Schema 定义函数签名 | SDK 自动处理函数调用->执行->返回的循环 |
| **Anthropic Tool Use** | 工具描述直接注入 prompt | Claude 原生支持，不需要额外协议 |
| **LangChain Tools** | `@tool` 装饰器自动生成 schema | 从 docstring 提取描述，但需要手动补充使用场景 |
| **Google ADK** | Tool 作为 Agent 的一等公民 | ADK 中每个 Agent 有一组绑定的工具 |

---

## 八、写在最后

工具设计是 Agent 工程中最被低估的环节。大多数人关注 Agent 架构、执行模式、prompt 工程，但忽略了工具才是 LLM 真正"动手做事"的接口。一个设计精良的工具，能让简单的 ReAct 循环表现优异；一个设计粗糙的工具，再复杂的架构也跑不起来。

记住四原则：**单一职责、自描述、容错输出、可测试**。写好工具之后，再考虑工具组合（管道、分支、聚合）和工具评估（LLM 能否选对、传对、处理错误）。
