Spring AI 实现让你的 AI “三思而后行”

你是否遇到过这样的情况:精心设计的 AI 应用,在面对稍微复杂点的问题时,给出的答案却驴唇不对马嘴?感觉它好像"看了一眼就答",根本没仔细"阅读理解"。

别急,今天就为你介绍一个能显著提升大模型推理能力的技巧------Re-Reading(重读) ,简称 Re2 。这个方法有 论文 背书,效果显著。

更棒的是,在 Spring AI 中,我们可以通过 Advisor(顾问) 模式,优雅地实现这一功能,让你的 AI 在回答前真正做到"三思而后行"。

什么是 Re-Reading (Re2)?

Re2 的原理出奇地简单:让模型把问题再读一遍

我们只需要将用户的原始问题({Input_Query})通过 Prompt 改造为以下格式:

java 复制代码
{Input_Query}
Read the question again: {Input_Query}

通过这种方式,强制模型在生成答案前重新审视问题,从而有效减少误解,提高复杂推理任务的准确率。

💡 友情提示:这种方法虽然能提升效果,但因为输入长度翻倍,API 调用成本也会随之翻倍。因此,在面向 C 端的、成本敏感的应用中请谨慎使用!

构建你的 Re2 Advisor

在 Spring AI 中,Advisor 是一种 AOP(面向切面编程)思想的体现,它允许我们在不侵入核心业务逻辑的情况下,对 AI 的请求和响应进行拦截和增强。

下面,我们来创建一个 ReReadingAdvisor,它会拦截用户请求并自动应用 Re2 模式。

java 复制代码
/**
 * @author BNTang
 * @version 1.0
 * @description 自定义 Re2 Advisor,通过让模型重读问题来提高其推理能力。
 **/
public class ReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {

    /**
     * 在 AI 调用前执行,负责改写用户请求。
     *
     * @param advisedRequest 原始请求
     * @return 应用了 Re2 模式的新请求
     */
    private AdvisedRequest before(AdvisedRequest advisedRequest) {
        // 将原始查询存入参数,以便在模板中使用
        Map<String, Object> advisedUserParams = new HashMap<>(advisedRequest.userParams());
        advisedUserParams.put("re2_input_query", advisedRequest.userText());

        // 使用新模板构建并返回 AdvisedRequest
        return AdvisedRequest.from(advisedRequest)
                .userText("""
                        {re2_input_query}
                        Read the question again: {re2_input_query}
                        """)
                .userParams(advisedUserParams)
                .build();
    }

    /**
     * 环绕处理非流式调用。
     */
    @NotNull
    @Override
    public AdvisedResponse aroundCall(@NotNull AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
        // 调用 before 方法修改请求,然后传递给调用链的下一个环节
        return chain.nextAroundCall(this.before(advisedRequest));
    }

    /**
     * 环绕处理流式调用。
     */
    @NotNull
    @Override
    public Flux<AdvisedResponse> aroundStream(@NotNull AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
        // 同样,调用 before 方法修改请求,然后传递给调用链
        return chain.nextAroundStream(this.before(advisedRequest));
    }

    /**
     * 返回 Advisor 的名称。
     */
    @NotNull
    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }

    /**
     * 定义 Advisor 的执行顺序,数值越小,优先级越高。
     */
    @Override
    public int getOrder() {
        return 0; // 设置为高优先级
    }
}

即插即用:在 ChatClient 中启用 Advisor

Advisor 写好了,用起来也非常简单。只需在构建 ChatClient 时,通过 .defaultAdvisors() 方法将其加入即可。

java 复制代码
/**
 * App 构造函数,初始化聊天客户端。
 *
 * @param ollamaChatModel 聊天模型实例
 */
public App(ChatModel ollamaChatModel) {
    ChatMemory chatMemory = new InMemoryChatMemory();
    chatClient = ChatClient.builder(ollamaChatModel)
            .defaultSystem(SYSTEM_PROMPT)
            .defaultAdvisors(
                    new MessageChatMemoryAdvisor(chatMemory), // 记忆顾问
                    new ReReadingAdvisor() // 启用 Re-Reading 顾问!
            )
            .build();
}

现在,所有通过这个 chatClient 发出的请求,都会自动被 ReReadingAdvisor 处理,实现推理增强,而我们的业务代码无需做任何改动。是不是非常优雅?

Advisor 最佳实践清单

为了让你更好地驾驭 Advisor,这里总结了几个最佳实践:

  1. 保持单一职责:每个 Advisor 应该只做一件事,比如日志、缓存、重试或像我们今天的 Re2。
  2. 注意执行顺序 :通过 getOrder() 控制 Advisor 的执行顺序,确保逻辑正确。
  3. 兼容流式与非流式 :尽可能同时实现 CallAroundAdvisorStreamAroundAdvisor 接口,让你的 Advisor 更通用。
  4. 保持高效:避免在 Advisor 中执行耗时操作,以免阻塞整个调用链。
  5. 充分测试:特别是边界情况,确保 Advisor 的健壮性。
  6. 善用 Reactor(进阶) :对于复杂的流式处理,可以利用 Reactor 的操作符进行精细控制。
java 复制代码
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
    return Mono.just(advisedRequest)
            .publishOn(Schedulers.boundedElastic())
            .map(this::modifyRequest) // 请求前处理
            .flatMapMany(chain::nextAroundStream)
            .map(this::modifyResponse); // 响应后处理
}
  1. 共享状态(进阶) :使用 advisedRequest.updateContext()advisedResponse.adviseContext() 在 Advisor 链中传递状态。
java 复制代码
// 在 Advisor A 中更新上下文
advisedRequest = advisedRequest.updateContext(context -> {
context.put("my_key", "my_value");
return context;
});

// 在 Advisor B 中读取上下文
Object value = advisedResponse.adviseContext().get("my_key");