把单一换到丰富时踩过的坑

最近在做公司内部的知识库搜索系统时,遇到一个很现实的问题:用户搜索"Python异步编程",结果只返回了包含完全匹配这个词的文档,但实际上很多关于"asyncio"、“协程”、“并发"的文章都在用户期望范围内,但因为关键词不完全匹配而被遗漏。AI查询扩展(Query Expansion)就是为了解决这个问题而存在的,但在实际落地过程中,我踩了很多坑,也积累了不少经验,所以整理出来分享给大家。

为什么要写这篇文章

最近在做公司内部的知识库搜索系统时,遇到一个很现实的问题:用户搜索"Python异步编程”,结果只返回了包含完全匹配这个词的文档,但实际上很多关于"asyncio"、“协程”、“并发"的文章都在用户期望范围内,但因为关键词不完全匹配而被遗漏。

这不是简单的"多加几个同义词"就能解决的问题。传统的关键词匹配有天然的局限性——用户的意图往往隐藏在一个简单的查询词背后。AI查询扩展(Query Expansion)就是为了解决这个问题而存在的,但在实际落地过程中,我踩了很多坑,也积累了不少经验,所以整理出来分享给大家。

背景:单一查询的局限性

真实场景

先描述一下我遇到的具体场景:我们公司有个技术文档平台,有几千篇技术文章。用户搜索"Vue3组件通信”,结果只有15篇文档,但我知道至少有50篇相关内容。

用户的查询词"Vue3组件通信"其实隐含了:

  • Vue 3 相关
  • 组件间传值
  • props、emit
  • provide/inject
  • 状态管理
  • 父子组件、兄弟组件通信

但传统搜索只能精确匹配"Vue3组件通信"这几个字,结果就漏掉了大量相关内容。

问题本质

这个问题的本质是:用户的真实意图和查询词之间存在语义差距。传统的搜索技术是基于关键词匹配的,无法理解用户查询背后的真正需求。

简单说,用户想找的不是"包含这个字符串的文章",而是"帮我解决这个问题的文章"。

需求:从单一到丰富的转变

核心需求

基于上述背景,我们的需求很明确:

  1. 扩展用户查询:从单一查询词扩展到多个相关查询词
  2. 保持语义相关性:扩展的查询词必须与原始查询语义相关
  3. 控制扩展范围:不能过度扩展导致结果太多太杂
  4. 平衡准确率和召回率:既要找到更多相关结果,又不能引入太多噪音

技术要求

从技术角度,我们需要:

  • 支持中文语义理解
  • 能够理解上下文
  • 处理同义词、近义词
  • 识别领域术语
  • 实时响应(不能太慢)
  • 可解释性(让用户知道为什么返回这些结果)

实现:AI查询扩展的落地

整体架构

先看看整体架构,这样有个全局概念:

graph TD A[用户查询] --> B[查询预处理] B --> C[语义理解] C --> D[扩展策略选择] D --> E[同义词扩展] D --> F[上下文扩展] D --> G[领域术语扩展] E --> H[查询重写] F --> H G --> H H --> I[搜索执行] I --> J[结果融合] J --> K[返回用户]

1. 查询预处理

先别急着上AI,基础的预处理很重要:

def preprocess_query(query: str) -> dict:
    """查询预处理"""
    result = {
        'original': query,
        'cleaned': query.strip(),
        'tokens': [],
        'entities': []
    }

    # 基础清洗
    query = query.strip().lower()

    # 分词(中文需要专门的分词器)
    tokens = tokenize_chinese(query)
    result['tokens'] = tokens

    # 命名实体识别(识别人名、地名、技术名词等)
    entities = extract_entities(query)
    result['entities'] = entities

    # 停用词处理(保留核心意思)
    tokens = [t for t in tokens if t not in STOP_WORDS]
    result['core_tokens'] = tokens

    return result

这里有个坑:不要过度清理停用词。比如"不"、“无”、“非"等词虽然常见,但能改变查询的含义,需要保留。

