别再只会发Prompt啦!用Spring AI Advisors API打造超靠谱的AI应用

举报
HELLO程序员 发表于 2025/12/29 12:28:53 2025/12/29
【摘要】 构建稳健的AI应用,可远不止“发个提示词就完事”这么简单。你得把上下文拿捏得死死的,还得能摸清对话的来龙去脉。而Spring AI的Advisors API,就是解决这些头疼问题的“神兵利器”。这篇教程就来扒一扒Advisors API的底:我们会看看怎么拦截并修改AI交互过程,还会在Spring Boot项目里实现日志记录和对话记忆功能,让你的AI应用瞬间“有脑子、可追溯”。 无状态交互的...

构建稳健的AI应用,可远不止“发个提示词就完事”这么简单。你得把上下文拿捏得死死的,还得能摸清对话的来龙去脉。而Spring AI的Advisors API,就是解决这些头疼问题的“神兵利器”。

这篇教程就来扒一扒Advisors API的底:我们会看看怎么拦截并修改AI交互过程,还会在Spring Boot项目里实现日志记录和对话记忆功能,让你的AI应用瞬间“有脑子、可追溯”。

无状态交互的坑,谁踩谁知道

大语言模型(LLMs)确实牛得一批,但有个致命短板:天生无状态。在它眼里,每一个请求都是全新的“初次见面”,压根不记得你上一秒问了啥。

这就导致用户体验稀碎:想象一下,你刚跟机器人自我介绍完,转头问个跟进问题,它反手就给你一句“你谁啊?”,换谁谁不崩溃?

上下文才是王道

要解决这个问题,就得给模型喂上下文——把相关数据拼到每个Prompt里。一般来说,这些上下文数据分两类:

第一类是你的私有数据。模型训练时根本没见过你的业务文档,所以你得动态把这些信息传过去。就算模型懂相关知识点,你的私有数据也得是“老大”,优先级最高。

第二类是对话历史。也就是用户和AI一来一回的聊天内容,每一次新请求都得把这段历史带上,不然AI的逻辑就会断片,直接“语无伦次”。

而Spring AI Advisors就是来帮你自动化这个重复劳动的。它就像个“中间商”,夹在你的代码和AI模型之间,默默帮你搞定这些繁琐操作。话不多说,咱们直接上实操!

搞懂Advisor架构:其实就是个“过滤器”

你可以把Advisor理解成Java Web里的Servlet Filter,功能几乎一模一样:在请求传给模型之前,它先把请求拦下来;等模型生成响应后,在你的代码拿到响应之前,它还能再修改一波响应。这种架构相当于给AI交互逻辑找了个“中央管控台”,贼方便。

这种方案的好处,肉眼可见

  1. 业务代码干干净净:不用每次写Prompt都手动拼接字符串,一次配置Advisor,它就会自动帮你扛下所有脏活累活。

  2. 复用性拉满:你可以写一个自定义Advisor做安全校验,然后直接套在多个ChatClient实例上用,不用重复造轮子。

配置ChatClient:你的AI交互主入口

ChatClient是你和AI打交道的核心接口,它提供了流畅的链式API,配置起来贼顺手。我们可以通过AdvisorSpec接口,把咱们的自定义逻辑“挂”到ChatClient上。

你可以直接单个添加参数,也可以批量配置,灵活性拉满,能精准控制每一次AI交互。

下面是个基础配置示例,我们在标准的Spring配置类里创建一个ChatClient Bean:


package com.example.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("你是一个精通Spring技术的贴心助手。")
                .build();
    }
}

这只是打了个基础,还没加Advisor呢,咱们得给它升级一下,添点实用功能。

日志记录:排查问题的“照妖镜”

调试AI响应简直是个大工程,很多时候你得搞清楚:模型到底收到了啥?Prompt里的上下文对不对?

Spring AI提供的SimpleLoggerAdvisor就是解决这个问题的神器,它能自动记录请求和响应数据,是监控AI应用的必备工具,谁用谁知道。

来,给ChatClient加上日志功能

