Agent_07_Tool设计
前面的专题讲了 Agent 架构、执行模式、上下文管理。这些都围绕一个问题:LLM 怎么和外部世界交互?答案是工具。
大多数 Agent 项目的问题不是"没给工具",而是"工具设计得不好"。Anthropic 总结过一句话——Agent 工具的质量,比 Agent 架构的质量更重要。一个设计精良的工具配合简单循环,胜过复杂架构配上设计粗糙的工具。
工具设计不是"写函数",而是"写 LLM 能理解和正确使用的函数"。人类调用函数时可以看文档、试参数、调试错误,LLM 只能靠工具名、描述和 schema 来决定怎么用。工具对 LLM 来说必须是自解释的。
一、好工具 vs 坏工具
先看同一个功能的三种写法。场景是查询天气:
# 写法一:坏工具
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 始终与函数定义一致:
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 能从失败信息中指导下一步行动。
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 能不能正确使用?需要专门的测试框架:
"""
工具测试 —— 评估 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 经常需要组合多个工具来完成复杂任务。三种常见模式:
# 模式一:管道模式
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 选择准确率明显下降 | 需要分层注入、工具搜索、或复合工具 |
工具设计和 API 设计有什么区别?
| 维度 | API 设计 | Agent 工具设计 |
|---|---|---|
| 调用者 | 人类开发者(可以看文档、试参数、调试) | LLM(只能看描述和 schema) |
| 错误处理 | 抛异常、HTTP 状态码 | 返回结构化错误 + suggestion |
| 文档 | OpenAPI/Swagger(人类可读) | docstring + schema(LLM 可读) |
| 参数 | 可以省略、可以有默认值 | 必填参数必须明确,默认值要合理 |
| 测试 | 单元测试(输入->输出) | LLM 理解测试(LLM 是否能选对、传对、处理错误) |
七、框架中的实际应用
| 框架/协议 | 工具设计实践 | 说明 |
|---|---|---|
| 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 能否选对、传对、处理错误)。