2. 语义理解

使用AI来理解查询的语义:

from openai import OpenAI
import json

client = OpenAI()

def understand_query(query: str) -> dict:
    """理解查询的语义"""
    prompt = f"""
    分析以下查询的语义,提取关键信息:

    查询:{query}

    请返回JSON格式:
    {{
        "main_intent": "主要意图(比如搜索、学习、解决问题)",
        "keywords": ["关键词1", "关键词2"],
        "synonyms": {{
            "关键词1": ["同义词1", "同义词2"],
            "关键词2": ["同义词1", "同义词2"]
        }},
        "context": "上下文或场景",
        "domain": "技术领域"
    }}
    """

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "你是一个专业的搜索查询分析师。"},
            {"role": "user", "content": prompt}
        ],
        temperature=0.3
    )

    try:
        result = json.loads(response.choices[0].message.content)
        return result
    except json.JSONDecodeError:
        return {
            "main_intent": "未知",
            "keywords": [],
            "synonyms": {},
            "context": "",
            "domain": "通用"
        }

踩坑经验

  • temperature不要设太高,否则每次返回的同义词都不一样
  • 必须有fallback处理,AI服务可能会挂
  • 返回格式要严格控制,便于后续处理

3. 扩展策略实现

同义词扩展

def expand_with_synonyms(query_analysis: dict) -> list[str]:
    """同义词扩展"""
    expanded_queries = [query_analysis['original']]

    # 对每个关键词进行同义词扩展
    for keyword, synonyms in query_analysis['synonyms'].items():
        for synonym in synonyms:
            # 替换关键词生成新查询
            new_query = query_analysis['original'].replace(keyword, synonym)
            expanded_queries.append(new_query)

    return list(set(expanded_queries))  # 去重

上下文扩展

def expand_with_context(query_analysis: dict) -> list[str]:
    """上下文扩展 - 添加相关概念"""
    context_expansions = {
        "前端开发": ["JavaScript", "TypeScript", "React", "Vue", "Angular"],
        "后端开发": ["Node.js", "Python", "Java", "Go", "API设计"],
        "数据库": ["MySQL", "PostgreSQL", "MongoDB", "Redis", "SQL优化"],
        # ... 更多上下文
    }

    domain = query_analysis.get('domain', '')
    if domain in context_expansions:
        concepts = context_expansions[domain]
        return [f"{query_analysis['original']} {concept}" for concept in concepts]
    return []

领域术语扩展

def expand_with_domain_terms(query_analysis: dict) -> list[str]:
    """领域术语扩展 - 添加领域相关的专业术语"""
    domain_terms = {
        "Vue": ["组件通信", "状态管理", "生命周期", "指令", "路由"],
        "React": ["Hooks", "Redux", "Context", "Component", "Props"],
        "Python": ["异步编程", "装饰器", "生成器", "多线程", "协程"],
        # ... 更多领域术语
    }

    expanded = []
    tokens = query_analysis.get('keywords', [])

    for token in tokens:
        for domain, terms in domain_terms.items():
            if domain.lower() in token.lower():
                expanded.extend([f"{query_analysis['original']} {term}" for term in terms])

    return expanded

4. 查询重写与融合

def rewrite_and_fuse(original_query: str, expansions: list[list[str]]) -> list[str]:
    """查询重写与融合"""
    all_queries = [original_query]

    # 收集所有扩展查询
    for expansion_list in expansions:
        all_queries.extend(expansion_list)

    # 去重,但保持顺序
    seen = set()
    unique_queries = []
    for query in all_queries:
        normalized = query.lower().strip()
        if normalized not in seen:
            seen.add(normalized)
            unique_queries.append(query)

    # 控制扩展数量(避免太多)
    MAX_QUERIES = 10
    if len(unique_queries) > MAX_QUERIES:
        # 优先保留原始查询和最相关的扩展
        unique_queries = unique_queries[:MAX_QUERIES]

    return unique_queries

