让 AI 学会用工具:基于 LangChain4j 的 Skills Agent 全栈落地实战

"大模型会说话,但不会干活。Skills Agent 的意义,就是让它真正上手。"
Git 仓库: langchain4j-spring-agent/langchain4j-spring-ai/langchain4j-spring-ai-skills


一、你是不是也踩过这个坑?

接了个需求:用 LLM 帮用户自动打开浏览器、搜索内容、导出报告。

然后你发现------

  • OpenAI 的 Function Calling?工具描述写了一堆,模型偶尔幻觉出个不存在的工具名;
  • LangChain 的 Tool?Python 写了半天,Java 那边没法直接集成;
  • MCP?协议够好,但写起来不简单,测起来更麻烦。

有没有一种方案,让工具定义简单到写个 Markdown 文件就行,然后 AI 开箱即用?

有的。这就是本文要聊的 Skills Agent


二、这玩意儿到底是什么?

langchain4j-spring-ai-skills 是我在 langchain4j-spring-agent 这个开源脚手架里实现的一个可独立部署的 AI Agent 服务

它的核心思路只有一句话:

把工具定义写进 SKILL.md 文件,放到本地目录,AI 自动加载、调用、热更新。

整体效果如下图:

技术栈一览:

层级 技术选型
后端框架 Spring Boot 4.0.2 + Java 17
AI 能力 LangChain4j 1.12.2 + Spring AI 1.1.3
工具引擎 langchain4j-skills + langchain4j-experimental-skills-shell
持久化 MySQL + MyBatis-Plus + Druid 连接池
缓存/记忆 Redis(多轮会话滑动窗口 + 摘要压缩)
接口文档 SpringDoc OpenAPI (Swagger UI)
前端 Vue3 + TypeScript + Vite + Pinia + Element Plus

三、核心架构:五层分层,职责不乱

别被"AI Agent"这个词唬住了,架构其实很清晰:

复制代码
┌──────────────────────────────────────────────┐
│          Vue3 前端 / REST 接口调用             │
├──────────────────────────────────────────────┤
│  SkillsAgentController / SessionController   │  ← REST 层
├──────────────────────────────────────────────┤
│           SkillsChatService                  │  ← 对话编排层
│   (构建 AiServices + ToolProvider + 记忆)    │
├──────────────────────────────────────────────┤
│        SkillsFacade → SkillsFacadeManager    │  ← 技能门面层
│          SkillRegistry → SkillSource         │
├──────────────────────────────────────────────┤
│  SkillsMemoryWindowService / SummaryService  │  ← 记忆压缩层(Redis)
├──────────────────────────────────────────────┤
│   MySQL: session / history / model 三张表    │  ← 持久化层
└──────────────────────────────────────────────┘

最关键的一个类:SkillsFacade

它是整个 Skills 能力对外暴露的统一入口,封装了 SkillsFacadeManager,对上层屏蔽了 registry/source 的所有细节:

java 复制代码
public class SkillsFacade {
    private final SkillsFacadeManager manager;

    public ToolProvider toolProvider() { return manager.toolProvider(); }
    public String availableSkills()    { return manager.availableSkills(); }
    public int skillCount()            { return manager.skillCount(); }
    public List<SkillSummary> skillSummaries() { return manager.skillSummaries(); }
    public void refresh()              { manager.refresh(); }
    public boolean isEnabled()         { return manager.isEnabled(); }
}

一个 refresh() 就能热重载 skills 目录,不重启服务,技能即时生效


四、AI 怎么"拿到"工具?--- ToolProvider 装配细节

很多人以为 LangChain4j 的工具只能用 @Tool 注解标在 Java 方法上,其实还有更灵活的方式:ToolProvider

langchain4j-skills 提供了把 Markdown 技能文件转成 ToolProvider 的能力,装配逻辑在 ToolProviderAssembler

java 复制代码
@Bean
public ToolProviderAssembler toolProviderAssembler() {
    return SkillBundle::getToolProvider;  // 从 SkillBundle 直接取出 ToolProvider
}

然后在对话时,这样把它塞进 AiServices

java 复制代码
SkillsAgentAiService service = AiServices.builder(SkillsAgentAiService.class)
    .streamingChatModel(streamingChatModel)
    .toolProvider(skillsFacade.toolProvider())          // ← 技能工具注入
    .hallucinatedToolNameStrategy(request ->            // ← 幻觉工具名兜底
        ToolExecutionResultMessage.from(request,
            skillsFacade.buildMissingToolMessage(request.name())))
    .systemMessage(skillsFacade.buildSystemPrompt())    // ← 系统提示词注入
    .build();

注意这里特意处理了 幻觉工具名(hallucinatedToolName)------当模型调用了一个不存在的工具时,不是直接报错,而是返回一条友好提示,让模型有机会自我修正。这个细节很多 demo 都忽略了。


