你是否遇到过这样的情况:精心设计的 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
,这里总结了几个最佳实践:
- 保持单一职责:每个 Advisor 应该只做一件事,比如日志、缓存、重试或像我们今天的 Re2。
- 注意执行顺序 :通过
getOrder()
控制 Advisor 的执行顺序,确保逻辑正确。 - 兼容流式与非流式 :尽可能同时实现
CallAroundAdvisor
和StreamAroundAdvisor
接口,让你的 Advisor 更通用。 - 保持高效:避免在 Advisor 中执行耗时操作,以免阻塞整个调用链。
- 充分测试:特别是边界情况,确保 Advisor 的健壮性。
- 善用 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); // 响应后处理
}
- 共享状态(进阶) :使用
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");