Skip to content

合并算法

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