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

相关推荐
anzhxu1 小时前
SpringBoot 3.x 整合swagger
java·spring boot·后端
gechunlian881 小时前
Spring Security 官网文档学习
java·学习·spring
小江的记录本2 小时前
【Bean】JavaBean(原生规范)/ Spring Bean 【重点】/ 企业级Bean(EJB/Jakarta Bean)
java·数据库·spring boot·后端·spring·spring cloud·mybatis
qqty12172 小时前
spring loC&DI 详解
java·spring·rpc
中国胖子风清扬2 小时前
Camunda 8 概念详解:梳理新一代工作流引擎的核心概念与组件
java·spring boot·后端·spring cloud·ai·云原生·spring webflux
闻哥2 小时前
MySQL InnoDB 缓存池(Buffer Pool)详解:原理、结构与链表管理
java·数据结构·数据库·mysql·链表·缓存·面试
AI科技星2 小时前
基于v≡c第一性原理的大统一力方程:严格推导、全维度验证与四大基本相互作用的统一
人工智能·线性代数·算法·机器学习·平面
殷紫川2 小时前
告别臃肿部署!Java Serverless 函数计算架构全解与实战选型指南
java·架构
俊哥V2 小时前
[特殊字符] 每日 AI 研究简报 · 2026-03-23
人工智能