咱们更新下配置,把SimpleLoggerAdvisor加到拦截链里:


package com.example.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
                .defaultSystem("你是一个精通Spring技术的贴心助手。")
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}

再配置下application.properties,开启调试日志:


logging.level.org.springframework.ai.chat.client.advisor=DEBUG

现在你再看控制台日志,就能看到原汁原味的交互数据了——包括Prompt文本和AI生成的响应,排查问题瞬间变得so easy!

对话记忆:让AI不再“鱼的记忆”

前面咱们吐槽了AI的无状态问题,现在就来解决它!Spring AI提供的MessageChatMemoryAdvisor,就是给AI装“脑子”的关键。

这个Advisor会管理一个对话存储库:发送请求前,它会先读取历史对话;AI生成新响应后,它又会把新的对话内容写回存储库,循环往复,让AI记住你们的“过往”。

把记忆Advisor接入系统

接下来,咱们把这个Advisor挂到ChatClient上,顺便指定一个对话ID,用来跟踪特定用户的会话(毕竟不能把用户A和用户B的对话搞混了):


@Bean
public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
    return builder
            .defaultSystem("你是一个贴心的助手。")
            .defaultAdvisors(
                    MessageChatMemoryAdvisor.builder(chatMemory).build(),
                    new SimpleLoggerAdvisor()                 // 记录交互日志
            )
            .build();
}

搞定!现在你的系统就变成有状态的了,MessageChatMemoryAdvisor会自动把历史消息拼到Prompt里,AI终于能记住你是谁了。

运行时Advisor配置:灵活切换“技能”

有时候,你并不想让Advisor作用于每一次请求——比如,只在某个接口里开启日志记录。这时候,AdvisorSpec接口就能实现动态配置,在交互时灵活添加或覆盖默认Advisor。

动态配置示例(控制器里搞事情)

比如在控制器方法里,我们只给这一个请求开启对话记忆参数配置:


@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    // 构造函数注入ChatClient
    public ChatController(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .advisors(a -> a
                    .param("chat_memory_conversation_id", "user-123")  // 动态指定用户123的对话ID
                    .param("chat_memory_response_size", 100)           // 限制记忆的响应长度
                )
                .call()
                .content();
    }
}

这里我们用了Lambda表达式动态设置参数,这样服务器就能同时处理多个用户的会话,互不干扰,简直完美!

高级玩法:自定义Advisor,实现专属逻辑

默认的Advisor已经很强了,但有时候你需要一些定制化功能——比如脱敏敏感数据、校验内容安全等。这时候,你可以实现CallAdvisor接口,打造属于自己的“专属拦截器”。

手把手教你写一个脱敏Advisor(隐藏手机号、邮箱等隐私数据)

结构很简单,核心就是实现around方法,话不多说,上代码:


import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

@Slf4j
public class PiiRedactionAdvisor implements CallAdvisor {

    // 正则匹配邮箱
    private static final Pattern EMAIL = Pattern.compile(
            "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}",
            Pattern.CASE_INSENSITIVE
    );
// 正则匹配国内11位手机号(支持带86/+86前缀、中间带空格/横杠分隔,兼容纯数字)
private static final Pattern PHONE = Pattern.compile(
        "\\b(?:\\+?86[-\\s]?)?(1[3-9]\\d{9})\\b"
);

// 正则匹配国内身份证号(18位为主,兼容15位老身份证)
private static final Pattern ID_CARD = Pattern.compile(
        "\\b(\\d{15}|\\d{17}([0-9]|X|x))\\b"
);

// 正则匹配国内银行卡/信用卡号(常见62开头等,支持4位一组分隔或纯数字)
private static final Pattern BANK_CARD = Pattern.compile(
        "\\b(?:\\d{4}[-\\s]?){4}\\d{4}|\\d{16,19}\\b"
);

// 正则匹配IP地址(保留原有准确匹配,支持IPv4格式)
private static final Pattern IP_ADDRESS = Pattern.compile(
        "\\b(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\b"
);

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
        List<Message> redactedMessages = new ArrayList<>();

