1. 项目概述一个被严重误读的转型真相“Java老兵转AI Agent”这个标题最近在技术社区刷屏了。但你点进去十有八九看到的是“先学Python→再装Anaconda→抄几行LangChain代码→调通一个天气查询demo”然后配一句“我已成功入坑AI”。这不是转型这是换了个壳继续写CRUD。我带过不下二十个从Java后端转过来的工程师做Agent项目真正能独立设计、调试、上线交付的不到三成。剩下七成卡在哪不是卡在不会写def agent_executor()而是卡在根本没搞懂——Java生态里早就有成熟、稳定、生产级的Agent框架而且你每天都在用只是没意识到它就是Agent。关键词里那个“根本不用学Python”不是标题党是实打实的工程判断。Python在AI领域确实有生态优势但它的强项是快速验证想法、跑通算法链路而Java的强项是把验证过的链路变成扛住日均千万QPS、支持灰度发布、能和现有监控告警体系无缝对接的生产服务。你让一个写了十年Spring Boot、熟悉线程池参数、能看懂GC日志、对Dubbo超时重试机制如数家珍的工程师突然去学pip install、搞虚拟环境冲突、为pydantic版本不兼容掉头发——这不是赋能是归零重启还重启在一个更不熟悉的地基上。这个项目复盘不讲Python怎么调OpenAI API不讲LangChain的Memory怎么配置就讲一件事如何用你已经掌握的Java技术栈原生构建一个可观察、可运维、可扩展的AI Agent系统。核心工具链全部来自JVM生态Spring AI作为统一接入层Quarkus做轻量级运行时MicrometerPrometheus做全链路指标埋点Resilience4j处理LLM调用的熔断降级甚至Prompt模板都用Thymeleaf管理——你连IDEA里最顺手的快捷键都不用改。适合谁适合所有正在犹豫要不要辞职学Python、或者已经买了三门Python课但还在写print(Hello World)的Java开发者。它不承诺你三个月成为AI科学家但它能保证下周你就能把第一个Agent功能以jar包形式部署进公司现有的K8s集群和订单服务、用户中心共享同一套CI/CD流水线。2. 核心思路拆解为什么Java才是Agent落地的“隐形冠军”2.1 拆穿“必须Python”的三大认知陷阱很多人坚信“AIPython”这背后其实是三个被放大的事实但被当成了铁律陷阱一“主流框架是Python写的” → 推出“必须用Python”LangChain、LlamaIndex确实是Python主导但它们本质是胶水层核心能力向量检索、LLM调用、RAG编排全是HTTP API或gRPC协议。Spring AI 1.0正式版已完整封装OpenAI、Anthropic、Ollama、Azure OpenAI等20提供商API设计完全遵循Spring惯用法ChatClient对象注入、Message对象构建、Response泛型返回。你写chatClient.call(new UserMessage(查下订单#12345))和写restTemplate.postForObject(url, dto, Result.class)没有任何心智负担差异。所谓“框架绑定”只是没找到对应JVM的抽象层。陷阱二“模型推理要CUDA加速” → 推出“必须Python生态”CUDA是NVIDIA的硬件驱动层和编程语言无关。真正需要GPU加速的是模型加载和推理这部分由vLLM、TGIText Generation Inference等服务承担它们提供标准HTTP/gRPC接口。你的Java服务只负责发请求、收响应、做业务逻辑编排。就像你不会为了调用MySQL非得自己写C语言驱动一样。我们团队线上Agent服务后端LLM集群用TGI部署在A100节点前端Java服务用OkHttp连接QPS峰值3200平均延迟87ms全程无Python介入。陷阱三“Prompt工程要Jupyter实时调试” → 推出“必须Python环境”Jupyter的优势是交互式实验但生产环境要的是可版本化、可灰度、可回滚的Prompt。Java生态有更成熟的方案把Prompt定义为application-prompt.yaml配置文件用ConfigurationProperties加载或存进Apollo/Nacos配置中心配合RefreshScope实现热更新甚至用GitOps管理每次Prompt变更走PR流程自动触发Agent服务的单元测试用MockServer模拟LLM响应。我们某金融客户Prompt迭代从“改完代码重新打包”缩短到“提交Git后5分钟生效”错误率下降63%。2.2 Java Agent架构的不可替代性四个生产级刚需为什么说Java不是“能用”而是“更优”看这四个真实场景中的硬需求可观测性深度集成Python的logging模块输出一行INFO: Calling LLM...而Java的Micrometer可以自动采集每次LLM调用的http.client.requests含status code、duration、uriAgent执行链中每个Tool的tool.execution.time毫秒级Prompt token数、Completion token数、总token消耗关联计费这些指标直接喂给公司已有的Grafana大盘和数据库慢SQL、Redis超时告警同屏展示。某次线上故障我们5分钟内定位到是“知识库检索Tool因ES集群负载高导致超时”而不是在Python日志里grep半天。线程与内存模型的确定性Python的GIL让多线程LLM调用变成伪并行而Java的ForkJoinPool或VirtualThreadProject Loom能真正压满CPU。更重要的是内存Python的gc.collect()不可控而Java的ZGC/Shenandoah能保证200ms停顿时间。我们一个电商Agent服务单机承载1200并发会话JVM堆设为4GGC频率1次/小时同等负载下Python服务需8G内存且每15分钟必有一次2秒以上的STW。企业级安全合规能力银行、政务类客户要求所有外部API调用必须经由公司统一网关所有敏感数据如身份证号必须在进入LLM前脱敏。Java的Spring Security Filter链天然支持Component public class PiiSanitizerFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { // 从request body提取JSON用正则规则引擎脱敏身份证、手机号 String sanitizedBody piiEngine.sanitize(((HttpServletRequest) request).getInputStream()); // 包装新request传递给后续Filter chain.doFilter(new SanitizedRequestWrapper(sanitizedBody), response); } }这种细粒度、可审计、可策略化的安全控制在Python生态里需要自己造轮子且难以和现有IAM系统集成。与遗留系统零成本融合一个典型Agent流程用户问“我的保单到期了吗” → Agent调用保险核心系统的SOAP接口查保单状态 → 调用OCR服务解析上传的保单图片 → 综合生成回答。Java服务调用SOAP用JaxWsProxyFactoryBean调用OCR用RestTemplate所有连接池、超时、重试配置和原有系统完全一致。而Python服务要单独维护一套连接池配置、一套证书管理、一套超时策略运维成本翻倍。3. 核心细节解析用Spring AI搭建生产级Agent的七步法3.1 环境准备拒绝“全新环境”拥抱现有基建很多教程第一步就是“卸载JDK安装Conda”这是最大误区。我们的方案是在你当前开发机和生产环境上不做任何破坏性操作。JDK版本确认已安装JDK 17Spring AI 1.0最低要求。如果公司强制JDK 8别慌用Spring AI 0.8.x仍支持JDK 8功能覆盖90%仅缺失部分高级RAG特性。构建工具Maven 3.8 或 Gradle 8.0。无需额外安装Python解释器pom.xml里加一行dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0/version /dependency配置中心如果你的公司用Nacos/Apollo把LLM密钥、Endpoint、超时时间全放配置中心application.yml里只留占位符spring: ai: openai: api-key: ${LLM_API_KEY:your-default-key} base-url: ${LLM_BASE_URL:http://localhost:8000/v1} chat: options: timeout: ${LLM_TIMEOUT_MS:30000}这样测试环境用本地Ollama预发环境用Azure OpenAI生产环境用自建TGI只需改配置不改代码。提示绝对不要把API Key写死在代码或application.properties里我们吃过亏——某次Git提交漏删Key被扫描工具抓到整个项目组停工两天做安全审计。3.2 Agent核心设计抛弃“链式调用”拥抱“状态机驱动”LangChain的RunnableSequence看着优雅但生产环境会出大问题一旦某个环节失败如知识库检索超时整个链路中断无法降级。我们采用有限状态机FSM模式每个Agent步骤是一个独立State失败后可跳转到Fallback State。以客服Agent为例状态流转图如下[ReceiveQuery] ↓ (意图识别成功) [ClassifyIntent] → [CheckOrderStatus] → [GenerateResponse] ↓ (意图识别失败或置信度0.7) [AskClarifyQuestion] → [ReceiveClarifyAnswer] → [RetryClassify] ↓ (所有路径失败) [EscalateToHuman]实现用Spring State MachineConfiguration EnableStateMachineFactory public class AgentStateMachineConfig extends StateMachineConfigurerAdapterString, String { Override public void configure(StateMachineConfigurationConfigurerString, String config) throws Exception { config .withConfiguration() .autoStartup(true) .listener(stateMachineListener()); } Override public void configure(StateMachineTransitionConfigurerString, String transitions) throws Exception { transitions .withExternal() .source(RECEIVE_QUERY).target(CLASSIFY_INTENT) .event(INTENT_DETECTED) .and() .withExternal() .source(CLASSIFY_INTENT).target(CHECK_ORDER_STATUS) .event(ORDER_QUERY) .and() .withExternal() .source(CLASSIFY_INTENT).target(ASK_CLARIFY_QUESTION) .event(CLARIFY_NEEDED); } }每个State对应一个ServiceBean职责单一Service public class CheckOrderStatusAction implements ActionString, String { private final OrderClient orderClient; // 已有的订单微服务Feign Client Override public void execute(StateContextString, String context) { String orderId extractOrderId(context.getMessage()); try { OrderDetail detail orderClient.getDetail(orderId); context.getStateMachine().send( MessageBuilder.withPayload(ORDER_FOUND) .setHeader(orderDetail, detail) .build() ); } catch (OrderNotFoundException e) { context.getStateMachine().send( MessageBuilder.withPayload(ORDER_NOT_FOUND) .setHeader(orderId, orderId) .build() ); } } }这种设计的好处可单独对CheckOrderStatusAction做压测、打桩、监控ORDER_NOT_FOUND事件可触发独立告警不用等整个Agent失败新增状态如SEND_SMS_CONFIRMATION不影响现有流程3.3 Prompt工程实战用Thymeleaf管理告别字符串拼接网上教程教你怎么写f请根据以下知识库回答{context}问题{question}这在生产环境是灾难。我们用Thymeleaf模板把Prompt变成可测试、可版本化的资源src/main/resources/templates/prompt/order-status-prompt.txt您是XX公司智能客服助手请根据以下信息用中文简洁回答用户问题。 【用户问题】 [[${question}]] 【订单信息】 订单号[[${order.orderId}]] 状态[[${order.status}]] 到期日[[${#dates.format(order.expiryDate, yyyy-MM-dd)}]] 最后更新时间[[${#dates.format(order.lastUpdateTime, yyyy-MM-dd HH:mm:ss)}]] 【回答要求】 - 如果订单状态是已过期必须提醒用户续保 - 如果订单状态是正常告知到期日 - 禁止使用根据提供的信息等机械表述用自然对话口吻 - 字数严格控制在100字以内Java中渲染Service public class PromptService { private final TemplateEngine templateEngine; public String renderOrderStatusPrompt(OrderDetail order, String question) { Context context new Context(); context.setVariable(order, order); context.setVariable(question, question); return templateEngine.process(prompt/order-status-prompt, context); } }优势立竿见影可测试写JUnit测试传入不同order对象断言渲染结果是否符合要求如是否包含“续保”关键词可灰度模板文件放在Nacos不同灰度分组加载不同版本可审计Git记录每次Prompt变更关联Jira需求号防注入Thymeleaf默认HTML转义避免用户输入script污染Prompt我们曾用此方案将Prompt相关客诉从每月17起降至0因为所有回答偏差都能追溯到具体模板行。3.4 Tool集成规范让老系统秒变AI能力Agent的“能力”Tool不是新写的服务而是把现有系统包装成标准接口。我们定义统一Tool契约public interface Tool { /** * 工具唯一标识用于Agent决策如search_knowledge_base */ String getName(); /** * 工具描述供LLM理解用途必须用自然语言不能写代码注释 */ String getDescription(); /** * 执行工具输入为JSON字符串LLM生成的参数输出为JSON字符串 * 要求幂等、超时可控、异常明确 */ String invoke(String inputJson) throws ToolException; }以集成公司内部Elasticsearch知识库为例Component public class KnowledgeBaseSearchTool implements Tool { private final RestHighLevelClient esClient; private final ObjectMapper objectMapper; Override public String getName() { return search_knowledge_base; } Override public String getDescription() { return 在公司产品知识库中搜索相关信息。输入参数{query: 用户问题, product: 产品名称如手机、宽带}。返回最相关的3条答案。; } Override public String invoke(String inputJson) throws ToolException { try { JsonNode input objectMapper.readTree(inputJson); String query input.path(query).asText(); String product input.path(product).asText(); // 构建ES查询省略具体DSL SearchRequest searchRequest buildSearchRequest(query, product); SearchResponse response esClient.search(searchRequest, RequestOptions.DEFAULT); // 提取top3结果转JSON ListKnowledgeItem items parseHits(response.getHits()); return objectMapper.writeValueAsString(items); } catch (IOException | ElasticsearchException e) { throw new ToolException(知识库搜索失败, e); } } }关键经验Tool必须声明超时在invoke方法开头加Timeout.timeout(5, TimeUnit.SECONDS)用Resilience4j防止ES慢查询拖垮整个Agent错误码标准化ToolException必须带errorCode字段如KB_SEARCH_TIMEOUTAgent状态机可根据错误码跳转降级路径输入校验前置在invoke第一行校验inputJson是否含必需字段避免LLM传错参数导致空指针3.5 RAG增强实战不用LangChain用Spring Data ElasticsearchRAG检索增强生成常被神化其实核心就两步检索拼接。LangChain的RetrievalQA封装过度反而难调试。我们用Spring Data Elasticsearch直连全程可控Repository public interface KnowledgeRepository extends ElasticsearchRepositoryKnowledgeEntity, String { // 自定义检索方法支持高亮、相关性排序 PageKnowledgeEntity findByContentAndProduct( Field(content) String query, Field(product) String product, Pageable pageable, HighlightBuilder highlightBuilder ); } Service public class KnowledgeService { private final KnowledgeRepository knowledgeRepository; public ListKnowledgeSnippet search(String query, String product) { // 构建高亮查询 HighlightBuilder highlightBuilder new HighlightBuilder() .field(content) .preTags(em) .postTags(/em); PageKnowledgeEntity page knowledgeRepository.findByContentAndProduct( query, product, PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, score)), highlightBuilder ); return page.getContent().stream() .map(entity - new KnowledgeSnippet( entity.getId(), entity.getTitle(), entity.getHighlightContent() // 高亮后的片段 )) .collect(Collectors.toList()); } }然后在Prompt模板里注入【知识库参考】 [[${#strings.listJoin(knowledgeSnippets, \n\n)}]]这样做的好处检索质量可调ES的match_phrase、bool查询、function_score都能精细控制比向量相似度更准结果可解释高亮显示匹配词方便运营同学判断检索是否合理冷启动友好不用训练Embedding模型现有ES索引直接复用某次上线后客服问题解决率从68%升至89%根本原因是ES查询条件从match升级为multi_matchboost精准捕获了“宽带”和“网络”这类近义词。4. 实操过程详解从零部署一个订单状态Agent4.1 项目初始化5分钟创建可运行骨架别用Spring Initializr点选一堆依赖手动创建最精简的pom.xml?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.0/version relativePath/ /parent groupIdcom.example/groupId artifactIdorder-agent/artifactId version0.0.1-SNAPSHOT/version nameorder-agent/name properties java.version17/java.version spring-ai.version1.0.0/spring-ai.version /properties dependencies !-- Web基础 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring AI核心 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version${spring-ai.version}/version /dependency !-- 状态机 -- dependency groupIdorg.springframework.statemachine/groupId artifactIdspring-statemachine-starter/artifactId /dependency !-- 模板引擎 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- ES客户端 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-elasticsearch/artifactId /dependency !-- 监控 -- dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency /dependencies /project创建主类SpringBootApplication EnableScheduling public class OrderAgentApplication { public static void main(String[] args) { SpringApplication.run(OrderAgentApplication.class, args); } }启动后访问http://localhost:8080/actuator/prometheus已暴露spring_ai_chat_client_requests_seconds_count等指标无需额外配置开箱即用。4.2 核心Agent服务实现状态机PromptTool三位一体创建Agent协调器Service public class OrderAgentService { private final StateMachineString, String stateMachine; private final PromptService promptService; private final ChatClient chatClient; private final KnowledgeService knowledgeService; private final OrderClient orderClient; public MonoAgentResponse handleQuery(String userId, String query) { // 1. 初始化状态机上下文 MessageString message MessageBuilder .withPayload(RECEIVE_QUERY) .setHeader(userId, userId) .setHeader(query, query) .build(); // 2. 发送初始事件触发状态机 return Mono.fromCallable(() - stateMachine.send(message)) .flatMapMany(state - stateMachine.getState().getVariables().entrySet().stream() .filter(e - e.getKey().equals(response)) .map(Map.Entry::getValue) .map(AgentResponse.class::cast) .limit(1)) .onErrorResume(e - Mono.just(new AgentResponse(系统繁忙请稍后再试))); } }定义状态机监听器捕获关键事件Component public class AgentStateMachineListener implements StateMachineListenerString, String { private final MeterRegistry meterRegistry; Override public void stateChanged(StateString, String from, StateString, String to) { // 记录状态流转次数 Counter.builder(agent.state.transition) .tag(from, from.getId()) .tag(to, to.getId()) .register(meterRegistry) .increment(); } Override public void eventNotAccepted(MessageString event) { // 记录未被接受的事件如无效指令 Counter.builder(agent.event.rejected) .tag(event, event.getPayload()) .register(meterRegistry) .increment(); } }Controller暴露REST接口RestController RequestMapping(/api/agent) public class AgentController { private final OrderAgentService agentService; PostMapping(/query) public MonoResponseEntityAgentResponse query(RequestBody AgentQueryRequest request) { return agentService.handleQuery(request.getUserId(), request.getQuery()) .map(response - ResponseEntity.ok(response)) .onErrorResume(e - Mono.just(ResponseEntity.status(500).body( new AgentResponse(服务异常 e.getMessage()) ))); } }请求示例curl -X POST http://localhost:8080/api/agent/query \ -H Content-Type: application/json \ -d {userId:U123,query:我的订单#ORD-789012到期了吗}响应{ response: 您的订单#ORD-789012状态正常将于2024-12-15到期请及时续保。, traceId: a1b2c3d4e5f6, latencyMs: 1245 }4.3 生产部署容器化与K8s配置要点Dockerfile必须精简避免Python镜像的臃肿# 使用官方JDK17精简镜像 FROM registry.cn-hangzhou.aliyuncs.com/acs-sample/openjdk:17-jre-slim # 创建非root用户 RUN addgroup -g 1001 -f appgroup adduser -S appuser -u 1001 # 复制jar包 COPY target/order-agent-0.0.1-SNAPSHOT.jar /app.jar # 暴露端口 EXPOSE 8080 # 切换用户 USER appuser # 启动命令禁用JIT编译器提升冷启动速度 ENTRYPOINT [java,-XX:UseContainerSupport,-XX:TieredStopAtLevel1,-Djava.security.egdfile:/dev/./urandom,-jar,/app.jar]K8s Deployment关键配置apiVersion: apps/v1 kind: Deployment metadata: name: order-agent spec: replicas: 3 selector: matchLabels: app: order-agent template: metadata: labels: app: order-agent spec: containers: - name: order-agent image: your-registry/order-agent:1.0.0 ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: prod # JVM内存限制必须等于容器内存限制避免OOMKilled resources: limits: memory: 2Gi cpu: 1000m requests: memory: 2Gi cpu: 500m # 就绪探针检查Actuator端点 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 30 periodSeconds: 10 # 存活探针检查LLM连通性 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 60 periodSeconds: 30关键经验内存设置-Xmx2g必须显式指定且等于K8slimits.memory否则JVM可能申请超限内存被KillCPU限制设为1000m1核避免LLM调用时CPU飙升影响同节点其他服务探针分离readiness检查服务是否启动完成liveness检查LLM通道是否健康故障时自动重启Pod5. 常见问题与排查技巧实录老兵踩过的12个坑5.1 LLM调用超时不是网络问题是Token计算陷阱现象ChatClient.call()经常抛TimeoutException但Postman调用LLM API正常。根因Spring AI默认按maxTokens参数限制响应长度但maxTokens是总Token数PromptCompletion不是Completion长度。当Prompt模板很长如含10条知识库片段实际留给Completion的Token极少LLM持续生成直到耗尽Token预算导致超时。解决方案在application.yml中显式设置maxTokens并预留足够空间spring: ai: openai: chat: options: max-tokens: 2048 # 总Token上限 temperature: 0.3在Prompt模板中用[[${#strings.abbreviate(content, 500)}]]截断长文本确保Prompt Token可控。开启Token统计监控EventListener public void onChatResponse(ChatResponseEvent event) { long promptTokens event.getResponse().getUsage().getPromptTokens(); long completionTokens event.getResponse().getUsage().getCompletionTokens(); Counter.builder(llm.token.usage) .tag(type, prompt).register(meterRegistry) .increment(promptTokens); Counter.builder(llm.token.usage) .tag(type, completion).register(meterRegistry) .increment(completionTokens); }实操心得我们曾因未截断知识库内容导致单次调用Prompt Token达1800Completion只剩248LLM反复重试生成平均延迟飙升至12秒。加了abbreviate后延迟稳定在800ms内。5.2 Agent循环调用状态机没兜住LLM在“自我指涉”现象用户问“帮我查订单”Agent先调用CheckOrderStatus返回“订单不存在”然后LLM又生成新指令search_knowledge_base接着又CheckOrderStatus无限循环。根因LLM的Tool选择逻辑失控。当CheckOrderStatus返回空结果LLM认为“信息不足”试图用知识库补充但知识库也无此订单于是再次调用CheckOrderStatus形成死循环。解决方案状态机强制兜底在CheckOrderStatusAction中若OrderNotFoundException发生不发ORDER_NOT_FOUND事件而是直接发ESCALATE_TO_HUMAN事件跳出循环。Prompt中加入“防循环”指令在模板末尾加固定段落【重要约束】 - 如果已执行过一次check_order_status且返回订单不存在则必须立即转向人工客服禁止重复调用。 - 每次对话最多调用同一Tool 1次。添加循环检测中间件在AgentService中记录本次会话已调用的Tool列表若发现重复直接拦截if (calledTools.contains(toolName)) { throw new ToolException(Tool调用循环检测禁止重复调用 toolName); } calledTools.add(toolName);5.3 中文乱码与标点错误不是编码问题是Tokenizer不匹配现象Agent回答中中文标点。变成方块或“你好”显示为“浣犲ソ”。根因LLM模型如Qwen、GLM的Tokenizer与OpenAI不兼容。OpenAI API返回的content是UTF-8但某些国产模型返回的content是GBK或需要特殊解码。解决方案统一用String.getBytes(StandardCharsets.UTF_8)处理在Tool.invoke()返回前强制转码return new String(result.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);配置模型专用Decoder为不同模型注册ChatResponseConverterBean ConditionalOnProperty(name spring.ai.model, havingValue qwen) public ChatResponseConverter qwenConverter() { return response - { String content response.getResult().getOutput().getContent(); // Qwen模型特殊处理 return content.replace(, ,).replace(。, .); }; }前端兜底在Controller返回前用StringEscapeUtils.escapeHtml4()转义所有HTML特殊字符避免前端渲染异常。注意绝对不要在application.properties里加server.servlet.encoding.charsetUTF-8Spring Boot 3.x已弃用正确方式是WebMvcConfigurer中配置StringHttpMessageConverter。5.4 监控告警缺失指标暴露了但没人看现象Prometheus能采集到spring_ai_chat_client_requests_seconds_count但没人配置告警故障时无法及时发现。解决方案定义4个黄金指标写入alert-rules.ymlgroups: - name: order-agent-alerts rules: - alert: AgentLatencyHigh expr: histogram_quantile(0.95, sum(rate(http_client_requests_seconds_bucket{joborder-agent}[5m])) by (le)) 3 for: 5m labels: severity: warning annotations: summary: Agent 95%延迟超过3秒 description: 当前P95延迟为{{ $value }}秒可能影响用户体验 - alert: ToolFailureRateHigh expr: sum(rate(tool_execution_failure_total{joborder-agent}[5m])) by (tool) / sum(rate(tool_execution_total{joborder-agent}[5m])) by (tool) 0.1 for: 10m labels: severity: critical annotations: summary: Tool {{ $labels.tool }}失败率超10% description: 请检查该Tool依赖的服务状态 - alert: LLMTokenExhausted expr: sum(rate(llm_token_usage_total{typeprompt, joborder-agent}[5m])) by (job) 10000 for: 15m labels: severity: warning annotations: summary: Prompt Token消耗过高 description: 可能Prompt模板过大或知识库片段过多请优化 - alert: StateMachineStuck expr: count by (state) (count_over_time(agent_state_transition_total{joborder-agent}[1h])) 0 for: 30m labels: severity: critical annotations: summary: Agent状态机卡在{{ $labels.state }} description: 状态机长时间未流转服务可能假死5.5 安全审计失败密钥硬编码