RAG系统中的查询重写:我是如何把响应速度提升40倍的

38 阅读6分钟

RAG系统中的查询重写:我是如何把响应速度提升40倍的

一个真实的电商搜索场景重构故事

写在前面

最近在做一个电商搜索系统的优化,遇到了一个很有意思的问题:用户输入"不上火的奶粉",系统应该怎么理解?是搜索"温和配方"?还是"易消化"?或者"低热量"?

更头疼的是,当用户说"这个怎么样"的时候,系统完全不知道"这个"指的是什么。

这就是RAG系统中查询重写(Query Rewrite)要解决的核心问题。今天想跟大家分享一下我们的解决方案,以及踩过的那些坑。

为什么不能直接用LLM重写所有查询?

一开始我们的方案很简单粗暴:所有查询都丢给LLM重写一遍。看起来很完美对吧?

然而现实很骨感:

# 简单查询:"手机"
# LLM处理时间:200ms
# 规则处理时间:5ms
# 性能差距:40倍!

对于"手机"这种简单查询,调用LLM纯属杀鸡用牛刀。更要命的是:

  • 成本太高:每次调用都要钱,日均百万查询扛不住
  • 不可控:同样的查询可能返回不同结果,用户体验不稳定
  • 缺乏领域知识:LLM不知道"苹果"在我们系统里对应哪些具体型号

于是我们开始思考:能不能根据查询的复杂程度,选择不同的处理策略?

分层架构:让合适的工具做合适的事

核心思路很简单:先分析查询特征,再决定用什么策略

第一步:查询特征分析

我们给每个查询打上三个维度的标签:

1. 主查询类型

  • 简单查询:手机(关键词型)
  • 复杂查询:什么牌子的护肤品适合敏感肌肤且价格实惠(描述型)
  • 对话查询:这个怎么样(指代型)

2. 意图标签(可以有多个)

  • 品牌相关:苹果手机
  • 价格敏感:便宜的奶粉
  • 推荐需求:什么手机好用
  • 指代查询:这款有其他颜色吗

3. 复杂度评分(0-1)

# 评分逻辑很简单
score = 词数权重 + 意图标签数量权重

# 例子
"手机" → 0.3(简单)
"不上火的奶粉" → 0.5(中等)
"什么牌子的护肤品适合敏感肌肤" → 0.8(复杂)

第二步:策略选择与执行

根据特征分析结果,系统会智能选择一个或多个策略:

策略1️⃣:规则扩展(适合简单查询)
# 输入:"手机"
# 输出:
# - "智能手机"
# - "移动电话"  
# - "手机设备"

# 优势:快(5ms)、稳定、可预测
# 劣势:覆盖面有限,需要维护规则库
策略2️⃣:同义词替换(通用兜底)
# 输入:"便宜的手机"
# 输出:
# - "实惠的手机"
# - "性价比高的手机"
# - "经济型手机"

# 这个策略我们设为默认兜底,保证每个查询都能有基本扩展
策略3️⃣:检索增强(适合品牌/品类查询)
# 知识库维护品牌映射
brand_mapping = {
    "苹果": ["iPhone", "iPad", "MacBook", "Apple Watch"]
}

# 输入:"苹果手机推荐"
# 输出:
# - "iPhone 推荐"
# - "Apple 手机推荐"
# 同时提取过滤条件:brand=苹果
策略4️⃣:上下文指代(适合对话查询)

这是个有意思的策略。处理指代词的关键是维护对话上下文

context = {
    "last_viewed_product": "iPhone 15 Pro",
    "last_category": "手机",
    "last_brand": "苹果"
}

# 输入:"这款有其他颜色吗"
# 分析:包含指代词"这款"
# 输出:"iPhone 15 Pro 有其他颜色吗"

一个小坑:不要把"这款"、"那款"直接替换成商品名,会导致重复:

# 错误示范
"这款手机" + "iPhone 15 Pro" = "iPhone 15 Pro款手机"# 正确做法
"这款手机""iPhone 15 Pro 手机"
策略5️⃣:LLM优化(适合复杂查询)

