跳至内容
Skills 与插件开发

Skills 与插件开发

Skills(技能)和 Plugin(插件)是同一概念的两种叫法:给 LLM 注册一个「可调用的外部能力」,让它在需要时自主决定是否调用、传什么参数。这篇从架构设计到生产级实现,完整讲清楚如何构建健壮的 LLM 插件体系。

插件的本质

插件 = 一段代码 + 一份精确的描述。LLM 根据描述决定「要不要用这个工具」,根据 JSON Schema 决定「传什么参数」。代码部分由你控制——LLM 永远不会直接执行你的代码,它只负责「建议调用」。

LLM 的视角:
  我有 [get_weather, search_db, send_email] 三个工具
  用户问:明天北京天气怎样,要不要带伞?
  → 调用 get_weather(city="北京", forecast_days=2)
  → 基于返回值给出回答

你的代码视角:
  检测到 stop_reason == "tool_use"
  → 执行实际函数
  → 把结果传回 LLM

工具描述的重要性

工具描述直接决定 LLM 能否正确调用工具。这是最常被忽视却最影响效果的地方。

{
    "name": "search",
    "description": "搜索信息",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {"type": "string"}
        }
    }
}

问题:什么信息?什么时候用?返回什么格式?

构建工具注册系统

生产环境中工具往往有几十上百个,需要一个统一的注册和管理机制。

# tool_registry.py
import inspect
import json
from typing import Callable, Any
from dataclasses import dataclass, field


@dataclass
class ToolDefinition:
    name: str
    description: str
    input_schema: dict
    handler: Callable
    requires_confirmation: bool = False  # 危险操作需要用户确认


class ToolRegistry:
    def __init__(self):
        self._tools: dict[str, ToolDefinition] = {}

    def register(
        self,
        description: str,
        requires_confirmation: bool = False,
    ):
        """装饰器:将函数注册为工具"""
        def decorator(func: Callable):
            schema = self._build_schema(func)
            self._tools[func.__name__] = ToolDefinition(
                name=func.__name__,
                description=description,
                input_schema=schema,
                handler=func,
                requires_confirmation=requires_confirmation,
            )
            return func
        return decorator

    def _build_schema(self, func: Callable) -> dict:
        """从函数签名自动生成 JSON Schema"""
        hints = func.__annotations__
        sig = inspect.signature(func)
        properties = {}
        required = []

        for name, param in sig.parameters.items():
            if name == "return":
                continue
            type_map = {str: "string", int: "integer", float: "number", bool: "boolean"}
            prop_type = type_map.get(hints.get(name, str), "string")
            doc = (func.__doc__ or "").split("\n")
            prop_desc = next(
                (l.strip().split(":", 1)[-1].strip() for l in doc if name + ":" in l),
                ""
            )
            properties[name] = {"type": prop_type, "description": prop_desc}
            if param.default is inspect.Parameter.empty:
                required.append(name)

        return {
            "type": "object",
            "properties": properties,
            "required": required,
        }

    def get_tools_for_api(self) -> list[dict]:
        """返回适用于 Claude API 的工具定义列表"""
        return [
            {
                "name": tool.name,
                "description": tool.description,
                "input_schema": tool.input_schema,
            }
            for tool in self._tools.values()
        ]

    def execute(self, name: str, inputs: dict) -> Any:
        if name not in self._tools:
            raise ValueError(f"工具不存在: {name}")
        return self._tools[name].handler(**inputs)

    def needs_confirmation(self, name: str) -> bool:
        return self._tools.get(name, ToolDefinition("", "", {}, lambda: None)).requires_confirmation

使用注册器

# tools/builtin.py
import datetime
import registry from tool_registry

reg = ToolRegistry()


@reg.register(
    description="获取当前日期和时间,用于回答'今天几号''现在几点'等时间相关问题"
)
def get_current_datetime() -> str:
    return datetime.datetime.now().isoformat()


@reg.register(
    description=(
        "在公司订单数据库中查询订单信息。"
        "需要提供订单号(order_id)。"
        "返回订单状态、金额、收货地址等详情。"
    )
)
def get_order_info(order_id: str) -> dict:
    """
    order_id: 订单号,格式为 ORD-XXXXXXXX
    """
    # 实际项目中替换为真实数据库查询
    return {
        "order_id": order_id,
        "status": "已发货",
        "amount": 299.00,
        "tracking": "SF1234567890",
    }