五、多轮记忆怎么不撑爆 Context?--- 滑动窗口 + 摘要压缩

这是 Agent 工程里最容易被忽视的问题:随着对话轮次增加,历史消息会把 context window 撑爆

我的方案是 Redis 滑动窗口 + 触发式摘要压缩,配置如下:

yaml 复制代码
nda:
  skills:
    agent:
      memory:
        enabled: true
        summary-trigger-rounds: 10   # 超过 10 轮用户提问,触发摘要压缩
        keep-recent-messages: 8      # 压缩后保留最近 8 条消息
        max-messages: 60             # 总上限 60 条
        ttl-seconds: 604800          # 会话 TTL 7 天

对话时的记忆拼接流程:

java 复制代码
// 1. 追加用户消息到 Redis 滑动窗口
memoryWindowService.append(sessionId, "user", question);
// 2. 持久化到 MySQL 历史表
historyManager.saveChatMessage(sessionId, "user", question);
// 3. 拼接完整上下文(含摘要 + 近期消息 + 任务执行约束)
String context = contextBuilder.buildContext(sessionId);
// 4. 对话完成后异步触发压缩(不阻塞当前响应)
summaryService.summarizeIfNeededAsync(sessionId, streamingChatModel);

摘要是用大模型自己压缩的,不是关键词截取,所以对复杂上下文保留效果明显好于简单滑动窗口。


六、模型管理:不再硬编码 API Key

早期很多项目把 API Key 直接写在配置文件里,换个模型要重启服务。

这里改成数据库驱动的模型管理,建表一张:

sql 复制代码
CREATE TABLE aigc_skills_model (
    id           BIGINT PRIMARY KEY AUTO_INCREMENT,
    model_code   VARCHAR(64) NOT NULL,    -- 唯一模型标识
    model_name   VARCHAR(128) NOT NULL,   -- 如 qwen-plus
    provider     VARCHAR(32) NOT NULL DEFAULT 'openai-compatible',
    base_url     VARCHAR(255) NOT NULL,
    api_key      VARCHAR(255) NOT NULL,
    enabled      TINYINT(1) NOT NULL DEFAULT 1,
    is_default   TINYINT(1) NOT NULL DEFAULT 0,
    log_requests TINYINT(1) NOT NULL DEFAULT 1,
    log_responses TINYINT(1) NOT NULL DEFAULT 1,
    UNIQUE KEY uk_model_code (model_code)
);

前端模型管理页效果:

