AI Agent的工具设计原则:让LLM能用好你的函数

3 阅读1分钟

工具调用是Agent能力的核心放大器

一个没有工具的LLM,只能生成文本。给它配上工具,它能:查询数据库、调用API、读写文件、发送邮件、执行代码——本质上,工具让LLM从"说话者"变成了"行动者"

但工具设计是门手艺。同样的功能,工具设计得好,Agent能精准调用;设计得差,Agent要么乱用,要么根本不知道该调哪个。

本文总结一套经过大量实践验证的工具设计原则,帮你的Agent变得更可靠。


原则一:单一职责,不要瑞士军刀

坏的设计

# 一个函数做太多事——LLM不知道什么时候该用它
@tool
def manage_database(operation: str, table: str, data: dict = None, 
                    filter: dict = None, order_by: str = None):
    """
    数据库操作:支持增删改查,operation可以是
    'insert', 'update', 'delete', 'select', 'count', 'aggregate'
    """
    if operation == 'select':
        ...
    elif operation == 'insert':
        ...
    # 更多逻辑...

好的设计

@tool
def query_users(filter_criteria: dict, limit: int = 20) -> list[dict]:
    """
    查询用户列表。
    
    filter_criteria示例:
    - {"status": "active"} 查询活跃用户
    - {"age_min": 18, "city": "北京"} 查询北京18岁以上用户
    """
    ...

@tool
def create_user(name: str, email: str, role: str = "user") -> dict:
    """创建新用户账户,返回创建的用户信息(含用户ID)"""
    ...

@tool
def update_user_status(user_id: str, new_status: str) -> bool:
    """更新指定用户的状态,status可以是active/suspended/deleted"""
    ...

为什么:LLM在选工具时做的是语义匹配。工具描述越聚焦,LLM越容易判断该调哪个、不该调哪个。


原则二:描述是工具的说明书,要写给LLM看

工具的描述(docstring/description)是LLM决定是否调用这个工具的唯一依据。要写得清晰、具体、包含使用示例:

# 不好的描述
@tool
def send_notification(user_id: str, message: str):
    """发送通知"""  # 太简略!
    ...

# 好的描述
@tool
def send_email_notification(
    user_id: str,
    subject: str,
    message: str,
    priority: str = "normal"
) -> dict:
    """
    向指定用户发送邮件通知。
    
    Args:
        user_id: 接收通知的用户ID(如 "user_123")
        subject: 邮件主题(50字以内)
        message: 邮件正文(支持Markdown格式)
        priority: 优先级,可选 "normal"(普通)或 "urgent"(紧急,会触发短信备用)
    
    Returns:
        dict: {"success": bool, "message_id": str, "delivered_at": str}
    
    使用场景:
    - 用户完成重要操作后发确认邮件
    - 系统异常需要通知用户时
    - 定时提醒类通知
    
    注意:不适合发送营销邮件,仅用于事务性通知
    """
    ...

原则三:参数类型明确,善用枚举

LLM生成参数时容易产生"创意"——它可能传"YES"而不是True,传"北京市"而不是"beijing"。明确的类型定义和枚举能大幅减少这种错误:

from enum import Enum
from pydantic import BaseModel, Field
from typing import Literal

class NotificationChannel(str, Enum):
    EMAIL = "email"
    SMS = "sms"
    PUSH = "push"
    WECHAT = "wechat"

class SendNotificationInput(BaseModel):
    user_id: str = Field(description="用户ID,格式为 'user_' 加数字,如 user_123")
    channel: NotificationChannel = Field(
        description="通知渠道:email(邮件)/sms(短信)/push(推送)/wechat(微信)"
    )
    message: str = Field(max_length=500, description="通知内容,最多500字")
    urgent: bool = Field(default=False, description="是否紧急消息,true时会立即发送")

@tool(args_schema=SendNotificationInput)
def send_notification(user_id: str, channel: str, message: str, urgent: bool = False):
    """向用户发送通知"""
    ...

原则四:返回结构化、信息丰富的结果

工具的返回值是LLM下一步推理的输入。返回值要:

  1. 明确说明成功/失败
  2. 在失败时给出可操作的错误信息
  3. 包含LLM可能需要的关联信息
# 不好的返回
@tool
def create_order(product_id: str, quantity: int) -> str:
    try:
        order = db.create_order(product_id, quantity)
        return "成功"  # LLM不知道订单ID,无法后续操作
    except Exception as e:
        return "失败"  # LLM不知道为什么失败,无法处理

# 好的返回
@tool
def create_order(product_id: str, quantity: int) -> dict:
    """创建订单,返回订单详情"""
    try:
        # 先验证库存
        stock = db.get_stock(product_id)
        if stock < quantity:
            return {
                "success": False,
                "error_code": "INSUFFICIENT_STOCK",
                "error_message": f"库存不足:当前库存{stock}件,需要{quantity}件",
                "suggestion": "可以减少购买数量,或查询其他可用商品"
            }
        
        order = db.create_order(product_id, quantity)
        return {
            "success": True,
            "order_id": order.id,
            "order_number": order.number,  # 人类可读的订单号
            "total_amount": order.total,
            "estimated_delivery": order.eta,
            "next_steps": ["可调用 get_order_status 查询订单状态", 
                          "可调用 cancel_order 取消订单"]
        }
    except Exception as e:
        return {
            "success": False,
            "error_code": "SYSTEM_ERROR",
            "error_message": str(e),
            "suggestion": "请稍后重试,或联系客服"
        }

