什么是 Re-Reading?如何基于 Spring AI 实现 Re-Reading Advisor?
·
Re-Reading 是一种在大型语言模型(LLM)应用中用于优化上下文管理的技术。它通过动态调整输入提示中的历史对话内容,确保模型能够关注到最相关的信息,从而提高响应质量和效率。
什么是 Re-Reading?
Re-Reading 的核心思想是:当用户与 LLM 进行多轮对话时,并非所有历史消息都对当前问题具有同等重要性。因此,在构建新的提示时,系统会重新评估并筛选出最有价值的历史交互片段作为参考信息传递给模型。这种机制有助于:
- 减少冗余信息:避免将过多无关或重复的内容发送至模型。
- 提升性能表现:降低 token 使用量和处理延迟。
- 增强准确性:聚焦关键背景以生成更精准的答案。
如何基于 Spring AI 实现 Re-Reading Advisor?
Spring AI 提供了丰富的抽象层来简化 LLM 应用程序开发流程。要实现一个具备 Re-Reading 功能的 Advisor 模块,可以按照以下步骤进行设计与编码:
步骤一:定义核心组件接口
首先创建两个主要接口:
ConversationHistoryManager
负责维护用户的完整聊天记录;ContextSelector
根据策略从历史中挑选有效部分形成精简版上下文;
示例代码结构如下所示:
// ConversationHistoryManager.java
public interface ConversationHistoryManager {
void addMessage(String userId, ChatMessage message);
List<ChatMessage> getFullHistory(String userId);
}
// ContextSelector.java
public interface ContextSelector {
List<ChatMessage> selectRelevantMessages(List<ChatMessage> fullHistory, String currentQuery);
}
其中 ChatMessage
类可表示单条对话内容,包含角色(如 USER/SYSTEM/ASSISTANT)、时间戳及文本等字段。
步骤二:编写具体实现类
接下来为上述接口提供基础版本的具体逻辑实现:
简易内存存储方案 —— MemoryBasedHistoryManagerImpl.java
@Component
public class MemoryBasedHistoryManager implements ConversationHistoryManager {
private final Map<String, Deque<ChatMessage>> userHistories = new ConcurrentHashMap<>();
@Override
public void addMessage(String userId, ChatMessage message) {
userHistories.computeIfAbsent(userId, k -> new ArrayDeque<>()).addLast(message);
}
@Override
public List<ChatMessage> getFullHistory(String userId) {
Deque<ChatMessage> history = userHistories.getOrDefault(userId, new ArrayDeque<>());
return new ArrayList<>(history);
}
}
基于相似度匹配的选择器 —— SimilarityBasedContextSelectorImpl.java
利用嵌入向量化工具计算各段落之间的语义距离,选取 Top-K 最相关的条目组成最终 Prompt 输入集。
@Service
public class SimilarityBasedContextSelector implements ContextSelector {
private final EmbeddingClient embeddingClient;
public SimilarityBasedContextSelector(EmbeddingClient embeddingClient) {
this.embeddingClient = embeddingClient;
}
@Override
public List<ChatMessage> selectRelevantMessages(List<ChatMessage> fullHistory, String currentQuery) {
// 获取当前查询对应的向量表示
float[] queryVector = embeddingClient.embed(currentQuery);
PriorityQueue<MessageWithScore> topMatches = new PriorityQueue<>(Comparator.comparingDouble(m -> m.score));
for (ChatMessage msg : fullHistory) {
float[] msgVector = embeddingClient.embed(msg.getContent());
double similarity = cosineSimilarity(queryVector, msgVector); // 自定义函数计算余弦相似度
if (topMatches.size() < MAX_CONTEXT_SIZE || similarity > topMatches.peek().score) {
topMatches.offer(new MessageWithScore(msg, similarity));
if (topMatches.size() > MAX_CONTEXT_SIZE)
topMatches.poll();
}
}
return topMatches.stream()
.sorted((a,b)->Double.compare(b.score,a.score))
.map(m->m.message)
.collect(Collectors.toList());
}
private static class MessageWithScore {
final ChatMessage message;
final double score;
MessageWithScore(ChatMessage message, double score) {
this.message = message;
this.score = score;
}
}
private double cosineSimilarity(float[] vectorA, float[] vectorB) { /* 具体数学公式略 */ }
}
步骤三:整合进 Spring AI 流程链路中
最后一步是在实际调用大模型前插入我们的“再读取”决策节点。可以通过扩展默认 Chain 或者自定义 Handler 来完成这项任务。
假设我们有一个标准的 QA Chain 构造过程如下:
PromptTemplate promptTemplate = new PromptTemplate("Previous context:\n{context}\n\nQuestion: {question}");
Map<String,Object> vars = new HashMap<>();
vars.put("context", StringUtils.join(selectedContext,"\n"));
vars.put("question", userInput);
Prompt prompt = promptTemplate.create(vars);
Generation generation = aiClient.call(prompt).getGeneration();
return generation.getText();
此时只需替换掉 {context}
占位符所使用的原始全量数据源即可启用 Re-Reading 特性。
总结图示说明
下面是一张 Mermaid 图形化展示了整个工作流概览:
更多推荐
所有评论(0)