9.3.5 常见工具集成
- 认识 Agent 中最常见的几类工具
- 理解每类工具分别适合解决什么问题
- 看懂一个统一工具注册与调度示例
- 理解工具集成时最常见的失败点和工程注意事项
为什么要把工具分类型来看?
Section titled “为什么要把工具分类型来看?”因为“工具”这个词太宽了
Section titled “因为“工具”这个词太宽了”搜索是工具,计算器是工具,数据库查询是工具,文件读写也是工具。 如果一股脑都看成“一个函数”,你很快就会混乱。
更实用的做法是先分几类:
- 检索类
- 计算类
- 数据访问类
- 文件 / 环境操作类
- 外部服务调用类
为什么分类有帮助?
Section titled “为什么分类有帮助?”因为不同类型工具的关注点不同:
- 搜索类看召回质量
- 计算类看精确性和安全
- 数据库类看权限和过滤
- 文件类看路径边界
- 外部服务类看超时和重试
也就是说:
不同工具虽然都叫工具,但工程风险完全不一样。
最常见的五类工具
Section titled “最常见的五类工具”搜索 / 检索类
Section titled “搜索 / 检索类”适合:
- 查文档
- 查知识库
- 查网页
特点:
- 输入通常是 查询
- 输出通常是一组候选结果
适合:
- 四则运算
- 统计指标
- 小型数据转换
特点:
- 输出必须稳定精确
- 安全风险要格外小心
适合:
- 查数据库
- 查订单
- 查用户状态
特点:
- 参数和权限最关键
- 很多业务逻辑在这一层决定
文件 / 环境操作类
Section titled “文件 / 环境操作类”适合:
- 读文件
- 写文件
- 列目录
- 执行代码
特点:
- 风险高
- 边界控制极其重要
外部服务调用类
Section titled “外部服务调用类”适合:
- 发邮件
- 调第三方 API
- 提交工单
特点:
- 失败率、超时、重试都很常见
一个统一的工具注册表
Section titled “一个统一的工具注册表”真实系统里,常常不会把工具散落在各处,而是统一注册。
最小可运行示例
Section titled “最小可运行示例”import astimport operator
OPS = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv,}
def safe_calculate(expression): def visit(node): if isinstance(node, ast.Expression): return visit(node.body) if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): return node.value if isinstance(node, ast.BinOp) and type(node.op) in OPS: return OPS[type(node.op)](visit(node.left), visit(node.right)) if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): return -visit(node.operand) raise ValueError("unsupported_expression")
return visit(ast.parse(expression, mode="eval"))
def search_docs(keyword): docs = { "退款": "课程购买后 7 天内可申请退款", "证书": "完成项目并通过测试后可获得证书" } return docs.get(keyword, "未找到相关文档")
def calculator(expression): return safe_calculate(expression)
def get_user_status(user_id): mock_db = { 1: {"name": "Alice", "progress": 0.15}, 2: {"name": "Bob", "progress": 0.35} } return mock_db.get(user_id, {"error": "user_not_found"})
TOOLS = { "search_docs": search_docs, "calculator": calculator, "get_user_status": get_user_status}
print(TOOLS.keys())预期输出:
dict_keys(['search_docs', 'calculator', 'get_user_status'])为什么统一注册很重要?
Section titled “为什么统一注册很重要?”因为后面你会需要:
- 统一描述 结构约束
- 统一做权限控制
- 统一打日志
- 统一调度和统计
如果工具没有注册表,系统会越来越难维护。
一个统一调度器
Section titled “一个统一调度器”最小调度器示例
Section titled “最小调度器示例”def dispatch(call): name = call["name"] arguments = call["arguments"]
if name not in TOOLS: return {"error": "unknown_tool"}
try: result = TOOLS[name](**arguments) return {"result": result} except Exception as e: return {"error": str(e)}
calls = [ {"name": "search_docs", "arguments": {"keyword": "退款"}}, {"name": "calculator", "arguments": {"expression": "12 * 7"}}, {"name": "get_user_status", "arguments": {"user_id": 1}}]
for call in calls: print(call, "->", dispatch(call))预期输出:
{'name': 'search_docs', 'arguments': {'keyword': '退款'}} -> {'result': '课程购买后 7 天内可申请退款'}{'name': 'calculator', 'arguments': {'expression': '12 * 7'}} -> {'result': 84}{'name': 'get_user_status', 'arguments': {'user_id': 1}} -> {'result': {'name': 'Alice', 'progress': 0.15}}这段代码教会你什么?
Section titled “这段代码教会你什么?”它教会你:
- 不同工具可以共享统一调用入口
- 程序端可以统一做错误处理
- 后面要扩工具时,结构也不会乱
不同类型工具到底要注意什么?
Section titled “不同类型工具到底要注意什么?”重点关注:
- 查询 是否改写
- 返回多少条结果
- 结果是否要 rerank
重点关注:
- 安全
- 精度
- 表达式是否合法
一个简单的安全计算器示例:
import astimport operator
OPS = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv,}
def safe_calculate(expression): def visit(node): if isinstance(node, ast.Expression): return visit(node.body) if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): return node.value if isinstance(node, ast.BinOp) and type(node.op) in OPS: return OPS[type(node.op)](visit(node.left), visit(node.right)) if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): return -visit(node.operand) raise ValueError("unsupported_expression")
return visit(ast.parse(expression, mode="eval"))
def safe_calculator(expression): allowed = set("0123456789+-*/(). ") if not set(expression) <= allowed: return {"error": "invalid_expression"} return {"result": safe_calculate(expression)}
print(safe_calculator("3 * (4 + 5)"))print(safe_calculator("__import__('os').system('rm -rf /')"))预期输出:
{'result': 27}{'error': 'invalid_expression'}数据库类工具
Section titled “数据库类工具”重点关注:
- 权限
- 参数完整性
- 查询边界
例如,不要让模型随意生成任意 SQL 再直接执行。
重点关注:
- 路径白名单
- 写入权限
- 是否需要人工确认
外部服务类工具
Section titled “外部服务类工具”重点关注:
- 超时
- 重试
- 幂等性
一个更贴近 Agent 的工具组合例子
Section titled “一个更贴近 Agent 的工具组合例子”场景:判断用户能不能退款
Section titled “场景:判断用户能不能退款”这件事可能需要两个工具:
- 查用户学习进度
- 查退款政策
def refund_eligibility_agent(user_id): status = get_user_status(user_id) if "error" in status: return {"error": "用户不存在"}
policy = search_docs("退款") progress = status["progress"]
can_refund = progress < 0.2 return { "user": status["name"], "progress": progress, "policy": policy, "can_refund": can_refund }
print(refund_eligibility_agent(1))print(refund_eligibility_agent(2))预期输出:
{'user': 'Alice', 'progress': 0.15, 'policy': '课程购买后 7 天内可申请退款', 'can_refund': True}{'user': 'Bob', 'progress': 0.35, 'policy': '课程购买后 7 天内可申请退款', 'can_refund': False}
这段代码真正说明了什么?
Section titled “这段代码真正说明了什么?”它说明:
工具集成不是每个工具单独存在,而是经常要协同完成一个目标。
这也是为什么后面 Agent 会越来越依赖工具编排能力。
工具集成最常见的失败点
Section titled “工具集成最常见的失败点”结构约束 对不上
Section titled “结构约束 对不上”例如:
- 工具需要
user_id - 模型却传了
id
返回值格式不统一
Section titled “返回值格式不统一”如果有的工具返回字符串,有的返回 dict,有的返回 list,系统会越来越难接。
没有统一错误处理
Section titled “没有统一错误处理”一个工具返回 None,另一个抛异常,第三个返回 "failed",后面逻辑很容易乱。
没有日志和回放
Section titled “没有日志和回放”线上一出错,就很难知道到底是哪类工具出了问题。
一个实用建议:统一工具返回格式
Section titled “一个实用建议:统一工具返回格式”最稳妥的做法之一是统一工具输出结构,例如都返回:
{ "ok": True, "data": ...}或者:
{ "ok": False, "error": ...}一个小示例:
def wrapped_search(keyword): try: result = search_docs(keyword) return {"ok": True, "data": result} except Exception as e: return {"ok": False, "error": str(e)}
print(wrapped_search("退款"))预期输出:
{'ok': True, 'data': '课程购买后 7 天内可申请退款'}这会让后面 Agent 层更容易做统一判断。
初学者最常踩的坑
Section titled “初学者最常踩的坑”把所有工具都接进来再说
Section titled “把所有工具都接进来再说”工具越多,系统越复杂。 更稳妥的做法是:
- 先接最刚需的 2~3 个
不区分高风险工具和低风险工具
Section titled “不区分高风险工具和低风险工具”文件删除、支付操作、数据库写入,和搜索文档不是一个风险等级。
工具接口没有统一约定
Section titled “工具接口没有统一约定”这是很多 Agent 系统越做越乱的直接原因。
学完这一页,至少保留这张证据卡:
- 工具契约
- 名称、描述、输入 schema、输出 schema
- 权限
- 工具允许读取或修改的内容
- 调用轨迹
- 参数、结果、错误、重试或回退
- 失败检查
- 错误的工具、参数不当、不安全操作,或缺少观察结果
- 安全动作
- 验证、确认、沙箱、限流,或回滚
这一节最重要的不是背“有哪些工具”,而是理解:
常见工具集成的关键,不只是把工具接进来,而是把它们用统一接口、统一错误处理、统一边界约束组织起来。
只有这样,工具层才会成为 Agent 的能力放大器,而不是故障制造器。
- 给本节工具注册表再加一个
get_weather(city)工具。 - 把所有工具的返回值统一成
{"ok": ..., "data": ..., "error": ...}格式。 - 想一想:为什么数据库写入工具和搜索工具不应该放在同一个权限等级?
- 用自己的话解释:为什么说工具注册表和统一调度器是 Agent 工程里非常重要的两个结构?
参考实现与讲解
get_weather(city)应放进 registry,并带上 schema、risk level、timeout 和统一返回格式。- 统一
{ok, data, error}会让下游逻辑更简单:成功时读data,失败时根据error分支,不需要解析自然语言。 - 数据库写入工具会改变记录,所以比搜索工具需要更强的权限、确认和回滚规则。
- registry 提供工具元数据的唯一来源;dispatcher 集中处理 validation、permission check、retry、logging 和 error handling。