在上一篇文章《用 Spring AI Alibaba 打造智能查询增强引擎》中,我们实现了查询增强 Hook,让 RAG
系统能听懂"弦外之音"。但增强后的查询依然要走完整的检索链路:向量召回、重排序、LLM
生成......如果用户反复问同一个问题,这套流程每次都要重跑,不仅慢,还烧钱。
今天我们来实现语义缓存(Semantic Cache),让高频问题实现毫秒级响应。
传统 Redis 缓存用的是精确匹配:
- 用户问:"iPhone 15 Pro Max 多少钱?"
- 缓存 Key:
iPhone 15 Pro Max 多少钱? - 用户问:"iPhone 15 Pro Max 价格是多少?" → 缓存 miss,明明意思一样
传统缓存无法理解语义相似性。
语义缓存的核心思想:
- 将用户查询向量化
- 在向量数据库中检索相似的历史查询
- 如果相似度超过阈值,直接返回历史答案
效果对比:
- 精确缓存:命中率低,只能覆盖完全相同的问法
- 语义缓存:命中率高,能覆盖"价格""多少钱""售价""报价"等所有同义表达
在 Spring AI Alibaba 的 Agent 框架中,我们在 BEFORE_MODEL 位置插入语义缓存 Hook:
BEFORE_MODEL在模型调用之前执行- 如果命中缓存,可以通过
JumpTo.end跳过后续所有节点(包括 RAG、LLM 生成) - 性能提升最明显
首先定义缓存服务:
java
/**
* 语义缓 Service
*/
@Service
public class SemanticCacheService {
@Autowired
@Qualifier("redisVectorStore")
private VectorStore redisVectorStore;
@Autowired
private JedisPooled jedisPooled;
// 阈值:相似度
private static final double SIMILARITY_THRESHOLD = 0.90;
// 缓存有效期
private static final long CACHE_TTL_SECONDS = 60 * 10;
/**
* 尝试命中缓存
* @return 命中则返回答案,未命中返回 null
*/
public String getIfPresent(String userQuestion) {
// 1. 搜索最相似的 1 个问题
List<Document> results = redisVectorStore.similaritySearch(
SearchRequest.builder().similarityThreshold(SIMILARITY_THRESHOLD)
.topK(1)
.query(userQuestion)
.build()
);
if (results.isEmpty()) {
return null;
}
Document doc = results.getFirst();
return doc.getMetadata().get("answer").toString();
}
/**
* 存入缓存
*/
public void put(String userQuestion, String llmAnswer) {
// 过滤无效回答 (可选)
if (llmAnswer == null || llmAnswer.trim().isBlank()) {
return;
}
String docId = UUID.randomUUID().toString();
String redisKey = "embedding:" + docId;
Document doc = new Document(
docId,
userQuestion,
Map.of("answer", llmAnswer)
);
// 1. 写入向量索引
redisVectorStore.add(List.of(doc));
// 2. 设置过期时间 (关键步骤)
try {
jedisPooled.expire(redisKey, CACHE_TTL_SECONDS);
} catch (Exception e) {
System.err.println("⚠️ 设置缓存 TTL 失败: " + e.getMessage());
}
}
}
下面是 Hook 的核心代码,这里存在两个bug:
-
[BUG\] Messages数量在AgentHook和MessagesModelHook之间出现异常增长 · Issue #4441 · alibaba/spring-ai-alibaba](https://github.com/alibaba/spring-ai-alibaba/issues/4441):
java
/**
* 语义缓存Hook
**/
@Slf4j
@RequiredArgsConstructor
@HookPositions({HookPosition.BEFORE_MODEL})
public class SemanticCacheHook extends MessagesModelHook {
private final SemanticCacheService semanticCacheService;
public static final String CACHE_HIT_KEY = "cache_hit";
@Override
public String getName() {
return "semantic_cache_check";
}
@Override
public List<JumpTo> canJumpTo() {
return List.of(JumpTo.end);
}
@Override
public AgentCommand beforeModel(List<Message> previousMessages, RunnableConfig config) {
previousMessages = previousMessages.stream().distinct().toList();
String queryToSearch = previousMessages.stream()
.filter(msg -> msg instanceof UserMessage)
.map(msg -> ((UserMessage) msg).getText())
.reduce((first, second) -> second)
.orElse("");
if (queryToSearch.isBlank()) {
return new AgentCommand(previousMessages); // 无查询,继续流程
}
// 执行向量搜索 (Redis)
String cache = semanticCacheService.getIfPresent(queryToSearch);
if (cache != null && !cache.isBlank()) {
// 元数据中增加内容结果,并结束流程
config.metadata().ifPresent(meta -> meta.put(CACHE_HIT_KEY, cache));
return new AgentCommand(JumpTo.end, previousMessages);
}
return new AgentCommand(null, previousMessages);
}
}
缓存命中后直接返回,但缓存未命中时,需要在正常流程结束后写入缓存。参考前文介绍。
效果对比:
| 场景 | 无缓存 | 精确缓存 | 语义缓存 |
|---|---|---|---|
| "iPhone 15 多少钱?" | 全流程 | 命中 | 命中 |
| "iPhone 15 价格是多少?" | 全流程 | miss | 命中 ✅ |
| "15 Pro Max 售价" | 全流程 | miss | 命中 ✅ |
| 响应时间 | 2-5 秒 | < 100ms | < 100ms |
| Token 成本 | 每次都有 | 仅首次 | 仅首次 |
语义缓存命中检测的阈值建议在 0.85~0.92 之间微调。
语义缓存是 RAG 系统性能优化的关键一环。通过 Spring AI Alibaba 的 MessagesModelHook,我们优雅地在模型调用前插入了缓存检查逻辑,实现了相似问题命中缓存时跳过整个 RAG 链路的效果,这大幅减少了 LLM 调用次数。
下一篇文章介绍Token自适应压缩。