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

相关推荐
Mr数据杨5 分钟前
车辆保险价值预测实战 从 Kaggle 回归赛题理解保险定价建模
人工智能·数据分析·kaggle竞赛
图王大胜11 分钟前
万物演化论07(第三章 ) 文明启动,开始有了意识
人工智能·ai·宇宙·演化·系统科学
QC777LX14 分钟前
制造业成本会计学怎么学AI,从哪几个工作环节开始更好?
人工智能
DolphinDB智臾科技14 分钟前
不懂复杂金融数据,也能让 AI 做投研:DolphinDB 股票分析 MCP 已开源
人工智能·金融·开源
霸道流氓气质16 分钟前
Spring AI 技术细节:VectorStore 多库统一抽象
java·人工智能·spring
AI天行健16 分钟前
文生视频与图生视频的技术区别及适用场景分析
人工智能·音视频
今天AI了吗17 分钟前
Codex 配置自定义 AI API 完整指南:从零到一接入你的专属模型
java·人工智能·python·数据分析·embedding
东离与糖宝17 分钟前
不用高端显卡!本地大模型量化入门|Ollama+transformers+llama.cpp实战
人工智能
森山冶仁17 分钟前
治理知识库构建:用 RAG 把制度、文档、经验变成 AI 能力
人工智能·智能问答·rag·企业知识库·大模型落地·ai治理
安科瑞黄益鸣24 分钟前
筑牢配电安全:安科瑞 ARB 弧光保护在半导体厂房的应用
人工智能