5. 完整流程

def ai_query_expansion(query: str) -> dict:
    """完整的AI查询扩展流程"""
    # 1. 预处理
    processed = preprocess_query(query)

    # 2. 语义理解
    query_analysis = understand_query(query)

    # 3. 多策略扩展
    synonyms_expansions = expand_with_synonyms(query_analysis)
    context_expansions = expand_with_context(query_analysis)
    domain_expansions = expand_with_domain_terms(query_analysis)

    # 4. 查询重写与融合
    all_expansions = [synonyms_expansions, context_expansions, domain_expansions]
    final_queries = rewrite_and_fuse(query, all_expansions)

    return {
        'original_query': query,
        'processed_query': processed,
        'query_analysis': query_analysis,
        'expanded_queries': final_queries,
        'expansion_count': len(final_queries) - 1
    }

踩坑:那些血泪教训

坑1:过度扩展导致结果太多

一开始我想着"多多益善”,把所有可能的相关词都加上,结果返回几百篇文档,用户根本看不过来。

解决方案

  • 设置扩展数量上限(比如最多10个查询)
  • 按相关性排序扩展词,只保留最相关的
  • 对结果进行重新排序,把最相关的放在前面

坑2:扩展词不相关

有时候AI返回的同义词完全离谱,比如"苹果"被扩展成了"香蕉"。

解决方案

  • 降低temperature值(0.1-0.3)
  • 添加few-shot示例,让AI知道什么是相关的同义词
  • 建立领域同义词库,优先使用领域内的同义词

坑3:响应速度太慢

每次查询都要调用AI,导致用户等待时间太长。

不同查询扩展方式的响应时间对比

解决方案

  • 缓存常见查询的扩展结果
  • 使用轻量级模型做快速判断,再调用大模型
  • 异步处理,先返回部分结果,扩展结果慢慢加载
import time
from functools import lru_cache

@lru_cache(maxsize=1000)
def cached_expand(query: str) -> dict:
    """带缓存的查询扩展"""
    return ai_query_expansion(query)

def expand_with_timeout(query: str, timeout: float = 2.0) -> dict:
    """带超时的查询扩展"""
    try:
        # 先查缓存
        cached = cached_expand(query)
        if cached:
            return cached

        # 设置超时
        start_time = time.time()
        result = ai_query_expansion(query)
        elapsed = time.time() - start_time

        if elapsed > timeout:
            print(f"警告:查询扩展耗时 {elapsed:.2f}秒,超过 {timeout}秒")

        return result

    except Exception as e:
        print(f"查询扩展失败:{e}")
        return {
            'original_query': query,
            'expanded_queries': [query],
            'expansion_count': 0
        }

坑4:中英文混合查询

用户输入"Vue3 props怎么传参",既有英文又有中文,处理起来很麻烦。

解决方案

  • 分别处理中英文部分
  • 建立中英文映射表
  • 统一转换为一种语言处理
def handle_mixed_language(query: str) -> dict:
    """处理中英文混合查询"""
    # 识别中英文部分
    chinese_parts = []
    english_parts = []

    for char in query:
        if '一' <= char <= '鿿':
            chinese_parts.append(char)
        else:
            english_parts.append(char)

    chinese_text = ''.join(chinese_parts).strip()
    english_text = ''.join(english_parts).strip()

    # 分别处理
    result = {
        'original': query,
        'chinese': chinese_text,
        'english': english_text,
        'expansions': []
    }

    if chinese_text:
        result['expansions'].extend(expand_chinese(chinese_text))
    if english_text:
        result['expansions'].extend(expand_english(english_text))

    return result

坑5:领域适应性差

通用的扩展策略在不同领域表现差异很大。

解决方案

  • 按领域配置不同的扩展策略
  • 建立领域词典
  • 根据用户历史行为调整扩展策略
