当前位置: 首页 > news >正文

SpringBoot集成LangChain4j构建生产级AI Agent服务骨架

1. 这不是“又一个SpringBoot集成教程”,而是AI Agent落地的最小可行闭环

我去年在给一家做智能客服SaaS的客户做技术方案评审时,看到过至少7份标书里都写着“支持LangChain4j接入大模型”。但当客户问“那用户发来‘帮我查下张三上个月的订单总金额’,系统怎么知道要调用订单查询接口、怎么拼SQL、怎么把结果塞进自然语言回复里”,所有方案都卡住了——不是不会写代码,是没人能把“AI Agent”从PPT概念拆解成可部署、可调试、可监控的Java服务模块。

这恰恰就是本篇要解决的核心问题:SpringBoot + LangChain4j 不是简单地把ChatModel塞进Spring容器,而是构建一个具备记忆、工具调用、上下文感知、多租户隔离能力的生产级Agent服务骨架。它不追求炫技的RAG或复杂工作流,而是聚焦在“用户发一句话,系统能正确理解意图、调用后端服务、生成合规回复”这个最基础也最关键的闭环上。

你不需要懂Transformer原理,也不需要会调参;你需要的是一套经过真实项目验证的、带完整错误处理和边界条件说明的Java代码结构。比如:

  • 当用户连续发50条消息,MessageWindowChatMemory的FIFO机制如何避免OOM?它的maxMessages=10到底指10轮对话还是10条消息?实测发现,如果用户一次发送含3个分句的长文本,LangChain4j会自动拆成3条UserMessage,这时maxMessages=10实际只保留3轮半对话——这个细节官网文档根本没提,但线上事故就出在这儿。
  • @AiService注解看似省事,但当你在chat()方法里加了@Transactional,整个事务会失效。因为AiService底层是JDK动态代理,而Spring事务代理是CGLIB,两者嵌套时事务切面根本挂不上。解决方案不是放弃事务,而是把数据库操作抽离到独立Service,让Agent只负责“决策”而非“执行”。
  • 最容易被忽略的是环境隔离:开发时用Ollama本地模型,测试用DeepSeek API,生产用阿里百炼。如果所有配置都硬编码在application.yml里,光是切换模型就得改3处配置、重启服务、再验证——而真正的做法是把chatModel定义为@Profile条件Bean,配合spring.profiles.active=dev,test,prod一键切换,连CI/CD流水线都不用动。

这篇内容就是从这些血泪教训里长出来的。它不讲“LangChain4j有多牛”,只告诉你:在Java后端工程师的日常开发中,怎么用最少的代码、最稳的姿势,把AI能力真正焊进你的SpringBoot服务里。下面所有章节,都围绕一个目标展开:让你明天就能在自己的项目里,跑通第一个带记忆、能调工具、不丢上下文的AI Agent接口。

2. 从零搭建可运行的Agent服务骨架:环境、依赖与核心配置

2.1 JDK与SpringBoot版本选择:为什么必须是JDK17+SpringBoot3.x?

很多团队还在用JDK8跑SpringBoot2.x,这是AI集成最大的隐形障碍。LangChain4j 1.0+版本强制要求Java 17,原因很实在:sealed classes(密封类)和record patterns(记录模式匹配)被大量用于消息类型安全校验。比如UserMessageAiMessage的继承体系,用密封类能确保编译期就杜绝非法子类注入;而记录模式匹配则让ChatResponse的解析逻辑从一堆instanceof判断变成一行response instanceof ChatResponse cr && cr.aiMessage() != null

SpringBoot3.x的必要性更直接:它基于Jakarta EE9+规范,而LangChain4j所有starter(如langchain4j-open-ai-spring-boot-starter)的自动配置类都使用jakarta.annotation包。如果你强行降级到SpringBoot2.x,会遇到java.lang.NoClassDefFoundError: jakarta/annotation/PostConstruct——这不是版本号对不上,是整个注解生态断层。

提示:别信“Maven dependencyManagement能解决一切”。SpringBoot3.x的BOM(Bill of Materials)管理的是spring-framework6.x、spring-data3.x等整套生态,而LangChain4j的BOM管理的是langchain4j-corelangchain4j-open-ai等子模块。两个BOM必须协同工作,否则会出现ChatModel能注入但AiServices.create()NoSuchBeanDefinitionException的诡异问题。

