Spring AI (GA)的advisor机制:开启 DeepThink 模式

备注:Spring AI 每个里程碑过大,导致以前能实现的 Deep Think 方式,在 GA 版失效。本期站在源码角度修复 Deep Think 模式

M6 版本不需要修改源码时实现的 Deep Think 源码可见(看后文发现,原来 M6 版实现有问题,确反而生效):github.com/GTyingzi/Sp...

本期最新 1.0 GA 版本实战代码可见:github.com/GTyingzi/sp... 下的 advisor/advisor-deep-think

AI 模型返回 reasoningcontent

调用 AI 模型的 API,返回对应的结果,这是比较常规的操作了

reasoningcontent 字段就是我们调用 AI 模型的 API 返回的思考内容,我们需要把该字段透传出来

  • 同时注意到此时的 finishreason 为 null,请先记住该字段,这是现阶段 SpringAI GA 版下 Deep Think 模式失效的罪魁祸首

M6 版的 Deep Think 实现

源码地址可见:github.com/GTyingzi/Sp...

效果如下:

机制是我们自定义 ReasoningContentAdvisor 类去实现 BaseAdvisor 接口,重写 after 方法,取出 reasoningContent 内容

java 复制代码
package com.yingzi.advisor.component;

import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Objects;

/**
 * @author yingzi
 * @date 2025/3/21:17:36
 */
public class ReasoningContentAdvisor implements BaseAdvisor {

    private static final Logger logger = LoggerFactory.getLogger(ReasoningContentAdvisor.class);

    private final int order;

    public ReasoningContentAdvisor(Integer order) {
        this.order = order != null ? order : 0;
    }

    @NotNull
    @Override
    public AdvisedRequest before(@NotNull AdvisedRequest request) {
        return request;
    }

    @NotNull
    @Override
    public AdvisedResponse after(AdvisedResponse advisedResponse) {

        ChatResponse resp = advisedResponse.response();
        if (Objects.isNull(resp)) {
            return advisedResponse;
        }

        logger.info(String.valueOf(resp.getResults().get(0).getOutput().getMetadata()));
        String reasoningContent = String.valueOf(resp.getResults().get(0).getOutput().getMetadata().get("reasoningContent"));

        if (StringUtils.hasText(reasoningContent)) {
            List<Generation> thinkGenerations = resp.getResults().stream()
                    .map(generation -> {
                        AssistantMessage output = generation.getOutput();
                        AssistantMessage thinkAssistantMessage = new AssistantMessage(
                                String.format("<think>%s</think>", reasoningContent) + output.getText(),
                                output.getMetadata(),
                                output.getToolCalls(),
                                output.getMedia()
                        );
                        return new Generation(thinkAssistantMessage, generation.getMetadata());
                    }).toList();

            ChatResponse thinkChatResp = ChatResponse.builder().from(resp).generations(thinkGenerations).build();
            return AdvisedResponse.from(advisedResponse).response(thinkChatResp).build();

        }

        return advisedResponse;
    }

    @Override
    public int getOrder() {
        return this.order;
    }

}

GA 版 Deep Think 实现

GA 版对于 Advisor 做了比较大的改动,详情可见:[Advisor基础] + [AdvisorChain链]

首先来看下正常请求的流程图:用户 Query 流式请求 -> DefaultAroundAdvisorChain 类的 nextStream 方法 ---> 调用 BaseAdvisor 接口类(被 ReasoningContentAdvisor 实现)的默认 adviseStream 方法 ---> 调用 ReasoningContentAdvisor 类的 before 方法 -> 调用 ChatModelStreamAdvisor 类获取 Flux -> 调用 ReasoningContentAdvisor 类的 after 方法

流式一段段的返回结果,在每段结果中是否会调用 ReasoningContentAdvisor 类的 after 方法由 AdvisorUtils 类的 onFinishReason 方法控制,校验通过才会调用

让我们回到 ReasoningContentAdvisor 类实现 BaseAdvisor 接口,重写 after 方法

java 复制代码
package com.spring.ai.tutorial.advisor.component;

import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.AdvisorChain;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Objects;

/**
 * @author yingzi
 * @date 2025/3/21:17:36
 */
public class ReasoningContentAdvisor implements BaseAdvisor {

    private static final Logger logger = LoggerFactory.getLogger(ReasoningContentAdvisor.class);

    private final int order;

    public ReasoningContentAdvisor(Integer order) {
        this.order = order != null ? order : 0;
    }

    @Override
    public int getOrder() {
        return this.order;
    }

    @Override
    public ChatClientRequest before(@NotNull final ChatClientRequest chatClientRequest, @NotNull final AdvisorChain advisorChain) {
        return chatClientRequest;
    }

