Spring AI Advisor切面机制深度解析:从调用链到自定义拦截实战

发布时间:2026/10/14 21:06:16
Spring AI Advisor切面机制深度解析:从调用链到自定义拦截实战 Spring AI 这套框架出来之后很多 Java 程序员第一反应是“不就是封装了 HTTP 调用嘛”。说实话我刚接触时也这么想过毕竟核心流程看起来就是拼 Prompt、调接口、拿字符串。但真正在项目里落地处理对话记忆、敏感信息拦截、上下文增强这些场景时才发现 Spring AI 藏得最深的一层设计——Advisor 切面拦截机制。这玩意儿之于 Spring AI就相当于拦截器之于 Spring MVC、切面之于 Spring AOP。没有它ChatClient 就只是一个“能用”但“不好用”的裸调用器。给大模型加持专用能力比如自动检索知识库、自动拼历史记录、自动给请求加日志审计通通离不开 Advisor。这篇文章就以第六课的形式把 Spring AI 的 Advisor 从接口设计、内置实现到自定义实战、问题排查一次性梳理明白。所有代码都基于我在模拟项目 X 里的落地经验踩过坑的细节我会单独标出来直接照着用就行。1. Advisor 架构拆解它到底在 AI 调用链路中扮演什么角色1.1 一个被忽略的抽象设计从 AOP 到 AI 调用链很多 Java 程序员看到 Advisor 这个词会心头一紧以为又是 AOP 那套切面理论。事实上的确有关联Spring AI 的设计者显然从 Spring AOP 和 Filter 链里吸取了不少灵感。Spring AI 的 ChatClient 每发起一次大模型调用中间会经过若干个 Advisor 组成的一条调用链每一个 Advisor 都可以在“真正调用大模型之前”或“拿到大模型响应之后”这两个时间点插入自己的逻辑。类比一下这就像去餐厅吃饭。你点的菜是用户 Prompt后厨是大模型Advisor 就是传菜通道上安插的几道检查口一个检查菜里有没有忌口食材敏感词过滤一个负责把上次吃过什么的口味偏好附在菜单后面聊天记忆一个在出菜时给菜品拍照留档访问日志。所有检查口走完菜才真正送到后厨加工加工完的菜再原路返回经过这些检查口回到你手里。这样一来“对话记忆”“知识检索”“访问控制”这些能力就不必侵入核心调用逻辑而是以可插拔的方式挂载在链路里这正是它被称为“灵魂架构”的原因。在具体代码层面Spring AI 1.0 GA 版本的 Advisor 体系主要落在org.springframework.ai.chat.client.advisor.api包下。核心接口是Advisor它有个全局的链路对象AdvisorChain。每次调用时ChatClient 会构建一个请求对象包含 Prompt、上下文资源、ChatModel 等然后把它抛给 AdvisorChain 去逐级处理后才真正触达 ChatModel模型返回的响应同样会倒着流经整个链每个 Advisor 都有机会对响应做二次加工。整个过程是响应式的底层基于 Reactive Streams所以 Advisor 内部往往返回MonoT类型上手时要有一点心理准备。1.2 内核接口逐个拆Advisor、AroundAdvisor、AdvisorChain为了不上手就懵我把这几个核心接口的职责边界先划清楚。从实际源码上看Advisor是最顶层的标记式抽象声明了获取名称、获取排序顺序等基础能力AroundAdvisor才是真正干活的接口它定义了一个方法用于接收请求对象和调用链对象返回响应对象。这里的“around”语义和 Spring AOP 里Around完全一致属于环绕通知——你可以在它的大包裹里做先置处理、调用下一个环节、拿到结果后做后置处理。AdvisorChain是链路顺序编排的关键。调用链内部维护一个有序的 Advisor 列表chain.next(request)方法会把当前请求交给列表里的下一个 Advisor 处理如果已经是最后一个 Advisor那么它内部就会去真正执行 ChatModel 的调用。所以站在自定义 Advisor 的角度你的代码本质上就是拿到请求 → 做点什么 → 调用chain.next(request)→ 拿到响应 → 做点什么 → 返回给你的调用方。这里有个特别容易绕晕的设计RequestResponseAdvisorRequest中存的是 Prompt、记忆、上下文资源这些“前端带到后端”的数据RequestResponseAdvisorResponse包装的是模型响应里面包含ResponseChatResponse。两个对象在链路中的可操作区域完全不同前者负责改“进门”的请求后者负责改“出门”的响应。如果你需要在请求阶段修改 Prompt 内容一定要记得把修改后的对象重新传给chain.next()因为整个链路是有不可变对象风格的改完不传就等于白改了。另外一个建议是阅读源码时核心只看两条执行路径CallAdvisorAdvisorChain非流式调用和StreamAdvisorAdvisorChain流式调用。因为两条链对响应的处理方式不同非流式把结果组装成一个完整响应流式则需要处理Flux数据流。日常开发时如果你只自定义了 Advisor 但没考虑流式场景经常会出现“非流式正常、流式没效果”的诡异问题这一点我在后面第五节会详细展开。2. 内置 Advisor 逐个击破每一个都是现成的“切面插件”2.1 聊天记忆MessageChatMemoryAdvisor 的前世今生先明确一个痛点大模型本身是无状态的。用户上一句问“推荐三本 Java 并发编程的书”下一句说“第二本的目录给我列一下”模型根本不知道“第二本”指的是什么除非你把前面的对话内容一起传过去。在引入 Advisor 之前每个 Java 程序员都要自己在业务层拼历史消息数组手动维护会话窗口代码丑且容易漏。MessageChatMemoryAdvisor就是为了解决这个问题而生的。它的工作方式从调用链视角看非常优雅请求进来时它先从ChatMemory存储器里取出该会话的历史消息把它们和当前 Prompt 合并成一个完整的消息列表并入 prompt再调用下一个环节等模型返回后它再把这次对话的用户输入和模型输出追加写入ChatMemory完成状态的更新。你甚至不需要知道历史消息是怎么拼接的只要配置一个会话 ID它就能自动完成“读历史 → 拼对话 → 调模型 → 存新记录”的完整循环。实际使用中有两个参数非常关键。一个是chatMemory存储实现框架内置了MessageWindowChatMemory和ConversationIdChatMemory等实现。前者是时间窗口记忆同时维护 N 条最近的会话消息超出窗口就丢弃适合控制 token 开销后者是按照会话 ID 精确存取适合做多轮长对话的完整持久化。另一个参数是defaultConversationId即默认会话 ID。它有一个设计上的巧妙之处如果请求里的 Prompt 没有显式携带会话 IDAdvisor 会自动使用这个默认值这样你在单会话测试场景下连会话 ID 都不用传。我最初踩过一个坑直接把从数据库查到的自定义历史消息手动拼进 Prompt结果由于发送的 Assistant 消息没有正确标注角色模型经常前后角色混乱。后来我彻底切到MessageChatMemoryAdvisor让每个消息对象严格使用UserMessage和AssistantMessage包装再由框架统一合并对话质量立刻稳定下来。装好之后最简单的调用方式是这样的ChatClient chatClient ChatClient.builder(chatModel) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory())) .build(); chatClient.prompt() .user(推荐三本并发编程的书) .advisors(advisor - advisor.param(ChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, session-001)) .call() .content();需要注意一个细节如果你的项目还同时切了多个自定义 Advisor一定要把MessageChatMemoryAdvisor放在链路中比较靠前的位置因为它负责重构整个 Prompt 的消息列表。如果它前面还有其他 Advisor 想基于“单条用户消息”做处理可能会因为消息顺序被重新编排而出现意外行为。2.2 知识库增强QuestionAnswerAdvisor 的 RAG 落地姿势如果说聊天记忆解决的是“多轮上下文”那么QuestionAnswerAdvisor解决的就是“让模型拥有你私有知识库的常识”。它的思路就是标准的 RAG检索增强生成请求进入时它先从向量数据库中检索和用户问题最相关的文档片段把片段拼成增强提示附在原 Prompt 后面再进入模型调用模型回答时就能参考这些片段而不是凭空编造。从切面实现的视角看QuestionAnswerAdvisor的核心逻辑全部集中在请求处理阶段。它会调用你注入的VectorStore做相似度搜索默认取 Top-4 的候选文档然后把这些文档内容拼接成一段系统提示词注明“请使用以下参考资料回答用户问题”。这样在模型眼里你的问题变成了“普通问题 一组带来源标记的参考资料”回答的准确性和可溯源程度都上升了一大截。检索参数同样支持动态覆盖比如你在调用时临时指定topK 8similarityThreshold 0.5它会按新参数重新检索这种灵活性在多租户场景下非常实用。一个常见的配置方式是Bean QuestionAnswerAdvisor questionAnswerAdvisor(VectorStore vectorStore) { return new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults() .withTopK(4) .withSimilarityThreshold(0.6)); }但是我强烈建议别把QuestionAnswerAdvisor当成万能检索入口用。在模拟项目 X 里我们把它的检索来源限定在系统内置的文档库同时业务侧的私有文件检索走单独的服务而不是把所有向量检索都堆到一个 Advisor 里。原因是QuestionAnswerAdvisor的检索只针对单条用户问题不太适合长文档、跨数据源的复杂检索编排强行堆功能到最后会变成一个大胶水层反而不利于排查问题。它最适合的场景就是“公司知识库问答”“产品说明书助手”这类明确、单轮或轻多轮的问答以及作为演示 RAG 落地最直观的切入点。2.3 日志与安全类 Advisor忽略它们可能会出事除了上面两个风头最劲的内置 AdvisorSpring AI 还提供了一些走“幕后”路线的实现它们在架构整洁度上的价值很容易被低估。这里说三个我在项目中实际会用到的SimpleLoggerAdvisor负责打印请求和响应的摘要日志包括 Prompt 的内容、模型响应的文本、调用耗时和 token 用量。在开发联调阶段它几乎就是排查问题的第一显微镜。我通常把它放在链路的最外层也就是最高优先级这样无论后续链路怎么处理你都能看到“原始请求”和“最终响应”不会遗漏任何环节的信息。SafeGuardAdvisor面向安全性而生它有一个内部机制检测用户输入是否存在恶意越狱提示或攻击性内容。简单说它会在请求进入模型前进行安全检查如果命中风险规则则会拦截并返回一个预设的拒绝响应而不是把危险 Prompt 原封不动传给模型。这个对生产环境的合规性和品牌保护极其重要。记忆落地时通常还要配合一个生产级的ChatMemory实现。如果只依赖内存版MessageWindowChatMemory进程一重启所有会话全部丢失所以我一般会用CassandraChatMemory或JdbcChatMemory这类支持持久化的实现配合固定会话 ID 做到跨重启连续对话。这个话题摊开讲篇幅不小后续可以单独聊。在真实项目里日志 Advisor 建议在测试环境全开、在生产环境保证请求摘要和 token 统计始终记录因为大模型调用成本肉眼可见没有日志复盘等于盲人摸象。3. 自定义 Advisor 手把手实战从 Lambda 到完整组件3.1 最简形态Lambda 表达式定义一个切面废话不多说先看最短的自定义 Advisor 长什么样。Spring AI 允许你直接用 Lambda 表达式挂一个处理函数到 ChatClient 上实现逻辑只有三步——改请求、传链、解响应ChatClient chatClient ChatClient.builder(chatModel) .defaultAdvisors((request, chain) - { // 1. 原始请求处理 Prompt prompt request.prompt(); String userText prompt.getInstructions().get(0).getText(); // 2. 把修改后的 Prompt 传给链路的下一个 Advisor或最终模型 RequestResponseAdvisorRequest newRequest RequestResponseAdvisorRequest.builder() .prompt(new Prompt(用户原始问题是 userText 请先重复一遍再做回答)) .context(request.context()) .build(); RequestResponseAdvisorResponse response chain.next(newRequest); // 3. 拿到模型原始响应包装加工后返回 return response; }) .build();这段代码虽然短但它是你能写出的最小可用自定义 Advisor完整展示了环绕语义。这里有一个对刚上手的人很重要的心理建设不要怕修改 request因为整个链路本身就属于函数式流对象创建成本远低于你的想象新建RequestResponseAdvisorRequest的时候如果业务上只需要改 Prompt其他字段如上下文资源、历史记忆原样从旧 request 拿出来复制即可。很多问题都出在链路的最后一段——如果你自定义的 Advisor 直接返回了一个全新的Response而不是基于chain.next(request)的响应做加工那么前面所有 Advisor 对响应的处理如记录日志、追加审计信息都会被绕过。正确的做事方式是永远把chain.next()返回的结果当作唯一的事实来源在其上包装。3.2 进阶实战一敏感词拦截与合规过滤光看 Lambda 形态可能会觉得不过瘾因为这种写法只能做简单加工没法把逻辑组织成一个可复用组件。接下来我用一个“敏感词过滤”的例子展示如何写一个结构完整的自定义 Advisor。核心思路是请求阶段检查用户输入的文本如果命中预先配置的敏感词库要么直接替换为星号要么直接抛异常终止链路或者返回一个固定拒绝提示。我们用一个实现AroundAdvisor和Ordered的 Spring 组件来完成Component public class SensitiveWordAdvisor implements AroundAdvisor, Ordered { private final ListString sensitiveWords List.of(违禁词A, 违规词B, 高风险词C); Override public int getOrder() { return 1; // 数值越小越靠前 } Override public String getName() { return sensitiveWordAdvisor; } Override public RequestResponseAdvisorResponse around(AdvisorChain chain, RequestResponseAdvisorRequest request) { String userText request.prompt().getInstructions().get(0).getText(); for (String word : sensitiveWords) { if (userText.contains(word)) { // 方案一直接抛异常终止调用 throw new IllegalArgumentException(输入包含敏感词禁止提交 word); // 方案二替换为星号然后继续执行 // userText userText.replace(word, ***); } } RequestResponseAdvisorRequest filteredRequest RequestResponseAdvisorRequest.builder() .prompt(new Prompt(userText, request.prompt().getOptions())) .context(request.context()) .build(); return chain.next(filteredRequest); } }这里有两个容易出错的地方。第一Prompt的构造函数有两个重载一个是只传文本一个还要传ChatOptions。在重构请求时如果不保留getOptions()OpenAI 这类模型会丢失 temperature、model 等参数配置导致输出行为不符合预期。我见过不少人在这一步直接丢 options 造成诡异行为排查半天发现是“Advisor 把参数吃了”。第二如果业务要求替换后继续调用记得把替换后的new Prompt传给chain.next()而不是拿旧的 request 传下去否则过滤形同虚设。当然替换后继续调用模型其实对用户感知不友好更推荐的做法是直接抛异常或返回固定文案因为这里涉及的往往是强合规场景你把关键词改写成星号再让模型回答反而可能造成“词义改变”的误判出错概率不低。3.3 进阶实战二给每一次 AI 调用挂上审计日志敏感词过滤属于“拦住请求”的场景。另一个高频需求是“记录每一次调用”。大模型调用和普通方法调用不一样你在代码里看到的返回只是一段文本它背后到底消耗了多少 token、花了多少时间、中间有没有重试这些元数据必须有地方能拿到。SimpleLoggerAdvisor能打日志看个大概但离业务审计要求还差得远。这时就值得自己写一个AuditLogAdvisor把每次请求的会话 ID、用户问题、响应摘要、模型耗时、token 用量写成结构化日志。表面上看起来这又是一个“请求进来打日志、响应出去打日志”的切面但有几个细节到位才能算生产级组件务必把会话 ID 作为关键路径的一部分贯穿整个链路。很多框架只把会话 ID 放在请求上下文里如果你在响应阶段想打印“本次请求来自哪个会话”可能拿不到。所以我会在请求阶段先把会话 ID 提取成局部变量等响应阶段再拼接日志。token 统计需要单独处理。ChatResponse响应对象里有usage信息包含 prompt tokens、completion tokens、total tokens这些在非流式调用下直接可取流式调用则需要聚合每一块增量。耗时统计最简单的方式是System.currentTimeMillis()差值但更好的做法是用TimeUnit.MILLISECONDS.toSeconds(...)换算成秒并保留两位小数不然日志里一大串毫秒数阅读体验极差。代码骨架大致是这样的Override public RequestResponseAdvisorResponse around(AdvisorChain chain, RequestResponseAdvisorRequest request) { String sessionId getSessionId(request); long start System.nanoTime(); RequestResponseAdvisorResponse response chain.next(request); long costMs TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); ChatResponse chatResponse response.response().getResult(); TokenUsage usage chatResponse.getMetadata().getUsage(); auditLogger.info(sessionId{}, questionLength{}, answerLength{}, costMs{}, promptTokens{}, completionTokens{}, sessionId, request.prompt().getInstructions().get(0).getText().length(), chatResponse.getResult().getOutput().getText().length(), costMs, usage.getPromptTokens(), usage.getCompletionTokens()); return response; }这样写完之后配合一个简单的 ELK 或普通文件日志就能实时监控每个会话、每个用户的大模型消费量。在模拟项目 X 中我们还额外把请求文本和响应文本分别做了截断和脱敏处理再入库确保敏感数据不会被直接写入日志系统。3.4 Order 控制多个 Advisor 的编排顺序魔法自定义 Advisor 一旦多起来“顺序”就会成为生死线。Spring AI 和 Spring AOP 一样通过Ordered接口的getOrder()控制顺序数值越小越先执行调用链。结合两个具体的例子请求阶段顺序上假设你有三个 AdvisorAuditLogAdvisororder0先记录原始请求SensitiveWordAdvisororder1再做敏感词过滤MessageChatMemoryAdvisororder2最后拼接历史消息。这个顺序的合理性在于日志记录要看到原始输入敏感词过滤要作用在原始输入上而历史消息拼接属于“为模型调用准备最后的消息列表”的动作必须最靠后。如果把MessageChatMemoryAdvisor放最前面它会把历史消息和用户问题合并成一个多消息 Prompt后面的敏感词 Advisor 就得遍历整个消息列表而不是只处理单条用户输入逻辑复杂度立刻变大。响应阶段的顺序则是反过来的。chain.next()返回时最内层的 Advisor 先拿到响应然后层层外扩。如果你有一个“给响应追加引用来源”的 Advisor又有“审计日志” Advisor那么前者越靠内order 值越大越有利因为这样日志能记录到已经加工完的最终响应。这个“请求阶段正向执行响应阶段逆向执行”的模型初看不习惯但只要记住一个口诀请求阶段先 order 小响应阶段后 order小 order 永远在外层。多画几次调用草图就熟了。4. 工具选型与依赖环境搭一个能跑通示例的工程4.1 Maven 依赖与版本坑要在项目里体验这一整套 Advisor 机制首先你需要一个能跑的 Spring AI 工程。我以模拟项目 X 实际使用的依赖组合为例dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-pgvector-store/artifactId /dependencySpring AI 的版本演进非常快早期 0.8.x 版本的 Advisor API 和 1.0 GA 有较大差异。网上很多教程还停留在旧的 API 写法上比如用AroundAdvisor的旧签名、Advisor接口里的before和after方法这些在 1.0 里都做了调整。如果你照着旧教程写代码经常遇到The method before(AdvisorRequest) is undefined之类的编译错误。我的建议很直接以你当前仓库里实际拉到的源码为准打开 IDEA 直接看接口签名胜过所有过时的博客。另外注意一个容易被忽略的点Spring AI 的 Starter 默认使用 Spring Boot 3.2 以上的版本因为响应式客户端依赖 JDK 17 相关特性。如果你的老项目还锁在 JDK 11 或 Boot 2.x那想上 Advisor 基本等于要先升级整个工程框架这个迁移成本要在项目立项前想清楚。4.2 快速验证环境的三种思路如果你不想一上来就接真实的大模型 API我想给三个由浅入深的验证思路最简单的本地 MockModel。Spring AI 有MockModel类你可以在测试环境直接让它模拟模型输出比如固定返回一段文案。这样可以先专心调试 Advisor 链路的请求加工逻辑不花钱也不怕限流。代价是 Mock 模型不具备真实语义理解能力所以只能验证“链路有没有走通”和“消息有没有正确加工”。本地小模型通过 Ollama 这类工具启动一个轻量模型然后把spring.ai.ollama.base-url指到本地端口采用与 OpenAI 兼容的方式接入。这适合开发环境里反复测试 Prompt 效果成本极低候选本地模型不少但选型时注意显存占用。生产级云模型 API配合随机 Key 做限流和预算管理。这适合验收阶段因为真实模型输出的不可控性只有在真实使用时才能暴露。我在项目的不同阶段分别用了这三种思路。最早的思路是用 Mock 推理整个 Advisor 流程因为它能让我把注意力全部集中在链路逻辑中后期接入真实模型跑通 Prompt 微调最后上生产前再切到正式模型压测链路性能。新手建议也走这个梯度别一上来就买一堆 API 调额度结果 Advisor 逻辑还没调通就烧了几百块。5. 常见问题与排查技巧实录初创团队最常踩的六个坑5.1 Advisor 不生效顺序、注册、方法名三大元凶这是群里问得最多的问题。New 出来的 Advisor 组件却完全不工作排查时我基本按这个顺序检查有没有注册到 ChatClient。ChatClient.builder(chatModel).defaultAdvisors(...)和Component自动注入是有区别的。如果你使用了自动配置Advisor 会被自动发现并挂载但如果你手写构建 ChatClient就必须显式调用defaultAdvisors()传入。这个坑最隐蔽因为项目可能一半地方用自动注入、一半地方手动构建结果自动注入生效了手动构建的地方完全没生效。Order 是不是被其他组件覆盖。有些组件在构造时会动态调整 Order 值或者你实现了Ordered但返回了Ordered.LOWEST_PRECEDENCE这样它会被排到很后面大多数前置处理早就做完了。你改写的是不是旧 signature。Spring AI 1.0 后around方法的签名有过调整如果你 IDE 里看到的是implements Advisor而非AroundAdvisor大概率跟到了旧版代码一定看当前依赖的源码。5.2 流式调用Stream下自定义 Advisor 像个“透明人”很多开发者只测试了.call()非流式调用一旦切到.stream()流式调用发现自定义 Advisor 里好像只执行了一半或者干脆完全没反应。原因在前面提到过流式调用走的是StreamAdvisorAdvisorChain每个 Advisor 拿到的响应对象类型不同是一个FluxChatResponse。如果你的自定义 Advisor 直接把RequestResponseAdvisorResponse里的响应取出来当普通对象处理那肯定拿不到任何有效数据。处理流式响应的正确思路是把自己想象成一个数据流管道工人你需要在Flux上做.map()、.doOnNext()之类的响应式变换而不是把整个流变成一个对象。比如你想对每个增量片段做日志累计可以这样做FluxChatResponse streamingResponses response.response().getChatResponseFlux(); response.response().setChatResponseFlux( streamingResponses.doOnNext(chunk - { if (chunk.getResult() ! null) { acc.append(chunk.getResult().getOutput().getText()); } }) );很多官方内置 Advisor 确实区分了call和stream两种执行路径自定义时也建议明确测试两种模式不要默认双向兼容。5.3 聊天记忆重复注入超长上下文MessageChatMemoryAdvisor用起来有一个常见副作用历史消息不断累积导致请求的 token 越来越多最终不但费用上升模型上下文也容易被撑爆。默认的MessageWindowChatMemory虽然有一个窗口上限但这个上限不会随 Prompt 长度变化动态调整设置得太小会丢失必要上下文设置得太大则浪费严重。我比较推荐的做法是结合一个“上下文压缩”自定义 Advisor在MessageChatMemoryAdvisor之后、真正调用模型之前检查当前合并后的消息总 token 数是否超出阈值如果超出则对最早的历史消息做粗粒度摘要替换。这个方案相当于给聊天记忆套上一道自适应闸门。如果没精力做复杂摘要退而求其次可以用按字符长度粗略计算 token 的方式做截断总好过无脑全量丢进模型。这个方法在长对话场景实测中效果显著成本和回答质量之间取得了不错的平衡。5.4 敏感词库更新后仍然生效旧规则最后说说运行期热更新的现实问题。自定义 Advisor 如果直接把敏感词列表定义成 static final 字段那么发布新规则就必须重新发布应用。在模拟项目 X 里我们把敏感词库外置到数据库或配置中心每次请求动态拉取并本地缓存三分钟。虽然这样每条请求会多出一次缓存查询但对生产环境而言是值得的——零重启更新规则在合规响应场景的重要性远高于那几毫秒延迟。如果你用的是云配置中心配合刷新机制其实也不难关键是组件内部别把规则列表缓存成永久静态变量至少加一个过期重载的逻辑。这也是“自定义 Advisor 从能用到好用”的分水岭。6. 最后的实战心得Advisor 带给我最深的三个认知变化如果只记住这篇文章的三个点我的建议是第一Advisor 的价值不是“省代码”而是把大模型调用从“业务代码里的一个函数调用”变成“一条前后端都暴露语义化切入点的链路”。这套机制一旦用熟你会不自觉地用“切面思维”去治理所有 AI 交互逻辑。第二别把 Spring AI 上乱七八糟的云特性当作主要学习对象本地模型跑通、自定义 Advisor 重演链路、日志审计和记忆持久化才是能把 AI 能力真正落地为业务竞争力的四块基石。第三多读源码。Spring AI 的源码量不多但里面有很多值得 Java 程序员学习的设计比如不可变 Request 对象、响应式链路、顺序编排。我在接手模拟项目 X 之前完全没想过“AI 框架竟能用 AOP 思想做得这么顺手”读完源码之后才真正理解标题里“灵魂架构”四个字的分量。最后分享一个小技巧当你自定义的 Advisor 越来越多时一定要在测试里加一个“链路自检”用例扫描当前 ChatClient 挂载的所有 Advisor 名称和它们的 order 值。这个用例大概只有二十行但它会在你重构代码、新增依赖、调整顺序时第一时间撕开潜在的故障面比任何代码 review 都来得及时。我正是因为栽过“不生效”这类坑才在项目里把这条链路自检做成了岗前必修。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询