        // 遍历用户消息,进行脱敏处理
        for (Message message : request.prompt().getUserMessages()) {
            if (message instanceof UserMessage) {
                String redacted = redactPii(message.getText());
                if(redacted != null) {
                    redactedMessages.add(new UserMessage(redacted));
                }

                // 打印原始消息和脱敏后的消息,方便验证
                log.info("原始消息:{}", message);
                log.info("脱敏后消息:{}", redacted);
            } else {
                // 非用户消息直接原样保留
                redactedMessages.add(message);
            }
        }

        // 构建脱敏后的Prompt
        Prompt promptWithHistory = new Prompt(redactedMessages);
        ChatClientRequest modifiedRequest = ChatClientRequest.builder()
                .prompt(promptWithHistory)
                .context(Map.copyOf(request.context())) // 保留原有上下文和工具信息
                .build();

        // 继续执行Advisor链
        return chain.nextCall(modifiedRequest);
    }

    // 核心脱敏方法:替换各类隐私数据
    private String redactPii(String text) {
        if (text == null) {
            return null;
        }

        String result = text;
        result = EMAIL.matcher(result).replaceAll("[邮箱]");
        result = PHONE.matcher(result).replaceAll("[手机号]");
        result = SSN.matcher(result).replaceAll("[社保号]");
        result = CREDIT_CARD.matcher(result).replaceAll("[信用卡号]");
        result = IP_ADDRESS.matcher(result).replaceAll("[IP地址]");

        return result;
    }

    @Override
    public String getName() {
        return "PiiRedactionAdvisor"; // 给Advisor起个名字,方便日志排查
    }

    @Override
    public int getOrder() {
        return 0; // 指定执行顺序,数字越小越先执行
    }

}

有了这个自定义Advisor,你就能完全掌控AI交互流水线,想怎么定制就怎么定制!

给脱敏Advisor做个全面测试

光写代码不行,还得验证效果,咱们整个集成测试:


import com.example.spring_ai.config.PiiRedactionAdvisor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Slf4j
class PiiRedactionAdvisorIntegrationTest {

    @Autowired
    private ChatModel chatModel;

    @Test
    void shouldRedactPiiInRealChatClient() {
        // 给定:创建脱敏Advisor和ChatClient
        PiiRedactionAdvisor advisor = new PiiRedactionAdvisor();
        ChatClient chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(advisor)
                .build();

        // 当:发送包含隐私数据的请求
        String response = chatClient.prompt()
                .user("我的邮箱是test@example.com,手机号是555-1234")
                .call()
                .content();

        // 打印响应,验证效果
        log.info("AI响应:{}", response);
    }
}

运行测试后,你会在日志里看到这样的输出,说明脱敏成功啦:


原始消息:UserMessage{content='我的邮箱是test@example.com,手机号是555-1234', metadata={messageType=USER}, messageType=USER}
脱敏后消息:我的邮箱是[邮箱],手机号是[手机号]

Advisor使用最佳实践:别踩这些坑

  1. 别滥用Advisor:Advisor会增加一点性能开销,按需使用就好。如果把一堆Advisor串起来,AI响应速度会变慢,得不偿失。

  2. 关注Token消耗:拼太多历史对话会占用大量Token,不仅增加成本,还会压缩AI回答的空间(毕竟Token有上限)。

  3. 监控Token使用情况:SimpleLoggerAdvisor能帮你查看token_usage指标,记得多关注日志。如果Token不够用,可以调整MessageChatMemoryAdvisor的窗口大小,限制发送的历史消息数量。

总结

咱们今天聊了不少干货:从AI模型的无状态痛点,到用Spring AI Advisors API解决问题;从配置ChatClient,到实现日志记录和对话记忆;还解锁了自定义Advisor的高级玩法。

未来可期

现在就可以动手试试啦:先整个自定义Advisor玩玩,再对接向量数据库实现RAG(检索增强生成)功能。Advisors API就是你打造智能、上下文感知AI应用的“金钥匙”。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。