工具抽象的艺术:可插拔Skill系统的设计哲学

工具抽象的艺术:可插拔Skill系统的设计哲学


上一篇文章我们理解了ReAct循环------LLM在"思考-行动-观察"中迭代。但当时我们只是"假设"工具已经存在。实际上,工具系统本身的设计就是一门学问:如何让Agent的工具像手机App一样,安装即可用,卸载即消失?


一、回顾上一篇文章的遗留问题

在Day1中,我们说Agent = LLM + 工具 + 循环。工具是怎么来的?最简单的做法:

java 复制代码
// 硬编码一个计算器工具
@Tool(description = "计算数学表达式")
public double calculate(String expr) { ... }

// 注册给LLM
chatClient.defaultTools(new CalculatorTool());

这样Agent就能算数学题了。但问题很快就来了:

你突然想让Agent也能查天气。你得写一个新的 WeatherTool 类、修改代码、重新编译、重新部署。过几天,用户想要一个翻译功能。再写代码、再部署。再后来,不同用户需要不同的工具,你没法为每个用户定制。

工具不能"安装"------必须"硬编码"------这是第一个要解决的问题。

二、Skill接口:给工具一个统一的"插槽"

2.1 从硬编码到接口抽象

所有工具都有一个共同点:有名字、有描述、能开关。所以用接口统一它们:

java 复制代码
public interface Skill {
    String name();           // 工具叫什么
    String description();    // 工具做什么(这是写给LLM看的)
    boolean isEnabled();     // 当前是否启用
    void setEnabled(boolean enabled);  // 开/关
}

任何实现了这个接口的类,就是一个"可插拔"的工具:

  • 实现了接口 → 可以被Agent发现
  • isEnabled() → 可以运行时开关
  • name() + description() → LLM得知这个工具的存在

2.2 这个接口故意不做"执行"

注意------Skill 接口没有执行方法 (比如 execute())。为什么?

因为执行逻辑由Spring AI的 @Tool 注解处理。把"工具管理"和"工具执行"分开:

  • Skill接口:负责生命周期管理(是什么、是否启用)
  • @Tool注解:负责执行逻辑(怎么做)

如果强行把 execute() 放到Skill接口里,反而会把事情搞复杂------不同工具的参数完全不一样,统一签名几乎不可能。

2.3 启用/禁用是如何工作的?

SkillRegistry 维护一个Skill列表。每次构建ChatClient时:

复制代码
1. 遍历所有Skill
2. 过滤出 isEnabled() == true 的
3. 把它们作为工具注册给LLM
4. 被禁用的Skill对LLM完全不可见

前端点一下"禁用计算器"按钮 → 更新 isEnabled 状态 → 下次对话时,LLM不知道有计算器这个工具了。零代码改动、零重启。

三、内置Skill vs 外部Skill:一个关键的分层

3.1 为什么要分两层?

不是所有工具都该以同样的方式管理。

内置Skill(代码层面):

  • 计算器、翻译、记忆读写------这些都是核心能力
  • 行为完全可预期(代码是我们写的)
  • 性能要求高(频繁调用,不能有中间层开销)

外部Skill(数据库存储):

  • 用户自己导入的天气查询、代码执行、第三方API
  • 来源不可信(用户写的脚本可能有风险)
  • 需要安全管理(沙箱、超时、白名单)

如果在同一个抽象里混在一起,就会产生矛盾:内置Skill不需要安全检查(拖慢性能),外部Skill必须安全检查。统一抽象要么让内置Skill承受不必要开销,要么让外部Skill缺少保护。

3.2 所以分层是必然选择

复制代码
内置Skill: Java代码 → @Tool注解 → Skill接口 → SkillRegistry管理
外部Skill: 数据库存储 → ImportedSkillRegistry解析 → ExternalSkillCallback适配 → 统一注册

层次有明确的边界:内置的在代码层,外部的在数据层。但在LLM面前,它们是一样的------都变成了工具说明书,LLM不关心工具是Java代码还是数据库查出来的。

四、外部Skill的三种类型:解决不同的问题

外部Skill分为PROMPT、TOOL、MCP三种。不是随意分的,每种解决一类问题。

4.1 PROMPT类型:不是"工具",是"行为准则"

PROMPT类型的本质是可插拔的System Prompt片段

举例:一个"代码审查规范"的PROMPT Skill,内容是:

复制代码
你是一个代码审查者,检查以下方面:
- 命名是否清晰?
- 是否有潜在的空指针?
- 是否有安全漏洞(SQL注入、XSS)?

当用户说"帮我审查这段代码"时,SkillRouter检测到"审查"这个关键词 → 激活该PROMPT → 内容被注入到System Prompt中。LLM不会知道这是一个"Skill"------它只看到System Prompt里多了一段审查规则。

4.2 TOOL类型:把外界API变成Agent的"肢体"

