合并算法¶
src/pipeline/enricher.py 中的 _merge_specialist_results() 方法将多个专业策略的输出合并为单个 list[EnrichedRow]。
优先级顺序¶
专业策略按固定优先级顺序合并。当多个专业策略为同一行的同一列提供值时,优先级最高的策略中第一个非空值胜出。
| 优先级 | 策略 | 理由 |
|---|---|---|
| 1(最高) | auxiliary_table |
直接数据查找是最可靠的来源 |
| 2 | image_legend |
图例图示提供明确的视觉映射 |
| 3 | text_rule |
文本规则权威但需要解读 |
| 4 | dimension_card |
尺寸数据补充其他来源 |
| 5(最低) | multi_label |
复合引用解析是对现有值的细化 |
该优先级在 STRATEGY_PRIORITY 列表中定义:
STRATEGY_PRIORITY = [
StrategyType.AUXILIARY_TABLE,
StrategyType.IMAGE_LEGEND,
StrategyType.TEXT_RULE,
StrategyType.DIMENSION_CARD,
StrategyType.MULTI_LABEL,
]
合并过程¶
对于所有专业策略输出中每个唯一的 row_id,合并按优先级顺序遍历专业策略条目,构建单个 EnrichedRow。
常规列¶
除 Special Notes 外,第一个遇到的非空值(来自优先级最高的专业策略)成为最终值。后续专业策略无法覆盖该值。
if col not in merged_data or not merged_data[col]:
merged_data[col] = value
if col in row.field_sources:
merged_sources[col] = row.field_sources[col]
Special Notes¶
Special Notes 是先到先得规则的例外。合并从所有专业策略中累积唯一片段。每个专业策略的 Special Notes 值按 ; 和 | 分隔符拆分为独立片段。每个片段使用不区分大小写的子串匹配与已有片段比对——若新片段是已有片段的子串(或反之),则视为重复并跳过。
for frag in fragments:
frag_lower = frag.lower()
is_dup = any(
frag_lower in existing.lower() or existing.lower() in frag_lower
for existing in special_notes_parts
)
if not is_dup:
special_notes_parts.append(frag)
最终 Special Notes 值用 | 连接所有唯一片段。
置信度¶
合并行的置信度为该行所有专业策略条目中的最低置信度。这确保最终置信度反映了贡献者中最不确定的一方。
推理依据¶
所有专业策略的推理字符串以 | 为分隔符拼接。每条记录以策略名称为前缀(如 [auxiliary_table] 将 GL-03 匹配至第 2 行)。
字段来源¶
field_sources 字典追踪哪个专业策略填充了每一列。由于常规列遵循先到先得规则,来源始终对应提供值的优先级最高的专业策略。
行恢复¶
合并后,算法检查主清单中没有任何专业策略产生输出的行。这些行通过比较 _assign_row_ids() 派生的权威 __row_id__ 集合与已合并的行 ID 来识别。
缺失的行作为空 EnrichedRow 恢复,所有模式列置为空字符串,置信度置为 0.0。这保证管道永远不会静默丢弃行——主清单中的每一行都会出现在输出中。
使用基于索引的 __row_id__ 值的表格(即主键列检测失败的情况)不参与行恢复,以避免误报。
单行合并流程¶
下图展示了当三个专业策略提供输出时,单行数据的合并流程。
flowchart TB
subgraph inputs["W1 行的专业策略输出"]
T1["T1: auxiliary_table<br/>Glass Type = SNX 62/27<br/>Special Notes = GMT-01 infill<br/>confidence = 0.95"]
T3["T3: image_legend<br/>Operability = Casement Single<br/>Special Notes = Style A<br/>confidence = 0.90"]
T2["T2: text_rule<br/>Glass Type = <i>空</i><br/>Special Notes = IBC 2406.4 tempered<br/>confidence = 0.85"]
end
subgraph merge["合并(按优先级顺序)"]
direction TB
P1["1. auxiliary_table<br/>Glass Type = SNX 62/27 (已设置)<br/>Special Notes += GMT-01 infill"]
P2["2. image_legend<br/>Glass Type 已设置,跳过<br/>Operability = Casement Single (已设置)<br/>Special Notes += Style A"]
P3["3. text_rule<br/>Glass Type 已设置,跳过<br/>Special Notes += IBC 2406.4 tempered"]
P1 --> P2 --> P3
end
subgraph output["合并后的 EnrichedRow"]
OUT["row_id = W1<br/>Glass Type = SNX 62/27<br/>Operability = Casement Single<br/>Special Notes = GMT-01 infill | Style A | IBC 2406.4 tempered<br/>confidence = 0.85<br/>reasoning = [auxiliary_table] ... | [image_legend] ... | [text_rule] ..."]
end
inputs --> merge --> output