Day 12 学习日记:Query Rewrite

2 阅读6分钟

Day 12 学习日记(阶段性):Query Rewrite Demo(自动识别类型 + 改写/分解)

关联代码:data/课程练习/RAG技术与应用/query_rewrite.py
目标:在进入 RAG 检索/联网搜索前,把用户 Query 做“结构化处理”——先识别类型,再选择策略改写/拆分/触发搜索,输出更“检索友好”的输入,并让回答可追溯(带来源)。


一、我对 Query Rewrite 的理解(为什么要做)

我把 Query Rewrite 当成 RAG 的“前置意图校准”:

  • 用户输入是“人话”:经常省略主语、指代不清、多个问题混在一起。
  • 检索需要“机器友好”:更明确的对象/范围/对比项,或者把多意图拆成可独立检索的子问题。

换句话说:Rewrite 负责把门开对(搜得全),Rerank 才有发挥空间去“排得准”。


二、Demo 里我沉淀的场景与策略(case1~case6)

下面是我对 case1~case6(以及对应方法)的总结。

场景类型典型痛点我在 Demo 里的处理方式期望收益
上下文依赖型“还有/其他/继续/那它呢”这类追问,需要历史才能自洽传入 conversation_history,让模型补全必要上下文避免检索“无主语/无对象”
模糊指代型“这个/它/都/那里”指代不清强约束“只输出一行改写问句”,要求结合历史做指代消解提升召回精度(Precision)
对比型“哪个更好/更刺激”但缺少对比对象或对比维度传入 context_info(候选对比对象),让模型显式把对象写进问题确保上下文覆盖对比双方
多意图型一句话多个需求(票价 + 必玩 + 交通)rewrite_multi_intent_query() 输出 JSON 数组,拆成多个子问题触发多路检索,减少信息丢失
反问/情绪型“难道就没有更快的办法吗?”容易被模型输出“分析”明确规则:只输出一行中立问句,不要分析保持可检索性与对话连贯性
其他/已清晰Query 本身足够明确尽量原样输出或轻改写避免“画蛇添足”

三、工程实现上我真正学到的点(把 demo 做“稳”)

1)“输出形态”要分流:一行文本 vs JSON

这次最大的坑也是最大的收获:不能用同一套清洗逻辑处理所有输出

  • 改写问句:适合 one_line_text(清洗为单行)
  • 类型识别:必须 json_object(完整 JSON,不允许被截断)
  • 多意图拆分:必须 json_array

所以我把调用统一收敛到 _call_rewrite_model(..., output_format=...),并加了:

  • _extract_first_json_object():容错解析 JSON 对象
  • _extract_first_json_array():容错解析 JSON 数组

2)Prompt 的关键不是“写长”,而是“约束输出”

我对提示词的重点变成了两类约束:

  • 只输出(不解释、不加前缀、不用 markdown code fence)
  • 必要时原样输出(Query 足够清晰就不强行改)

四、我认为 Rewrite 和 Rerank 的分工(定位边界)

  • Query Rewrite(前置):解决“要搜什么”的问题,目标是 搜得全
  • Rerank(后置):解决“哪个更相关”的问题,目标是 排得准

结论:Rewrite 是“把检索入口对齐意图”,Rerank 是“在候选里做精排”。


五、把“联网搜索能力”接进 demo(Query + Web Search)

今天把 Query Rewrite 往前推进了一步:不止改写/拆分 query,而是把 联网搜索(Web Search) 接进 demo,形成可闭环的:

  • 需要搜索 → 搜索 → LLM 整合回答 → 给出来源

1)我对“联网搜索能力”的理解

我把它理解为给 LLM 增加一个“可查证的外部知识入口”,用于弥补:

  • 时效性:最新活动、实时票价、开放时间、政策变更等信息会变化,本地知识库可能不含最新版本。
  • 外部事实核验:当回答必须有来源时(尤其是数字、日期),搜索结果能提供可追溯的 URL。

但联网搜索不是越多越好:它会带来 延迟成本,还会引入“网页噪声”,所以需要一套“何时搜/怎么搜/搜完怎么用”的工程约束。


2)我在代码里落地了两条链路(非 FC vs FC)

A. 非 Function Calling:程序编排式联网搜索

对应代码(核心在 query_rewrite.py):

  • 判定与改写auto_web_search_rewrite()
    • identify_web_search_needs():先判断是否需要联网(need_web_search + reason)
    • rewrite_for_web_search():把 query 改成更适合搜索引擎的 query,并给出关键词/意图/建议来源
    • generate_search_strategy():输出搜索策略(关键词拆分、平台、时间范围)
  • 执行搜索web_search.search_web()TavilyClient.search(...)
  • 回答生成format_web_search_context()call_dashscope_chat()chat_answer_text()

优点:

  • 可控、稳定、易调试(每一步都由代码决定是否执行)

缺点:

  • 灵活性较低:LLM 无法“临时决定再多搜一次/换关键词/换时间范围”

B. Function Calling:让 LLM 自己决定是否调用 web_search

对应代码:

  • 工具闭环:dashscope_generation.call_generation_can_search()
    • web_search 作为 tools 传给模型
    • 模型返回 function_calltool_calls 后,代码执行搜索并以 role=function 回填结果
    • 再次调用模型生成最终回答
  • Demo:auto_web_search_rewrite_demo_with_search_function_call()

这条链路跑通后,我的最大收获是:工具调用发生与否,必须可观测,否则很容易“以为搜了,其实没搜”。


六、我踩到的坑(以及修复点)

1)“明明工具调用了,但没有日志”——其实是日志级别问题

  • Python 默认只显示 WARNING 以上日志;我在工具闭环里打的是 INFO。
  • 解决:在 demo 里 logging.basicConfig(level=logging.INFO, ...),这样能看到:
    • tool_call web_search ...
    • web_search ok ... results=...
    • done (no tool call)(代表第二轮模型已拿到搜索结果,不再调用工具)

2)DashScope 的工具调用字段不止 function_call

一开始只解析 function_call,结果 DashScope 返回 tool_calls 时就会“误判无工具调用”,导致助手 message 没 content。

修复:

  • call_generation_can_search() 同时兼容 function_calltool_calls
  • chat_answer_text() 增强兜底:支持 content 为 list、多块拼接,必要时尝试 output.text

七、我目前的原则(写给未来自己)

  • 优先让结果可追溯:回答里尽量附 URL 或编号引用。
  • 先不做筛选,但要做长度控制:demo 阶段不筛选内容,但至少对摘要长度做截断,避免 token 爆炸。
  • 工具调用一定要可观测:日志 + trace_messages 数量是最直观的“确实调用过工具”的证据。

课程练习 RAG技术与应用 目录(含 query_rewrite.py 等):
Cyning12/auto-gpt-work-demo · data/课程练习/RAG技术与应用