TOOL类型用一个通用的JSON格式描述一个外部工具:

json 复制代码
{
  "name": "get_weather",
  "description": "查询城市天气",
  "parameters": { "type": "object", "properties": { "city": {"type": "string"} } },
  "executor": {
    "type": "http",
    "url": "https://api.weather.com/v1/{city}",
    "method": "GET"
  }
}

当LLM调用 get_weather(city="北京") 时,ExternalSkillCallback做了几件事:

  1. {city} 替换为 北京 → URL变成 https://api.weather.com/v1/北京
  2. 用RestClient发送GET请求
  3. 拿到响应 → 返回给LLM

对LLM来说,它不知道也不关心这个工具背后是Java代码还是HTTP请求。它只看到"工具返回了天气数据"。

这就是统一使用体验的威力------不同来源、不同协议的工具,在LLM面前没有区别。

4.3 Script执行器的安全沙箱

如果外部工具是一个脚本(比如用户写了一个Python脚本让Agent执行),安全问题就来了。脚本可以做任何事------删文件、访问网络、fork进程。

我们的防御是三层:

第一层:白名单

只允许 python3bashnode 这三种运行时。用户想用 shperl?拒绝。

第二层:隔离

脚本被写入一个临时文件,在独立进程中运行。脚本执行完了,临时文件删除。即使脚本有问题,也不影响Agent主进程。

第三层:超时

30秒必须结束。超过30秒,进程被 destroyForcibly() 强制终止。防止死循环或无限等待。

这三层组合起来,把风险降到了可控范围。

五、@Tool注解:LLM的"工具说明书"

5.1 注解不只是注解

java 复制代码
@Tool(description = "计算表达式,支持+ - * / ** sqrt")
public double calculate(
    @ToolParam(description = "数学表达式,如 3.14*2") String expr) { ... }

这个注解经历了什么:

  1. Spring AI扫描 → 发现 @Tool → 提取方法签名和参数
  2. description 字段生成JSON Schema(兼容OpenAI格式)
  3. Schema被拼接到System Prompt中
  4. LLM读取所有工具的Schema → 用户提问时选择最匹配的工具
  5. LLM根据 @ToolParam 的description构造参数

从这个流程可以看出:@Tool(description="...") 不是写给人看的注释,而是写给LLM看的说明书。 description的质量直接决定LLM能否正确选择和使用工具。

5.2 描述措辞的实验

两个描述,同一个工具:

复制代码
描述A: "计算表达式"
描述B: "计算数学表达式。支持加减乘除、幂运算。当用户问计算题时必须调用此工具,禁止心算。"

测试"3.14 × 256等于多少":

  • 描述A → Agent有约40%机率心算(不调工具),可能得到错误答案
  • 描述B → Agent几乎100%调用工具,返回精确结果

差的描述让LLM"偷懒"(反正我会心算,不调工具了)。好的描述明令禁止偷懒。

六、从工具系统到"Agent App Store"

回顾整个设计:

  1. Skill接口定义了统一的"插槽"
  2. 内置/外部分层保证了安全性和灵活性
  3. 外部Skill的三种类型覆盖了主流需求
  4. @Tool注解标准化了LLM与工具的沟通方式

这些组合在一起,形成了一个可扩展的能力体系。未来可以做一个"Agent App Store"------开发者提交遵循规范的Skill JSON,用户一键安装,就像手机App Store一样。

MCP协议(下一篇文章)就是往这个方向走的一大步------它标准化了工具的"通信协议",让任何语言写的工具都能被任何Agent消费。


项目链接

项目代码:day6-agent

相关推荐
IT_陈寒1 小时前
为什么我的React状态更新总是不按套路出牌?
前端·人工智能·后端
新知图书1 小时前
向量数据库集成(Chroma/Pinecone/Milvus)
人工智能·agent·多模态·ai agent·智能体
大鱼>1 小时前
宠物监控数据安全与隐私保护:端到端加密与合规实践
人工智能·深度学习·算法·iot·宠物
小二·1 小时前
WebGPU 浏览器端跑大模型:让AI在网页里跑起来(WebLLM/Transformers.js实战)
开发语言·javascript·人工智能
某林2121 小时前
构建高精度 6-DoF 灵巧手控制系统
人工智能·3d·机器人·ros2·技术复盘
qcx231 小时前
SpaCellAgent:用 LLM 多智能体怎么用于单细胞轨迹分析的?效率提升了多少?
人工智能·gpt·安全·ai·机器人·llm·agent
lisw052 小时前
计算免疫学的前沿领域
人工智能·机器学习
陕西企来客2 小时前
陕西企来客科技 AI 营销大模型深度解析:GEO 赛道技术优势与落地实践
大数据·人工智能·科技
多年小白2 小时前
AI 日报 - 2026年7月14日
人工智能·ai