用 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:

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

相关推荐
minge000137 分钟前
【世界杯中的AI】(2026-07-05)当足球遇见AI:7月5日世界杯的“人机共舞”与冷门之夜
人工智能·科技·世界杯
IT_陈寒1 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
wumingxiaoyao1 小时前
从 0 开始学 AI:第 2 课,AI、机器学习、深度学习和大模型是什么关系?
人工智能·深度学习·机器学习·ai·大模型·llm
2601_962683891 小时前
治理遗留系统中的“生肉 SQL”:一次用多模型协作优化慢查询的实战复盘
数据库·人工智能·sql
踩着两条虫1 小时前
可视化 vs 终端 vs 云端:VTJ.PRO、Claude Code、Codex 三强横评
前端·vue.js·人工智能·低代码·架构
字节跳动视频云技术团队1 小时前
Agentic 范式下的视频画质优化:火山引擎的新路径
人工智能·音视频开发
欢子2 小时前
Android 端接入AI
人工智能
酱学编程2 小时前
【从零到一实现一个 AI Agent 框架 · 第四篇】04. 任务规划:拆解复杂目标 -
服务器·网络·数据库·人工智能
手写码匠2 小时前
手写 AI 上下文压缩系统:从零实现 Prompt 压缩与选择性上下文管理
人工智能·深度学习·算法·aigc