🛠️ 自定义工具开发

自定义工具是 Agent 能力的扩展点。无论是对接企业内部 API、操作数据库,还是执行特定的业务逻辑,都需要遵循一套规范化的开发流程,确保工具的安全性、可靠性和可维护性。

🏗️ 开发模式

根据工具复杂度选择合适的开发模式:

模式描述适用场景示例
函数装饰器 用装饰器标记函数,自动提取 Schema 简单函数、快速原型 LangChain @tool、Python 类型注解
类继承 继承 BaseTool 基类,实现 _run 方法 有状态工具、复杂逻辑 LangChain BaseTool、OpenAI function 封装
配置驱动 YAML/JSON 定义 Schema + 实现函数分离 需要非开发者定义工具 企业工具市场、低代码平台
MCP Server 实现 MCP 协议的标准化工具服务 跨平台复用、社区分享 MCP Python/TS SDK

示例:三种方式实现天气查询工具

方式一:函数装饰器(LangChain)

from langchain.tools import tool
from pydantic import BaseModel, Field

class WeatherInput(BaseModel):
    city: str = Field(description="城市名称")
    unit: str = Field(default="celsius", description="温度单位")

@tool(args_schema=WeatherInput)
def get_weather(city: str, unit: str = "celsius") -> str:
    """获取指定城市的天气信息"""
    # 实际调用天气 API
    return f"{city}天气:晴,28{unit}"

# 自动生成 Schema,直接可用
print(get_weather.name)        # "get_weather"
print(get_weather.description) # "获取指定城市的天气信息"
print(get_weather.args)        # WeatherInput schema

方式二:MCP Server(Python SDK)

from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationCapabilities
from mcp.types import Tool, TextContent
import mcp.server.stdio

server = Server("weather-server")

@server.list_tools()
async def handle_list_tools() -> list[Tool]:
    return [
        Tool(
            name="get_weather",
            description="获取指定城市的天气信息",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称"},
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "温度单位"
                    }
                },
                "required": ["city"]
            }
        )
    ]

@server.call_tool()
async def handle_call_tool(name: str, args: dict) -> list[TextContent]:
    if name == "get_weather":
        city = args["city"]
        unit = args.get("unit", "celsius")
        # 实际调用天气 API
        return [TextContent(type="text", text=f"{city}天气:晴,28{unit}")]
    raise ValueError(f"Unknown tool: {name}")

async def main():
    async with mcp.server.stdio.stdio_server() as (r, w):
        await server.run(r, w, InitializationCapabilities(...))

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

🛡️ 安全最佳实践

自定义工具是 Agent 的"手和脚",也是最容易出安全问题的地方。

安全清单

1
输入验证与清洗

永远不要信任 LLM 传来的参数。对字符串做长度限制、格式校验,对数值做范围检查。防止 SQL 注入、命令注入、路径遍历。

2
最小权限原则

工具只授予完成其功能所需的最小系统权限。文件操作限制目录白名单,数据库操作使用只读账户,API 调用使用最小 Scope 的 Token。

3
敏感信息保护

不要将 API Key、密码、Token 等敏感信息硬编码在工具代码中。使用环境变量或密钥管理服务(如 AWS Secrets Manager)。不要在工具返回结果或日志中暴露敏感数据。

4
沙箱执行

对于执行代码、Shell 命令等高风险工具,在独立沙箱中运行(Docker 容器、gVisor、Firecracker microVM)。限制 CPU/内存/磁盘/网络资源。

5
用户确认机制

对不可逆的高风险操作(删除数据、发送消息、执行支付),必须经过用户二次确认。实现 "human-in-the-loop" 审批流程。

6
速率限制

对工具调用频率进行限制,防止 LLM 陷入循环或遭遇滥用。按工具、用户、IP 等维度设置不同的限流策略。

常见安全漏洞及防护

漏洞类型攻击示例防护措施
命令注入 LLM 传入 city="北京; rm -rf /" ✅ 避免使用 os.system/subprocess;必须使用时做参数白名单校验
路径遍历 LLM 传入 file="../../../etc/passwd" ✅ 规范化路径后检查是否在白名单目录内
Prompt 注入 LLM 输出中包含恶意指令,通过工具参数二次注入 ✅ 工具结果与 system prompt 隔离;使用独立的消息结构
信息泄露 工具返回结果包含数据库连接串、内部 IP ✅ 工具输出做脱敏过滤;日志中敏感字段打码
DoS 攻击 LLM 反复调用耗时工具或传入超大参数 ✅ 设置超时、最大输入大小、调用频率限制

🧪 测试方法

测试金字塔

E2E 测试
LLM + 工具集成测试(少量)
集成测试
工具 + 真实依赖(适中)
单元测试
工具函数逻辑(大量)

各层测试要点

测试层测试内容工具/方法
单元测试 Schema 校验、参数解析、边界值、异常处理、幂等性 pytest、unittest
集成测试 对接真实 API/数据库,验证请求响应格式、超时处理 pytest + fixtures、WireMock(API mock)
E2E 测试 LLM 在真实对话中正确选择并调用工具,验证输出质量 手动测试 + 自动化评测脚本
安全测试 输入注入、权限绕过、敏感信息泄露 手动渗透测试 + 自动化安全扫描
性能测试 并发调用压力、大数据量处理、内存泄漏 locust、k6

📋 完整开发流程

1
需求分析

明确工具的业务目的、输入输出、约束条件。回答:解决了什么问题?LLM 在什么场景下会用到它?

2
Schema 设计

定义工具名称、描述、参数 JSON Schema。写好 description(这是 LLM 理解工具用途的关键),设置合理的 required 字段。

3
实现核心逻辑

编写处理函数,进行输入验证,调用底层服务,格式化返回结果。遵循 SOLID 原则,保持函数职责单一。

4
安全加固

对照安全清单逐项检查:参数校验、权限控制、敏感信息处理、速率限制、沙箱隔离。

5
编写测试

单元测试覆盖正常/异常/边界场景,集成测试验证真实依赖,安全测试覆盖注入和越权场景。

6
注册与配置

将工具注册到工具注册中心(如果是 MCP Server 则配置到 Host),设置权限、速率限制、超时等运行参数。

7
监控与迭代

上线后持续监控工具调用成功率、延迟、错误分布。根据实际使用反馈优化 Schema 描述和实现。

💡 开发建议
  • 先跑通再优化:用最简实现验证工具思路正确,再完善错误处理和性能
  • 优先使用 MCP:新工具建议按 MCP 协议实现,获得跨平台复用能力
  • 注重返回格式:工具的返回值是 LLM 理解的唯一依据,返回结构化、信息密度高的结果
  • 错误信息友好:工具出错时返回人类可读的错误描述,帮助 LLM 做出正确的后续决策
  • 版本兼容:Schema 变更时尽量向后兼容,大版本升级提供迁移窗口