@reg.register(
    description="向用户发送短信通知。仅在用户明确要求发送通知时使用。",
    requires_confirmation=True,  # 标记为需要确认的危险操作
)
def send_sms(phone: str, message: str) -> str:
    """
    phone: 手机号,11位数字
    message: 短信内容,不超过70字
    """
    # 实际项目接短信服务商 API
    return f"短信已发送至 {phone}"

完整 Agent 执行流程

# agent.py
import anthropic

client = anthropic.Anthropic()


def run_agent(user_message: str, max_turns: int = 10) -> str:
    messages = [{"role": "user", "content": user_message}]
    tools = reg.get_tools_for_api()

    for turn in range(max_turns):
        response = client.messages.create(
            model="claude-opus-4-8",
            max_tokens=2048,
            tools=tools,
            messages=messages,
        )

        if response.stop_reason == "end_turn":
            return next(b.text for b in response.content if b.type == "text")

        if response.stop_reason == "tool_use":
            messages.append({"role": "assistant", "content": response.content})
            tool_results = []

            for block in response.content:
                if block.type != "tool_use":
                    continue

                # 需要确认的操作,询问用户
                if reg.needs_confirmation(block.name):
                    confirm = input(
                        f"[确认] 即将执行 {block.name},参数:{block.input}。是否继续?(y/n) "
                    )
                    if confirm.lower() != "y":
                        result = "用户取消了此操作"
                    else:
                        result = reg.execute(block.name, block.input)
                else:
                    try:
                        result = reg.execute(block.name, block.input)
                    except Exception as e:
                        result = f"工具执行失败: {e}"

                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result),
                })

            messages.append({"role": "user", "content": tool_results})

    return "已达到最大执行步数,任务未完成。"

并行工具调用

Claude 支持在一次响应中请求多个工具,应当并发执行以降低延迟:

import asyncio

async def execute_tools_parallel(tool_calls: list) -> list[dict]:
    async def call_one(block):
        loop = asyncio.get_event_loop()
        # 在线程池中执行同步函数
        result = await loop.run_in_executor(
            None, reg.execute, block.name, block.input
        )
        return {
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": str(result),
        }

    return await asyncio.gather(*[call_one(b) for b in tool_calls])

工具安全设计

工具是 LLM 和真实系统之间的桥梁,安全问题比普通 API 更复杂:

防提示注入:工具返回值可能包含恶意指令,要对外部数据做标记:

def safe_tool_result(raw_result: str) -> str:
    """把工具返回值包在标签里,防止被当作指令执行"""
    return f"<tool_output>{raw_result}</tool_output>"

参数校验:不要信任 LLM 生成的参数:

import re

def validate_order_id(order_id: str) -> str:
    if not re.match(r"^ORD-[A-Z0-9]{8}$", order_id):
        raise ValueError(f"无效的订单号格式: {order_id}")
    return order_id

权限沙箱:每个工具只拥有完成它职责所需的最小权限(数据库只读账户、只写入特定目录等)。

工具搜索(大规模工具集)

当工具超过 20 个,全部放在 tools 参数里会占用大量 token 并干扰选择。可以先让模型「搜索」需要的工具:

ALL_TOOLS = reg.get_tools_for_api()

# 先用一个小模型从工具库中选出本次需要的工具子集
def select_relevant_tools(user_message: str, top_k: int = 5) -> list[dict]:
    resp = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=200,
        messages=[{
            "role": "user",
            "content": (
                f"以下是可用工具列表(仅名称和简介):\n"
                + "\n".join(f"- {t['name']}: {t['description'][:50]}" for t in ALL_TOOLS)
                + f"\n\n用户请求:{user_message}\n\n"
                + f"请列出最可能需要的 {top_k} 个工具名称,每行一个,不要解释。"
            )
        }]
    )
    selected_names = set(resp.content[0].text.strip().split("\n"))
    return [t for t in ALL_TOOLS if t["name"] in selected_names]

插件版本管理

生产环境中工具会不断迭代,需要管理版本兼容性:

@reg.register(
    description="查询产品库存(v2:支持多仓库查询)",
)
def check_inventory_v2(product_id: str, warehouse: str = "all") -> dict:
    """
    product_id: 产品 SKU
    warehouse: 仓库代码,传 'all' 查询所有仓库合计
    """
    ...

建议保留旧版工具 1–2 个版本,在描述里注明「推荐使用 v2」,待流量全切换后再下线旧版。

最后更新于