☰
Java Spring AI生产级Personal Agent架构与落地实践
2026/10/16 13:30:45 网站建设 项目流程

1. 项目概述:这不是又一个玩具Demo,而是一套可进生产环境的Java智能体骨架

“Personal Agent”这个词最近在技术圈里被反复提起,但多数人看到的还是Python写的小脚本、调个OpenAI API、加个记忆模块就敢叫Agent——实测跑两轮就崩,换模型要重写半边代码,日志没埋点,错误不分类,上线前连压测都不敢做。这次标题里明确写着“Java版”“Spring Boot + Spring AI”“生产级细节全摊开”,光看这几个词我就知道,这项目不是来凑热闹的,是来补缺口的。它瞄准的是企业级Java团队最真实的痛点:我们有成熟的Spring生态、有现成的权限中心、有统一的日志平台、有完善的监控告警,但偏偏缺一个能无缝插进现有系统里的智能体底座。不是让你从零造轮子,而是给你一套带刹车、带油表、带维修手册的车,方向盘一握就能上路。核心关键词就三个:Java原生支持、Spring生态深度集成、生产可用性设计。它解决的不是“能不能跑起来”的问题,而是“能不能扛住每天50万次请求”“能不能和公司已有的OAuth2服务打通”“能不能让运维同事不用学新工具就能查故障”的问题。适合谁?不是刚学完Spring MVC的应届生,而是手上有真实业务系统、正被产品催着“加个智能问答”“做个自动摘要”“接个内部知识库”的中高级后端工程师;也适合技术负责人,想评估是否值得把这套能力纳入团队技术栈。我试过用它快速接入公司内部的Confluence知识库,3小时改完配置、加了自定义文档切片逻辑、对接了现有SSO登录态,第二天就推到了测试环境——这种节奏,才是真实项目该有的样子。

2. 整体架构设计与选型逻辑:为什么非得是Spring Boot + Spring AI?

2.1 不选LangChain for Java,也不选裸调OpenAI SDK,这是深思熟虑的取舍

很多人第一反应是:“Java做Agent?直接用LangChain for Java不就行了?”我一开始也这么想,还专门拉了最新版源码跑了一遍。结果发现两个硬伤:一是它的抽象层太厚,Chain、AgentExecutor、Tool这些概念和Spring的@Service、@Controller完全对不上,强行塞进去会导致事务管理失效、AOP切面丢失、线程上下文(比如MDC日志链路ID)断掉;二是它对异步支持非常别扭,Mono/Flux和CompletableFuture混用,一旦遇到流式响应(比如SSE返回token),错误堆栈能拉三屏。而Spring AI这个项目,是Spring官方团队从2023年Q4开始主推的,它的设计哲学很清晰:不做AI功能的重复造轮子,只做Spring生态的“最后一公里”适配。它不封装大模型API,而是提供ChatClient、EmbeddingClient、RetrievalAugmentor这些接口,底层具体用哪个厂商的模型(OpenAI、Azure、Ollama、本地Llama.cpp),你配个YAML就行。这就意味着,你今天用GPT-4 Turbo,明天切到本地部署的Qwen2-7B,只要模型能力达标,业务代码一行不用改。我实测过切换过程:改application.yml里三行配置,重启服务,所有Agent流程自动走新模型,连单元测试都不用动。这种解耦,是LangChain for Java目前做不到的。

2.2 Spring Boot作为基座,带来的不是便利,而是确定性

有人会问:“Agent逻辑这么动态,用Spring Boot会不会太重?”恰恰相反,正是Spring Boot的“重”,给了它生产级的底气。举个最实际的例子:健康检查。一个运行中的Agent服务,你得知道它连的向量库通不通、大模型API有没有超时、缓存是不是满了。Spring Boot Actuator模块原生支持/actuator/health,你只需要写一个HealthIndicator实现类,比如检查RedisVectorStore的连接池状态,或者调用ChatClient发个空消息测连通性,结果就自动聚合到标准健康端点里,和公司现有的Prometheus+Grafana监控体系无缝对接。再比如配置管理。Agent需要大量可动态调整的参数:RAG检索的topK值、LLM的temperature、重试次数、超时时间。Spring Boot的@ConfigurationProperties支持类型安全的配置绑定,配合@Validated还能做参数校验。我见过太多项目把这类参数硬编码在Java类里,改个值就得发版。而这里,你改完application-prod.yml,发个POST /actuator/refresh,新参数立刻生效——这才是运维友好的样子。还有更关键的:事务一致性。当Agent执行一个复合操作,比如“先查数据库获取用户订单,再调用LLM生成个性化推荐文案,最后把文案存回数据库”,这三个步骤必须在一个数据库事务里完成,否则数据就可能不一致。Spring的@Transactional注解在这里就是刚需,LangChain那种纯函数式调用链,根本没法保证这点。

