接下来我们重学习 Advisors

首先是它的定义:一系列可插拔的拦截器,在调用 AI 前和调用 AI 后可以执行一些额外的操作,在前面也有使用的例子:

// 创建 ChatClient时,并注册 Advisor

ChatClient chatClient = ChatClient
    .builder(chatModel)
    .defaultAdvisors( new MessageChatMemoryAdvisor(chatMemory), // 对话记忆 advisor
QuestionAnswerAdvisor(vectorStore) // RAG 检索增强 advisor )
    .build();

// 调用时,设定Advisor参数

String response = this.chatClient.prompt() // 指定对话记忆的 id 和长度
    .advisors(advisor ->     advisor
    .param("chat_memory_conversation_id", "678") 
    .param("chat_memory_response_size", 100))
    .user(userText)
    .call()
    .content();

上面都是已经内置的Advisor,为了更好的理解,我们来学习自定义 Advisor

自定义 Advisor 步骤

1)接口实现‍‍,‍实‍现‍以下接‎‎口之‎一或‎同时‎‏‏实现两‏者:

  • CallAroundAdvisor:用于处理同步请求和响应(非流式)
  • StreamAroundAdvisor:用于处理流式请求和响应

(Around = 你把 before + 调用 + after 包成一个整体,自己控制执行顺序)

public class MyCustomAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
    // 实现方法...
}

2)实现核心方法

// 对于非流式处理 (CallAroundAdvisor),实现 aroundCall 方法
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
    // 1. 处理请求(前置处理)
    AdvisedRequest modifiedRequest = before(advisedRequest); // 自定义的before方法,返回新的advisedRequest
    
    // 2. 调用链中的下一个Advisor,也就是执行调用方法了,因为有很多Advisor
    AdvisedResponse response = chain.nextAroundCall(modifiedRequest);
    
    // 3. 处理响应(后置处理)
    return processResponse(response);
}

// 对于流式处理 (StreamAroundAdvisor),实现 aroundStream 方法
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
    // 1. 处理请求
    AdvisedRequest modifiedRequest = before(advisedRequest);
    
    // 2. 调用链中的下一个Advisor并处理流式响应
    return chain.nextAroundStream(modifiedRequest)
               .map(response -> processResponse(response)); // 每一段流式返回处理
}

3)还有其他方法设置

@Override
public int getOrder() {
    // 值越小优先级越高,越先执行
    return 100; 
}

@Override
public String getName() {
    return "为每个 A‏dvisor 提供‍一个唯一标识符";
}
接下来我们尝试自己实现一个日志Advisor
@Slf4j
public class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
@Override
public String getName() {
    return this.getClass().getSimpleName();
}

@Override
public int getOrder() {
    return 0;
}

// 定义前置方法
private AdvisedRequest before(AdvisedRequest request) {
    log.info("AI Request: {}", request.userText());
    return request;
}

// 定义后置方法
private void after(AdvisedResponse response) {
    log.info("AI Response: {}", response.response().getResult().getOutput().getText());
}

// 重写CallAroundAdvisor中的方法
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
    advisedRequest = this.before(advisedRequest); // 执行所定义的前置方法
    AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest); // 执行被拦截方法
    this.after(advisedResponse); // 执行后置方法
    return advisedResponse;
}

// 重写StreamAroundAdvisor中的方法
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
    advisedRequest = this.before(advisedRequest);
    Flux<AdvisedResponse> advisedResponses = chain.nextAroundStream(advisedRequest);
    // 通过 MessageAggregator 工具类将 Flux 响应聚合成单个 AdvisedResponse
    // 与map的一段段处理不同,是等全部返回完,再变成一个完整结果处理,即后面的after后置处理
    return (new MessageAggregator()).aggregateAdvisedResponse(advisedResponses, this::after);
}
}


// 使用自定义的日志Advisor
chatClient = ChatClient.builder(dashscopeChatModel)
        .defaultSystem(SYSTEM_PROMPT)
        .defaultAdvisors(
                new MessageChatMemoryAdvisor(chatMemory),
                // 自定义日志 Advisor,可按需开启
                new MyLoggerAdvisor(),
        )
        .build();
补充:
  1. 更复杂处理的‍流‍式‍场景,可以‎使用‎ R‎eac‏tor‏ 的操‏作⁡符(比如请求本身是异步/流式 Mono/Flux时,就要使用Mono响应式方法(Mono.just、.flatMapMany等))
    public Flux aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
        return Mono.just(request) // “开启一个响应式流水线”,后面可以接 .map() / .flatMap() 等操作
               .publishOn(Schedulers.boundedElastic())  // 切换执行线程,确保后续操作在一个可伸缩的异步线程池里执行
               .map(request -> {
                   // 请求前处理逻辑
                   return before(request);
               })
               .flatMapMany(request -> chain.nextAroundStream(request)) // 把 Mono(request是单值)变成 Flux(多值流)来调用方法
               .map(response -> {
                   // 响应处理逻辑
                   return after(response);
               });
    }

