package distill // 字段拆分的独立金标准。 // // ★ 为什么不能用 LLM 自己的输出当判据 // ---------------------------------- // 本会话已经踩过两次:早期「20/20 拆分正确」和「15/15 跨记录召回」用的 // 都是 LLM 产出的字段做参照 —— 拆分对了说「拆分没问题」,召回对了说 // 「召回没问题」,一旦两者同源就形成闭环验证,判据失去独立性。 // // 这里的每个期望值都是**从原句里人工核对出来的**(不是从拆分结果反推的), // 而且用例本身能区分「拆对了」与「拆得刚好能用」—— 见下方各条备注。 // // ★ 用例设计的三个坑 // ------------------ // 1. **属性型 vs 叙述型**:库里真实存在两类记录 —— // 叙述型「第183批 告警规则9条·…」有「第N批」主语,能拆; // 属性型「值班室分机号 4324,值班 老周」**没有批次号**,主语是属性名本身。 // 只测前者会漏掉整个属性型(端口针就死在这)。 // 2. **值覆盖**:同一属性先后给两个值,期望是**能查到两个**(时序仲裁是 // 召回层的事,拆分层不该合并或丢弃)。若把「只该留最新值」写成期望, // 就把召回层的职责塞进了拆分层。 // 3. **不可拆句**:「第129~143批均仅评审通过」这类纯叙述句**期望零字段** // —— 空结果是对的,不是失败。 // goldCase 是一条金标准用例。 type goldCase struct { // name 用于失败输出,描述这条记录考什么。 name string // record 是原始记录(与真库一字不差)。 record string // wantSubject 是期望推出的主语;空串表示「不该拆出三元组」。 wantSubject string // wantFields 是期望的 (字段名, 值) 对。**值必须原样出现在 record 里**。 wantFields []wantField // noTriple 表示这条记录不应该产出任何三元组(无主语或无可拆字段)。 noTriple bool // wantSubjects 是期望的主语列表(多主语时逐个比对)。 // nil 表示不校验主语。wantSubject 仍是单个主语的简写形式。 wantSubjects []string } type wantField struct { name string value string // why 记录这个期望的判断依据,用于 review 时回溯。 why string } // goldCases 是字段拆分的金标准集。 // // 来源:/var/tmp/ha-c/memory/graph.db 的 entities 表(188 个实体里挑的), // 加上 order-gw 叙事里的端口针。全部是**真实入库形态**,不是构造的干净输入。 var goldCases = []goldCase{ { name: "属性型-分机号新值", record: "值班室分机号 4324,值班 老周(下周起;旧号 4379 停用)", // 属性型记录的主语是属性名本身,不是「第N批」 wantSubject: "值班室分机号", wantFields: []wantField{ {"值班分机号", "4324", "新值是本周要生效的那个"}, {"值班人", "老周", "跟新号一起出现的值班人"}, }, }, { name: "属性型-分机号旧值", record: "值班室分机号 4379,值班人 阿李", // 与上面同属性、异值。拆分层**两个都要留**(覆盖仲裁是召回层的职责)。 // 若这里期望「只留 4324」,就把两层职责混在一起了。 wantSubject: "值班室分机号", wantFields: []wantField{ {"值班分机号", "4379", "旧值,上一条的对照"}, {"值班人", "阿李", "旧值对应的值班人"}, }, }, { name: "属性型-改述句", record: "下周起值班室分机号改为 4324,旧号 4379 停用,值班轮换到 老周", // 同一事实的改述形态。改述能拆出来,说明拆分不依赖固定模板。 wantSubject: "值班室分机号", wantFields: []wantField{ {"值班分机号", "4324", "改述句里的新值"}, {"值班人", "老周", "改述句里的轮换值班人"}, }, // 注意:这条实测模型会多拆出「停用旧号=4379」「生效时间=下周起」, // 那是**正确的**(原句里确实有这两项信息)。所以判据只要求 // 「期望字段全命中 + 值原样」,不禁止额外字段 —— 见 judgeFields // 的说明:幻觉由「值必须原样出现在原句」把关,不靠字段数封顶。 }, { name: "叙述型-批次多字段", record: "第183批 告警规则9条·值班手册第4版·容量预警70%·排期10月", // 叙述型:有「第N批」主语。这是拆分器原本唯一支持的形态。 wantSubject: "第183批", wantFields: []wantField{ {"告警规则数", "9条", "第183批的告警规则条数"}, {"值班手册版本", "第4版", "第183批的值班手册版本"}, {"容量预警", "70%", "第183批的容量预警阈值"}, {"排期", "10月", "第183批的排期月份"}, }, }, { name: "多服务复合句", record: "admin服务端口8861·billing服务端口8499·oauth服务端口8271", // ★ 最难的一条:一句里三个服务各自的端口。 // 主语不是单一实体(admin/billing/oauth 三个),拆成三元组需要 // 「一个记录产出多组主语」的能力 —— 现有 Extractor's subjectFrom // 是 func(record) string,**只能返回一个主语**,结构上做不到。 // 这条是本轮要解决的核心缺口。 wantSubject: "admin服务|billing服务|oauth服务", wantFields: []wantField{ {"端口", "8861", "admin 服务的端口"}, {"端口", "8499", "billing 服务的端口"}, {"端口", "8271", "oauth 服务的端口"}, }, // ★ 这条考的是「一句多主语」:三个服务的端口必须各归各主语, // 不能混成「admin服务→8861,billing服务→8499,oauth服务→8271」以外的乱配。 wantSubjects: []string{"admin服务", "billing服务", "oauth服务"}, }, { // ★ 真库实测暴露的「第四种形态」:句子型(无属性名、无批次号、 // 无分隔符),但确有可拆的值。 // // 三种示例(批次型 / 属性型 / 复合型)都覆盖不到它,而模型遇到 // 不像示例的输入就交白卷 {"fields":[]} —— 实测 6/6 真库实体 // 全是零字段,其中包括: // // 「连接池从 32/64 扩到 128/256,等待队列长度告警阈值 300~500」 // 明明有 32/64、128/256、300~500 三个值,模型返回空 // 「第15批与第18批之间缺失,待老大确认是否遗漏」 // 主语推出了(第15批),但字段为空 // // ★ 金标准原先 6 条恰好都在三种示例形态内 —— 判据太贴合示例, // 于是「换形态就交白卷」这个缺陷完全看不见。 name: "句子型-连接池扩容", record: "连接池从 32/64 扩到 128/256,等待队列长度告警阈值 300~500", // 这条的主语是「连接池」,但它不在受控别名表里 —— 期望它被推导出来 wantSubject: "连接池", wantFields: []wantField{ {"连接池容量", "32/64", "扩容前的值"}, {"连接池容量", "128/256", "扩容后的值"}, {"等待队列告警阈值", "300~500", "告警阈值区间"}, }, }, { name: "句子型-批次缺失", record: "第15批与第18批之间缺失,待老大确认是否遗漏", // 期望:推不出可用字段值(「缺失」「待确认」不是值),零三元组是对的。 // 但主语「第15批」应该推得出来 —— 记录在这里是为了锁住 // 「主语可推但字段为空」这个组合,不让它悄悄变成「主语也推不出」。 wantSubject: "第15批", noTriple: true, }, { name: "不可拆-纯叙述句", record: "第129~143批均仅评审通过", noTriple: true, // 「均仅评审通过」没有可拆的字段值 —— 期望零三元组是**正确**结果。 // 如果判据要求「至少拆出1个」,就会逼着模型编造字段。 }, }