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。目前新建了一个交流群,一个人走得快,一群人走得远,另外,本人长期维护一套飞书云文档笔记,涵盖后端、大数据系统化的面试资料,可私信免费获取

相关推荐
码事漫谈13 分钟前
Visual Studio 2026真的值得升级吗中国开发者实测报告
后端
MediaTea1 小时前
Python 第三方库:Flask(轻量级 Web 框架)
开发语言·前端·后端·python·flask
q***72561 小时前
Spring Boot + Vue 全栈开发实战指南
vue.js·spring boot·后端
清晨细雨~1 小时前
SpringBoot整合EasyExcel实现Excel表头校验
spring boot·后端·excel
canonical_entropy2 小时前
API无缝升级方案:从推模式到拉模式的架构演进
后端·restful·graphql
摆烂工程师2 小时前
今天 Cloudflare 全球事故,连 GPT 和你的网站都一起“掉线”了
前端·后端·程序员
追逐时光者2 小时前
快速构建一个基础、现代化的 WinForm 管理系统
后端·.net
在人间负债^4 小时前
Rust 实战项目:TODO 管理器
开发语言·后端·rust
Moonbit4 小时前
入围名单公布|2025 MGPIC 决赛即将拉开帷幕!
后端·算法
爱吃烤鸡翅的酸菜鱼4 小时前
用【rust】实现命令行音乐播放器
开发语言·后端·rust