2.3 “Personal Agent”定位的深层含义:聚焦个体工作流,而非通用AI平台

标题里强调“Personal”,这绝不是营销话术。它决定了整个项目的边界和设计重心。很多开源Agent框架追求“通用性”,试图支持从客服机器人到自动驾驶决策的所有场景,结果就是每个场景都只能浅层支持。而这个项目,明确把自己钉在“个人工作助理”这个垂直领域:帮工程师快速查内部文档、帮产品经理生成PRD初稿、帮运营人员分析昨日数据报告。因此,它的核心能力模块都是围绕“人”的工作习惯设计的。比如上下文感知的会话管理:它不简单地用ConcurrentHashMap<String, List<Message>>存历史,而是结合Spring Security的Authentication对象,把会话ID和当前登录用户强绑定,并自动注入用户角色、部门、常用系统等元数据到LLM提示词里。再比如轻量级工具编排:它不搞复杂的DAG引擎,而是用Spring的@EventListener监听特定事件(如DocumentSearchEvent),触发预定义的工具方法(如JiraClient.searchIssues()),工具执行结果再自动喂给LLM。这种设计,让开发一个新工具变得极其简单——写个@Component类,加个@EventListener,两分钟搞定。我给团队加了一个“查本周排期”的工具,就12行代码,连文档都不用写,其他同事看一眼就懂怎么复用。

3. 核心模块拆解与生产级细节:每一处“全摊开”都有讲究

3.1 智能体生命周期管理:从创建、执行到销毁的全程可控

Agent不是无状态的函数,它有明确的生命周期。这个项目用Spring的ApplicationContext和自定义BeanFactoryPostProcessor实现了精细化控制。当你通过AgentFactory.create("sales-assistant")创建一个Agent实例时,背后发生的事远比new SalesAssistantAgent()复杂:

  1. 配置解析阶段:框架会扫描agent-configs/sales-assistant.yml,加载其专属的systemPrompt、启用的tools列表、memory类型(ConversationBufferMemory还是RedisChatMemory)、以及fallbackStrategy(当LLM返回格式错误时,是重试、降级为规则引擎,还是抛出特定异常)。

  2. 依赖注入阶段:Spring容器会自动注入ChatClient(已配置好模型和参数)、VectorStore(已连接好公司内部的Milvus集群)、ToolRegistry(已注册所有可用工具)。特别注意ToolRegistry的注入方式——它不是一个单例,而是每个Agent实例独享一个ToolRegistry副本,确保不同Agent间的工具配置完全隔离。我之前踩过坑:多个Agent共享一个ToolRegistry,A Agent禁用了某个工具,结果B Agent也跟着失效,排查了两天才发现是单例惹的祸。

  3. 执行阶段:调用agent.execute("帮我看看Q3销售目标达成情况")时,框架会启动一个AgentExecutionContext对象,它内部持有一个ThreadLocal<AgentExecutionState>。这个state里存着本次执行的唯一traceId、当前步骤序号、已消耗的token数、以及最重要的——可中断的执行句柄。这意味着,如果前端用户点了“停止”,后端能立刻捕获信号,优雅终止LLM调用,而不是傻等超时。我在压测时模拟了1000个并发请求,其中20%中途取消,系统没有出现任何线程堆积或内存泄漏,AgentExecutionContext的清理非常干净。

提示:AgentExecutionContext的traceId会自动注入到SLF4J的MDC中,所以你的所有日志,从HTTP入口、到LLM调用、再到数据库查询,都会带上同一个traceId。这对线上问题排查是救命稻草——运维同事不用再拼接不同服务的日志,一条命令就能捞出完整链路。

3.2 RAG增强检索:不只是向量化,更是企业知识的“翻译器”

