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_call或tool_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_call和tool_callschat_answer_text()增强兜底:支持content为 list、多块拼接,必要时尝试output.text
七、我目前的原则(写给未来自己)
- 优先让结果可追溯:回答里尽量附 URL 或编号引用。
- 先不做筛选,但要做长度控制:demo 阶段不筛选内容,但至少对摘要长度做截断,避免 token 爆炸。
- 工具调用一定要可观测:日志 + trace_messages 数量是最直观的“确实调用过工具”的证据。
课程练习 RAG技术与应用 目录(含 query_rewrite.py 等):
Cyning12/auto-gpt-work-demo · data/课程练习/RAG技术与应用