原则五:幂等性设计

Agent可能因为种种原因重试工具调用(超时、误判等)。写操作工具应该尽可能是幂等的

@tool
def send_welcome_email(user_id: str) -> dict:
    """向新用户发送欢迎邮件(幂等:同一用户只发一次)"""
    
    # 检查是否已发送
    if db.email_log.exists(user_id=user_id, type="welcome"):
        return {
            "success": True,
            "skipped": True,
            "reason": "欢迎邮件已于之前发送,避免重复发送"
        }
    
    # 发送邮件
    email_service.send(
        to=db.get_user_email(user_id),
        template="welcome"
    )
    
    # 记录日志
    db.email_log.insert(user_id=user_id, type="welcome")
    
    return {
        "success": True,
        "skipped": False,
        "sent_at": datetime.now().isoformat()
    }

原则六:危险操作需要二次确认机制

删除、支付等不可逆操作,应该设计两步式工具:

@tool
def prepare_delete_user(user_id: str) -> dict:
    """
    【第一步】准备删除用户账号,返回确认码和影响分析。
    需要用户(或后续流程)提供确认码后,才能执行实际删除。
    """
    user = db.get_user(user_id)
    if not user:
        return {"success": False, "error": "用户不存在"}
    
    # 分析删除影响
    orders = db.count_orders(user_id)
    files = db.count_files(user_id)
    confirmation_code = generate_confirmation_code()
    
    # 将确认码存储(5分钟有效)
    cache.set(f"delete_confirm_{user_id}", confirmation_code, ttl=300)
    
    return {
        "success": True,
        "confirmation_code": confirmation_code,
        "impact_analysis": {
            "user_name": user.name,
            "orders_affected": orders,
            "files_to_delete": files,
            "warning": "此操作不可逆!数据将被永久删除"
        },
        "next_step": "调用 execute_delete_user 并提供此确认码"
    }

@tool
def execute_delete_user(user_id: str, confirmation_code: str) -> dict:
    """
    【第二步】执行用户删除。必须先调用 prepare_delete_user 获取确认码。
    """
    stored_code = cache.get(f"delete_confirm_{user_id}")
    if not stored_code or stored_code != confirmation_code:
        return {
            "success": False,
            "error": "确认码无效或已过期,请重新调用 prepare_delete_user"
        }
    
    # 执行删除
    db.delete_user(user_id)
    cache.delete(f"delete_confirm_{user_id}")
    
    return {"success": True, "deleted_user_id": user_id}

原则七:工具集要有合理的粒度层级

把工具组织成粗粒度的高层工具细粒度的底层工具

# 高层工具:Agent优先使用
@tool
def process_customer_refund(order_id: str, reason: str) -> dict:
    """处理客户退款申请(端到端流程:验证→退款→通知)"""
    ...

# 底层工具:高层工具不够用时才用
@tool
def validate_order_refundable(order_id: str) -> dict:
    """检查订单是否可退款"""
    ...

@tool
def process_payment_refund(payment_id: str, amount: float) -> dict:
    """执行支付退款(底层操作)"""
    ...

@tool
def update_order_status(order_id: str, status: str) -> dict:
    """更新订单状态"""
    ...

这种设计让Agent在简单场景用高层工具一步完成,在复杂场景(如高层工具失败)能灵活组合底层工具。


原则八:控制工具集的大小

给LLM太多工具是个陷阱。研究表明,当工具数量超过20个时,LLM的工具选择准确率会显著下降。

按任务动态选择工具

class DynamicToolSelector:
    """根据当前任务动态提供相关工具子集"""
    
    def __init__(self, all_tools: dict):
        self.all_tools = all_tools
        self.tool_embeddings = {
            name: embed_text(tool.__doc__) 
            for name, tool in all_tools.items()
        }
    
    def get_relevant_tools(self, task: str, max_tools: int = 10) -> list:
        """检索与当前任务最相关的工具"""
        task_emb = embed_text(task)
        
        scores = {
            name: cosine_sim(task_emb, emb)
            for name, emb in self.tool_embeddings.items()
        }
        
        # 始终包含的核心工具
        core_tools = ["get_current_time", "ask_user_clarification"]
        
        # 动态选择最相关的工具
        ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
        dynamic_tools = [name for name, _ in ranked[:max_tools - len(core_tools)]]
        
        return [self.all_tools[name] for name in core_tools + dynamic_tools]

实战检查清单

设计每个工具时,过一遍这份清单:

  • 职责单一:这个工具只做一件事
  • 描述清晰:包含使用场景、参数说明、返回值说明
  • 参数明确:使用强类型,枚举可选值
  • 返回规范:始终返回 success 字段,失败时给出 suggestion
  • 幂等设计:写操作能安全重试
  • 危险防护:不可逆操作有二次确认
  • 错误处理:捕获所有异常并返回结构化错误信息

结语

工具设计是AI Agent工程中最容易被低估的环节。大家往往花大量时间在LLM选型和Prompt设计上,却用一个下午草草定义了20个工具。

但实践证明,工具的质量直接决定了Agent的可靠性上限。一个设计精良的工具集,能让一个普通的LLM表现得出乎意料地好;反之,再强的LLM也会在劣质工具集前束手无策。

投资工具设计,是提升Agent可靠性回报率最高的事情之一。