商城 Agent 实战(一):在 Spring Boot 商城接入 LangChain4j
前面学习 Agent 时,我用 Java 手写过一个最小循环:
用户问题→ 模型判断是否调用工具→ Java 执行工具→ 工具结果写回消息历史→ 模型继续回答理解原理以后,我准备把 Agent 真正接入一个商城项目。
这次使用的项目是 mall 开源项目 macrozheng/mall 。
原项目提供了一套完整的 Spring Boot 商城后端,已经包含商品、会员、订单、优惠券、售后、Redis、RabbitMQ、Elasticsearch 和 JWT 登录等功能。在这个系列中,我会保留商城原有的业务能力,在此基础上逐步加入 LangChain4j、DeepSeek、Tool Calling、RAG、Memory 和客服工作流。
本文涉及的商城模块、数据模型和基础业务代码主要来自 macrozheng/mall;本系列重点记录的是我在二次开发过程中如何设计和接入商城 Agent。
计划分成八个阶段:
1. 调通 LangChain4j + DeepSeek 聊天2. 把商城查询接口包装成 Tool3. 实现订单查询 Agent4. 增加登录用户权限校验5. 接入商城知识库 RAG6. 增加多轮 ChatMemory7. 实现售后客服工作流8. 增加日志、评测和人工接管这一篇只完成第一步:
新建共享的
mall-ai模块接入 LangChain4j,并让mall-portal和mall-admin都能通过 DeepSeek API 完成普通聊天。
此时还没有 Tool、RAG 和 Memory,因此它只是一个基于大模型的商城助手,还不能算完整的商城 Agent。
1. AgentShop 当前结构
AgentShop 当前仍然沿用 macrozheng/mall 的 Maven 多模块结构,并在此基础上进行后续二次开发。
项目的主要模块如下:
AgentShop├── mall-admin 商城后台管理├── mall-ai 前后台共享的 AI 基础能力(本系列新增)├── mall-common 通用返回、异常和工具类├── mall-mbg MyBatis Generator 生成的模型与 Mapper├── mall-portal 前台商城、会员、购物车和订单├── mall-search Elasticsearch 商品搜索└── mall-security JWT 与 Spring Security父项目当前使用:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.5.14</version></parent>
<properties> <java.version>17</java.version></properties>LangChain4j 当前的 Spring Boot 3 集成要求 Java 17,并支持 Spring Boot 3.5+,因此不需要为了接入 AI 修改项目的 Java 或 Spring Boot 版本。
因为前台和后台都需要助手,本系列不会把 LangChain4j 直接放进 mall-portal,而是新增一个平级的 mall-ai 模块:
mall-ai├── assistant/│ └── MallAssistant.java├── controller/│ └── MallAssistantController.java├── dto/│ └── AiChatRequest.java└── resources/ └── mall-ai-langchain4j.yml模块职责如下:
mall-ai负责 LangChain4j 依赖、DeepSeek 配置、聊天 Service、Controller 和 DTO;mall-portal和mall-admin依赖mall-ai,分别启动时都会加载这个 Controller;- 两个应用仍通过自己的 Spring Security 过滤链处理
/ai/chat。
mall-ai 是 Maven 共享模块,不是独立部署的微服务。当前第一阶段只有普通聊天,所以前后台暂时共用同一个 MallAssistantController。由于两个应用分别启动、使用不同端口和 JWT 配置,请求仍处在各自的安全边界内。
这一点很重要:后台可以查询运营订单、商品和会员统计,前台只能查询当前登录用户自己的信息。共享模型接入不等于共享业务权限。
这种共用只适用于当前“无 Tool”阶段。后续接入商品、订单和运营数据后,需要拆分前后台 Assistant 和 Tool,避免共享错误的业务权限。
2. 为什么使用 OpenAI-compatible 接口
DeepSeek API 提供与 OpenAI Chat Completions 兼容的接口,所以可以使用 LangChain4j 的 OpenAI 集成,只需要替换:
baseUrlapiKeymodelName请求链路是:
mall-ai:MallAssistantController ↓mall-ai:MallAssistant(Spring Service) ↓mall-ai:ChatModel ↓LangChain4j OpenAI Client ↓DeepSeek API这里使用的是:
Base URL:https://api.deepseek.comModel:deepseek-v4-flash3. 添加 LangChain4j 依赖
本文编写时使用:
LangChain4j Core:1.18.1LangChain4j Spring Boot Starter:1.18.1-beta28Java:17Spring Boot:3.5.14实际使用时应该再次检查官方版本,不要长期复制文章里的版本号。
先在父项目 pom.xml 中注册新模块并统一定义版本:
<modules> <!-- 省略原有模块 --> <module>mall-ai</module> <module>mall-admin</module> <module>mall-portal</module></modules>
<properties> <!-- 省略项目原有配置 --> <langchain4j.version>1.18.1</langchain4j.version> <langchain4j-spring-boot-starter.version>1.18.1-beta28</langchain4j-spring-boot-starter.version> <mall-ai.version>1.0-SNAPSHOT</mall-ai.version></properties>在父项目的 dependencyManagement 中统一管理依赖:
<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-core</artifactId> <version>${langchain4j.version}</version></dependency><dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId> <version>${langchain4j-spring-boot-starter.version}</version></dependency>创建 mall-ai/pom.xml:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<parent> <groupId>com.macro.mall</groupId> <artifactId>mall</artifactId> <version>1.0-SNAPSHOT</version> </parent>
<artifactId>mall-ai</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging>
<dependencies> <dependency> <groupId>com.macro.mall</groupId> <artifactId>mall-common</artifactId> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-core</artifactId> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId> </dependency> </dependencies></project>实际项目使用 Starter 的自动配置,不再手动编写 OpenAiChatModel.builder()。mall-ai 依赖 mall-common,是因为共享 Controller 需要返回项目原有的 CommonResult。
然后在 mall-admin/pom.xml 和 mall-portal/pom.xml 中加入同一个内部依赖:
<dependency> <groupId>com.macro.mall</groupId> <artifactId>mall-ai</artifactId></dependency>版本已经在父项目的 dependencyManagement 中统一管理,子模块不需要重复填写。
先运行依赖解析:
mvn -pl mall-ai,mall-admin,mall-portal -am -DskipTests compile如果这里失败,应该先处理依赖或版本兼容问题,不要继续写 Controller。
4. 配置 DeepSeek
Starter 会根据 langchain4j.open-ai.chat-model 配置自动创建 ChatModel Bean。为了让前后台共用配置,在 mall-ai 中创建:
mall-ai/src/main/resources/mall-ai-langchain4j.ymllangchain4j: open-ai: chat-model: base-url: ${DEEPSEEK_BASE_URL:https://api.deepseek.com} api-key: ${DEEPSEEK_API_KEY} model-name: ${DEEPSEEK_MODEL_NAME:deepseek-v4-flash} temperature: ${DEEPSEEK_TEMPERATURE:0.2} timeout: ${DEEPSEEK_TIMEOUT:60s} max-retries: ${DEEPSEEK_MAX_RETRIES:2} log-requests: ${DEEPSEEK_LOG_REQUESTS:false} log-responses: ${DEEPSEEK_LOG_RESPONSES:false}再在 mall-admin/src/main/resources/application.yml 和 mall-portal/src/main/resources/application.yml 中导入它:
spring: config: import: optional:classpath:mall-ai-langchain4j.ymloptional: 表示资源不存在时不会立即因导入失败而停止启动;但只要创建 MallAssistant 需要的 ChatModel 失败,应用仍然会报错,所以这不等于 AI 配置可以随意缺失。
配置说明:
| 配置 | 作用 |
|---|---|
base-url | DeepSeek OpenAI-compatible 接口地址 |
api-key | 密钥 |
model-name | 当前使用的 DeepSeek 模型,可由环境变量覆盖 |
temperature | 降低回答随机性,便于后续业务 Agent 使用 |
timeout | 防止模型请求无限等待 |
max-retries | 短暂网络错误时进行有限重试 |
log-requests / log-responses | 默认不记录完整对话内容 |
API Key 只能由环境变量或密钥管理服务提供,不能写进 YAML,更不能提交到 Git。
5. 定义第一个聊天 Service
创建:
mall-ai/src/main/java/com/macro/mall/ai/assistant/MallAssistant.java内容如下:
package com.macro.mall.ai.assistant;
import dev.langchain4j.model.chat.ChatModel;import org.springframework.stereotype.Service;
@Servicepublic class MallAssistant {
private static final String SYSTEM_PROMPT = """ You are the AgentShop mall AI assistant. Current capabilities: 1. You can answer general shopping, product, order, after-sales, and mall usage questions. 2. You cannot query real products, inventory, orders, logistics, coupons, or after-sales records yet. 3. Do not claim that you have queried AgentShop business data. 4. If the user asks for real business data, explain that tool access is not connected yet. 5. Answer in concise and friendly Chinese.
User question: """;
private final ChatModel chatModel;
public MallAssistant(ChatModel chatModel) { this.chatModel = chatModel; }
public String chat(String message) { return chatModel.chat(SYSTEM_PROMPT + message); }}这里最重要的不是“你是一个有帮助的助手”,而是明确当前能力边界:
没有 Tool → 不得声称查过商品和订单没有 RAG → 不得声称引用了商城规则没有 Memory → 不得声称记住了之前的会话系统提示词不能真正保证模型永远不犯错,但它至少把当前阶段的限制告诉了模型。
6. Starter 如何把模型注入 Service
这一版没有自定义 MallAiProperties,也没有 LangChain4jConfig。调用链是:
mall-ai-langchain4j.yml→ LangChain4j Starter 自动配置→ 创建 ChatModel Bean→ Spring 通过构造器注入 MallAssistantMallAssistant 仍然是必要的业务边界。Controller 不直接调用 ChatModel,后续加入 Tool、Memory 和 RAG 时,可以优先在 Service 层演进。当前直接拼接系统提示词是第一阶段的最小实现,复杂 Agent 再转向 AI Services 抽象。
7. 创建聊天请求 DTO
创建:
mall-ai/src/main/java/com/macro/mall/ai/dto/AiChatRequest.javapackage com.macro.mall.ai.dto;
import jakarta.validation.constraints.NotBlank;import jakarta.validation.constraints.Size;
public class AiChatRequest {
@NotBlank(message = "Message cannot be blank") @Size(max = 2000, message = "Message cannot exceed 2000 characters") private String message;
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }}即使模型支持很长的上下文,业务接口也不应该无限接收字符串。
限制输入长度可以减少:
- 意外的大请求;
- 无意义 Token 消耗;
- 日志和审计压力;
- 后续 Prompt Injection 的攻击面。
2000 个字符只是第一阶段的保守值,后续应根据真实客服问题调整。
8. 暴露共享聊天入口
当前项目把第一阶段的 Controller 也放在 mall-ai:
mall-ai/src/main/java/com/macro/mall/ai/controller/MallAssistantController.javapackage com.macro.mall.ai.controller;
import com.macro.mall.ai.assistant.MallAssistant;import com.macro.mall.ai.dto.AiChatRequest;import com.macro.mall.common.api.CommonResult;import io.swagger.v3.oas.annotations.Operation;import io.swagger.v3.oas.annotations.tags.Tag;import jakarta.validation.Valid;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;
@RestController@Tag(name = "MallAssistantController", description = "Mall AI assistant")@RequestMapping("/ai")public class MallAssistantController {
private final MallAssistant mallAssistant;
public MallAssistantController(MallAssistant mallAssistant) { this.mallAssistant = mallAssistant; }
@Operation(summary = "Chat with mall AI assistant") @PostMapping("/chat") public CommonResult<String> chat(@Valid @RequestBody AiChatRequest request) { String answer = mallAssistant.chat(request.getMessage()); return CommonResult.success(answer); }}由于 Controller 位于 com.macro.mall.ai.controller,需要确保两个启动类都会扫描 com.macro.mall。mall-admin 的启动类本来就在 com.macro.mall;mall-portal 的启动类在 com.macro.mall.portal,因此显式扩大扫描范围:
@SpringBootApplication(scanBasePackages = "com.macro.mall")public class MallPortalApplication { // 启动方法省略}为了让 Swagger 也能发现这个共享 Controller,在两个应用的 springdoc.group-configs 中加入:
packages-to-scan: - com.macro.mall.ai.controller两个应用都会暴露 /ai/chat,但这不是两个独立 Controller,而是同一个 mall-ai Bean 被两个 Spring Boot 应用分别加载。两个进程依然使用各自的端口、JWT 密钥和 Security 过滤链。
这里继续使用 AgentShop 已有的 CommonResult,保持接口风格一致:
{ "code": 200, "message": "操作成功", "data": "你好,我是 AgentShop 商城智能助手。"}当前代码没有把 /ai/** 加入 secure.ignored.urls,所以聊天接口继续经过前后台各自已有的 JWT 鉴权。
虽然真正的 Tool 权限会在后续实现,但聊天入口本身先要求登录,可以减少接口被公开滥用和 API 额度被消耗。
9. 调用接口
前台与后台分别启动:
mvn -pl mall-portal -am spring-boot:runmvn -pl mall-admin -am spring-boot:run这两条命令应该在不同终端执行。
先通过对应应用原有的登录接口获得 JWT,然后调用前台或后台的 /ai/chat。例如前台:
curl -X POST "http://localhost:8085/ai/chat" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"message\":\"你好,你现在能帮我做什么?\"}"预期回答应该表达:
可以进行普通商城咨询,但暂时不能查询真实商品、订单、库存和物流。再测试能力边界:
{ "message": "帮我查询最近一笔订单的物流"}这一阶段正确的行为不是编造一个物流状态,而是明确告诉用户订单查询尚未接入。
后台入口也可以使用管理员 JWT 调用。第一阶段前后台复用同一个基础提示词,等到加入业务 Tool 时,再分别建立 PortalAssistant 和 AdminAssistant,避免后台能力暴露给前台模型上下文。
10. 不使用真实 DeepSeek 的 Controller 测试
普通单元测试不应该每次都请求 DeepSeek,否则会产生几个问题:
- 需要真实 API Key;
- 测试结果受网络影响;
- 每次执行都会消耗额度;
- 模型回答并不完全确定;
- CI 环境难以稳定运行。
共享 Controller 测试应该 Mock MallAssistant:
@WebMvcTest(MallAssistantController.class)class MallAssistantControllerTest {
@Autowired private MockMvc mockMvc;
@MockBean private MallAssistant mallAssistant;
@Test @WithMockUser void shouldReturnAssistantAnswer() throws Exception { when(mallAssistant.chat("你好")) .thenReturn("你好,我是 AgentShop 商城智能助手。");
mockMvc.perform(post("/ai/chat") .contentType(MediaType.APPLICATION_JSON) .content(""" { "message": "你好" } """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data") .value("你好,我是 AgentShop 商城智能助手。")); }
@Test @WithMockUser void shouldRejectBlankMessage() throws Exception { mockMvc.perform(post("/ai/chat") .contentType(MediaType.APPLICATION_JSON) .content(""" { "message": "" } """)) .andExpect(status().isBadRequest());
verifyNoInteractions(mallAssistant); }}测试目标是:
HTTP 参数是否正确校验Controller 是否调用 AI Service返回结构是否符合 AgentShop 规范非法输入是否在调用模型前被拒绝它不应该断言 DeepSeek 一定返回某一句自然语言。
11. 单独保留一次真实集成验证
真实模型仍然需要验证,但应该和普通单元测试分开。
可以准备一个手动执行的集成测试:
@SpringBootTest@EnabledIfEnvironmentVariable( named = "DEEPSEEK_API_KEY", matches = ".+")class DeepSeekChatIntegrationTest {
@Autowired private MallAssistant mallAssistant;
@Test void shouldCallDeepSeek() { String answer = mallAssistant.chat("只回复:连接成功");
assertThat(answer).isNotBlank(); }}这个测试只证明:
配置能够创建 ChatModelAgentShop 能访问 DeepSeekAPI Key 有效模型能返回非空结果不要断言回答必须逐字等于“连接成功”。自然语言模型可能增加标点或解释。
12. 常见问题
找不到 ChatModel Bean
检查:
mall-ai是否已加入根项目的<modules>;mall-admin和mall-portal是否正确依赖mall-ai;mall-ai-langchain4j.yml是否被打包到 classpath;application.yml是否通过spring.config.import导入共享配置;- 配置前缀是否为
langchain4j.open-ai.chat-model; DEEPSEEK_API_KEY是否存在;- 项目中是否意外声明了多个
ChatModelBean。
返回 401
可能有两种来源:
- AgentShop JWT 校验失败;
- DeepSeek API Key 无效。
要先判断 401 是商城接口返回的,还是 DeepSeek 请求返回的。
模型不存在
不要继续使用已经停用的 deepseek-chat。检查 DeepSeek 官方当前支持的模型名称,并更新:
model-name: deepseek-v4-flash请求超时
先确认:
- 当前机器能否访问
https://api.deepseek.com; - 代理配置是否生效;
- DeepSeek 服务是否可用;
- 超时时间是否过短。
日志里看不到请求内容
这是刻意配置:
log-requests: falselog-responses: false商城对话以后会包含商品、订单和售后信息,不应为了调试默认记录完整内容。
需要临时排查时,也应该使用测试数据,并在排查结束后关闭。
13. 这一阶段还不是什么
到这里,只完成了:
用户消息→ Portal / Admin 各自的应用进程与权限链→ mall-ai 中的共享 Controller→ mall-ai 中的 MallAssistant→ DeepSeek→ 普通文本回答它还没有:
- 查询真实商品;
- 查询订单;
- 调用 Java Tool;
- 使用商城知识库;
- 保存多轮 Memory;
- 执行售后工作流;
- 记录完整 Agent 轨迹。
因此现在更准确的名称是“商城 AI 助手”,而不是已经完成的商城 Agent。
如果此时问:
我的订单什么时候发货?模型没有任何真实订单数据。无论回答听起来多合理,都不能相信。
14. 为什么第一步只做聊天
把接入拆成独立阶段,可以先验证最基础的连接问题:
依赖版本是否兼容配置能否正确加载DeepSeek API 是否可访问API Key 是否有效请求和响应能否正常转换项目异常是否会被破坏如果一开始同时加入 Tool、Memory 和 RAG,出现错误时很难判断问题来自:
- 模型连接;
- Tool Schema;
- 数据库查询;
- Memory;
- 向量检索;
- Prompt;
- 权限。
先建立一条最小可验证链路,下一步再增加一个变量。
15. 下一步
下一篇准备实现:
《商城 Agent 实战(二):把商品查询包装成 Tool》
会从只读商品能力开始:
search_productsget_product_detailget_product_stock第二篇会先在 mall-portal 中实现面向普通用户的商品 Tool。后台如果需要商品管理、订单统计等能力,则在 mall-admin 中实现独立 Tool,继续复用 mall-ai 的模型接入层。不会为了复用代码,把前后台业务权限混在一起。
下一步最重要的问题不是如何写 @Tool,而是:
- Tool 应该调用 Controller 还是 Service;
- 哪些商品字段可以交给模型;
- 如何限制搜索数量;
- 查询失败怎样写回模型;
- 如何证明模型确实使用了 Tool,而不是自己编造商品。
当 DeepSeek 能够根据用户问题主动调用商城查询方法,并基于真实返回结果回答时,Agent 才开始真正连接商城业务。
参考资料
If this article helped you, please share it with others!
Some information may be outdated






