大模型问答系统缓存优化

大模型问答系统缓存优化:让重复问题秒回,让计算资源不白费!

作者:孔帅 | 转载请注明出处

大家好!今天我们来聊聊大模型问答系统中的一个”偷懒”技巧——离线查询功能。想象一下,如果有100个人同时问你”今天天气怎么样?”,你会不厌其烦地重复回答100次吗?当然不会!你会直接说:“跟刚才那个人问的一样,看前面的回答!”

今天,我就来揭秘如何让AI系统也学会这种”偷懒”的艺术,实现重复问题秒级响应!

系统架构:三层缓存,层层过滤

我们的优化方案就像一个精明的图书馆管理员:


用户提问 → 直接答案缓存(脱口而出) → 相似问题匹配(翻小本本) → ID查询数据库(查编号找书) → 大模型生成(现场创作)

核心代码架构


class SmartQASystem:
    def query(self, question):
        # 第一层:直接缓存匹配(就像熟人见面直接叫出名字)
        answer = self.redis.get_direct_answer(question)
        if answer: return answer
        
        # 第二层:相似问题匹配(就像看到长得像的人,翻通讯录找名字)
        answer, mysql_id = self.bm25_search_in_redis(question)
        if answer: return answer
        
        # 第三层:通过ID查询MySQL(用编号快速找到答案)
        if mysql_id:
            answer = self.mysql.get_answer_by_id(mysql_id)
            if answer:
                self.redis.cache_direct_answer(question, answer)
                return answer
        
        # 第四层:终极方案(实在不认识,现场问大模型)
        return self.call_big_model(question)

文本预处理:让AI学会”断句”

什么是文本预处理?

想象一下,如果我说:“我爱自然语言处理技术”,你怎么理解这句话?你会把它拆分成”我/爱/自然语言/处理/技术”来理解,对吧?


_preprocess_text
函数就是干这个的——让计算机学会中文断句!


def _preprocess_text(text):
    """
    文本预处理:中文分词 + 清洗过滤
    就像把一长串文字切成有意义的词语块
    """
    # 原句:"苹果手机多少钱?"
    words = jieba.lcut(text.lower())  # 分词:["苹果", "手机", "多少钱", "?"]
    
    # 过滤停用词和标点
    filtered_words = [word for word in words if word not in stop_words and len(word) > 1]
    # 结果:["苹果", "手机", "多少钱"]
    
    return filtered_words

为什么要分词?

中文没有空格分隔,计算机看不懂连续的文字分词后每个词都有明确含义,便于计算相似度去掉”的、了、是”等无意义词,减少干扰

Redis数据存储:聪明的小本本

问题索引的JSON格式(优化版)

我们的Redis就像一本智能通讯录,现在增加了MySQL ID字段:


{
  "q_123456": {
    "text": "苹果手机多少钱",
    "id": "mysql_789",  // 新增:MySQL中的主键ID
    "timestamp": 1620000000.123456
  },
  "q_123457": {
    "text": "华为手机价格", 
    "id": "mysql_790",
    "timestamp": 1620000000.123457
  },
  "q_123458": {
    "text": "如何学习机器学习",
    "id": "mysql_791", 
    "timestamp": 1620000000.123458
  }
}

为什么这样设计?

ID关联:通过MySQL ID快速定位数据库记录文本存储:保存原始问题,用于相似度计算时间戳:记录添加时间,实现自动清理老旧问题

答案缓存分离设计

这里我们巧妙地把问题和答案分开存储:


# 问题索引(占用空间小)
"question_index" = '{"q_123456": {"text": "苹果手机多少钱", "id": "mysql_789", "timestamp": 1620000000.123456}}'

# 答案缓存(按需存储)
"answer:5d41402abc4b2a76b9719d911017c592" = "目前苹果手机价格在5000-10000元之间"

内存优化秘诀

问题索引只存文本和ID,1000个问题也就几百KB答案缓存只存热点问题,自动过期淘汰通过ID查询MySQL,避免全表扫描

BM25算法:智能的”长相识别”

BM25是什么?

想象你在人群中找朋友:

传统方法:必须长得一模一样才认出来BM25方法:长得像就可以认出来(发型像+眼镜像+身高像)


def bm25_similarity(question1, question2):
    """
    BM25计算两个问题的相似度
    考虑词频、词权重、文档长度等因素
    """
    # 问题1:"苹果手机价格" → ["苹果", "手机", "价格"]
    # 问题2:"苹果手机多少钱" → ["苹果", "手机", "多少钱"]
    # 问题3:"苹果很好吃" → ["苹果", "很", "好吃"]
    
    # BM25会给出:
    # 问题1 vs 问题2:高分(都有"苹果"、"手机")
    # 问题1 vs 问题3:低分(只有"苹果"相同)
    return similarity_score

Softmax归一化:把分数变成概率

BM25给出的原始分数就像考试卷面分,我们需要转换成百分比:


def softmax_scores(scores):
    """
    把BM25分数转换为概率分布
    就像把考试分数转换成班级排名百分比
    """
    # 原始分数:[2.1, 1.8, 0.3]
    exp_scores = np.exp([2.1, 1.8, 0.3])  # [8.17, 6.05, 1.35]
    total = sum(exp_scores)                # 15.57
    probabilities = exp_scores / total     # [0.52, 0.39, 0.09]
    
    # 解释:第一个问题有52%的概率是最佳匹配
    return probabilities

完整工作流程:一场精密的协同作战

步骤1:直接缓存匹配(闪电战)


def get_direct_answer(question):
    # 把问题变成哈希值:"苹果手机多少钱" → "5d41402abc4b2a76b9719d911017c592"
    key = f"answer:{hashlib.md5(question.encode()).hexdigest()}"
    
    # 直接查缓存,命中就立即返回
    answer = redis.get(key)
    if answer:
        redis.expire(key, 3600)  # 命中后刷新过期时间
        return answer
    return None

效果:命中时响应时间<1ms,真正的”秒回”!

步骤2:相似问题匹配(智能匹配 + ID返回)


def bm25_search_in_redis(question):
    # 1. 从Redis获取所有缓存的问题
    question_index = redis.get("question_index")  # 获取那个JSON格式的索引
    
    # 2. 用BM25计算相似度
    similarities = calculate_bm25_similarity(question, question_index.values())
    
    # 3. 用Softmax转换成概率
    probabilities = softmax(similarities)
    
    # 4. 找到最佳匹配(概率>80%)
    best_match_idx = probabilities.argmax()
    if probabilities[best_match_idx] > 0.8:
        best_question_id = list(question_index.keys())[best_match_idx]
        best_mysql_id = question_index[best_question_id]['id']  # 获取MySQL ID
        
        # 5. 先尝试从缓存获取答案
        cached_answer = get_cached_answer_by_question_id(best_question_id)
        if cached_answer:
            return cached_answer, best_mysql_id
        else:
            # 返回MySQL ID,让上层去数据库查询
            return None, best_mysql_id
    
    return None, None

智能之处:即使问题表述不同(“价格” vs “多少钱”),也能识别为相同意图!

步骤3:通过ID查询MySQL(性能飞跃)


def query_mysql_by_id(mysql_id):
    """
    通过主键ID查询MySQL,性能极佳!
    相比全表扫描或文本匹配,速度提升10-100倍
    """
    # 执行SQL: SELECT answer FROM qa_table WHERE id = %s
    cursor.execute("SELECT answer FROM qa_pairs WHERE id = %s", (mysql_id,))
    result = cursor.fetchone()
    return result[0] if result else None

性能优势

主键查询:MySQL的聚簇索引,O(1)时间复杂度避免全表扫描:不需要
LIKE '%苹果手机%'
这样的低效查询减少网络传输:只返回一条记录

步骤4:大模型回退与缓存更新


def final_fallback(question):
    # 调用大模型生成答案
    rag_answer = call_rag_system(question)
    
    # 把新问答对插入MySQL,获取自增ID
    mysql_id = mysql.insert_qa_pair(question, rag_answer)
    
    # 更新Redis缓存
    redis.cache_direct_answer(question, rag_answer)
    redis.update_question_index(mysql_id, question, rag_answer)
    
    return rag_answer

性能成果:从”蜗牛”到”猎豹”

经过优化后,我们的系统表现:

查询类型 优化前 优化后 提升倍数
重复问题 2-3秒 1-10毫秒 200-300倍
相似问题 2-3秒 10-50毫秒 40-60倍
ID查询MySQL 100-500ms 1-5ms 50-100倍
全新问题 2-3秒 2-3秒 基本不变

资源节省

缓存命中率:60-70%GPU计算量减少:约65%数据库查询压力减少:约80%系统吞吐量提升:5-8倍

总结:聪明的”偷懒”艺术

我们的离线查询优化就像给AI系统配了一个”智能秘书”:

记忆高手:记住所有问过的问题和答案联想达人:能识别相似问题,不要求完全一致编号专家:通过ID快速定位,避免低效查询空间管理师:智能管理缓存空间,热点留存,冷门淘汰速度狂魔:缓存命中时实现毫秒级响应

技术要点回顾

分词预处理:让计算机理解中文BM25算法:智能相似度匹配Softmax归一化:把分数变成概率Redis分层缓存:平衡性能与内存使用ID关联查询:通过MySQL主键实现极速查询

最重要的是,这种优化让我们的AI系统变得更”人性化”——它知道什么时候该”偷懒”,什么时候该”认真工作”。毕竟,好的技术不就是要让复杂的事情变简单吗?

希望这篇文章对你有帮助!如果你有更好的想法,欢迎在评论区交流讨论~


PS:我们的AI系统现在可开心了,因为它终于不用反复回答”什么是机器学习”这种问题了!而且查询数据库时再也不用”大海捞针”了!

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
龙基乐建的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容