客户先问 P-204 电池的售后,接着问“那这个能换吗?”第二句话自然,却几乎没有检索所需的实体。如果只把最后一句送进向量库,结果很可能变成所有产品的一般换货规则。
检索前的查询处理,要把用户意图转换成足够明确的查询,同时保留不确定性。它不能悄悄把“用户觉得不好用”改成“已核实质量故障”。本篇从这条边界出发,区分改写、多查询、问题分解与多跳。
改写首先是恢复已有信息
把“那这个能换吗”展开为“P-204 电池的换货条件是什么”,实体来自上一轮明确提到的产品。应记录指代依据,否则模型把“这个”错误关联到更早的另一件商品时,很难追查。
如果历史里同时提到 P-204 与 P-205,就不应强行选一个。应用可以澄清当前产品,或分别检索并明确区分。将歧义藏在一条看似完整的查询里,会让后续模型以为实体已经确认。
时间也需要上下文。“去年买的,现在能否退”可能涉及购买时间、签收时间与当前时间。哪个时间决定政策版本、哪个时间计算退货窗口,应由业务规则确定。改写器负责保留这些角色,不负责猜日期。
一个中间查询对象可以包含原问题、独立查询文本、产品号、时间条件和未解决指代。权限范围则由后端附加,不能让模型生成 owner 或 tenant 决定查谁的数据。格式符合 Schema,只能证明对象结构可用,不能证明字段来源正确。
多查询为什么可能找到更多材料
同一需求可以表达成“质量退货运费”“售后寄回费用”“瑕疵商品运输成本”。不同表达可能覆盖不同文档措辞,多路结果再融合。这与第一篇把一个问题编码成一个向量不同,是增加了搜索入口。
多查询也会扩大主题范围。把“退货运费”扩成“赔偿政策”,可能找出无关赔偿案例;如果融合时相关材料被挤掉,召回增加不代表最终更好。应保留原问题一路,限制扩展数量,比较新增的有效证据。
重复查询要规范化和去重,但不要删除关键差异。“P-204 质量退货”和“P-205 质量退货”不能因为大部分字相同就合并;三十天与四十天也不能当成无关数字去掉。去重服务于减少冗余,不能破坏任务含义。
HyDE 中的假想答案只能用于找资料
有些查询短到很难与长文档对齐。HyDE 先生成一段假想相关文档,再编码它去搜索真实文档,借此形成更接近资料写法的检索表示。原论文描述了这一流程。
例如把“质量运费谁出”扩成一段售后说明,可能补充“申请、核实、寄回”等相关表达。但假想文本中的三十天、商家承担等细节尚未核验,绝不能直接进入最终证据集。真正答案仍要由实际检索到的条款支持。
如果用户明确问 P-205,假想文档却写成 P-204,查询扩展会改变问题。可以保留原实体约束、保留原查询分支,并检查生成内容没有替换已知条件。HyDE 不是事实补全器,也不是每个问题的默认必要步骤。
分解问题之前,先看依赖关系
“退货期限与发票申请方法分别是什么”可以拆成两个独立问题,分别检索再合并。“订单 A100 的产品适用哪份特殊规则”则需要先知道订单对应产品,不能并行瞎搜所有产品。
问题分解把复杂目标拆成子目标,多跳强调后一跳依赖前一跳的新事实。多查询只是不同表述,同一句话换词搜索三次并没有形成多跳。三者可以组合,但日志应分别记录它们。
对 A100,可写出依赖链:订单 → 产品 P-204 → 产品适用政策 → 条款与订单条件比较。任何一环缺失,后续结论就需要收缩。产品记录不存在时,找到通用政策不等于证明通用政策一定适用于它。
下面是固定两跳的只读程序,用人工数据展示依赖。它没有模型自主选动作,完整文件见 query_plan.py下载。
"""执行人工给定的两跳查询计划;不是模型规划器,也不执行数据库写入。"""
orders = {"A100": {"owner": "u1", "product": "P-204"}}
policies = {"P-204": {"id": "p204-v1", "text": "经核实质量问题,三十天内由商家承担退货运费。"}}
def retrieve_for_order(order_id, actor):
# actor 应由认证层提供,不从自然语言或模型计划取得。
order = orders.get(order_id)
if order is None or order["owner"] != actor:
return {"state": "unavailable", "evidence": []}
# 后一跳使用第一跳查得的事实,不允许自行猜产品号。
product = order["product"]
policy = policies.get(product)
if policy is None:
return {"state": "missing_policy", "evidence": [f"order:{order_id}"]}
return {
"state": "evidence_found",
"query": f"{product} 质量问题退货 运费",
"evidence": [f"order:{order_id}", policy["id"]],
"policy": policy["text"],
}
if __name__ == "__main__":
print(retrieve_for_order("A100", "u1"))
# 找到政策仍未检查签收日期和质量核实记录,不返回最终资格结论。
程序返回 evidence_found,不返回 approved。原因是“查到政策”和“满足期限、核实状态”是不同判断。最后一层业务判断可以是程序规则,也可以是对复杂文本的解释,但必须能指出自己用了哪些前提。
结构化查询与文本检索怎样分工
订单归属、产品 ID、累计退款额这类结构化事实适合通过受限查询取得,不必把它们改写成长段自然语言再做近似匹配。规则解释、例外条款与产品说明适合检索原文。
例如“这个月所有 P-204 订单的退款总额”需要授权后的完整记录与确定性聚合。取向量搜索前十条再求和,只会得到部分样本总额,不能回答总体统计。自然语言可以帮助识别查询意图,但 SQL、字段和范围仍应由程序约束。
如果用户要总结“所有供应商投诉的主要原因”,还要区分主题抽样与完整覆盖。向量 Top-k 常偏向最相似的几条,可能漏掉少见主题。此类问题需要明确数据范围和聚合策略,不能用一段流畅总结掩盖覆盖缺失。
给查询处理留出失败出口
改写失败可以回退原问题,但需保留错误信息;缺少关键实体时应澄清;依赖查询无权限时停止,不通过换词继续探测。查询结果为空与工具超时不同,超时不能被写成“没有这条政策”。
每个子任务应记录原问题、展开后的查询、过滤条件、返回证据与调用状态。这样最终答错时,可以判断是改写改变了意图,还是正确查询没有找到材料。
当固定分解已经足够清楚,普通工作流往往更易解释。第八篇再讨论让模型根据新证据选择动作。现在先进入上下文、证据与引用,把找到的材料转成可核验回答。