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

相关推荐
IT_陈寒1 小时前
SpringBoot 3.2 踩坑实录:这5个‘自动配置’的坑,让我加班到凌晨三点!
前端·人工智能·后端
绝无仅有1 小时前
系统面试设计架构的深度解析:方法论、宏观与微观分析
后端·面试·github
期待のcode2 小时前
SpringMVC的请求接收与结果响应
java·后端·spring·mvc
风象南2 小时前
SpringBoot 「热补丁加载器」:线上紧急 bug 临时修复方案
后端
Victor3562 小时前
Redis(43)Redis哨兵(Sentinel)是什么?
后端
Victor3563 小时前
Redis(42)Redis集群如何处理键的迁移?
后端
程序员爱钓鱼5 小时前
Go语言实战案例- Redis实现简单排行榜
后端·google·go
angushine5 小时前
Spring Boot 工程启动时自动执行任务方法
java·spring boot·后端
野犬寒鸦7 小时前
力扣hot100:缺失的第一个正数(哈希思想)(41)
java·数据结构·后端·算法·leetcode·哈希算法