大模型问答系统缓存优化:让重复问题秒回,让计算资源不白费!
作者:孔帅 | 转载请注明出处
大家好!今天我们来聊聊大模型问答系统中的一个”偷懒”技巧——离线查询功能。想象一下,如果有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系统现在可开心了,因为它终于不用反复回答”什么是机器学习”这种问题了!而且查询数据库时再也不用”大海捞针”了!















暂无评论内容