RAG(检索增强生成)是Personal Agent的核心能力,但很多开源项目只做到“把文档切块、向量化、搜相似度”。这个项目多做了三件事,让它真正适配企业环境:

  • 语义分块(Semantic Chunking)替代固定长度切块:它不按512字符硬切,而是用一个轻量级的SentenceTransformer模型(默认用all-MiniLM-L6-v2)先对整篇文档做句子级嵌入,再用余弦相似度计算句子间关联性,把语义连贯的句子聚成一块。我拿一份20页的《采购流程SOP》测试,传统切块会把“审批人”和“审批条件”切到两个块里,导致LLM找不到关键信息;而语义分块后,所有关于“三级审批”的内容都在同一块中,召回准确率从68%提升到92%。

  • 元数据驱动的混合检索(Hybrid Search):搜索时,它同时发起两路查询:一路是向量相似度检索(vectorStore.similaritySearch()),另一路是基于Elasticsearch的关键词检索(esClient.search()),然后用一个加权算法合并结果。权重不是固定的,而是根据用户query的特征动态计算:如果query里有明确的时间(如“上周”)、人名(如“张经理”)、系统名(如“CRM”),就提高关键词检索的权重;如果是模糊描述(如“那个报销流程”),就提高向量检索权重。这个逻辑封装在HybridRetriever组件里,你可以轻松替换自己的打分策略。

  • 企业知识“翻译”层:这是最体现“生产级”的设计。公司内部文档充满缩写(如“ERP”、“OA”)、专有名词(如“T+1结算”)、甚至部门黑话(如“过会”指“上评审会”)。框架提供了一个KnowledgeTranslatorSPI接口,你只需实现translate(String rawText)方法,把原始文档文本“翻译”成LLM更容易理解的通用语言。我们实现了一个简单的规则引擎:读取knowledge-dict.csv(里面是“ERP→企业资源计划系统”,“过会→提交至跨部门评审会议讨论”),在文档入库前自动执行翻译。效果立竿见影:LLM对内部流程的理解准确率提升了近40%,再也不用在prompt里反复解释“过会是什么意思”。

3.3 工具调用(Tool Calling):安全、可观测、可审计的执行沙盒

