springboot+langchain4j 实战 Day15——打造一个“生产“级 Agent 服务:单个 Agent 同时持有多个 Tool,LLM 自主判断调用哪个
Day 15 — 单 Agent 多 Tool + MySQL 数据源 + Redis 缓存 + AOP 追踪 + SSE 流式输出
一、目标
融合 Day 1-14 全部能力,打造一个生产级 Agent 服务:单个 Agent 同时持有多个 Tool,LLM 自主判断调用哪个;数据落 MySQL、Tool 结果走 Redis 缓存、每次调用被 AOP 追踪。
二、架构
浏览器 (static/index.html) ├── GET /agent/chat?message=... → JSON 同步响应 └── GET /stream/chat?message=... → SSE 逐 token 流式推送 ↓ UnifiedAgentController ↓ UnifiedAgentService(单 Agent) ├── OrderTool ← MyBatis-Plus → MySQL t_order ├── RefundTool ← MyBatis-Plus → MySQL t_refund ├── KnowledgeBaseTool ← MyBatis-Plus → MySQL t_knowledge └── LLM (DeepSeek-V3 via 硅基流动) ↓ ToolTraceAspect(AOP 环绕通知,记录每次 @Tool 调用) ↓ Redis(@Cacheable,TTL 10min)与 Day 13/14 的关键区别:
| 维度 | Day 13/14 | Day 15 |
|---|---|---|
| 路由方式 | Router LLM 先分类,再分发到子 Agent | 无需 Router— 单 Agent 挂多个 Tool,LLM 自己判断 |
| 数据来源 | Mock 硬编码 | MySQL 真实数据库(Druid 连接池 + MyBatis-Plus) |
| 缓存层 | 无 | Redis@Cacheable缓存 Tool 查询结果 |
| 调用追踪 | 无(仅 log.info) | AOP 切面记录入参 / 耗时 / 结果 |
| 前端 | 无 | SSE 打字机效果HTML 页面(static/index.html) |
| 流式对话 | Day 12 独立项目 | 集成在统一项目中(双端点) |
三、技术栈
| 组件 | 版本 | 用途 |
|---|---|---|
| Spring Boot | 2.7.18 | 应用框架 |
| Tomcat | 9.0.83(内嵌) | Web 容器 |
| Java | 17 | 运行语言 |
| LangChain4j | 0.36.2 | Agent 框架(AiServices+@Tool) |
| DeepSeek-V3 | via 硅基流动 | LLM 模型 |
| MyBatis-Plus | 3.5.3.1 | ORM + Lambda 查询 |
| Druid | 1.2.20 | 数据库连接池(含监控页/druid) |
| MySQL | 8.0(Docker 3307) | 业务数据库 |
| Redis | 7(Docker 6379) | Tool 结果缓存 |
| Lombok | 1.18.30 | 减少样板代码 |
| Jackson | 2.13.x(Spring Boot 内置) | JSON 序列化 |
四、项目结构
day15/ ├── pom.xml ├── README.md └── src/main/ ├── java/com/day15/demo/ │ ├── Day15Application.java # 启动类 │ ├── aop/ │ │ └── ToolTraceAspect.java # AOP 切面:环绕所有 @Tool 方法 │ ├── config/ │ │ ├── CacheConfig.java # Redis 缓存配置(TTL 10min) │ │ └── ChatModelConfig.java # LLM 模型 Bean(Chat + Streaming 双实例) │ ├── controller/ │ │ └── UnifiedAgentController.java # 双端点:/agent/chat + /stream/chat │ ├── dto/ │ │ └── Result.java # 统一响应体 {code, message, data} │ ├── entity/ │ │ ├── Order.java # t_order 映射(@TableId + @JsonIgnore + @JsonFormat) │ │ ├── Refund.java # t_refund 映射 │ │ └── Knowledge.java # t_knowledge 映射 │ ├── mapper/ │ │ ├── OrderMapper.java # MyBatis-Plus BaseMapper │ │ ├── RefundMapper.java │ │ └── KnowledgeMapper.java │ ├── service/ │ │ └── UnifiedAgentService.java # 单 Agent 注入 3 个 Tool │ └── tool/ │ ├── OrderTool.java # 订单查询 / 列表(@Cacheable + MySQL) │ ├── RefundTool.java # 退款创建 / 政策(写入 MySQL) │ ├── KnowledgeBaseTool.java # 知识库检索 / 目录(@Cacheable + MySQL) │ └── WeatherTool.java # 天气(备用,暂未挂载) └── resources/ ├── application.yml # MySQL + Druid + Redis 配置 ├── schema.sql # DDL 建表(LONGTEXT 兼容 MySQL) ├── data.sql # 种子数据(INSERT IGNORE,4 订单 + 1 退款 + 5 知识库) └── static/ └── index.html # 前端聊天页面(SSE 打字机效果)五、核心代码
5.1 双模型注入(ChatModelConfig)
@Bean("openAiChatModel")publicOpenAiChatModelopenAiChatModel(){// 普通对话用returnOpenAiChatModel.builder().apiKey(apiKey).baseUrl(baseUrl).modelName(modelName).temperature(0.3).timeout(Duration.ofSeconds(60)).maxRetries(2).build();}@Bean("openAiStreamingChatModel")publicOpenAiStreamingChatModelopenAiStreamingChatModel(){// SSE 流式用returnOpenAiStreamingChatModel.builder().apiKey(apiKey).baseUrl(baseUrl).modelName(modelName).temperature(0.3).timeout(Duration.ofSeconds(60)).build();}两种模式用同一个AiServices.Builder构建,AiServices自动根据接口方法返回值分发:
- 返回
String→ 走chatLanguageModel - 返回
TokenStream→ 走streamingChatLanguageModel
5.2 单 Agent 多 Tool(UnifiedAgentService)
@PostConstructpublicvoidinit(){agent=AiServices.builder(UnifiedAgent.class).chatLanguageModel(chatModel).streamingChatLanguageModel(streamingChatModel).tools(orderTool,refundTool,knowledgeBaseTool)// 一次注入 3 个.chatMemory(MessageWindowChatMemory.withMaxMessages(10)).build();}@SystemMessage引导 LLM 行为:
- 订单/物流 → 优先用 Tool 查
- 退款 → 主动创建工单
- 技术问题 → 先搜知识库再回答
- 不要凭自身知识猜测
5.3 MySQL 数据源(application.yml)
spring:datasource:type:com.alibaba.druid.pool.DruidDataSourceurl:jdbc:mysql://localhost:3307/ai_logs?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername:rootpassword:root123driver-class-name:com.mysql.cj.jdbc.Driverdruid:initial-size:5min-idle:5max-active:20max-wait:60000filter:stat:enabled:trueslow-sql-millis:2000log-slow-sql:truewall:enabled:false# MyBatis-Plus Lambda 查询不支持 wall 拦截为什么
wall: false:MyBatis-Plus LambdaWrapper 生成的 SQL 会触发 Druid WallFilter 误判,关闭后不影响安全(SQL 由框架生成,无拼接注入风险)。
5.4 Entity 注解规范
@Data@TableName("t_order")publicclassOrder{@JsonIgnore// 不暴露给前端(内部主键)@TableId(type=IdType.AUTO)// 数据库自增privateLongid;privateStringorderId;// 业务编号privateStringproduct;privateStringstatus;privateStringlogistics;privateBigDecimalamount;@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="Asia/Shanghai")privateLocalDateTimecreatedAt;// 格式化输出,避免序列化为数组 [2025,6,1,...]}5.5 AOP 链路追踪(ToolTraceAspect)
@Around("@annotation(dev.langchain4j.agent.tool.Tool)")publicObjecttrace(ProceedingJoinPointpjp)throwsThrowable{Stringmethod=pjp.getSignature().toShortString();Stringargs=/* 拼接参数 */;longstart=System.currentTimeMillis();log.info("[ToolTrace] ▶ {} | args=({})",method,args);Objectresult=pjp.proceed();longelapsed=System.currentTimeMillis()-start;log.info("[ToolTrace] ✔ {} | {}ms | result={}",method,elapsed,truncate(result,120));returnresult;}输出示例:
[ToolTrace] ▶ OrderTool.queryOrder(..) | args=(20250615) [OrderTool] 查询订单(DB): 20250615 ==> Preparing: SELECT ... FROM t_order WHERE order_id = ? ==> Parameters: 20250615(String) <== Total: 1 [ToolTrace] ✔ OrderTool.queryOrder(..) | 45ms | result=订单 20250615 ...5.6 Redis 缓存(CacheConfig+@Cacheable)
@Tool("Query order by orderId")@Cacheable(value="order",key="#orderId")// 同一订单号 10 分钟内走缓存publicStringqueryOrder(StringorderId){...}- Key 序列化:
StringRedisSerializer - Value 序列化:
GenericJackson2JsonRedisSerializer - TTL:10 分钟(
spring.cache.redis.time-to-live: 600000)
5.7 SSE 流式推送
@GetMapping(value="/stream/chat",produces="text/event-stream;charset=UTF-8")publicSseEmitterstream(@RequestParamStringmessage){SseEmitteremitter=newSseEmitter(TimeUnit.MINUTES.toMillis(2));TokenStreamtokenStream=unifiedAgentService.stream(message);Executors.newSingleThreadExecutor().execute(()->{tokenStream.onNext(token->emitter.send(SseEmitter.event().data(token))).onComplete(resp->emitter.complete()).onError(emitter::completeWithError).start();});returnemitter;}关键 API 匹配(LangChain4j 0.36.2):
onNext(Consumer<String>)— 每个 token 回调onComplete(Consumer<Response<AiMessage>>)— 流结束onError(Consumer<Throwable>)— 异常
六、双端点 API
GET /agent/chat— JSON 同步
curl"http://localhost:8088/agent/chat?message=查订单20250615"# → {"code":200,"message":"success","data":"订单 20250615\n商品: ..."}GET /stream/chat— SSE 流式
curl-N"http://localhost:8088/stream/chat?message=hello"# → data:你好# → data:呀# → data:!前端页面
浏览器打开http://localhost:8088/index.html:
- 左侧:Agent 信息 + 快捷提问
- 右侧:对话区(SSE 打字机逐字渲染)
- 支持:回车发送、Tool 调用标记、超时提示
七、Druid 监控
http://localhost:8088/druid/→ 用户名admin/ 密码admin123
八、启动方式
前置条件
# MySQL(已运行)dockerps|grepai-mysql# → 0.0.0.0:3307->3306/tcp# Redis(已运行)dockerps|grepai-redis# → 0.0.0.0:6379->6379/tcp启动
cdday15 mvn clean compile spring-boot:run-DskipTests输出关键日志:
Tomcat started on port(s): 8088 (http) with context path '' Day15 UnifiedAgent 初始化完成: OrderTool + RefundTool + KnowledgeBaseTool (MySQL数据源 + Redis缓存 + AOP追踪) Started Day15Application in 3.3 seconds九、演进路线
Day 1-2 基础环境 + LangChain4j Demo Day 3 RAG (InMemoryEmbeddingStore) 英文全称:Retrieval-Augmented Generation(检索增强生成)。意思就是:让 AI 在回答之前,先去「查资料」,再基于查到的资料来回答。就像考试时允许你翻书,而不是只靠脑子记忆答题。 Day 4 PGVector 向量库 Day 5 Redis 聊天记忆 Day 6-7 单元测试 + 统一响应体 Day 8-9 AOP 日志 + 降级Sringboot 2.7.18 Day 10 工具类完善,并且 MySQL + PGVector + Redis 一键docker-compose部署依赖的开发环境 Day 11 Agent 联网搜索,调用天气api Day 12 网页 + SSE 流式对话 Day 13 多 Agent 协作 (Router) Day 14 子 Agent 工具注入 Day 15 ← 融合全部能力:单 Agent 多 Tool + MySQL + Redis + AOP + SSEDay 15 是项目集大成的里程碑 —— 不再需要 Router 分流,LLM 自己看懂意图并选择 Tool,数据落库、结果缓存、调用可追踪,同时支持 JSON 和 SSE 两种输出模式。