接下来是结构化输出

为保证大模型输出我们规定的格式,有两种约束:

  1. 只靠 Prompt 本质:自然语言约束 问题:AI 可以“偏离”

  2. 模型原生结构化输出,比如现在很多模型支持:
    JSON mode、Function calling、Tool calling
    这些不是普通 prompt,而是通过 API 参数,强制模型按结构输出

首先先了解一个定义:结构化输出转换器(Structured Output Converter)是 Spring AI 提供的一种实用机制,用于将大语言模型返回的文本输出转换为结构化数据格式,如 JSON、XML 或 Java 类,这对于需要可靠解析 AI 输出值的下游应用程序非常重要

那 Spring AI 的 Converter 用了哪个约束呢?

答案是:优先使用模型的“结构化能力”(比如:JSON mode、Function / Tool calling),如果模型不支持才会用 prompt + schema 去“尽量约束”

使用示例:

// 定义一个记录类
record Actors(String actor, List movies) {}
// 使用高级 ChatClient API
Actors actors = ChatClient.create(chatModel).prompt()
        .user("Generate 5 movies for Tom Hanks.")
        .call()
        .entity(actors.class); // 这一步是把“创建 converter + 转换结果”这件事帮你隐藏掉了,
        // 当然为避免泛型擦除可用以下
        .entity(new ParameterizedTypeReference<List>() {});

对话记忆持久化

之前我们使用了基于内存的对话记忆来保存对话上下文,但是服务器一旦重启了,对话记忆就会丢失

如果我们要将对话持久化到数据库中,优先用官方内置 JDBC 方案,只有当默认策略不满足时,再自定义

步骤如下:

  1. 先加依赖
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
    </dependency>
    
  2.  配置,自动配置会在启动时创建 SPRING_AI_CHAT_MEMORY 表,并且会根据数据库厂商选择对应 SQL 脚本
    spring:
        datasource:
            url: jdbc:mysql://localhost:3306...
            username: root
            password: 123456
            driver-class-name: com.mysql.cj.jdbc.Driver
    ai:
     chat:
      memory:
       repository:
        jdbc:
         initialize-schema: always // 这个配置控制是否初始化表结构

PromptTemplate 模板

PromptTemplate 是 Spring AI 框架中用于构建和管理提示词的核心组件。允许开发者创建带有占位符的文本模板,然后在运行时动态替换这些占位符

使用示例

// 定义带有变量的模板,{{即为转义的{
String template = "你好,{name}。今天是{day},天气{weather}。";
// 创建模板对象
PromptTemplate promptTemplate = new PromptTemplate(template);
// 准备变量映射
Map<String, Object> variables = new HashMap<>();
variables.put("name", "kui");
variables.put("day", "星期一");
variables.put("weather", "晴朗");
// 生成最终提示文本
String prompt = promptTemplate.render(variables);
// 结果: "你好,kui。今天是星期一,天气晴朗。"

如何从文件加载模板

这样才是更真实的项目写法了,(基本不会再写死字符串了)

  1. 在 resources 里建一个文件 movie.st
    给我推荐一部关于 {topic} 的电影。
    请按如下 JSON 返回:
    {{ "title": "", "year": "" }}

  2. 用 Resource 读取文件
    Resource resource = new ClassPathResource("prompts/movie.st");
  3. 创建 PromptTemplate
    PromptTemplate template = new PromptTemplate(resource);
  4. 填参数生成 Prompt(create()返回Prompt 对象,render()返回String 字符串)
    Prompt prompt = template.create(Map.of("topic", "科幻"));
  5. 交给 ChatClient
    chatClient.prompt(prompt).call().content();

还有对应不同角色消息的模版类

SystemPromptTemplate:用于系统消息,设置 AI 的行为和背景;AssistantPromptTemplate:用于助手消息,用于设置 AI 回复的结构

SystemPromptTemplate systemTemplate =
    new SystemPromptTemplate("你是一个{role}专家");
PromptTemplate userTemplate =
    new PromptTemplate("请解释 {topic}");
Map<String, Object> params = Map.of(
    "role", "Java",
    "topic", "Spring Boot"
);
Prompt prompt = new Prompt(List.of(
    systemTemplate.createMessage(params),
    userTemplate.createMessage(params)
));

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