
写过传统 Web 后端的人都知道,认证、日志、鉴权这类逻辑绝不能复制粘贴到每个 Controller 里——所以我们有了 Filter 和 Interceptor。但转到 AI 应用开发后,我发现很多团队又开始犯同样的错:把对话历史的加载、敏感信息脱敏、请求日志这些横切逻辑,散落手写进每一个调用大模型的 Service 方法里。
Spring AI 给出的答案叫 Advisor。这篇文章结合我自己的实践经验,讲清楚它解决什么问题、怎么用,以及一个几乎没人提但会真金白银造成事故的坑:Advisor 的顺序。
大模型是无状态的——每次请求都从零开始,它不记得上一轮对话。要把孤立的调用变成真正的应用,总得有人在每次调用前加载历史、调用后保存结果。手写进每个服务里,就是重复代码的温床。
Advisor 的工作方式和 Spring MVC 的拦截器几乎一模一样:它包裹住对模型的调用,让你可以在请求发出前、响应返回后执行自己的逻辑。它的能力边界很清晰:
Spring 官方自己也大量用这个机制构建框架能力:MessageChatMemoryAdvisor 让模型"记住"历史消息,SafeGuardAdvisor 可以在请求到达模型前拦截包含违禁词的内容,QuestionAnswerAdvisor 则是 RAG 的标准入口——取向量库检索相似文档塞进 prompt。
三个最常用的内置 Advisor,每个都只是一行 defaultAdvisors(...) 配置:
// 日志:打印每个请求和响应,DEBUG 级别
builder.defaultAdvisors(new SimpleLoggerAdvisor());
// 对话记忆:调用前加载历史,调用后保存
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(10)
.build();
builder.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build());
// RAG:检索向量库并注入上下文
builder.defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore).build());注意它们的共同点:调用方代码一行不改。这就是横切逻辑该有的样子。
内置的覆盖通用场景,但真正有意思的问题永远是业务专属的。举个例子:支付应用的客服助手,用户迟早会把卡号和 IBAN 直接贴进对话框。没有防护的话,这些敏感信息会直接发给第三方模型,还会落进你的日志。
这种规则就该收敛在一个地方、挡在每次模型调用之前——这正是 Advisor 的位置。
第一步:检测逻辑独立成普通类。 不带任何 Spring 依赖,方便单测:
public static String mask(String text) {
// 先处理 IBAN:它的长串数字容易被误判成卡号
String masked = IBAN.matcher(text).replaceAll("[IBAN]");
return CARD.matcher(masked).replaceAll(match ->
passesLuhn(match.group()) ? "[CARD_NUMBER]"
: Matcher.quoteReplacement(match.group()));
}两个细节:IBAN 要先脱敏,否则它的数字串会被当成卡号;卡号候选必须通过 Luhn 校验,否则订单号、手机号都会被误杀。
第二步:Advisor 本身极薄。 实现 BaseAdvisor,只需写 before 和 after:
public class PiiMaskingAdvisor implements BaseAdvisor {
private final int order;
public PiiMaskingAdvisor(int order) { this.order = order; }
@Override
public ChatClientRequest before(ChatClientRequest request, AdvisorChain chain) {
String original = request.prompt().getUserMessage().getText();
String masked = SensitiveDataMasker.mask(original);
if (masked.equals(original)) {
return request; // 没有敏感信息,原样放行
}
return request.mutate()
.prompt(request.prompt().augmentUserMessage(masked))
.build();
}
@Override
public ChatClientResponse after(ChatClientResponse response, AdvisorChain chain) {
return response; // 只保护入站方向
}
@Override
public int getOrder() { return order; }
}关键点:请求对象是不可变的,不能原地改消息,必须构造一个新请求返回——before 返回什么,什么就继续往链下游走。
第三步:一行注册。
builder.defaultAdvisors(new PiiMaskingAdvisor(100));从此经过这个 Client 的每个 prompt 都会先脱敏,而调用方代码对此一无所知。
这是全篇最重要的部分。假设你的链上有脱敏 Advisor 和日志 Advisor,两个都工作正常。但它们的顺序会决定一件致命的事:日志里记录的到底是哪个版本。
正确姿势——脱敏先执行,日志只能看到安全版本:
builder.defaultAdvisors(
new PiiMaskingAdvisor(0), // 先执行
new RequestLoggingAdvisor("log", 100)); // 只能看到脱敏后的内容日志输出:My card is [CARD_NUMBER] and my IBAN is [IBAN]
如果把顺序值对调,日志 Advisor 先执行:
builder.defaultAdvisors(
new RequestLoggingAdvisor("log", 0), // 先执行
new PiiMaskingAdvisor(100)); // 脱敏太迟了日志输出:My card is 4111 1111 1111 1111 and my IBAN is TR33 0006...
脱敏 Advisor 依然在正常工作,模型收到的依然是脱敏后的内容,没有任何测试会失败,没有任何异常会抛出。但真实卡号已经写进了日志文件——而日志往往保留数月、被同步到外部系统、能被远比数据库更多的人看到。一个为保护数据而生的功能,变成了数据泄漏的源头。
两个容易踩的细节:defaultAdvisors(...) 里的书写顺序不是执行顺序,只有 getOrder() 的数值说了算;两个 Advisor 用了相同的 order 值,相对顺序不保证——相关联的 Advisor 永远不要共用一个数值。
Advisor 很好用,所以也容易被滥用。判断标准很简单:
大模型无状态、不了解你的数据、并且会照单全收你发给它的任何东西。严肃的应用终究需要围绕模型调用的记忆、检索、安全与可观测能力——没有明确的安放位置,这些逻辑就会扩散、重复、腐烂。Advisor 就是那个位置。只是别忘了脱敏案例的教训:在链式结构里,顺序本身就是设计的一部分,一个"能正常工作"的配置照样可以泄漏数据。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。