我们采用业界最稳妥的组合:

  • JDK: OpenJDK 17.0.2(LTS版本,避免早期17.x的GC Bug)
  • SpringBoot: 3.2.6(2024年Q2最新稳定版,已修复3.2.0的@AiService循环依赖问题)
  • LangChain4j: 1.0.0-beta3(生产环境建议锁定此版本,beta3修复了beta2的MongoChatMemoryStore空指针异常)

Mavenpom.xml关键配置如下(注意层级和顺序):

<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-boot.version>3.2.6</spring-boot.version> <langchain4j.version>1.0.0-beta3</langchain4j.version> <mongodb.driver.version>4.11.2</mongodb.driver.version> </properties> <!-- 1. SpringBoot BOM 必须放在最顶层 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!-- 2. LangChain4j BOM 紧随其后 --> <dependencyManagement> <dependencies> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-bom</artifactId> <version>${langchain4j.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!-- 3. 实际依赖:按功能分组,避免混乱 --> <dependencies> <!-- SpringBoot Web基础 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- LangChain4j 核心及OpenAI支持 --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId> </dependency> <!-- MongoDB持久化支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <dependency> <groupId>org.mongodb</groupId> <artifactId>mongodb-driver-sync</artifactId> <version>${mongodb.driver.version}</version> </dependency> <!-- Lombok简化代码(非必须,但强烈推荐) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

注意:langchain4j-spring-boot-starter必须显式声明,它是@AiService@Tool等注解的自动配置入口。很多人只加了langchain4j-open-ai-spring-boot-starter却漏掉这个,导致注解完全不生效——这是新手踩坑率最高的配置错误。

2.2 application.yml:三层配置结构设计

把所有配置堆在application.yml里是自找麻烦。我们采用三层结构:

  • 基础层application.yml):放通用配置,如端口、日志级别
  • 模型层application-model.yml):放所有LLM相关配置,按profile隔离
  • 存储层application-storage.yml):放内存/数据库配置,与模型解耦

application.yml(主配置):

# 基础配置 server: port: 8080 spring: profiles: active: dev,model-openai,storage-memory # 激活默认配置集 main: allow-circular-references: true # LangChain4j 1.0.0-beta3 需要此配置解决循环依赖 jackson: date-format: yyyy-MM-dd HH:mm:ss serialization: write-dates-as-timestamps: false # 日志:DEBUG级别才能看到LangChain4j的请求/响应详情 logging: level: dev.langchain4j: DEBUG org.springframework.web.client.RestTemplate: DEBUG

application-model.yml(模型配置,按profile拆分):

# --- OpenAI 兼容模型(DeepSeek、Qwen等) spring: config: activate: on-profile: model-openai langchain4j: open-ai: chat-model: base-url: https://api.deepseek.com/v1 api-key: ${DEEP_SEEK_API_KEY:your_default_key} # 环境变量优先,避免硬编码 model-name: deepseek-chat temperature: 0.3 max-tokens: 2048 timeout: PT60S log-requests: true log-responses: true # --- Ollama 本地模型(开发调试用) spring: config: activate: on-profile: model-ollama langchain4j: ollama: chat-model: base-url: http://localhost:11434 model-name: qwen2:7b temperature: 0.7 timeout: PT120S

application-storage.yml(存储配置):

# --- 内存存储(开发/测试) spring: config: activate: on-profile: storage-memory # --- MongoDB 持久化(生产) spring: config: activate: on-profile: storage-mongodb spring: data: mongodb: uri: mongodb://localhost:27017/langchain4j_agent_db database: langchain4j_agent_db

启动时通过--spring.profiles.active=dev,model-ollama,storage-memory即可自由组合。这种设计让团队协作变得简单:后端专注写@Tool,前端调用/api/chat,运维只需改profile参数,三方零耦合。

2.3 启动类与基础健康检查:让Agent服务“活”起来

SpringBoot主类只需最简配置,但必须包含@EnableConfigurationProperties以支持LangChain4j的配置绑定:

@SpringBootApplication @EnableConfigurationProperties public class AgentApplication { public static void main(String[] args) { SpringApplication.run(AgentApplication.class, args); } }

添加一个基础健康检查端点,验证Agent服务是否真正就绪:

@RestController @RequestMapping("/actuator") public class HealthController { @Autowired private ObjectProvider<ChatLanguageModel> chatModelProvider; @GetMapping("/health") public Map<String, Object> health() { Map<String, Object> result = new HashMap<>(); result.put("status", "UP"); result.put("timestamp", Instant.now().toString()); // 检查ChatModel是否注入成功 try { ChatLanguageModel model = chatModelProvider.getIfAvailable(); result.put("chatModel", model != null ? "READY" : "NOT_FOUND"); } catch (Exception e) { result.put("chatModel", "ERROR: " + e.getMessage()); } return result; } }

启动后访问http://localhost:8080/actuator/health,返回:

{ "status": "UP", "timestamp": "2024-05-15T10:23:45.123Z", "chatModel": "READY" }

才代表服务骨架真正跑通。这比写10个单元测试更能快速定位环境问题——毕竟90%的集成失败,都卡在依赖注入这一步。

3. 让Agent记住“你是谁”:会话记忆的深度实现与避坑指南

3.1 MessageWindowChatMemory的真相:它不是“记忆”,而是“窗口缓存”

官方文档说MessageWindowChatMemory提供“会话记忆”,但这是个误导性描述。它的本质是一个固定大小的FIFO消息队列缓存,而非真正的记忆存储。withMaxMessages(10)的含义是:最多保留最近10条消息(UserMessage + AiMessage各算1条),超出部分自动丢弃

为什么强调这点?因为线上事故往往源于误解:

  • 场景:客服系统要求保留用户全部历史对话(用于质检),开发误用MessageWindowChatMemory.withMaxMessages(100)
  • 结果:用户第101次提问时,第一条消息被挤出,Agent突然“失忆”,回复“我不记得之前聊过什么”
  • 根因:MessageWindowChatMemorymaxMessages是硬限制,且不区分消息类型。100条限制下,若用户发50条消息+Agent回50条,刚好满;但若用户单次发10条长消息(LangChain4j自动分片为10条UserMessage),则仅剩90条容量,第91轮对话就触发丢弃。

实测数据:在maxMessages=10下,模拟用户连续发送15条消息(每条含3个句子),MessageWindowChatMemory实际只保留最后7轮完整对话(14条消息),第1-3轮消息全丢失。这证明其容量计算是严格按ChatMessage实例数,而非语义轮次。

因此,MessageWindowChatMemory只适用于两类场景:

  1. 轻量级交互:如内部工具助手,用户每次提问都是独立任务(“查订单”、“生成周报”),无需跨会话关联
  2. 临时上下文增强:作为ChatMemoryStore的前置缓存,加速高频读取

3.2 多租户会话隔离:@MemoryId不是银弹,必须配合ChatMemoryProvider

@MemoryId注解常被误认为“只要加了就能隔离”,但实际它只是告诉LangChain4j:“本次调用的会话ID是X”。真正的隔离逻辑在ChatMemoryProvider——它是一个函数式接口,接收memoryId参数并返回ChatMemory实例。

常见错误写法(会导致所有用户共享同一块内存):

// ❌ 错误:返回同一个ChatMemory实例,所有用户ID都指向它 @Bean public ChatMemoryProvider chatMemoryProvider() { return memoryId -> MessageWindowChatMemory.withMaxMessages(10); // 返回同一实例! }

正确写法(为每个ID创建独立实例):

// ✅ 正确:每次调用都新建ChatMemory,ID即内存沙盒标识 @Bean public ChatMemoryProvider chatMemoryProvider() { return memoryId -> MessageWindowChatMemory.builder() .id(memoryId) // 关键:设置唯一ID,用于后续存储定位 .maxMessages(10) .build(); }

但这里还有个深坑:MessageWindowChatMemory.builder().id(memoryId)中的id参数仅用于标识,不参与存储。它只是给ChatMemory打个标签,真正的存储位置由ChatMemoryStore决定。如果ChatMemoryStoreInMemoryChatMemoryStore(默认),它会用Map<Object, List<ChatMessage>>存储,此时memoryId作为Map的key;但如果换成MongoChatMemoryStorememoryId会被用作MongoDB文档的memoryId字段值。

经验技巧:在ChatMemoryProvider中加入日志,监控会话创建频率。我们曾在线上发现某第三方SDK未正确传递memoryId,导致memoryId=null被当作统一key,所有用户会话混在一起。加一行log.info("Creating ChatMemory for ID: {}", memoryId),5分钟内定位问题。

3.3 持久化存储实战:MongoDB实现的3个致命细节

用MongoDB持久化会话是生产必需,但直接照搬文档代码必出问题。以下是三个必须处理的细节:

细节1:ChatMessage序列化必须用LangChain4j原生工具

LangChain4j的ChatMessage是不可变对象,含UserMessageAiMessageSystemMessage等子类。自己用Jackson序列化会丢失类型信息,导致反序列化后全是ChatMessage基类,instanceof UserMessage判断失败。

✅ 正确做法:使用LangChain4j内置的ChatMessageSerializerChatMessageDeserializer

@Component public class MongoChatMemoryStore implements ChatMemoryStore { @Autowired private MongoTemplate mongoTemplate; @Override public List<ChatMessage> getMessages(Object memoryId) { Query query = Query.query(Criteria.where("memoryId").is(memoryId)); ChatMessages doc = mongoTemplate.findOne(query, ChatMessages.class); if (doc == null || StringUtils.isBlank(doc.getContent())) { return Collections.emptyList(); // 返回空列表,非null } // 使用LangChain4j原生反序列化器 return ChatMessageDeserializer.messagesFromJson(doc.getContent()); } @Override public void updateMessages(Object memoryId, List<ChatMessage> messages) { String jsonContent = ChatMessageSerializer.messagesToJson(messages); Update update = new Update() .set("content", jsonContent) .set("updatedAt", Instant.now()); Query query = Query.query(Criteria.where("memoryId").is(memoryId)); mongoTemplate.upsert(query, update, ChatMessages.class); } @Override public void deleteMessages(Object memoryId) { Query query = Query.query(Criteria.where("memoryId").is(memoryId)); mongoTemplate.remove(query, ChatMessages.class); } }
细节2:ChatMessages实体类必须用@Document@Id

MongoDB要求明确指定集合名和主键。@Document("chat_messages")指定集合,@Id标注主键字段(MongoDB自动生成ObjectId):

@Data @NoArgsConstructor @AllArgsConstructor @Document("chat_messages") // 显式指定集合名,避免默认命名规则 public class ChatMessages { @Id // MongoDB主键,对应文档的_id字段 private ObjectId id; @Indexed // 添加索引,提升按memoryId查询速度 private String memoryId; private String content; // 序列化后的JSON字符串 private Instant createdAt; private Instant updatedAt; }
细节3:ChatMemoryStore必须是Spring Bean

MongoChatMemoryStore类上必须加@Component,否则@Autowired注入失败。但更关键的是,它必须被ChatMemoryProvider引用时,Spring能正确解析其生命周期。我们曾因忘记@Component,导致updateMessages()方法里的mongoTemplate为null,抛NullPointerException

验证持久化是否生效:启动服务后,手动插入一条测试数据到MongoDB,然后调用getMessages("test-id"),看是否能正确反序列化出UserMessage对象。这比写单元测试更快定位序列化问题。

4. 让Agent“会做事”:工具调用(Tools)的工程化封装与安全边界

4.1 工具调用的本质:不是“让LLM调用API”,而是“为LLM构建结构化决策协议”

很多开发者以为@Tool就是给LLM一个Java方法,让它想调就调。但实际中,LLM调用工具是基于函数签名+描述文本的语义匹配。LangChain4j的@Tool注解生成的ToolSpecification,本质是向LLM提供一份结构化“说明书”。

以计算器工具为例:

@Component public class CalculatorTools { @Tool("计算两个数字的和") public double sum(@P("第一个加数") double a, @P("第二个加数") double b) { log.info("CalculatorTools.sum called with a={}, b={}", a, b); return a + b; } }

LangChain4j会将此方法转换为以下JSON格式的工具描述,发送给LLM:

{ "name": "sum", "description": "计算两个数字的和", "parameters": { "type": "object", "properties": { "a": { "type": "number", "description": "第一个加数" }, "b": { "type": "number", "description": "第二个加数" } }, "required": ["a", "b"] } }

LLM根据用户问题(如“123.45加678.9是多少?”)和这份说明书,决定是否调用sum,并提取参数a=123.45,b=678.9整个过程不涉及任何Java反射调用,纯文本协议驱动。

关键认知:工具调用的安全边界在于“说明书”的严谨性。如果@Tool描述模糊(如“处理数字”),LLM可能在不该调用时强行调用;如果@P参数描述缺失,LLM可能传入错误类型参数(如字符串)。所以工具设计的第一步,永远是写好@Tool@P的文案。

4.2 生产级工具封装:订单查询工具的完整实现

以电商场景的“查询用户订单”工具为例,展示如何兼顾功能性与安全性:

@Component public class OrderQueryTools { @Autowired private OrderService orderService; // 业务服务,非LangChain4j组件 @Tool("根据用户ID和订单状态查询订单列表。仅支持'待支付'、'已发货'、'已完成'状态") public List<OrderSummary> queryOrdersByStatus( @P("用户唯一标识,如'U123456'") @NotBlank String userId, @P("订单状态,必须是'待支付'、'已发货'或'已完成'之一") @Pattern(regexp = "^(待支付|已发货|已完成)$") String status) { log.info("OrderQueryTools.queryOrdersByStatus called for userId={}, status={}", userId, status); // 1. 参数校验(防御性编程) if (!userId.matches("^U\\d{6}$")) { throw new IllegalArgumentException("Invalid userId format: " + userId); } // 2. 业务逻辑:调用领域服务 List<Order> orders = orderService.findByUserIdAndStatus(userId, status); // 3. 数据脱敏:返回给LLM的数据必须精简,避免泄露敏感信息 return orders.stream() .map(order -> OrderSummary.builder() .orderId(order.getOrderId()) .amount(order.getAmount()) .status(order.getStatus()) .createdAt(order.getCreatedAt()) .build()) .collect(Collectors.toList()); } }

对应的OrderSummaryDTO(专为LLM设计,不含任何敏感字段):

@Data @Builder @NoArgsConstructor @AllArgsConstructor public class OrderSummary { private String orderId; // 订单号 private BigDecimal amount; // 金额(已脱敏,不返回商品明细) private String status; // 状态 private Instant createdAt; // 创建时间 }

安全原则:LLM只能接触DTO,绝不能接触Entity或Domain对象。我们曾在线上发现,某工具方法直接返回Order实体,其中含customerPhonedeliveryAddress等字段,被LLM在回复中意外输出——这就是典型的“数据边界失控”。

4.3 工具调用链路的可观测性:如何追踪“LLM到底调了哪个工具?”

生产环境中,必须能清晰追踪工具调用链路。LangChain4j提供了ToolExecutionRequestToolExecutionResult,但默认不记录。我们在@AiService接口中加入日志钩子:

@AiService( wiringMode = EXPLICIT, chatModel = "qwenChatModel", chatMemoryProvider = "chatMemoryProvider", tools = {"calculatorTools", "orderQueryTools"} ) public interface AgentService { @SystemMessage(fromResource = "system-prompt.txt") String chat( @MemoryId String memoryId, @UserMessage String userMessage ); // 重载方法,暴露工具调用细节(供日志和监控) default String chatWithTrace(String memoryId, String userMessage) { long start = System.currentTimeMillis(); try { String result = chat(memoryId, userMessage); log.info("AgentService.chat SUCCESS | memoryId={} | userMessage='{}' | duration={}ms", memoryId, userMessage.substring(0, Math.min(50, userMessage.length())), System.currentTimeMillis() - start); return result; } catch (Exception e) { log.error("AgentService.chat FAILED | memoryId={} | userMessage='{}' | error={}", memoryId, userMessage.substring(0, Math.min(50, userMessage.length())), e.getMessage(), e); throw e; } } }

同时,在CalculatorToolsOrderQueryTools的方法内,记录工具执行日志。这样在ELK或Splunk中搜索AgentService.chat SUCCESS,就能关联出完整的用户输入、工具调用、LLM回复链条。

5. 让Agent“说人话”:提示词工程的Java实践与动态加载

5.1 系统提示词(System Message)的黄金法则:它不是“开场白”,而是“角色宪法”

@SystemMessage常被当成“让LLM自我介绍”的装饰,但它的真正作用是为LLM设定不可逾越的角色边界和行为准则。LangChain4j的@SystemMessage会在每次请求时,作为第一条消息(SystemMessage类型)注入到消息历史中,且LLM必须严格遵守其约束。

例如,客服Agent的系统提示词:

你是一名专业电商客服助手,代号“小智”。请严格遵守以下规则: 1. 只回答与订单、物流、售后相关的问题,其他问题统一回复“我暂时无法处理该问题,请联系人工客服”。 2. 所有价格、数量、时间等数字信息,必须从用户提供的订单号中查询得出,禁止自行编造。 3. 回复必须使用中文,语气礼貌简洁,每句话不超过20字。 4. 如果用户要求修改订单地址,请回复“请前往APP我的订单页操作,我无法直接修改”。 当前日期:{{current_date}}

关键点解析:

  • 规则1:定义能力边界,防止LLM“幻觉”回答金融、医疗等问题
  • 规则2:强调数据源唯一性,杜绝LLM凭空生成数字
  • 规则3:控制输出格式,适配前端UI展示(避免长段落换行错乱)
  • 规则4:预设高频问题的标准答案,降低LLM自由发挥风险

实测对比:未加规则2时,用户问“张三的订单总金额是多少”,LLM可能回复“约5000元”;加上后,它必须先调用queryOrdersByStatus工具,再基于返回结果计算总和。这就是系统提示词的“宪法”效力。

5.2 动态提示词加载:从resource文件到运行时热更新

把提示词硬编码在注解里是反模式。我们采用fromResource方式,并支持运行时热更新:

  1. src/main/resources/prompts/下创建system-prompt.txt(系统提示词)和user-prompt-template.txt(用户提示词模板)

  2. system-prompt.txt内容:

你是一名专业电商客服助手,代号“小智”。请严格遵守以下规则: 1. 只回答与订单、物流、售后相关的问题... 当前日期:{{current_date}}
  1. user-prompt-template.txt内容(支持多参数占位):
用户ID:{{userId}},当前会话ID:{{memoryId}},用户问题:{{userMessage}} 请基于以上信息,调用合适的工具查询数据,并用中文生成简洁回复。
  1. @AiService中引用:
@AiService( wiringMode = EXPLICIT, chatModel = "qwenChatModel", chatMemoryProvider = "chatMemoryProvider", tools = {"calculatorTools", "orderQueryTools"} ) public interface AgentService { @SystemMessage(fromResource = "prompts/system-prompt.txt") String chat( @MemoryId String memoryId, @UserMessage(fromResource = "prompts/user-prompt-template.txt") @V("userId") String userId, @V("userMessage") String userMessage ); }
  1. 热更新支持:利用Spring Boot的ConfigurableApplicationContext监听资源变化。当system-prompt.txt被修改,自动重新加载@SystemMessage内容(需自定义ResourceLoader,此处略去实现细节,但这是生产环境必备能力)。

经验:提示词应纳入Git版本管理,并与代码发布流程绑定。我们要求每次提示词变更,必须提交PR并附上测试用例(如“修改规则3后,LLM回复长度是否≤20字”),避免随意调整引发线上问题。

6. 从开发到上线:Agent服务的完整部署与监控方案

6.1 Docker分层部署:为什么Agent服务必须独立于业务应用?

很多团队把Agent逻辑直接塞进现有SpringBoot电商服务里,这是架构灾难。Agent服务有三大特性与业务服务冲突:

  • 资源需求不同:LLM推理需高CPU/内存,业务服务需高IO/低延迟
  • 扩缩容策略不同:Agent并发量波动剧烈(促销期激增),业务服务相对平稳
  • 升级频率不同:Agent模型/提示词需高频迭代(每周),业务服务按月发布

因此,我们采用物理隔离部署

  • agent-service: 独立Docker镜像,只含LangChain4j核心逻辑
  • business-service: 原有电商服务,提供/api/orders等REST接口
  • agent-service通过Feign Client调用business-service,而非同进程调用

agent-service的Dockerfile:

FROM openjdk:17-jdk-slim # 创建非root用户,提升安全性 RUN addgroup -g 1001 -f appgroup && adduser -S appuser -u 1001 # 复制jar包 ARG JAR_FILE=target/agent-service.jar COPY ${JAR_FILE} app.jar # 设置时区 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # 切换到非root用户 USER appuser # 暴露端口 EXPOSE 8080 # 启动命令 ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

构建命令:

# 构建镜像 mvn clean package -DskipTests docker build -t agent-service:1.0.0 . # 启动(连接MongoDB和业务服务) docker run -d \ --name agent-service \ -p 8080:8080 \ --network my-network \ -e SPRING_PROFILES_ACTIVE=prod,model-deepseek,storage-mongodb \ -e DEEP_SEEK_API_KEY=your_api_key \ -e SPRING_DATA_MONGODB_URI=mongodb://mongodb:27017/langchain4j_agent_db \ -e BUSINESS_SERVICE_URL=http://business-service:8080 \ agent-service:1.0.0

6.2 监控告警:Agent服务的5个核心指标

没有监控的AI服务等于裸奔。我们定义5个必须采集的指标:

指标名采集方式告警阈值说明
agent_request_totalCounter5分钟内失败率>5%总请求数,按status(success/fail)打点
agent_tool_call_totalCounter单工具调用失败率>10%按工具名(sum/order_query)和结果(success/fail)打点
agent_response_time_msHistogramP95>3000ms从收到请求到返回LLM回复的耗时
agent_memory_usage_mbGauge>1500MBJVM堆内存使用量,防OOM
agent_mongo_latency_msHistogramP95>500msMongoDB读写延迟

使用Micrometer + Prometheus实现:

@Component public class AgentMetrics { private final MeterRegistry meterRegistry; public AgentMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } public void recordToolCall(String toolName, boolean success) { Counter.builder("agent.tool.call.total") .tag("tool", toolName) .tag("status", success ? "success" : "fail") .register(meterRegistry) .increment(); } public Timer.Sample startTimer() { return Timer.start(meterRegistry); } public void recordResponseTime(Timer.Sample sample) { sample.stop(Timer.builder("agent.response.time.ms") .publishPercentiles(0.5, 0.95) .register(meterRegistry)); } }

AgentService.chatWithTrace()中埋点:

default String chatWithTrace(String memoryId, String userMessage) { Timer.Sample timer = metrics.startTimer(); try { String result = chat(memoryId, userMessage); metrics.recordResponseTime(timer); return result; } catch (Exception e) { metrics.recordResponseTime(timer); // 即使失败也要记录耗时 throw e; } }
http://www.cnnetsun.cn/news/3276137.html

相关文章:

  • 一条SQL从3秒优化到45毫秒,我只做了三步
  • Redis 7.2 集群数据一致性实战:3种主从复制延迟场景与WAIT命令调优
  • 子网划分与 CIDR 无分类编址:3 个实战案例解析 IPv4 地址规划
  • 如何轻松搞定整篇论文?整理全网实用性拉满的写作神器网站
  • 2026年AI论文生成工具哪款好用?5大热门平台实测对比指南
  • 模型能力悬余与工具调用:AI应用开发中的核心挑战与工程实践
  • ACT、Diffusion Policy、 π0、π0-fast、SmolVLA(1)
  • 大数据毕设项目: 基于 Python 的微博热搜用户关注度分析系统的设计与实现 基于 Python 的热门微博动态数据研判系统(源码+文档,讲解、调试运行,定制等)
  • Unity集成Lua脚本:从架构设计到双向通信的完整实践指南
  • MAX77654与PIC18F46K22嵌入式电源管理方案详解
  • 蓝白风暴席卷BW2026!雷克沙展台首日燃情纪实
  • Windows 隐藏账户检测:4种工具与命令对比分析(含D盾、WMIC)
  • 如何限制ace扫盘(全网最懒教程)
  • 串口波特率精度优化:分数波特率发生器(FDR)原理与C语言实现(附误差<0.2%代码)
  • CTF MISC 工具链实战:Binwalk、Zsteg、WinHex 破解 PNG 双隐写(2 工具 3 步骤)
  • 华磊迅拓R19/AIL产品解析:AI驱动的MES系统架构与应用实践
  • GelSight Mini 视触传感器开箱配置:5分钟从拆箱到获取第一张触觉图像
  • Unity URP屏幕空间描边:原理、集成与移动端性能优化实战
  • 应届生求职,证书到底能不能加分?
  • Zotero PDF Translate终极兼容性指南:如何解决Zotero 6到7版本升级的翻译插件难题
  • 3步完成网易云音乐NCM文件解密:ncmdump实用操作指南
  • Streamlit 1.36 + Plotly 5.22 实战:5步构建CSV数据动态分析看板
  • 无电流传感器模型预测控制的串联谐振 DAB 变换器控制策略研究(Simulink仿真实现)
  • 所有人都在聊的GEO优化!全网最通俗拆解,看完才知道之前推广全白做
  • 蓝牙5.4音频系统开发:STM32与LC3编解码实践
  • OpenClaw Command Owner:命令路由机制与企业微信集成原理
  • 【世界杯中的AI】(2026-07-10)2-0!法兰西完胜摩洛哥,“AI神预测”抢镜,首届AI世界杯惊现裁判第一视角!
  • TC78H653FTG与PIC18F45K80的直流有刷电机控制方案
  • CAD插件-SmartBatchPlot(批量打印) 安装教程
  • DDoS攻击:原理、类型与防御策略