    @Override
    public ChatClientResponse after(@NotNull final ChatClientResponse chatClientResponse, @NotNull final AdvisorChain advisorChain) {
        ChatResponse resp = chatClientResponse.chatResponse();
        if (Objects.isNull(resp)) {

            return chatClientResponse;
        }

        logger.debug(String.valueOf(resp.getResults().get(0).getOutput().getMetadata()));
        String reasoningContent = String.valueOf(resp.getResults().get(0).getOutput().getMetadata().get("reasoningContent"));

        if (StringUtils.hasText(reasoningContent)) {
            List<Generation> thinkGenerations = resp.getResults().stream()
                    .map(generation -> {
                        AssistantMessage output = generation.getOutput();
                        AssistantMessage thinkAssistantMessage = new AssistantMessage(
                                String.format("<think>%s</think>", reasoningContent) + output.getText(),
                                output.getMetadata(),
                                output.getToolCalls(),
                                output.getMedia()
                        );
                        return new Generation(thinkAssistantMessage, generation.getMetadata());
                    }).toList();

            ChatResponse thinkChatResp = ChatResponse.builder().from(resp).generations(thinkGenerations).build();
            return ChatClientResponse.builder().chatResponse(thinkChatResp).build();

        }

        return chatClientResponse;
    }
}

发现只有 AI 模型 API 返回为"stop"的才被传递出来,此时的 reasoncontent 是无值的

  • reasoncontent 有值时:finishreason 的 AI 模型 API 返回为"null"
  • reasoncontent 无值时:finishreason 的 AI 模型 API 返回为"stop"

我们回到 AdvisorUtils 类的 onFinishReason 方法,发现要求 finishreason 有字段,才通过校验

而我们现阶段 reasoncontent 有值时,finishreason 的 AI 模型 API 返回为"null",后续一些逻辑处理,使得 finishReason="",导致校验不通过

到这里,解决方案就出来了,重写 AdvisorUtils 类,去掉 finishReason 判断为空的校验逻辑即可

java 复制代码
package org.springframework.ai.chat.client.advisor;

import java.util.function.Predicate;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.util.StringUtils;

public final class AdvisorUtils {
    private AdvisorUtils() {
    }

    public static Predicate<ChatClientResponse> onFinishReason() {
        return (chatClientResponse) -> {
            ChatResponse chatResponse = chatClientResponse.chatResponse();
            return chatResponse != null && chatResponse.getResults() != null && chatResponse.getResults().stream().anyMatch((result) -> result != null && result.getMetadata() != null);
        };
    }
}

流式输出结果如下:

不对啊,M6 版也有 BaseAdvisor 啊,那为什么那边可以测验通过呢?让我们重新回到 M6 的 BaseAdvisor 类进行 debug,发现 finishReash 字段存了个"NULL"的字符串,这里因为实现不完善留下的 Bug 反而正常调用了 ReasoningContentAdvisor 类 😆

往期资料

Spring AI + Spring Ai Aliabba系统化学习资料

本教程将采用2025年5月20日正式的GA版,给出如下内容

  1. 核心功能模块的快速上手教程
  2. 核心功能模块的源码级解读
  3. Spring ai alibaba增强的快速上手教程 + 源码级解读

版本:

  • JDK21
  • SpringBoot3.4.5
  • SpringAI 1.0.0
  • SpringAI Alibaba 跟着最新

免费渠道:

  1. 为Spring Ai Alibaba开源社区解决解决有效的issue or 提供有价值的PR,可免费获取上述教程
  2. 往届微信推文

收费服务:收费69.9元

  1. 飞书在线云文档
  2. Spring AI会员群教程代码答疑

学习交流圈

你好,我是影子,曾先后在🐻、新能源、老铁就职,兼任Spring AI Alibaba开源社区的Committer。目前新建了一个交流群,一个人走得快,一群人走得远,另外,本人长期维护一套飞书云文档笔记,涵盖后端、大数据系统化的面试资料,可私信免费获取

相关推荐
间彧44 分钟前
Windows Server,如何使用WSFC+nginx实现集群故障转移
后端
间彧1 小时前
Nginx + Keepalived 实现高可用集群(Linux下)
后端
间彧1 小时前
在Kubernetes中如何部署高可用的Nginx Ingress Controller?
后端
间彧1 小时前
Ribbon负载均衡器和Nginx负载均衡器有什么区别
后端
间彧1 小时前
Nacos详解与项目实战
后端
间彧1 小时前
nginx、网关Gateway、Nacos、多个服务实例之间的数据链路详解
后端
间彧1 小时前
Nacos与Eureka在性能上有哪些具体差异?
后端
间彧1 小时前
详解Nacos健康状态监测机制
后端
间彧1 小时前
如何利用Nacos实现配置的灰度发布?
后端
毕业设计制作和分享1 小时前
springboot159基于springboot框架开发的景区民宿预约系统的设计与实现
java·spring boot·后端