资讯动态

Java Spring AI生产级Personal Agent架构与落地实践

发布时间:2026/10/16 13:30:49 来源:尧图企业网站定制
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 AI2.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发个空消息测连通性结果就自动聚合到标准健康端点里和公司现有的PrometheusGrafana监控体系无缝对接。再比如配置管理。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初稿、帮运营人员分析昨日数据报告。因此它的核心能力模块都是围绕“人”的工作习惯设计的。比如上下文感知的会话管理它不简单地用ConcurrentHashMapString, ListMessage存历史而是结合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()复杂配置解析阶段框架会扫描agent-configs/sales-assistant.yml加载其专属的systemPrompt、启用的tools列表、memory类型ConversationBufferMemory还是RedisChatMemory、以及fallbackStrategy当LLM返回格式错误时是重试、降级为规则引擎还是抛出特定异常。依赖注入阶段Spring容器会自动注入ChatClient已配置好模型和参数、VectorStore已连接好公司内部的Milvus集群、ToolRegistry已注册所有可用工具。特别注意ToolRegistry的注入方式——它不是一个单例而是每个Agent实例独享一个ToolRegistry副本确保不同Agent间的工具配置完全隔离。我之前踩过坑多个Agent共享一个ToolRegistryA Agent禁用了某个工具结果B Agent也跟着失效排查了两天才发现是单例惹的祸。执行阶段调用agent.execute(帮我看看Q3销售目标达成情况)时框架会启动一个AgentExecutionContext对象它内部持有一个ThreadLocalAgentExecutionState。这个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”、专有名词如“T1结算”、甚至部门黑话如“过会”指“上评审会”。框架提供了一个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需要总结对话时它可以优先采纳sourcetool且confidence0.9的信息而不是盲目相信LLM的“幻觉”。长期记忆Long-Term Memory存储在Redis中Key是user:{userId}:long-term-memoryValue是一个ListMemoryEntry。MemoryEntry包含timestamp、category如“工作目标”、“学习笔记”、“会议纪要”、summary一段不超过50字的摘要、fullContent完整内容Base64编码。最关键的是category字段——它不是人工填写的而是由一个轻量级的CategoryClassifier模型基于TF-IDF朴素贝叶斯训练自动打标。用户说“我下周要去上海出差”系统自动归类到“行程安排”说“这个需求文档要补充技术方案”自动归类到“工作目标”。这为后续的个性化推荐打下了基础。关联记忆Associative Memory这是最酷的设计。当Agent处理一个新请求时它会主动从长期记忆中检索与之语义相关的旧条目。比如用户问“上次那个关于支付网关的会议结论是什么”框架会用向量检索找到所有category会议纪要且summary含“支付网关”的条目再用LLM做一次精排最终只把最相关的一条fullContent喂给主LLM。这避免了把海量无关记忆一股脑塞给LLM既节省token又提升回答质量。我实测过开启关联记忆后对历史信息的引用准确率从71%提升到94%。4. 实操落地全流程从零搭建一个可用的销售助手Agent4.1 环境准备与依赖引入三步搞定基础骨架第一步创建一个标准的Spring Boot 3.2.x项目JDK 17。在pom.xml中引入核心依赖dependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring AI 核心 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-core/artifactId version0.8.1/version /dependency !-- Spring AI 对 OpenAI 的支持 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version0.8.1/version /dependency !-- 向量存储Redis -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-redis-spring-boot-starter/artifactId version0.8.1/version /dependency !-- 企业级监控 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-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(RedisTemplateString, 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(ListTool 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真正“干活”我们以“查询销售线索”为例创建一个LeadSearchToolComponent 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 11; ListObject 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被长列表淹没 ListMapString, Object results jdbcTemplate.queryForList(sql, params.toArray()); if (results.isEmpty()) { return 未找到匹配的销售线索。; } // 3. 格式化为简洁文本供LLM理解 StringBuilder sb new StringBuilder(共找到 ).append(results.size()).append( 条线索\n); for (MapString, 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. 从长期记忆中检索相关条目关联记忆 ListMemoryEntry relatedMemories retrieveRelatedMemories(userQuery); // 2. 从知识库中检索相关文档RAG ListDocument relevantDocs vectorStore.similaritySearch( SearchRequest.builder() .query(userQuery) .topK(3) .build()); // 3. 构建完整的Prompt String systemPrompt buildSystemPrompt(relatedMemories, relevantDocs); ListChatMessage 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(ListMemoryEntry relatedMemories, ListDocument 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 ListChatMessage buildChatMessages(String userQuery) { // 加载短期记忆最近对话 ListChatMessage history memory.load(); // 添加当前用户提问 history.add(new UserMessage(userQuery)); return history; } private String handleToolCalls(ListToolCall 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.javaController 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检索的相关性得分NDCG5从0.42提升到0.79。5.3 Agent响应越来越慢内存占用飙升警惕“记忆泄露”上线一周后运维报警服务内存持续增长GC频繁。jmap -histo发现ConversationBufferMemory的ListMessage对象占了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内部用ConcurrentHashMapString, 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里那句‘请保持专业’改成‘请用亲切的语气’”。如果每次都要改代码、发版、重启运维

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