第9集:Spring AI Advisor链:像Spring MVC拦截器一样优雅 上集回顾老张通过MCP协议接入了企业所有系统工具调用变得简单了。但老李总监提出了新要求“代码太乱了检索、记忆、安全校验都堆在一起不好维护。”一、故事继续代码混乱的代价周三下午老张的客服系统代码ServicepublicclassCustomerServiceAgent{// 1. 检索知识库ListDocumentdocsvectorStore.similaritySearch(SearchRequest.builder().query(query).topK(5).build());// 2. 获取对话历史最近10条ListMessagehistorychatMemory.get(userId,10);// 3. 安全检查if(isSensitive(query)){return这个问题我不能回答;}// 4. 调用工具StringtoolResultorderTools.queryOrder(orderId);// 5. 构建PromptStringpromptbuildPrompt(history,docs,toolResult,query);// 6. 调用AIStringanswerchatClient.prompt().user(prompt).call().content();// 7. 保存历史逐条 addchatMemory.add(userId,newUserMessage(query));chatMemory.add(userId,newAssistantMessage(answer));returnanswer;}老李的批评“小张啊这代码能跑但维护起来太痛苦了。换个向量库、改个安全策略都要动这里。”二、技术引入什么是Advisor2.1 Advisor的本质Advisor 对请求/响应的增强器原始请求 → [Advisor1] → [Advisor2] → [Advisor3] → AI → [Advisor3] → [Advisor2] → [Advisor1] → 响应老张的类比“就像Spring MVC的拦截器链每个Advisor做一件事组合起来就是完整功能。”2.2 Advisor vs 手写代码方式优点缺点手写逻辑灵活耦合严重、难测试Advisor链解耦、可复用、易测试需要理解链式执行三、内置Advisor一览3.1 Spring AI内置AdvisorAdvisor作用类比QuestionAnswerAdvisorRAG检索增强给AI一本参考书MessageChatMemoryAdvisor会话记忆给AI一个记事本ToolCallingAdvisor工具调用给AI一双操作手SafeGuardAdvisor内容安全给AI一个过滤器ReReadingAdvisorRE2重读策略让AI多读一遍SimpleLoggerAdvisor请求日志AI的日志本3.2 使用示例ServicepublicclassEnhancedAgent{AutowiredprivateChatClientchatClient;AutowiredprivateVectorStorevectorStore;AutowiredprivateChatMemorychatMemory;AutowiredprivateOrderToolsorderTools;publicStringchat(StringuserId,Stringquestion){returnchatClient.prompt().user(question)// 第1层RAG检索.advisors(newQuestionAnswerAdvisor(vectorStore))// 第2层对话记忆.advisors(newMessageChatMemoryAdvisor(chatMemory,userId))// 第3层工具调用.advisors(newToolCallingAdvisor(List.of(FunctionToolCallback.builder(queryOrder,orderTools::queryOrder).description(查询订单状态).inputType(String.class).build())))// 第4层内容安全.advisors(newSafeGuardAdvisor()).call().content();}}老张的发现“哇代码从30行变成10行而且每层职责清晰”四、自定义Advisor4.1 场景业务日志记录ComponentpublicclassBusinessLogAdvisorimplementsCallAdvisor{OverridepublicChatClientResponseadviseCall(ChatClientRequestrequest,CallAdvisorChainchain){longstartTimeSystem.currentTimeMillis();// 执行后续链ChatClientResponseresponsechain.nextAdviseCall(request);// 记录日志log.info(用户{}提问{}耗时{}ms结果长度{},request.getContext().getUserId(),request.getPrompt().getTextContents(),System.currentTimeMillis()-startTime,response.getResult().getOutput().getText().length());returnresponse;}}4.2 场景敏感词过滤ComponentpublicclassSensitiveWordAdvisorimplementsCallAdvisor{privatestaticfinalListStringSENSITIVE_WORDSList.of(政治敏感词,广告关键词,...);OverridepublicChatClientResponseadviseCall(ChatClientRequestrequest,CallAdvisorChainchain){// 检查输入for(Stringword:SENSITIVE_WORDS){if(request.getPrompt().getTextContents().contains(word)){returnChatClientResponse.builder().result(newChatResponse(List.of(newGeneration(newAssistantMessage(抱歉该问题涉及敏感内容)))).build();}}// 执行后续ChatClientResponseresponsechain.nextAdviseCall(request);// 检查输出Stringcontentresponse.getResult().getOutput().getText();for(Stringword:SENSITIVE_WORDS){if(content.contains(word)){returnChatClientResponse.builder().result(newChatResponse(List.of(newGeneration(newAssistantMessage(抱歉该回答包含敏感内容)))).build();}}returnresponse;}}五、Advisor链的执行顺序5.1 顺序规则Advisor顺序值越小越先处理请求越后处理响应栈式// 定义顺序BeanpublicAdvisorragAdvisor(VectorStorestore){returnnewQuestionAnswerAdvisor(store){OverridepublicintgetOrder(){returnInteger.MIN_VALUE;}// 最先执行};}BeanpublicAdvisormemoryAdvisor(ChatMemorymemory){returnnewMessageChatMemoryAdvisor(memory){OverridepublicintgetOrder(){return0;}};}BeanpublicAdvisortoolAdvisor(){returnnewToolCallingAdvisor(...){OverridepublicintgetOrder(){returnInteger.MAX_VALUE;}// 最后执行};}5.2 执行流程图请求进入 ↓ [SafeGuardAdvisor] 安全检查最外层 ↓ [BusinessLogAdvisor] 日志记录 ↓ [QuestionAnswerAdvisor] RAG检索 ↓ [MessageChatMemoryAdvisor] 加载记忆 ↓ [ToolCallingAdvisor] 工具调用最内层 ↓ AI模型 ↓ [ToolCallingAdvisor] 处理工具结果 ↓ [MessageChatMemoryAdvisor] 保存记忆 ↓ [QuestionAnswerAdvisor] 增强Prompt ↓ [BusinessLogAdvisor] 记录响应 ↓ [SafeGuardAdvisor] 最终检查 ↓ 返回给用户六、实战老张的重构6.1 重构后的代码ConfigurationpublicclassAgentConfig{BeanpublicAdvisor[]advisors(VectorStorevectorStore,ChatMemorychatMemory,OrderToolsorderTools,McpClientmcpClient){returnnewAdvisor[]{// 顺序从高到低newSafeGuardAdvisor(),// 安全newBusinessLogAdvisor(),// 日志newQuestionAnswerAdvisor(vectorStore),// RAGnewMessageChatMemoryAdvisor(chatMemory),// 记忆newToolCallingAdvisor(mcpClient),// 工具};}}ServicepublicclassCustomerServiceAgent{AutowiredprivateChatClientchatClient;publicStringchat(StringuserId,Stringquestion){returnchatClient.prompt().user(question).advisors(advisors)// 一次性注入所有Advisor.call().content();}}老张的感慨“以前30行的代码现在5行搞定。以后加新功能加个Advisor就行不用动主逻辑。”七、下一集预告老张的代码终于优雅了但总监老李又提了挑战“现在AI只会按固定流程走能不能让它自己规划、自主决策”第10集《Agent不是遥不可及ChatClient Tool 实现你的第一个Agent》将讲述如何让AI从被动回答变成主动行动。 完整代码GitHub分支https://github.com/yinclei/SpringAITutorial/chapter09-advisor-chain 互动环节老兵问答你在项目中用过AOP或拦截器吗A. 用过Spring AOPB. 用过MyBatis拦截器C. 知道概念但没实操过D. 完全没用过下周一晚9点我们继续作者Java老兵搞AI | 关注我看Java如何玩转AI