支持:

  • 新增/编辑模型(base_url + api_key 在线填写)
  • 切换默认模型 (一键设置 is_default=1
  • 启用/禁用(不删记录,仅切换状态)
  • 每个会话独立绑定模型 (创建会话时传 modelCode

七、会话管理:多会话隔离,历史消息分页回放

会话管理页效果:
会话主表 aigc_skills_session,历史消息表 aigc_skills_chat_history,设计要点:

  • 每条消息有 message_typechat(普通对话)或 summary(摘要压缩记录)
  • 摘要记录额外标记 compressed=1,方便回放时区分
  • 归档会话(status=0)后继续 chat 会返回业务异常,提示创建新会话
  • 消息支持按 messageType 过滤分页,接口:GET /sessions/{sessionId}/messages?messageType=chat

八、对话页面:流式输出 + Markdown 渲染

前端对话页的关键能力:

  • SSE 流式输出:字符级实时渲染,不等全文返回
  • Markdown 渲染:Assistant 回复支持代码块、列表、表格样式
  • 会话列表侧边栏:滚动分页加载,支持切换/归档
  • 历史消息回放:向上滚动触发分页加载历史
  • 回到底部悬浮按钮 + 新消息计数提示

后端流式接口:

java 复制代码
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(
        @RequestParam(required = false) String sessionId,
        @RequestParam @NotBlank String question) {
    return skillsChatService.streamChat(sessionId, question);
}

九、接口文档:Swagger 开箱即用

引入 springdoc-openapi-starter-webmvc-ui 后,所有接口自动生成文档:

复制代码
http://localhost:9015/swagger-ui.html

Controller 全部标注了中文说明:

java 复制代码
@Tag(name = "Skills Agent", description = "skills agent 管理与对话接口")
@Operation(summary = "同步对话", description = "传入 question + 可选 sessionId,返回完整回答字符串")
@PostMapping("/chat")
public ApiResponse<SkillsChatResponse> chat(@Valid @RequestBody SkillsChatRequest request) { ... }

十、五分钟跑起来

10.1 数据库初始化

sql 复制代码
-- 依次执行三个建表脚本
source langchain4j-spring-ai-skills/doc/init-skills-model.sql
source langchain4j-spring-ai-skills/doc/init-skills-session.sql
source langchain4j-spring-ai-skills/doc/init-skills-history.sql

-- 插入一条默认模型(替换为真实 key)
INSERT INTO aigc_skills_model(model_code, model_name, provider, base_url, api_key, enabled, is_default)
VALUES ('qwen-plus', 'qwen-plus', 'openai-compatible',
        'https://dashscope.aliyuncs.com/compatible-mode/v1', 'YOUR_KEY', 1, 1);

10.2 配置 application.yml

yaml 复制代码
server:
  port: 9015

spring:
  datasource:
    druid:
      url: jdbc:mysql://127.0.0.1:3306/langchain_skills?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: your_password
  data:
    redis:
      host: 127.0.0.1
      port: 6379

nda:
  skills:
    source-type: FILESYSTEM
    filesystem-path: ${user.dir}/.claude/skills   # 存放 SKILL.md 文件的目录
    prompt-inject: true
    agent:
      memory:
        enabled: true
        summary-trigger-rounds: 10
        keep-recent-messages: 8

10.3 启动后端

powershell 复制代码
Set-Location "D:\workspace\langchain4j-spring-agent\langchain4j-spring-ai"
mvn -pl langchain4j-spring-ai-skills -am spring-boot:run

10.4 启动前端

powershell 复制代码
Set-Location "D:\workspace\langchain4j-spring-agent\langchain4j-spring-ai-ui\langchain4j-spring-ai-ui-skills"
pnpm install
pnpm dev

10.5 快速验证

powershell 复制代码
# 健康检查
Invoke-RestMethod -Method Get -Uri "http://localhost:9015/api/skills-agent/health"

# 查看加载的技能列表
Invoke-RestMethod -Method Get -Uri "http://localhost:9015/api/skills-agent/info"

# 发起一次对话
$body = @{ question = "帮我打开浏览器搜索 langchain4j" } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "http://localhost:9015/api/skills-agent/chat" `
  -ContentType "application/json" -Body $body

十一、有哪些坑值得注意?

坑一:langchain4j-skills 版本和 langchain4j 主版本必须对齐

不对齐会出现 ClassCastException 或工具描述解析失败,找半天找不到原因。

坑二:流式对话需要前端正确处理 SSE 边界

后端用 TEXT_EVENT_STREAM_VALUE 输出,前端用 EventSource 或手动处理 fetch 流,不要用普通 Axios 请求流式接口。

坑三:Redis 里的记忆 key 要带 sessionId 隔离

我用 skills:session:{sessionId}:msgsskills:session:{sessionId}:summary 分开存,不隔离的话多用户并发会串上下文。

坑四:幻觉工具名不处理会让对话直接报错

LangChain4j 提供了 hallucinatedToolNameStrategy,一定要配,否则模型一旦幻觉出个工具名,整条链路抛异常,用户体验很差。


十二、总结

Skills Agent 这套方案的核心价值在于:

  1. 工具与代码解耦 --- SKILL.md 文件定义工具,不需要每次改工具就重新部署
  2. 热加载 --- /refresh 接口一调,新技能立刻生效
  3. 记忆不爆炸 --- 滑动窗口 + 摘要压缩,长对话也稳
  4. 模型可切换 --- 数据库驱动,不同会话可以用不同模型
  5. 全栈可跑 --- 后端 Spring Boot + 前端 Vue3,有图有码,可直接 fork 跑起来

代码已开源:https://gitee.com/zhangjq123/langchain4j-spring-agent

如果你也在做 AI Agent 相关的 Java 工程落地,欢迎 star 一下,有问题评论区见 👇


相关推荐
我登哥MVP2 小时前
【SpringMVC笔记】 - 2 - @RequestMapping
java·spring boot·spring·servlet·tomcat·intellij-idea·springmvc
财迅通Ai2 小时前
天立国际控股:AI赋能再造新增长 中期净利大增21%
大数据·人工智能·天立国际控股
砍材农夫2 小时前
Hermes 搭建可视化web-dashboard界面
前端·人工智能
2301_780789662 小时前
什么是端口?端口攻击如何检测和防御
服务器·人工智能·游戏·架构·零信任
hqyjzsb2 小时前
传统教师升级AI教育产品设计师后收入增长路径
人工智能·职场和发展·aigc·文心一言·学习方法·业界资讯·ai写作
QQ676580082 小时前
智慧AI甲骨文检测 目标检测图像数据集 甲骨文识别第10341期
人工智能·yolo·目标检测·目标跟踪·甲骨文检测·甲骨文识别
米小虾2 小时前
从"金鱼脑"到"长期记忆":AI Agent 记忆机制的设计与实现
人工智能·agent
AI视觉网奇2 小时前
探索 InternVL3.5:从权重解析到多模态推理的全栈实践笔记
人工智能·大模型
xixixi777772 小时前
智算中心建设新范式:GPT-6/Rubin架构+1.6T光模块+量子安全网关+AI安全沙箱,算力·效率·安全·成本的最优平衡
人工智能·gpt·安全·机器学习·架构·大模型·通信