【Bug已解决】MultiQueryRetriever.unique_union() crashes on list/dict metadata
【Bug已解决】MultiQueryRetriever.unique_union() crashes on listdict metadata一、现象长什么样MultiQueryRetriever会针对一个问题生成多个变体 query各自检索出一批Document再用unique_union()把结果去重合并成一份。去重逻辑是按page_content分组把同一内容的多条Document的metadata取并集。当metadata里含有列表或字典值比如metadata{tags: [a, b], source: {file: x}}时unique_union()直接崩溃典型报错TypeError: unhashable type: list或TypeError: unsupported operand type(s) for |: dict and dict某些版本还会静默产出错误结果把 list 当 set 处理时报set object is not subscriptable。一旦你的文档 metadata 用了嵌套结构多标签、来源对象MultiQuery 检索就彻底不可用。二、背景unique_union的初衷是同一个page_content被多个 query 都检索到应当只保留一条但把各来源的 metadata 合并方便知道这条内容被哪些角度命中。它的经典实现大概是这样def unique_union(docs): by_content {} for d in docs: if d.page_content not in by_content: by_content[d.page_content] d else: # 合并 metadata merged dict(by_content[d.page_content].metadata) merged.update(d.metadata) # 或做集合并集 by_content[d.page_content] Document( page_contentd.page_content, metadatamerged) return list(by_content.values())问题就出在合并 metadata这一步它假设 metadata 值要么是不可变标量str/int要么能做集合操作但现实里 metadata 常含list/dict既不能被set()哈希也不能被|合并。三、根因根因两点不可哈希值进集合部分实现对 metadata 值做set(existing) | set(new)去重而list/dict不可哈希直接TypeError: unhashable type。合并语义缺失对dict值用|Python 3.9 dict merge没问题但老版本或update会直接覆盖对list值既没有拼接去重也没有覆盖的明确语义代码走到一半类型不符就崩。本质去重合并逻辑只考虑了标量 metadata没有为嵌套结构 metadata定义任何合并策略而嵌套 metadata 在现代 RAG 里非常普遍。四、最小可运行复现下面缩略逻辑复现崩溃def bad_unique_union(docs): by_content {} for d in docs: c d[page_content] if c not in by_content: by_content[c] dict(d[metadata]) else: # 想对 metadata 值做并集但 list/dict 不可哈希 merged {} for k in set(list(by_content[c]) list(d[metadata])): a by_content[c].get(k) b d[metadata].get(k) merged[k] set([a, b]) # list/dict 进 set - 崩溃 by_content[c] merged return by_content docs [ {page_content: X, metadata: {tags: [a]}}, {page_content: X, metadata: {tags: [b]}}, ] bad_unique_union(docs) # TypeError: unhashable type: list五、解决方案第一层最小直接修复最小修法对 metadata 合并做按类型分支——标量后者覆盖list 拼接去重dict 递归合并。def merge_meta(a: dict, b: dict) - dict: out dict(a) for k, v in b.items(): if k not in out: out[k] v elif isinstance(out[k], list) and isinstance(v, list): out[k] out[k] [x for x in v if x not in out[k]] elif isinstance(out[k], dict) and isinstance(v, dict): out[k] {**out[k], **v} else: out[k] v # 标量覆盖 return out def safe_unique_union(docs): by_content {} for d in docs: c d[page_content] if c not in by_content: by_content[c] dict(d[metadata]) else: by_content[c] merge_meta(by_content[c], d[metadata]) return by_content这一层让 list/dict metadata 不再崩溃且语义合理。六、解决方案第二层结构化改进把metadata 合并策略固化成策略对象作为单一事实来源明确每种类型的合并方式。from dataclasses import dataclass, field from typing import Any, Dict dataclass(frozenTrue) class LangChainMultiQueryUnionPolicy: MultiQueryRetriever metadata 合并策略的单一事实来源。 list_merge: str concat_dedup # concat_dedup | overwrite dict_merge: str deep # deep | overwrite scalar_merge: str overwrite # overwrite | keep_first def merge_value(self, a: Any, b: Any) - Any: if isinstance(a, list) and isinstance(b, list): if self.list_merge concat_dedup: return a [x for x in b if x not in a] return b if isinstance(a, dict) and isinstance(b, dict): if self.dict_merge deep: return {**a, **b} return b if self.scalar_merge overwrite: return b return a def merge(self, a: Dict, b: Dict) - Dict: out dict(a) for k, v in b.items(): out[k] self.merge_value(out.get(k, _MISSING), v) if k in out else v return out注_MISSING为哨兵表示 key 不存在时直接采用b。转换层用policy.merge替代手写合并策略集中、可测。七、解决方案第三层断言 / CI 守护用 pytest 锁死合并语义import pytest from policy import LangChainMultiQueryUnionPolicy as P def test_list_concat_dedup(): p P() assert p.merge({tags: [a]}, {tags: [a, b]})[tags] [a, b] def test_dict_deep_merge(): p P() out p.merge({s: {f: 1}}, {s: {g: 2}}) assert out[s] {f: 1, g: 2} def test_scalar_overwrite(): p P() assert p.merge({n: 1}, {n: 2})[n] 2 def test_no_crash_on_nested(): p P() docs [ {page_content: X, metadata: {tags: [a], s: {x: 1}}}, {page_content: X, metadata: {tags: [b], s: {y: 2}}}, ] by {} for d in docs: by[d[page_content]] p.merge(by.get(d[page_content], {}), d[metadata]) assert by[X][tags] [a, b]CI 加一条MultiQueryRetriever单测必须覆盖含 list/dict metadata 的文档断言不抛TypeError。八、排查清单unique_union报unhashable type: list→ metadata 值进了set()需按类型分支。dict metadata 合并报错→ 老版本update覆盖或|不兼容需深合并。list 重复标签→ 合并应拼接去重而非覆盖。是否所有 metadata 都是标量→ 现代 RAG 常有嵌套结构需支持。合并策略是否集中→ 用LangChainMultiQueryUnionPolicy统一避免散落 if。其他 retrieverEnsemble是否同样问题→ 见下一篇 EnsembleRetriever 的同类坑。九、小结MultiQueryRetriever.unique_union()在合并 metadata 时只考虑了标量遇到list/dict值就因不可哈希进集合或无合并语义而崩溃。第一层按值类型分支做拼接去重/深合并第二层用LangChainMultiQueryUnionPolicy把合并策略固化成单一事实来源第三层用 pytest 守护嵌套结构不崩溃。RAG 检索结果合并的通用原则metadata 去重合并必须先定义每种值类型的合并语义不能假设所有值都可哈希或可集合化。