首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >别再复制粘贴 LLM 逻辑了:Spring AI Advisors 的正确打开方式

别再复制粘贴 LLM 逻辑了:Spring AI Advisors 的正确打开方式

原创
作者头像
HELLO程序员
发布于 2026-11-08 13:24:42
发布于 2026-11-08 13:24:42
370
举报

别再复制粘贴 LLM 逻辑了:Spring AI Advisors 的正确打开方式

写过传统 Web 后端的人都知道,认证、日志、鉴权这类逻辑绝不能复制粘贴到每个 Controller 里——所以我们有了 Filter 和 Interceptor。但转到 AI 应用开发后,我发现很多团队又开始犯同样的错:把对话历史的加载、敏感信息脱敏、请求日志这些横切逻辑,散落手写进每一个调用大模型的 Service 方法里。

Spring AI 给出的答案叫 Advisor。这篇文章结合我自己的实践经验,讲清楚它解决什么问题、怎么用,以及一个几乎没人提但会真金白银造成事故的坑:Advisor 的顺序。

Advisor 到底解决什么问题

大模型是无状态的——每次请求都从零开始,它不记得上一轮对话。要把孤立的调用变成真正的应用,总得有人在每次调用前加载历史、调用后保存结果。手写进每个服务里,就是重复代码的温床。

Advisor 的工作方式和 Spring MVC 的拦截器几乎一模一样:它包裹住对模型的调用,让你可以在请求发出前、响应返回后执行自己的逻辑。它的能力边界很清晰:

  • 入站方向:修改请求(追加上下文、脱敏),甚至直接拦截,让模型根本收不到这次请求;
  • 出站方向:检查或改写响应,确保模型的输出里没有不该给用户看的东西。

Spring 官方自己也大量用这个机制构建框架能力:MessageChatMemoryAdvisor 让模型"记住"历史消息,SafeGuardAdvisor 可以在请求到达模型前拦截包含违禁词的内容,QuestionAnswerAdvisor 则是 RAG 的标准入口——取向量库检索相似文档塞进 prompt。

内置 Advisor 速览

三个最常用的内置 Advisor,每个都只是一行 defaultAdvisors(...) 配置:

代码语言:javascript
复制
// 日志:打印每个请求和响应,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());

注意它们的共同点:调用方代码一行不改。这就是横切逻辑该有的样子。

手写一个自定义 Advisor:敏感数据脱敏

内置的覆盖通用场景,但真正有意思的问题永远是业务专属的。举个例子:支付应用的客服助手,用户迟早会把卡号和 IBAN 直接贴进对话框。没有防护的话,这些敏感信息会直接发给第三方模型,还会落进你的日志。

这种规则就该收敛在一个地方、挡在每次模型调用之前——这正是 Advisor 的位置。

第一步:检测逻辑独立成普通类。 不带任何 Spring 依赖,方便单测:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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 返回什么,什么就继续往链下游走。

第三步:一行注册。

代码语言:javascript
复制
builder.defaultAdvisors(new PiiMaskingAdvisor(100));

从此经过这个 Client 的每个 prompt 都会先脱敏,而调用方代码对此一无所知。

顺序错了,系统"正常"但数据在泄漏

这是全篇最重要的部分。假设你的链上有脱敏 Advisor 和日志 Advisor,两个都工作正常。但它们的顺序会决定一件致命的事:日志里记录的到底是哪个版本。

正确姿势——脱敏先执行,日志只能看到安全版本:

代码语言:javascript
复制
builder.defaultAdvisors(
        new PiiMaskingAdvisor(0),               // 先执行
        new RequestLoggingAdvisor("log", 100)); // 只能看到脱敏后的内容

日志输出:My card is [CARD_NUMBER] and my IBAN is [IBAN]

如果把顺序值对调,日志 Advisor 先执行:

代码语言:javascript
复制
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 很好用,所以也容易被滥用。判断标准很简单:

  • 规则对每次模型调用都适用(记忆、脱敏、日志、安全护栏)→ 放进 Advisor 链。漏掉一处就是 bug 的逻辑,属于链上。
  • 逻辑只属于某个具体用例(为"退款申请"功能拼特定 prompt、为某个页面选择检索哪些文档)→ 留在普通 Service 代码里。那里本来就没有重复,搬进 Advisor 只会让逻辑更难找。

总结

大模型无状态、不了解你的数据、并且会照单全收你发给它的任何东西。严肃的应用终究需要围绕模型调用的记忆、检索、安全与可观测能力——没有明确的安放位置,这些逻辑就会扩散、重复、腐烂。Advisor 就是那个位置。只是别忘了脱敏案例的教训:在链式结构里,顺序本身就是设计的一部分,一个"能正常工作"的配置照样可以泄漏数据。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 别再复制粘贴 LLM 逻辑了:Spring AI Advisors 的正确打开方式
    • Advisor 到底解决什么问题
    • 内置 Advisor 速览
    • 手写一个自定义 Advisor:敏感数据脱敏
    • 顺序错了,系统"正常"但数据在泄漏
    • 什么时候不该用 Advisor
    • 总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档