只有在其他策略搞不定的时候,才调用LLM:

# 输入:"什么牌子的护肤品适合敏感肌肤且价格实惠"
# 这种复杂语义理解,还是得靠LLM

# LLM Prompt设计
system_prompt = """
你是电商搜索专家,将用户查询改写为3个不同角度的搜索查询。
要求:
1. 提取核心需求
2. 每个改写3-6个词
3. 只返回JSON,不要废话
"""

第三步:智能决策逻辑

这是整个系统的大脑,决定用哪些策略:

def recommend_strategies(query_characteristics):
    strategies = []
    
    # 规则1:指代词优先处理
    if has_reference_word(query):
        strategies.append(CONTEXT_REFERENCE)  # 必须先解决指代
    
    # 规则2:根据复杂度选择主策略
    if complexity < 0.4:
        strategies.extend([RULE_EXPANSION, SYNONYM])  # 简单快速
    elif complexity > 0.6:
        strategies.append(LLM_OPTIMIZATION)  # 复杂语义
    
    # 规则3:根据意图标签增强
    if has_brand_or_category(query):
        strategies.append(RETRIEVAL_AUGMENTED)  # 知识库加持
    
    # 规则4:同义词兜底
    if SYNONYM not in strategies:
        strategies.append(SYNONYM)
    
    return strategies

一些实现细节和踩过的坑

坑1️⃣:区分推荐查询 vs 知识查询

这两个很容易混淆:

"什么手机好用" → 推荐查询(要产品列表)
"什么是5G手机" → 知识查询(要概念解释)

解决方案:看"什么"后面跟什么词

def is_recommendation_query(query):
    # 明确的推荐词
    if any(word in query for word in ["推荐", "选什么", "哪个好"]):
        return True
    
    # "什么 + 商品类目"且查询较短
    words = jieba.cut(query)
    if "什么" in words:
        next_word = words[words.index("什么") + 1]
        if next_word in PRODUCT_CATEGORIES and len(words) <= 5:
            return True  # "什么手机好用"
        if next_word in ["是", "叫"]:
            return False  # "什么是5G"
    
    return False

坑2️⃣:上下文管理不要过度设计

一开始我们想做一个超级复杂的对话状态追踪,结果发现根本用不上。

最终方案:只保留三个字段就够了

context = {
    "last_viewed_product": "最近查看的商品",
    "last_category": "最近关注的类目",  
    "conversation_history": ["最近3轮对话"]
}

简单有效,维护成本低。

坑3️⃣:重写结果要去重和限制数量

多个策略可能生成重复结果,必须处理:

# 保序去重(保留第一次出现的)
unique_rewrites = list(dict.fromkeys(all_rewrites))

# 限制数量(避免后续检索开销过大)
final_rewrites = unique_rewrites[:3]

效果如何?

上线一个月后的数据:

查询类型优化前优化后提升
简单查询(60%)200ms5ms40x
复杂查询(30%)200ms150ms1.3x
对话查询(10%)指代解析失败率40%指代解析失败率15%准确率+25%

成本节省

  • 60%的简单查询不再调用LLM

用户体验

  • 搜索结果相关性提升18%
  • 用户满意度+12%

未来还能做什么?

目前的系统还有优化空间:

1. 策略权重学习 现在的策略选择是硬编码规则,可以用强化学习根据检索效果动态调整。

2. 个性化重写 根据用户画像定制策略。比如高端用户少考虑价格因素。

3. 多语言支持 目前只支持中文,可以扩展到英文、日文等。

总结

查询重写看似简单,实则是个系统工程。核心要点:

不要迷信LLM - 简单问题用简单方法

分层设计 - 根据查询特征选择策略

保持简单 - 过度设计是大敌

数据驱动 - 用真实数据验证效果

完整代码在Github,欢迎交流探讨。


关于作者

搬砖码农一枚,最近在折腾RAG相关的东西。如果你也在做类似的项目,欢迎交流~


本文基于真实生产环境的重构经验,代码示例经过简化处理,实际生产代码会更复杂一些。