Spring AI Advisor:构建智能拦截器实现AI调用治理与内容安全
1. 项目概述为什么我们需要一个“智能拦截器”在构建基于大语言模型的应用程序时我们常常会遇到一个核心矛盾一方面我们希望模型能够自由、灵活地生成内容以应对千变万化的用户请求另一方面我们又必须对模型的输入和输出施加必要的控制以确保内容的安全性、合规性以及符合特定的业务逻辑。直接修改提示词Prompt是一种方法但它在复杂场景下往往显得笨拙且难以维护。比如你需要在每次调用前检查用户输入是否包含敏感词或者在模型输出后统一为其添加特定的格式或水印。如果把这些逻辑都硬编码在业务代码里很快就会变得一团糟。这就是 Spring AI Advisor 登场的时候。你可以把它理解为一个专为 AI 调用设计的、功能强大的“拦截器”或“中间件”框架。它允许你在模型调用链的特定位置调用前、调用后甚至调用出错时插入自定义的逻辑而无需侵入核心的业务代码。想象一下你的应用是一个繁忙的机场每次 AI 模型调用就像一架飞机的起降。Advisor 就是那套高度自动化的空中交通管制系统它可以对所有航班请求进行安全检查内容过滤、添加飞行计划提示词增强、记录飞行日志审计甚至在出现异常时启动应急预案错误处理。最近随着 AI 应用从 demo 走向生产如何有效治理和管控 AI 调用成为了开发者社区热议的话题。Spring AI Advisor 正是 Spring 生态为应对这一挑战提供的“官方解决方案”。它不仅仅是技术上的一个工具更代表了一种清晰、解耦的架构思想。通过自定义 Advisor你可以轻松实现诸如对话上下文管理、多租户隔离、成本控制与计费、输出格式标准化、以及我们今天会重点探讨的内容安全审查等高级功能。接下来我将带你从零开始深入 Spring AI Advisor 的核心机制并手把手教你打造一个属于你自己的、功能强大的智能拦截器。2. 核心架构与设计哲学拆解要玩转 Advisor首先得理解它在 Spring AI 调用链中的位置和它的设计哲学。这能帮助你在未来设计自己的拦截逻辑时做出更合理的选择。2.1 Spring AI 调用链与 Advisor 的定位Spring AI 将一次 AI 模型的调用抽象为一条清晰的链式管道Prompt - Response。这个管道的核心是ChatClient。而Advisor则作为可插拔的组件环绕在这个核心调用周围。其核心接口ChatClientAdvisor定义了三个关键方法before(...): 在ChatClient.call()执行之前被调用。这里是干预输入Prompt的黄金位置。after(...): 在ChatClient.call()成功执行之后被调用。你可以在这里处理或转换模型的响应Response。onError(...): 当ChatClient.call()执行过程中抛出异常时被调用。用于统一的错误处理和降级。这种“环绕通知”的设计模式对于熟悉 Spring AOP面向切面编程的开发者来说会感到非常亲切。实际上Advisor 的设计理念正是借鉴了 AOP将横切关注点如日志、安全、事务从业务逻辑中剥离出来。在 AI 场景下这些横切关注点就变成了提示词工程、内容过滤、审计日志、重试机制等。一个关键的设计优势是链式组合。你可以注册多个 Advisor它们会按照注册的顺序形成一个执行链。例如你可以先用一个ContentFilterAdvisor过滤敏感输入再用一个ContextEnhancerAdvisor为提示词添加上下文最后用一个LoggingAdvisor记录本次调用。这种设计使得每个 Advisor 职责单一易于测试和复用。2.2 自定义 Advisor 的核心组件与生命周期创建一个自定义 Advisor不仅仅是实现一个接口那么简单。你需要理解以下几个核心组件及其交互的生命周期ChatClientAdvisor接口这是你的出发点。实现它并决定在before,after,onError中具体做什么。Advisor的注册与排序通过Bean方法将你的 Advisor 实现类注入 Spring 容器。Spring AI 会自动发现它们。通过Order注解或实现Ordered接口你可以精确控制多个 Advisor 的执行顺序这在某些场景下至关重要例如必须先过滤内容再添加上下文。Prompt与Response的不可变性这是一个非常重要的细节。Spring AI 中的Prompt和Response对象在设计上是不可变的。这意味着在before方法中你不能直接修改传入的Prompt对象。正确的做法是基于原Prompt创建一个新的、包含你修改内容的Prompt对象并将其返回。after方法处理Response时同理。这保证了调用链中数据传递的可靠性和可预测性。执行上下文 (AdvisorContext)before和after方法都会接收一个AdvisorContext参数。这个上下文对象是一个键值对存储它在整个 Advisor 调用链中共享。你可以在第一个 Advisor 的before方法中放入一些数据例如用户ID、会话ID然后在后续的 Advisor 或after方法中取出使用。这是 Advisor 之间传递数据的官方桥梁。理解了这个生命周期你就知道在何时、何地、以何种方式施加你的影响了。下面我们将进入实战环节通过两个典型的场景来具体实现。3. 实战一构建内容安全过滤 Advisor内容安全是生产级 AI 应用的底线。我们来实现一个ContentFilterAdvisor它在模型调用前对用户输入进行敏感词过滤和提示词注入。3.1 敏感词过滤与提示词注入假设我们有一个内部的敏感词列表任何包含这些词的查询都应该被拦截或净化。同时我们希望给每个提示词都加上一个系统级的指令比如“请用中文回答”。import org.springframework.ai.chat.client.advisor.Advisor; import org.springframework.ai.chat.client.advisor.AdvisorContext; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import java.util.List; import java.util.Set; Component Order(Ordered.HIGHEST_PRECEDENCE) // 设置为最高优先级确保最先执行 public class ContentFilterAdvisor implements Advisor { // 模拟一个敏感词库 private static final SetString SENSITIVE_WORDS Set.of(暴力, 违禁词A, 机密信息); // 系统指令 private static final String SYSTEM_INSTRUCTION 你是一个有帮助的助手请始终使用中文进行回复。; Override public Prompt before(Prompt prompt, AdvisorContext context) { String userMessage prompt.getContents(); // 1. 敏感词检测与处理 for (String word : SENSITIVE_WORDS) { if (userMessage.contains(word)) { // 策略1直接拦截返回一个安全的提示 // return new Prompt(您的问题包含不当内容我已过滤。请重新提问。); // 策略2替换敏感词示例 userMessage userMessage.replace(word, ***); // 这里我们将策略2的修改记录到上下文供后续可能使用 context.putAttribute(contentFiltered, true); } } // 2. 构建新的消息列表注入系统指令 // 注意Prompt 的 messages 是不可变列表我们需要新建一个。 ListMessage newMessages new ArrayList(prompt.getMessages()); // 检查是否已存在系统消息避免重复添加。通常系统消息是第一个。 boolean hasSystemMessage !newMessages.isEmpty() newMessages.get(0).getMessageType() MessageType.SYSTEM; if (!hasSystemMessage) { // 在列表头部插入系统消息 newMessages.add(0, new SystemMessage(SYSTEM_INSTRUCTION)); } // 3. 更新用户消息如果被修改过 // 通常用户消息是最后一个。这里我们简单处理假设最后一条是用户消息。 if (!userMessage.equals(prompt.getContents())) { int lastIndex newMessages.size() - 1; Message oldUserMessage newMessages.get(lastIndex); newMessages.set(lastIndex, new UserMessage(userMessage)); } // 4. 返回全新的、不可变的 Prompt 对象 return new Prompt(newMessages); } Override public ChatResponse after(ChatResponse response, AdvisorContext context) { // 后置处理例如记录本次调用是否触发了过滤 Boolean filtered (Boolean) context.getAttribute(contentFiltered); if (filtered ! null filtered) { // 可以在这里打日志或进行审计 System.out.println(警告本次对话输入内容已被过滤。); } // 通常直接返回原 response除非你需要修改它同样要创建新的 return response; } Override public ChatResponse onError(Throwable error, AdvisorContext context) { // 错误处理例如当内容过滤导致异常时返回一个友好的错误响应 // 注意这里返回的 ChatResponse 需要根据你的 ChatClient 实现来构造可能比较复杂。 // 更常见的做法是让异常向上传播由全局异常处理器处理。 // 这里仅作示例生产环境需谨慎。 return null; // 通常返回 null 或抛出异常 } }关键点解析与实操心得Order(HIGHEST_PRECEDENCE)内容安全必须是第一道关卡所以我们将它的优先级设为最高。确保它在任何其他修饰提示词的 Advisor如添加上下文的 Advisor之前运行。不可变性与新建对象这是最容易出错的地方。prompt.getMessages()返回的是一个不可修改的列表。任何修改都必须通过创建新的ArrayList并添加元素来完成最后用新的列表构造新的Prompt。直接操作原列表会导致UnsupportedOperationException。策略选择敏感词处理有多种策略直接拒绝、替换关键词、或返回一个完全不同的安全提示。选择哪种取决于你的业务容忍度。示例中展示了替换策略并将标记存入上下文。系统消息管理盲目添加系统消息可能导致重复。好的实践是检查现有消息列表如果已存在系统消息则可以选择合并或跳过。示例中做了简单判断。3.2 上下文管理与多轮对话支持在聊天场景中让模型记住之前的对话历史上下文至关重要。我们可以创建一个ConversationContextAdvisor来管理它。Component Order(Ordered.HIGHEST_PRECEDENCE 10) // 在内容过滤之后模型调用之前执行 public class ConversationContextAdvisor implements Advisor { // 假设我们有一个简单的内存存储来保存会话上下文。生产环境请用 Redis 或数据库。 private final MapString, ListMessage conversationStore new ConcurrentHashMap(); Override public Prompt before(Prompt prompt, AdvisorContext context) { // 1. 从请求中获取会话ID例如可以从HTTP请求头或上下文属性中获取 // 这里简化处理假设上下文里已经放好了。 String conversationId (String) context.getAttribute(conversationId); if (conversationId null) { // 如果没有会话ID则按单次请求处理不添加上下文 return prompt; } // 2. 从存储中获取该会话的历史消息 ListMessage history conversationStore.getOrDefault(conversationId, new ArrayList()); // 3. 构建新的消息列表历史消息 本次的新消息 ListMessage newMessages new ArrayList(); newMessages.addAll(history); newMessages.addAll(prompt.getMessages()); // 加入本次用户可能还有系统消息 // 4. 可选上下文窗口截断AI模型有token限制历史不能无限长。 // 这里实现一个简单的“保留最近N轮”的策略。 int maxHistoryTurns 5; // 假设最多保留5轮对话每轮一问一答算2条消息 int maxHistoryMessages maxHistoryTurns * 2; if (newMessages.size() maxHistoryMessages) { // 截断掉最老的消息保留最新的。注意要保留可能存在的系统消息。 // 更复杂的策略需要计算token数。 int startIndex newMessages.size() - maxHistoryMessages; // 确保不会把第一条系统消息截掉如果存在且在最前面 if (newMessages.get(0).getMessageType() MessageType.SYSTEM) { startIndex Math.max(1, startIndex); // 至少保留索引0的系统消息 } newMessages new ArrayList(newMessages.subList(startIndex, newMessages.size())); } // 5. 返回包含上下文的新Prompt return new Prompt(newMessages); } Override public ChatResponse after(ChatResponse response, AdvisorContext context) { // 在模型成功响应后将本次交互用户消息和AI回复存入历史 String conversationId (String) context.getAttribute(conversationId); if (conversationId ! null) { // 注意这里需要能获取到原始的用户消息和当前的AI回复消息。 // 原始用户消息可能在之前的 before 方法中已被修改如被过滤。 // 一种方法是在 before 方法中将处理后的用户消息也存入上下文。 Message userMessage (Message) context.getAttribute(processedUserMessage); Message aiMessage response.getResult().getOutput(); if (userMessage ! null aiMessage ! null) { ListMessage history conversationStore.getOrDefault(conversationId, new ArrayList()); history.add(userMessage); history.add(aiMessage); conversationStore.put(conversationId, history); } } return response; } // ... onError 方法省略 }注意事项与高级技巧上下文键值设计conversationId的获取是关键。在实际 Web 应用中通常可以从HttpServletRequest或安全上下文中获取用户身份再结合时间戳或随机数生成唯一会话ID并将其设置到AdvisorContext中。这通常需要一个更前置的过滤器或拦截器来完成。Token 管理与截断策略示例中的按轮数截断非常粗糙。生产环境必须基于 Token 数进行精确管理。你需要估算每条消息的 Token 消耗可以调用模型的 Tokenizer 接口或使用近似算法并确保合并后的消息列表总 Token 数不超过模型上限如 GPT-4 的 8192。这是一个复杂的工程问题可能需要实现一个 LRU最近最少使用式的消息淘汰算法。存储与状态示例使用了内存Map这在单机开发时没问题但生产环境必须使用分布式缓存如 Redis来支持多实例部署并设置合理的过期时间。before和after的协作为了在after中准确存储消息需要在before里将处理好的用户消息暂存到AdvisorContext。这展示了上下文对象在链式 Advisor 中传递数据的能力。4. 实战二实现审计、限流与降级 Advisor除了内容处理运维层面的管控同样重要。我们来实现几个用于可观测性和稳定性的 Advisor。4.1 调用日志与性能监控 Advisor一个基础的LoggingAdvisor可以帮助你追踪每一次 AI 调用的细节用于调试和监控。Component Order(Ordered.LOWEST_PRECEDENCE - 10) // 通常放在链的末尾确保记录的是最终状态 public class LoggingAdvisor implements Advisor { private static final Logger logger LoggerFactory.getLogger(LoggingAdvisor.class); Override public Prompt before(Prompt prompt, AdvisorContext context) { long startTime System.currentTimeMillis(); context.putAttribute(callStartTime, startTime); String conversationId (String) context.getAttribute(conversationId); String userId (String) context.getAttribute(userId); // 记录请求日志注意生产环境可能需要对消息内容脱敏 if (logger.isDebugEnabled()) { logger.debug(AI Call START - Conversation: {}, User: {}, Prompt: {}, conversationId, userId, prompt.getContents().substring(0, Math.min(100, prompt.getContents().length())) ...); } return prompt; // 这个Advisor不修改Prompt只是记录。 } Override public ChatResponse after(ChatResponse response, AdvisorContext context) { long endTime System.currentTimeMillis(); Long startTime (Long) context.getAttribute(callStartTime); long duration startTime ! null ? (endTime - startTime) : -1; String conversationId (String) context.getAttribute(conversationId); // 记录响应日志和耗时 logger.info(AI Call END - Conversation: {}, Duration: {}ms, ResponseTokens: {}, conversationId, duration, response.getResult().getOutput().getContent().length()); // 简单用长度近似token // 你可以将更详细的指标发送到监控系统如 Prometheus, Micrometer // Metrics.counter(ai.calls.total).increment(); // Metrics.timer(ai.call.duration).record(duration, TimeUnit.MILLISECONDS); return response; } Override public ChatResponse onError(Throwable error, AdvisorContext context) { logger.error(AI Call FAILED - Conversation: {}, Error: {}, context.getAttribute(conversationId), error.getMessage(), error); // 错误计数 // Metrics.counter(ai.calls.errors).increment(); return null; // 传播错误 } }实操心得日志级别请求/响应的完整内容通常很冗长建议在DEBUG级别记录而在INFO级别只记录元数据如会话ID、耗时、Token数。内容脱敏非常重要直接记录用户输入和AI输出可能涉及隐私和数据合规问题。在生产环境中必须对日志内容进行脱敏处理例如屏蔽手机号、邮箱等个人信息。指标收集结合 Micrometer 等指标库可以轻松地将调用次数、耗时、错误率等指标集成到 Grafana 等监控面板中这是生产可观测性的基础。4.2 基于令牌桶的限流 Advisor为了防止滥用或控制成本我们需要对 AI 调用进行限流。这里实现一个简单的基于内存令牌桶的RateLimitAdvisor。Component public class RateLimitAdvisor implements Advisor { // 使用Guava的RateLimiter简单示例。生产环境需考虑分布式限流。 private final RateLimiter globalLimiter RateLimiter.create(10.0); // 全局QPS10 private final MapString, RateLimiter userLimiterMap new ConcurrentHashMap(); Override public Prompt before(Prompt prompt, AdvisorContext context) { // 1. 全局限流 if (!globalLimiter.tryAcquire()) { throw new RuntimeException(系统繁忙请稍后再试); // 应使用自定义业务异常 } // 2. 用户级限流假设从上下文获取userId String userId (String) context.getAttribute(userId); if (userId ! null) { RateLimiter userLimiter userLimiterMap.computeIfAbsent(userId, key - RateLimiter.create(2.0)); // 每个用户QPS2 if (!userLimiter.tryAcquire()) { throw new RuntimeException(您的请求过于频繁请稍后再试); } } // 3. 可选基于对话的限流 // String conversationId (String) context.getAttribute(conversationId); // ... return prompt; // 限流通过不修改Prompt } // after 和 onError 方法通常无需实现除非需要记录限流情况。 }注意事项分布式限流上述实现是单机的。在微服务架构下必须使用 Redis 等中心化存储配合算法如 Redis-Cell 模块的漏桶算法来实现集群级别的精准限流。维度与策略限流的维度可以很多样全局、按用户、按 API Key、按模型类型等。策略也可以是 QPS、每日总调用次数、总 Token 消耗等。你需要根据业务需求灵活设计。用户体验直接抛出异常对用户不友好。更好的做法是返回一个特定的、友好的提示信息或者将请求放入队列稍后重试。4.3 故障降级与后备响应 Advisor当主要 AI 模型服务不稳定或返回不符合要求的内容时一个降级策略可以提升系统的整体韧性。Component Order(Ordered.LOWEST_PRECEDENCE) // 放在链的最末端作为最后的保障 public class FallbackAdvisor implements Advisor { Override public ChatResponse onError(Throwable error, AdvisorContext context) { // 1. 判断错误类型决定是否降级 // 例如网络超时、模型服务不可用、内容策略违规等 if (isServiceUnavailable(error)) { logger.warn(主服务不可用触发降级策略, error); // 2. 执行降级逻辑 // 策略A返回一个预设的静态友好提示 // return createStaticFallbackResponse(); // 策略B切换到更稳定但能力较弱的备用模型如另一个ChatClient // ChatClient fallbackClient ...; // try { // Prompt originalPrompt (Prompt) context.getAttribute(originalPrompt); // return fallbackClient.call(originalPrompt); // } catch (Exception e) { // // 备用也挂了返回静态响应 // return createStaticFallbackResponse(); // } // 策略C从缓存中获取历史相似问题的答案 // String userQuery extractQuery(context); // String cachedAnswer cache.get(userQuery); // if (cachedAnswer ! null) { // return createResponseFromCache(cachedAnswer); // } // 这里演示策略A return createStaticFallbackResponse(); } // 其他非降级范围的错误继续抛出 throw new RuntimeException(AI调用失败, error); } private boolean isServiceUnavailable(Throwable error) { // 判断逻辑连接超时、读取超时、5xx状态码等 return error instanceof ConnectTimeoutException || error instanceof SocketTimeoutException || (error.getMessage() ! null error.getMessage().contains(5)); } private ChatResponse createStaticFallbackResponse() { // 构造一个简单的降级响应 // 注意这里需要根据你使用的 ChatClient 实现来构建合法的 ChatResponse 对象。 // 以下为示例性伪代码具体构造方法取决于 Spring AI 版本和底层适配。 // 通常需要创建一个 AssistantMessage 并包装进 ChatResponse。 // 例如 // AssistantMessage fallbackMessage new AssistantMessage(当前服务暂时不可用请稍后重试。); // Generation generation new Generation(fallbackMessage); // ChatResponse response new ChatResponse(List.of(generation)); // return response; return null; // 实际实现需替换 } // before 和 after 方法通常空实现 Override public Prompt before(Prompt prompt, AdvisorContext context) { // 可以在这里保存原始的Prompt供降级时使用 context.putAttribute(originalPrompt, prompt); return prompt; } }高级技巧与考量降级触发条件精确判断何时降级是一门艺术。网络超时、服务端 5xx 错误通常是明确的降级信号。但对于模型返回的内容违规如被安全策略拦截是否降级、降级成什么需要和产品策略紧密结合。多级降级降级策略本身也可以是多级的。例如首先尝试备用模型备用模型失败再返回缓存缓存没有则返回静态提示。这提供了更强的韧性。副作用与一致性降级可能带来副作用。例如一个用于数据库查询的 AI 调用如果降级为静态回复则不会执行真正的查询。需要确保这种不一致性在业务可接受范围内。before中保存状态如示例所示在before中保存原始Prompt到上下文确保了在onError时你仍有原始数据可供降级逻辑使用如调用备用模型。5. 高级应用与集成模式掌握了基础组件的构建后我们可以探索一些更高级的应用模式和集成方案。5.1 链式 Advisor 的执行顺序与数据流当注册了多个 Advisor 时理解它们的执行顺序和数据流至关重要。Spring AI 会按照Order注解的值从小到大来排序 Advisor。一个典型的数据流示例假设我们有三个 Advisor顺序如下ContentFilterAdvisor(Order(100))过滤敏感词注入系统指令。ConversationContextAdvisor(Order(200))添加上下文历史。LoggingAdvisor(Order(300))记录日志。执行流程如下用户请求 | v [ContentFilterAdvisor.before] | - 过滤敏感词创建新Prompt1 v [ConversationContextAdvisor.before] | - 从Prompt1和历史创建包含上下文的Prompt2 v [LoggingAdvisor.before] | - 记录开始时间Prompt2的摘要 v ChatClient.call(Prompt2) -- 调用大模型 | v [LoggingAdvisor.after] | - 记录响应和耗时 v [ConversationContextAdvisor.after] | - 将本轮对话存入历史存储 v [ContentFilterAdvisor.after] | - 可选记录过滤行为 v 返回最终响应给用户AdvisorContext的数据流这个上下文对象会贯穿整个链条。ContentFilterAdvisor可以将filtered标记放入ConversationContextAdvisor可以放入processedUserMessageLoggingAdvisor则可以取出这些信息进行记录。这实现了 Advisor 间的低耦合协作。5.2 与 Spring AOP 及现有中间件的集成虽然 Spring AI Advisor 自成体系但它可以与 Spring 生态的其他组件无缝集成。与 Spring AOP 结合你可以使用Around注解的切面在更外层包裹整个ChatClient.call()方法实现一些 Advisor 本身不擅长或更全局的逻辑例如基于注解的细粒度权限控制。Aspect Component public class ChatClientAspect { Around(annotation(com.yourcompany.RequiresPermission)) public Object checkPermission(ProceedingJoinPoint pjp) throws Throwable { // 检查权限逻辑... if (!hasPermission) { throw new SecurityException(Permission denied); } return pjp.proceed(); } }集成 Sleuth/Zipkin 用于分布式追踪在LoggingAdvisor中你可以获取当前的 Trace ID 和 Span ID并将其与 AI 调用关联起来从而在分布式追踪系统中清晰看到一次用户请求背后调用了多少次 AI 服务以及每次调用的耗时。import brave.Tracer; // 在 before 中 Span span tracer.nextSpan().name(ai-chat-call).start(); context.putAttribute(aiCallSpan, span); // 在 after/onError 中 span.finish();集成 Sentry/ELK 用于错误日志聚合在onError方法中除了本地日志还可以将错误信息、上下文如 conversationId, userId发送到 Sentry 或结构化日志系统便于集中告警和分析。5.3 动态配置与热更新策略生产环境的策略如敏感词列表、限流阈值、降级开关可能需要动态调整而不重启应用。基于配置中心将 Advisor 的关键配置如敏感词列表、QPS 值、开关状态外置到 Apollo、Nacos 或 Spring Cloud Config 等配置中心。在 Advisor 中监听配置变化。RefreshScope // Spring Cloud 的刷新作用域 Component public class DynamicContentFilterAdvisor implements Advisor { Value(${ai.filter.sensitive-words:}) private ListString sensitiveWords; // 从配置中心读取 // ... 在过滤逻辑中使用 sensitiveWords }当你在配置中心更新词库后通过 Spring Cloud 的/actuator/refresh端点或配置中心的推送机制可以实时更新 Advisor 的行为。基于数据库对于更复杂的规则如不同用户组的不同限流策略可以将规则存储在数据库中。Advisor 可以定期或通过事件驱动从数据库加载最新规则。需要注意缓存和一致性。热更新挑战动态更新时需考虑线程安全。例如正在替换一个大的敏感词列表时可能有并发请求正在使用旧列表进行匹配。可以使用CopyOnWriteArrayList或通过原子引用切换整个策略对象来避免锁竞争。6. 测试、调试与性能考量将 Advisor 用于生产必须经过充分的测试和性能验证。6.1 单元测试与集成测试策略单元测试针对单个 Advisor 的逻辑进行测试。Mock 掉AdvisorContext和相邻的依赖。SpringBootTest class ContentFilterAdvisorTest { Autowired private ContentFilterAdvisor advisor; Test void testBefore_SensitiveWordFiltered() { Prompt inputPrompt new Prompt(new UserMessage(这里包含暴力内容)); AdvisorContext context new AdvisorContext(); Prompt outputPrompt advisor.before(inputPrompt, context); assertThat(outputPrompt.getContents()).contains(***); assertThat(context.getAttribute(contentFiltered)).isEqualTo(true); } Test void testBefore_SystemInstructionAdded() { Prompt inputPrompt new Prompt(new UserMessage(你好)); AdvisorContext context new AdvisorContext(); Prompt outputPrompt advisor.before(inputPrompt, context); ListMessage messages outputPrompt.getMessages(); assertThat(messages).hasSize(2); assertThat(messages.get(0).getMessageType()).isEqualTo(MessageType.SYSTEM); assertThat(messages.get(0).getContent()).contains(使用中文); } }集成测试测试多个 Advisor 组合在一起并与真实的ChatClient可以是测试专用的 Mock 或轻量级模型协同工作。SpringBootTest class AdvisorChainIntegrationTest { Autowired private ChatClient chatClient; // 这个ChatClient应该已经自动装配了你定义的所有Advisor Test void testFullChain() { String response chatClient.prompt() .user(用户输入可能包含敏感词) .advisors(new ParameterAdvisor()) // 也可以在这里动态添加 .call() .content(); // 断言响应符合预期例如被过滤或添加了上下文 assertThat(response).isNotNull(); } }6.2 调试技巧与日志追踪当链式 Advisor 行为不符合预期时调试可能会有点棘手。启用详细日志为你的 Advisor 类设置DEBUG级别日志并在before/after方法的关键节点打印AdvisorContext的内容和Prompt的摘要。使用“诊断”Advisor临时插入一个高优先级和低优先级的诊断 Advisor它只做一件事打印出经过它处理前后的Prompt完整内容。这能帮你清晰地看到数据在链中是如何被一步步改变的。Component Order(1) // 非常高的优先级 public class DebugAdvisor implements Advisor { Override public Prompt before(Prompt prompt, AdvisorContext context) { System.out.println(DEBUG [BEFORE] Prompt: prompt); return prompt; } Override public ChatResponse after(ChatResponse response, AdvisorContext context) { System.out.println(DEBUG [AFTER] Response: response); return response; } }利用AdvisorContext传递调试信息在复杂的 Advisor 中可以将一些中间状态或决策原因放入context并在链末端的日志 Advisor 中统一输出形成一次调用的“诊断报告”。6.3 性能影响评估与优化建议每个 Advisor 都会增加一次方法调用和可能的对象拷贝创建新的Prompt/Response在超高并发下需要关注性能。性能基准测试使用 JMH 或简单的单元测试对比添加 Advisor 前后单次ChatClient.call()的耗时增加。对于简单的逻辑如日志记录开销通常可以忽略不计1ms。但对于复杂的操作如从远程服务进行敏感词检测、从 Redis 加载大量上下文开销可能显著。优化建议异步与非阻塞对于耗时的操作如调用外部安全 API考虑将其异步化。但要注意Advisor接口本身是同步的。一种模式是在before中发起异步调用并返回原始 Prompt在after中检查异步结果并进行后续处理或补偿但这会大大增加复杂度。更常见的做法是确保外部调用本身足够快或使用缓存。缓存一切可缓存的敏感词列表、用户限流计数器、对话上下文等都应使用内存缓存或 Redis 缓存避免每次请求都进行昂贵的 I/O 操作。懒加载与批量处理如果多个 Advisor 需要相同的数据如用户信息可以考虑在第一个需要的 Advisor 中加载并放入AdvisorContext共享避免重复查询。精简Prompt操作创建新的Prompt和Message列表会有对象创建开销。确保你的逻辑是必要的并避免在链中进行多次不必要的列表拷贝。选择性启用并非所有请求都需要所有 Advisor。可以通过在AdvisorContext中设置标志位或者实现一个智能的路由 Advisor根据请求特征动态跳过某些后续 Advisor。通过遵循这些测试、调试和性能优化实践你可以确保自定义的 Spring AI Advisor 不仅在功能上强大而且在生产环境中也是稳定、高效和可维护的。它将成为你构建鲁棒、可控、可观测的 AI 应用不可或缺的架构基石。

相关新闻