用 SpringAIAlibab 让高频问题实现毫秒级响应

在上一篇文章《用 Spring AI Alibaba 打造智能查询增强引擎》中,我们实现了查询增强 Hook,让 RAG

系统能听懂"弦外之音"。但增强后的查询依然要走完整的检索链路:向量召回、重排序、LLM

生成......如果用户反复问同一个问题,这套流程每次都要重跑,不仅慢,还烧钱。

今天我们来实现语义缓存(Semantic Cache),让高频问题实现毫秒级响应。

传统 Redis 缓存用的是精确匹配

  • 用户问:"iPhone 15 Pro Max 多少钱?"
  • 缓存 Key:iPhone 15 Pro Max 多少钱?
  • 用户问:"iPhone 15 Pro Max 价格是多少?" → 缓存 miss,明明意思一样

传统缓存无法理解语义相似性

语义缓存的核心思想:

  1. 将用户查询向量化
  2. 在向量数据库中检索相似的历史查询
  3. 如果相似度超过阈值,直接返回历史答案

效果对比:

  • 精确缓存:命中率低,只能覆盖完全相同的问法
  • 语义缓存:命中率高,能覆盖"价格""多少钱""售价""报价"等所有同义表达

在 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自适应压缩。

相关推荐
952365 小时前
MyBatis
后端·spring·mybatis
科技小花6 小时前
全球化深水区,数据治理成为企业出海 “核心竞争力”
大数据·数据库·人工智能·数据治理·数据中台·全球化
zhuiyisuifeng7 小时前
2026前瞻:GPTimage2镜像官网或将颠覆视觉创作
人工智能·gpt
徐健峰7 小时前
GPT-image-2 热门玩法实战(一):AI 看手相 — 一张手掌照片生成专业手相分析图
人工智能·gpt
weixin_370976357 小时前
AI的终极赛跑:进入AGI,还是泡沫破灭?
大数据·人工智能·agi
FQNmxDG4S7 小时前
Java多线程编程:Thread与Runnable的并发控制
java·开发语言
Slow菜鸟7 小时前
AI学习篇(五) | awesome-design-md 使用说明
人工智能·学习
冬奇Lab8 小时前
RAG 系列(五):Embedding 模型——语义理解的核心
人工智能·llm·aigc
深小乐8 小时前
AI 周刊【2026.04.27-05.03】:Anthropic 9000亿美元估值、英伟达死磕智能体、中央重磅定调AI
人工智能
码点滴8 小时前
什么时候用 DeepSeek V4,而不是 GPT-5/Claude/Gemini?
人工智能·gpt·架构·大模型·deepseek