DOMAIN_CONFIG = {
    "前端": {
        "expansion_strategies": ["synonyms", "framework_specific"],
        "max_expansions": 8,
        "min_relevance": 0.7
    },
    "后端": {
        "expansion_strategies": ["synonyms", "database_specific"],
        "max_expansions": 10,
        "min_relevance": 0.6
    },
    "AI/ML": {
        "expansion_strategies": ["synonyms", "context", "domain_terms"],
        "max_expansions": 12,
        "min_relevance": 0.8
    }
}

def get_domain_config(domain: str) -> dict:
    """获取领域配置"""
    return DOMAIN_CONFIG.get(domain, DOMAIN_CONFIG["通用"])

def domain_aware_expansion(query: str, domain: str) -> dict:
    """领域感知的查询扩展"""
    config = get_domain_config(domain)

    result = ai_query_expansion(query)

    # 根据领域配置过滤扩展结果
    if config.get('max_expansions'):
        result['expanded_queries'] = result['expanded_queries'][:config['max_expansions']]

    return result

结果:实际的改善效果

定量指标

实施AI查询扩展后,我们的搜索系统有了明显改善:

查询扩展效果对比

指标扩展前扩展后改善幅度
平均返回文档数15.342.7+179%
用户点击率32%58%+81%
搜索转化率18%35%+94%
平均响应时间0.8s1.2s+50%

定性改善

除了数据指标,用户体验也有明显改善:

  1. 搜索结果更丰富:用户能找到更多相关内容
  2. 查询意图更准:即使查询词不精确,也能找到想要的结果
  3. 用户满意度提升:反馈中"找不到结果"的投诉明显减少

真实案例

举几个真实案例:

案例1:Vue组件通信

查询扩展生成的查询词示例

  • 用户查询:“Vue3组件通信”
  • 扩展前:15篇文档
  • 扩展后查询词:
    • Vue3组件通信
    • Vue3父子组件传值
    • Vue3 props emit
    • Vue3 provide inject
    • Vue3组件间数据传递
  • 结果:38篇文档,用户满意度大幅提升

案例2:Python异步

  • 用户查询:“Python异步编程”
  • 扩展前:12篇文档
  • 扩展后查询词:
    • Python异步编程
    • Python asyncio
    • Python协程
    • Python并发编程
    • Python async await
  • 结果:45篇文档,用户找到了更多实用资源

优化方向与总结

还可以做得更好的地方

虽然效果不错,但还有优化空间:

  1. 个性化扩展:根据用户的搜索历史、专业领域调整扩展策略
  2. 实时反馈学习:根据用户的点击行为不断优化扩展质量
  3. 多模态查询:支持图片、代码片段等多模态输入
  4. 跨语言扩展:支持英文查询返回中文结果,反之亦然
  5. 解释性增强:让用户知道为什么返回这些结果

核心经验总结

回顾整个实践过程,我觉得这些经验最重要:

  1. 不要过度复杂化:简单的同义词扩展往往比复杂的语义分析效果更好
  2. 领域知识很重要:通用模型不如针对特定领域优化的模型
  3. 用户体验第一:宁可少扩展几个词,也不要返回不相关的结果
  4. 性能不能忽视:AI扩展虽好,但不能让用户等太久
  5. 持续迭代优化:根据用户反馈不断调整策略

技术选型建议

如果你们也想做查询扩展,我建议:

  • 小团队:先用规则+词典的方式,成本低见效快
  • 中等团队:可以引入轻量级语言模型,平衡效果和成本
  • 大团队:可以考虑训练专门的扩展模型,或使用大模型+蒸馏的方案

AI查询扩展不是什么高深的技术,关键在于理解用户需求,选择合适的策略,然后不断迭代优化。希望这篇文章能给大家一些参考,少踩一些坑,更快地实现更好的搜索体验。

最后,如果你也在做类似的东西,欢迎交流经验。毕竟,技术实践的价值在于分享和碰撞。

版权声明: 本文首发于 指尖魔法屋-把单一换到丰富时踩过的坑https://blog.thinkmoon.cn/post/369-ai-query-expansion-single-rich-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!