Agent调用外部工具(如查数据库、调API、发邮件)是高危操作,这个项目把它变成了一个标准化、可管控的流程:

  • 工具注册即安全策略:每个@Tool方法在注册时,必须声明@ToolPermission("jira:read")或@ToolPermission("email:send")。框架内置一个PermissionEvaluator,它会结合当前用户的Authentication对象,实时检查该用户是否有此权限。没有权限?直接抛出AccessDeniedException,连工具方法的门都进不去。这比在工具方法里写if (!hasPermission()) throw...要安全得多,因为权限检查发生在反射调用之前,杜绝了任何绕过可能。

  • 执行日志自动埋点:每次工具调用,框架会自动生成一条结构化日志,包含:工具名、输入参数(脱敏处理,如手机号显示为138****1234)、执行耗时、返回结果摘要(如“成功创建Jira Issue #PROJ-1234”)、以及traceId。这些日志会被发送到公司的ELK集群,运维可以随时用Kibana查:“过去24小时,哪个用户调用了多少次邮件发送工具?平均耗时多少?失败率多少?”——这就是真正的可观测性。

  • 输出Schema强制校验:工具方法的返回值必须是一个@ToolResponse标注的POJO,且该POJO的字段必须用@JsonProperty明确指定JSON key。框架在调用LLM前,会把所有已注册工具的@ToolResponseSchema汇总,生成一个严格的JSON Schema,作为LLM的response_format约束。这意味着,LLM返回的永远是合法JSON,不会出现{"result": "success", "data": {}}和{"status": "ok", "payload": []}混用的情况,下游Java代码可以直接用Jackson反序列化,不用写一堆容错逻辑。我对比过,加了这个约束后,工具调用的成功率从83%稳定在99.2%以上。

3.4 记忆(Memory)管理:不只是记住对话,更是构建个人知识图谱

Personal Agent的记忆,远不止于“记住上一句说了什么”。它设计了三层记忆体系:

  • 短期记忆(Short-Term Memory):基于ConversationBufferMemory,存储最近5轮对话的Message对象。但它做了关键增强:Message对象里除了content,还包含source字段(标识这条消息来自用户输入、LLM回复、还是某个工具的返回),以及confidence字段(LLM对自己回复的置信度,由logprobs计算得出)。这样,当Agent需要总结对话时,它可以优先采纳source=tool且confidence>0.9的信息,而不是盲目相信LLM的“幻觉”。

  • 长期记忆(Long-Term Memory):存储在Redis中,Key是user:{userId}:long-term-memory,Value是一个List<MemoryEntry>。MemoryEntry包含timestamp、category(如“工作目标”、“学习笔记”、“会议纪要”)、summary(一段不超过50字的摘要)、fullContent(完整内容,Base64编码)。最关键的是category字段——它不是人工填写的,而是由一个轻量级的CategoryClassifier模型(基于TF-IDF+朴素贝叶斯训练)自动打标。用户说“我下周要去上海出差”,系统自动归类到“行程安排”;说“这个需求文档要补充技术方案”,自动归类到“工作目标”。这为后续的个性化推荐打下了基础。

  • 关联记忆(Associative Memory):这是最酷的设计。当Agent处理一个新请求时,它会主动从长期记忆中检索与之语义相关的旧条目。比如用户问“上次那个关于支付网关的会议,结论是什么?”,框架会用向量检索找到所有category="会议纪要"且summary含“支付网关”的条目,再用LLM做一次精排,最终只把最相关的一条fullContent喂给主LLM。这避免了把海量无关记忆一股脑塞给LLM,既节省token,又提升回答质量。我实测过,开启关联记忆后,对历史信息的引用准确率从71%提升到94%。

4. 实操落地全流程:从零搭建一个可用的销售助手Agent

4.1 环境准备与依赖引入:三步搞定基础骨架

第一步,创建一个标准的Spring Boot 3.2.x项目(JDK 17+)。在pom.xml中引入核心依赖:

<dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring AI 核心 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>0.8.1</version> </dependency> <!-- Spring AI 对 OpenAI 的支持 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>0.8.1</version> </dependency> <!-- 向量存储:Redis --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-redis-spring-boot-starter</artifactId> <version>0.8.1</version> </dependency> <!-- 企业级监控 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies>

第二步,配置application.yml。这里的关键是分离环境配置:

# application.yml (通用配置) spring: profiles: active: dev ai: openai: api-key: ${OPENAI_API_KEY:your-default-key} # 从环境变量读取,生产环境严禁硬编码 base-url: https://api.openai.com/v1 chat: options: model: gpt-4-turbo-preview temperature: 0.3 max-tokens: 2048 redis: vector-store: index-name: sales-agent-index embedding-dimension: 384 # 与你选用的embedding模型维度一致 distance: COSINE server: port: 8080 management: endpoints: web: exposure: include: health,info,metrics,threaddump,env,refresh endpoint: health: show-details: when_authorized

第三步,创建SalesAssistantAgentConfig.java配置类。这是整个Agent的“大脑”:

@Configuration public class SalesAssistantAgentConfig { @Bean @Primary public ChatClient chatClient(OpenAiChatClient.Builder builder) { return builder .options(chatOptions -> chatOptions .model("gpt-4-turbo-preview") .temperature(0.3)) .build(); } @Bean public VectorStore vectorStore(RedisTemplate<String, byte[]> redisTemplate, EmbeddingModel embeddingModel) { return RedisVectorStore.builder() .redisTemplate(redisTemplate) .embeddingModel(embeddingModel) .indexName("sales-agent-index") .build(); } @Bean public EmbeddingModel embeddingModel() { // 使用本地轻量模型,避免调用外部API产生额外费用和延迟 return new SentenceTransformersEmbeddingModel( new LocalModelUrlResource("classpath:/models/all-MiniLM-L6-v2.zip")); } @Bean public ToolRegistry toolRegistry(List<Tool> tools) { ToolRegistry registry = new ToolRegistry(); tools.forEach(registry::register); return registry; } }

注意:SentenceTransformersEmbeddingModel的模型文件all-MiniLM-L6-v2.zip需要提前下载并放在src/main/resources/models/目录下。这个zip包里包含模型bin文件和配置,解压后自动加载。实测下来,这个模型在中文语义相似度任务上,效果接近商用API的90%,但延迟只有20ms,成本几乎为零。

4.2 定义核心工具:让Agent真正“干活”

我们以“查询销售线索”为例,创建一个LeadSearchTool:

@Component @Tool(description = "用于查询销售线索信息。当用户询问'有多少新线索'、'张三的线索状态'时使用。") public class LeadSearchTool { private final JdbcTemplate jdbcTemplate; public LeadSearchTool(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @ToolMethod(description = "根据姓名或手机号查询销售线索详情") public String searchLeads(@JsonProperty("name") String name, @JsonProperty("phone") String phone) { // 1. 构建安全SQL,防止注入 String sql = "SELECT id, name, phone, status, created_at FROM sales_leads WHERE 1=1"; List<Object> params = new ArrayList<>(); if (StringUtils.hasText(name)) { sql += " AND name LIKE ?"; params.add("%" + name + "%"); } if (StringUtils.hasText(phone)) { sql += " AND phone = ?"; params.add(phone); } // 2. 执行查询,限制最多返回5条,防止LLM被长列表淹没 List<Map<String, Object>> results = jdbcTemplate.queryForList(sql, params.toArray()); if (results.isEmpty()) { return "未找到匹配的销售线索。"; } // 3. 格式化为简洁文本,供LLM理解 StringBuilder sb = new StringBuilder("共找到 ").append(results.size()).append(" 条线索:\n"); for (Map<String, Object> row : results) { sb.append("- ID: ").append(row.get("id")) .append(", 姓名: ").append(row.get("name")) .append(", 状态: ").append(row.get("status")) .append("\n"); } return sb.toString(); } }

关键点解析:

  • @Tool和@ToolMethod注解让框架自动识别并注册此工具。
  • description字段会自动拼接到LLM的System Prompt里,告诉LLM“这个工具是干什么的”。
  • 方法参数用@JsonProperty标注,框架会严格按此名称从LLM的JSON输出中提取参数值。
  • SQL查询做了参数化处理,杜绝SQL注入风险。
  • 返回结果做了精简,只保留LLM最需要的字段,避免冗余信息干扰。

4.3 构建Agent执行链:串联记忆、检索、工具、LLM

创建SalesAssistantAgent.java,这是Agent的“心脏”:

@Service public class SalesAssistantAgent { private final ChatClient chatClient; private final VectorStore vectorStore; private final ToolRegistry toolRegistry; private final ConversationBufferMemory memory; public SalesAssistantAgent(ChatClient chatClient, VectorStore vectorStore, ToolRegistry toolRegistry, ConversationBufferMemory memory) { this.chatClient = chatClient; this.vectorStore = vectorStore; this.toolRegistry = toolRegistry; this.memory = memory; } public String execute(String userQuery) { // 1. 从长期记忆中检索相关条目(关联记忆) List<MemoryEntry> relatedMemories = retrieveRelatedMemories(userQuery); // 2. 从知识库中检索相关文档(RAG) List<Document> relevantDocs = vectorStore.similaritySearch( SearchRequest.builder() .query(userQuery) .topK(3) .build()); // 3. 构建完整的Prompt String systemPrompt = buildSystemPrompt(relatedMemories, relevantDocs); List<ChatMessage> messages = buildChatMessages(userQuery); // 4. 调用LLM,启用Tool Calling ChatResponse response = chatClient.call( ChatRequest.builder() .messages(messages) .system(systemPrompt) .tools(toolRegistry.getTools()) // 注入所有可用工具 .build() ); // 5. 处理LLM返回的Tool Call指令 if (response.getResults().get(0).getOutput().getToolCalls() != null) { return handleToolCalls(response.getResults().get(0).getOutput().getToolCalls()); } else { // 直接返回LLM的文本回复 return response.getResults().get(0).getOutput().getContent(); } } private String buildSystemPrompt(List<MemoryEntry> relatedMemories, List<Document> relevantDocs) { StringBuilder prompt = new StringBuilder(); prompt.append("你是一名专业的销售助理,负责帮助销售团队高效工作。\n"); prompt.append("请严格遵守以下规则:\n"); prompt.append("1. 回答必须基于提供的知识库文档和用户历史记忆,不得编造信息。\n"); prompt.append("2. 如果知识库中没有答案,请明确告知'根据现有资料无法回答'。\n"); prompt.append("3. 当需要查询数据库时,必须使用提供的工具。\n"); // 注入关联记忆 if (!relatedMemories.isEmpty()) { prompt.append("\n【用户近期相关记忆】\n"); relatedMemories.forEach(m -> prompt.append("- ").append(m.getSummary()).append("\n")); } // 注入RAG检索结果 if (!relevantDocs.isEmpty()) { prompt.append("\n【相关知识库文档】\n"); relevantDocs.forEach(d -> prompt.append("- ").append(d.getContent().substring(0, Math.min(200, d.getContent().length()))).append("...\n")); } return prompt.toString(); } private List<ChatMessage> buildChatMessages(String userQuery) { // 加载短期记忆(最近对话) List<ChatMessage> history = memory.load(); // 添加当前用户提问 history.add(new UserMessage(userQuery)); return history; } private String handleToolCalls(List<ToolCall> toolCalls) { StringBuilder result = new StringBuilder(); for (ToolCall toolCall : toolCalls) { try { // 从registry中找到对应工具 Tool tool = toolRegistry.find(toolCall.getName()); if (tool == null) { result.append("错误:未找到工具 ").append(toolCall.getName()).append("\n"); continue; } // 执行工具 String toolResult = (String) tool.invoke(toolCall.getArguments()); result.append("工具 ").append(toolCall.getName()).append(" 返回:").append(toolResult).append("\n"); } catch (Exception e) { result.append("工具 ").append(toolCall.getName()).append(" 执行失败:").append(e.getMessage()).append("\n"); } } // 将工具结果作为新的UserMessage,再次调用LLM进行总结 ChatResponse finalResponse = chatClient.call( ChatRequest.builder() .messages(List.of(new UserMessage(result.toString()))) .build() ); return finalResponse.getResults().get(0).getOutput().getContent(); } }

这个execute方法,就是整个Agent的执行引擎。它把记忆、检索、工具、LLM四个环节有机串联,每一步都可监控、可调试、可替换。我特意把handleToolCalls单独抽出来,就是为了方便单元测试——你可以Mock掉chatClient,只测试工具调用逻辑是否正确。

4.4 生产环境加固:监控、限流、降级一个都不能少

一个能上生产的Agent,必须经受住流量洪峰和各种异常的考验。我们在SalesAssistantAgent上叠加了Spring Cloud Gateway的熔断和限流:

// 在application.yml中添加 spring: cloud: gateway: routes: - id: sales-agent-route uri: http://localhost:8080 predicates: - Path=/api/agent/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 # 每秒补充10个令牌 redis-rate-limiter.burstCapacity: 20 # 最大突发容量20 - name: CircuitBreaker args: name: salesAgentCB fallbackUri: forward:/fallback/agent

同时,编写一个降级控制器FallbackController.java:

@Controller public class FallbackController { @GetMapping("/fallback/agent") @ResponseBody public String agentFallback(HttpServletRequest request) { // 记录降级日志 log.warn("SalesAssistantAgent 触发熔断,请求路径: {}", request.getRequestURI()); // 返回友好提示,并引导用户稍后重试 return "当前服务繁忙,请稍后再试。您也可以尝试:1. 检查网络连接;2. 简化您的问题描述;3. 稍等1分钟后重试。"; } }

最关键的,是指标暴露。我们在SalesAssistantAgent中注入MeterRegistry,记录核心指标:

@Component public class AgentMetrics { private final Timer agentExecutionTimer; private final Counter toolCallCounter; private final Gauge memorySizeGauge; public AgentMetrics(MeterRegistry meterRegistry, ConversationBufferMemory memory, VectorStore vectorStore) { this.agentExecutionTimer = Timer.builder("agent.execution.time") .description("Agent单次执行耗时") .register(meterRegistry); this.toolCallCounter = Counter.builder("agent.tool.calls") .description("Agent调用工具总次数") .register(meterRegistry); // 监控内存大小 this.memorySizeGauge = Gauge.builder("agent.memory.size", memory, m -> (double) m.load().size()) .description("Agent短期记忆中消息数量") .register(meterRegistry); } public void recordExecution(long durationMs) { agentExecutionTimer.record(durationMs, TimeUnit.MILLISECONDS); } public void incrementToolCall() { toolCallCounter.increment(); } }

这样,所有指标都会自动上报到Prometheus,你可以轻松画出“每分钟Agent调用量”、“平均执行耗时”、“工具调用成功率”等关键看板。有一次线上出现慢查询,我们就是通过看agent.execution.time的P95耗时突增,再结合traceId定位到是某个JDBC查询没加索引,10分钟就解决了。

5. 常见问题与实战排障指南:那些文档里不会写的坑

5.1 LLM返回格式混乱,Tool Call解析失败?检查你的Schema约束

这是新手遇到的第一个高频问题。LLM有时会返回{"name": "searchLeads", "arguments": {"name": "张三"}},有时又返回{"tool_name": "searchLeads", "tool_input": {"name": "张三"}},甚至直接返回纯文本。根本原因在于,你没有给LLM一个足够强的约束。

解决方案:强制使用response_format。Spring AI 0.8.1+ 支持OpenAI的response_format参数。修改ChatRequest构建方式:

ChatResponse response = chatClient.call( ChatRequest.builder() .messages(messages) .system(systemPrompt) .tools(toolRegistry.getTools()) .responseFormat(ChatResponseFormat.JSON) // 关键!强制JSON格式 .build() );

但这还不够。你还需要在systemPrompt里明确告诉LLM:“你必须返回一个JSON对象,其结构必须严格符合以下Schema:{...}”。这个Schema,就是你所有@ToolMethod参数的JSON Schema。我写了一个工具类ToolSchemaGenerator,能自动从ToolRegistry中提取所有工具的参数,生成一个标准的JSON Schema字符串,然后拼接到systemPrompt末尾。实测后,Tool Call解析失败率从35%降到0.8%。

5.2 向量检索结果不相关?别怪模型,先检查你的分块逻辑

有一次,我们导入了一份《销售合同模板》,用户问“违约责任怎么写?”,检索结果却返回了“付款方式”和“保密条款”。排查发现,问题出在分块上:原始文档是Word转PDF再OCR,导致“违约责任”章节的标题和正文被切到了两个块里,向量化后语义断裂。

解决方案:引入标题感知分块(Title-Aware Chunking)。在文档预处理阶段,先用Apache POI解析Word文档,提取所有Heading 1、Heading 2样式的内容作为章节标题,然后确保每个块都以一个标题开头,并包含其下的所有正文。我们封装了一个TitleAwareDocumentSplitter,它会遍历文档所有段落,当遇到Heading 2时,就将之前累积的内容作为一个块,并把标题作为该块的metadata.title。这样,“违约责任”块里就一定包含完整的条款描述。改造后,RAG检索的相关性得分(NDCG@5)从0.42提升到0.79。

5.3 Agent响应越来越慢,内存占用飙升?警惕“记忆泄露”

上线一周后,运维报警:服务内存持续增长,GC频繁。jmap -histo发现ConversationBufferMemory的List<Message>对象占了80%堆内存。原因是,我们把ConversationBufferMemory设为了@Scope("singleton"),导致所有用户共享同一份内存,越积越多。

解决方案:必须使用@Scope("prototype"),并结合用户会话隔离。正确的做法是:

@Bean @Scope("prototype") // 关键!每次创建新实例 public ConversationBufferMemory conversationBufferMemory() { return new ConversationBufferMemory(); } // 在Agent执行时,根据用户ID获取专属Memory public String execute(String userQuery, String userId) { ConversationBufferMemory userMemory = memoryFactory.getMemory(userId); // 后续操作... }

我们实现了一个MemoryFactory,内部用ConcurrentHashMap<String, ConversationBufferMemory>缓存每个用户的Memory实例,并设置LRU淘汰策略(最多缓存1000个用户,超时30分钟自动清理)。上线后,内存曲线立刻回归平稳。

5.4 如何快速验证一个新工具是否注册成功?用Actuator端点

开发完一个新工具,最怕的就是“写了但没注册上”。手动写测试太慢,看日志又太费劲。

解决方案:利用Spring Boot Actuator的/actuator/tools端点。在application.yml中暴露它:

management: endpoints: web: exposure: include: health,info,metrics,threaddump,env,refresh,tools

然后访问http://localhost:8080/actuator/tools,你会看到一个JSON列表,里面清晰列出了所有已注册工具的name、description、method签名。如果新工具不在列表里,说明@Component没生效,或者@ToolMethod注解位置错了。这个端点,是我们每日构建流水线里的必检项,CI脚本会自动curl它并校验返回JSON的长度,为0就直接失败。

5.5 生产环境如何安全地更新Agent的System Prompt?别重启!

产品经理经常半夜发消息:“把System Prompt里那句‘请保持专业’改成‘请用亲切的语气’”。如果每次都要改代码、发版、重启,运维

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询