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

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它的maxMessages10到底指10轮对话还是10条消息实测发现如果用户一次发送含3个分句的长文本LangChain4j会自动拆成3条UserMessage这时maxMessages10实际只保留3轮半对话——这个细节官网文档根本没提但线上事故就出在这儿。AiService注解看似省事但当你在chat()方法里加了Transactional整个事务会失效。因为AiService底层是JDK动态代理而Spring事务代理是CGLIB两者嵌套时事务切面根本挂不上。解决方案不是放弃事务而是把数据库操作抽离到独立Service让Agent只负责“决策”而非“执行”。最容易被忽略的是环境隔离开发时用Ollama本地模型测试用DeepSeek API生产用阿里百炼。如果所有配置都硬编码在application.yml里光是切换模型就得改3处配置、重启服务、再验证——而真正的做法是把chatModel定义为Profile条件Bean配合spring.profiles.activedev,test,prod一键切换连CI/CD流水线都不用动。这篇内容就是从这些血泪教训里长出来的。它不讲“LangChain4j有多牛”只告诉你在Java后端工程师的日常开发中怎么用最少的代码、最稳的姿势把AI能力真正焊进你的SpringBoot服务里。下面所有章节都围绕一个目标展开让你明天就能在自己的项目里跑通第一个带记忆、能调工具、不丢上下文的AI Agent接口。2. 从零搭建可运行的Agent服务骨架环境、依赖与核心配置2.1 JDK与SpringBoot版本选择为什么必须是JDK17SpringBoot3.x很多团队还在用JDK8跑SpringBoot2.x这是AI集成最大的隐形障碍。LangChain4j 1.0版本强制要求Java 17原因很实在sealed classes密封类和record patterns记录模式匹配被大量用于消息类型安全校验。比如UserMessage和AiMessage的继承体系用密封类能确保编译期就杜绝非法子类注入而记录模式匹配则让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的BOMBill of Materials管理的是spring-framework6.x、spring-data3.x等整套生态而LangChain4j的BOM管理的是langchain4j-core、langchain4j-open-ai等子模块。两个BOM必须协同工作否则会出现ChatModel能注入但AiServices.create()抛NoSuchBeanDefinitionException的诡异问题。我们采用业界最稳妥的组合JDK: OpenJDK 17.0.2LTS版本避免早期17.x的GC BugSpringBoot: 3.2.62024年Q2最新稳定版已修复3.2.0的AiService循环依赖问题LangChain4j: 1.0.0-beta3生产环境建议锁定此版本beta3修复了beta2的MongoChatMemoryStore空指针异常Mavenpom.xml关键配置如下注意层级和顺序properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding spring-boot.version3.2.6/spring-boot.version langchain4j.version1.0.0-beta3/langchain4j.version mongodb.driver.version4.11.2/mongodb.driver.version /properties !-- 1. SpringBoot BOM 必须放在最顶层 -- dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring-boot.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement !-- 2. LangChain4j BOM 紧随其后 -- dependencyManagement dependencies dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-bom/artifactId version${langchain4j.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement !-- 3. 实际依赖按功能分组避免混乱 -- dependencies !-- SpringBoot Web基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency !-- LangChain4j 核心及OpenAI支持 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai-spring-boot-starter/artifactId /dependency !-- MongoDB持久化支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-mongodb/artifactId /dependency dependency groupIdorg.mongodb/groupId artifactIdmongodb-driver-sync/artifactId version${mongodb.driver.version}/version /dependency !-- Lombok简化代码非必须但强烈推荐 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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: DEBUGapplication-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: PT120Sapplication-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.activedev,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 ObjectProviderChatLanguageModel chatModelProvider; GetMapping(/health) public MapString, Object health() { MapString, 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突然“失忆”回复“我不记得之前聊过什么”根因MessageWindowChatMemory的maxMessages是硬限制且不区分消息类型。100条限制下若用户发50条消息Agent回50条刚好满但若用户单次发10条长消息LangChain4j自动分片为10条UserMessage则仅剩90条容量第91轮对话就触发丢弃。实测数据在maxMessages10下模拟用户连续发送15条消息每条含3个句子MessageWindowChatMemory实际只保留最后7轮完整对话14条消息第1-3轮消息全丢失。这证明其容量计算是严格按ChatMessage实例数而非语义轮次。因此MessageWindowChatMemory只适用于两类场景轻量级交互如内部工具助手用户每次提问都是独立任务“查订单”、“生成周报”无需跨会话关联临时上下文增强作为ChatMemoryStore的前置缓存加速高频读取3.2 多租户会话隔离MemoryId不是银弹必须配合ChatMemoryProviderMemoryId注解常被误认为“只要加了就能隔离”但实际它只是告诉LangChain4j“本次调用的会话ID是X”。真正的隔离逻辑在ChatMemoryProvider——它是一个函数式接口接收memoryId参数并返回ChatMemory实例。常见错误写法会导致所有用户共享同一块内存// ❌ 错误返回同一个ChatMemory实例所有用户ID都指向它 Bean public ChatMemoryProvider chatMemoryProvider() { return memoryId - MessageWindowChatMemory.withMaxMessages(10); // 返回同一实例 }正确写法为每个ID创建独立实例// ✅ 正确每次调用都新建ChatMemoryID即内存沙盒标识 Bean public ChatMemoryProvider chatMemoryProvider() { return memoryId - MessageWindowChatMemory.builder() .id(memoryId) // 关键设置唯一ID用于后续存储定位 .maxMessages(10) .build(); }但这里还有个深坑MessageWindowChatMemory.builder().id(memoryId)中的id参数仅用于标识不参与存储。它只是给ChatMemory打个标签真正的存储位置由ChatMemoryStore决定。如果ChatMemoryStore是InMemoryChatMemoryStore默认它会用MapObject, ListChatMessage存储此时memoryId作为Map的key但如果换成MongoChatMemoryStorememoryId会被用作MongoDB文档的memoryId字段值。经验技巧在ChatMemoryProvider中加入日志监控会话创建频率。我们曾在线上发现某第三方SDK未正确传递memoryId导致memoryIdnull被当作统一key所有用户会话混在一起。加一行log.info(Creating ChatMemory for ID: {}, memoryId)5分钟内定位问题。3.3 持久化存储实战MongoDB实现的3个致命细节用MongoDB持久化会话是生产必需但直接照搬文档代码必出问题。以下是三个必须处理的细节细节1ChatMessage序列化必须用LangChain4j原生工具LangChain4j的ChatMessage是不可变对象含UserMessage、AiMessage、SystemMessage等子类。自己用Jackson序列化会丢失类型信息导致反序列化后全是ChatMessage基类instanceof UserMessage判断失败。✅ 正确做法使用LangChain4j内置的ChatMessageSerializer和ChatMessageDeserializerComponent public class MongoChatMemoryStore implements ChatMemoryStore { Autowired private MongoTemplate mongoTemplate; Override public ListChatMessage 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, ListChatMessage 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); } }细节2ChatMessages实体类必须用Document和IdMongoDB要求明确指定集合名和主键。Document(chat_messages)指定集合Id标注主键字段MongoDB自动生成ObjectIdData 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; }细节3ChatMemoryStore必须是Spring BeanMongoChatMemoryStore类上必须加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并提取参数a123.45,b678.9。整个过程不涉及任何Java反射调用纯文本协议驱动。关键认知工具调用的安全边界在于“说明书”的严谨性。如果Tool描述模糊如“处理数字”LLM可能在不该调用时强行调用如果P参数描述缺失LLM可能传入错误类型参数如字符串。所以工具设计的第一步永远是写好Tool和P的文案。4.2 生产级工具封装订单查询工具的完整实现以电商场景的“查询用户订单”工具为例展示如何兼顾功能性与安全性Component public class OrderQueryTools { Autowired private OrderService orderService; // 业务服务非LangChain4j组件 Tool(根据用户ID和订单状态查询订单列表。仅支持待支付、已发货、已完成状态) public ListOrderSummary 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. 业务逻辑调用领域服务 ListOrder 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实体其中含customerPhone、deliveryAddress等字段被LLM在回复中意外输出——这就是典型的“数据边界失控”。4.3 工具调用链路的可观测性如何追踪“LLM到底调了哪个工具”生产环境中必须能清晰追踪工具调用链路。LangChain4j提供了ToolExecutionRequest和ToolExecutionResult但默认不记录。我们在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; } } }同时在CalculatorTools和OrderQueryTools的方法内记录工具执行日志。这样在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方式并支持运行时热更新在src/main/resources/prompts/下创建system-prompt.txt系统提示词和user-prompt-template.txt用户提示词模板system-prompt.txt内容你是一名专业电商客服助手代号“小智”。请严格遵守以下规则 1. 只回答与订单、物流、售后相关的问题... 当前日期{{current_date}}user-prompt-template.txt内容支持多参数占位用户ID{{userId}}当前会话ID{{memoryId}}用户问题{{userMessage}} 请基于以上信息调用合适的工具查询数据并用中文生成简洁回复。在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 ); }热更新支持利用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的DockerfileFROM openjdk:17-jdk-slim # 创建非root用户提升安全性 RUN addgroup -g 1001 -f appgroup adduser -S appuser -u 1001 # 复制jar包 ARG JAR_FILEtarget/agent-service.jar COPY ${JAR_FILE} app.jar # 设置时区 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone # 切换到非root用户 USER appuser # 暴露端口 EXPOSE 8080 # 启动命令 ENTRYPOINT [java,-Djava.security.egdfile:/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_ACTIVEprod,model-deepseek,storage-mongodb \ -e DEEP_SEEK_API_KEYyour_api_key \ -e SPRING_DATA_MONGODB_URImongodb://mongodb:27017/langchain4j_agent_db \ -e BUSINESS_SERVICE_URLhttp://business-service:8080 \ agent-service:1.0.06.2 监控告警Agent服务的5个核心指标没有监控的AI服务等于裸奔。我们定义5个必须采集的指标指标名采集方式告警阈值说明agent_request_totalCounter5分钟内失败率5%总请求数按statussuccess/fail打点agent_tool_call_totalCounter单工具调用失败率10%按工具名sum/order_query和结果success/fail打点agent_response_time_msHistogramP953000ms从收到请求到返回LLM回复的耗时agent_memory_usage_mbGauge1500MBJVM堆内存使用量防OOMagent_mongo_latency_msHistogramP95500msMongoDB读写延迟使用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; } }