你是否遇到过这样的情况:精心设计的 AI 应用,在面对稍微复杂点的问题时,给出的答案却驴唇不对马嘴?感觉它好像“看了一眼就答”,根本没仔细“阅读理解”。
别急,今天就为你介绍一个能显著提升大模型推理能力的技巧——Re-Reading(重读),简称 Re2。这个方法有 论文 背书,效果显著。
更棒的是,在 Spring AI 中,我们可以通过 Advisor(顾问) 模式,优雅地实现这一功能,让你的 AI 在回答前真正做到“三思而后行”。
什么是 Re-Reading (Re2)?
Re2 的原理出奇地简单:让模型把问题再读一遍。
我们只需要将用户的原始问题({Input_Query}
)通过 Prompt 改造为以下格式:
{Input_Query}Read the question again: {Input_Query}
通过这种方式,强制模型在生成答案前重新审视问题,从而有效减少误解,提高复杂推理任务的准确率。
💡 友情提示:这种方法虽然能提升效果,但因为输入长度翻倍,API 调用成本也会随之翻倍。因此,在面向 C 端的、成本敏感的应用中请谨慎使用!
构建你的 Re2 Advisor
在 Spring AI 中,Advisor
是一种 AOP(面向切面编程)思想的体现,它允许我们在不侵入核心业务逻辑的情况下,对 AI 的请求和响应进行拦截和增强。
下面,我们来创建一个 ReReadingAdvisor
,它会拦截用户请求并自动应用 Re2 模式。
/** * @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()
方法将其加入即可。
/** * 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
,这里总结了几个最佳实践:
- 保持单一职责:每个 Advisor 应该只做一件事,比如日志、缓存、重试或像我们今天的 Re2。注意执行顺序:通过
getOrder()
控制 Advisor 的执行顺序,确保逻辑正确。兼容流式与非流式:尽可能同时实现 CallAroundAdvisor
和 StreamAroundAdvisor
接口,让你的 Advisor 更通用。保持高效:避免在 Advisor 中执行耗时操作,以免阻塞整个调用链。充分测试:特别是边界情况,确保 Advisor 的健壮性。善用 Reactor(进阶):对于复杂的流式处理,可以利用 Reactor
的操作符进行精细控制。@Overridepublic Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) { return Mono.just(advisedRequest) .publishOn(Schedulers.boundedElastic()) .map(this::modifyRequest) // 请求前处理 .flatMapMany(chain::nextAroundStream) .map(this::modifyResponse); // 响应后处理}
- 共享状态(进阶):使用
advisedRequest.updateContext()
和 advisedResponse.adviseContext()
在 Advisor 链中传递状态。// 在 Advisor A 中更新上下文advisedRequest = advisedRequest.updateContext(context -> {context.put("my_key", "my_value");return context;});// 在 Advisor B 中读取上下文Object value = advisedResponse.adviseContext().get("my_key");