Skip to content

fix(graph): 丢弃超出列长度的实体名,避免一个坏值毁掉整块图谱 - #1039

Open
zgpnuaa wants to merge 1 commit into
xerrors:mainfrom
zgpnuaa:fix/graph-entity-name-length
Open

zgpnuaa wants to merge 1 commit into
xerrors:mainfrom
zgpnuaa:fix/graph-entity-name-length

Conversation

@zgpnuaa

@zgpnuaa zgpnuaa commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

变更说明

问题:LLM 图谱抽取偶尔会把整段正文当成实体名。实测一例:一篇 NASA 技术备忘录的表单封面页里,模型把整段摘要抽成了一个「实体」,并通过 has abstract 关系连到论文标题:

[An Object-Oriented Computer Code for Aircraft Engine Weight Estimation]
    --has abstract-->
[Reliable engine-weight estimation at the conceptual design stage is critical…]   ← 1672 字符

knowledge_graph_entities.name / normalized_namevarchar(512)

后果被放大,而且不可自愈

  1. upsert_chunk_graph 把一个分块的所有实体放进一条多行 INSERT,一个值超长,整批被 PostgreSQL 拒绝 —— 该分块的图谱全部丢失(这一批是 12 个实体、11 条关系)。
  2. 紧接着 mark_graph_structure_indexed 也不执行,graph_indexed 恒为 false。
  3. count_graph_pending_by_kb_id 的判据就是 graph_indexed IS NOT TRUE —— 于是这个分块永远停在 pending,每次「重试索引」都会重新抽一遍(再烧一次模型调用),然后以同样的原因再次失败。
  4. Neo4j 是先写的,所以还留下跨库不一致:Neo4j 里那 12 个实体/11 条关系,PG 里没有。界面上能看见这些节点,系统却认为这个分块没建过图谱。

目击的那次构建停在 913/914,日志里只有一句 write_failed=1,界面上看不出原因。

决策记录:按维护者在 #1031 中的意见(「可以将 Decision 文件……移除后合并」)本 PR 未附 decision record。取舍见「替代方案」。

工程主张与 Owner

  • 主张 1:规范化后的实体名/标签、关系类型都不超过对应列的长度,绝不会因为长度而让 PostgreSQL 拒绝整批。
    Owner:normalize_extraction_resultknowledge/graphs/extractors/base.py)。
  • 主张 2:丢弃只影响越界的那个单位——同批的其它实体与关系原样保留。
    Owner:同上。
  • 主张 3:丢弃可观察,不静默。
    Owner:metadata.dropped_entities / metadata.dropped_relations
  • 主张 4:守卫常量与列定义不会漂移。
    Owner:test_normalizer_limits_match_graph_column_lengths

验证情况

主张 1 + 2:真实失败数据上的前后对比

  • 直接证据 / 命令:把生产库里那条真实抽取结果过一遍修复后的规范化:

    raw = <knowledge_chunks.extraction_result for chunk file_0462e3_chunk_8>
    normalize_extraction_result(raw, "llm")
    规范化前: 实体 12,关系 11
    规范化后: 实体 11,关系 10     ← metadata: dropped_entities=1, dropped_relations=1
    规范化后最长实体名 = 117        ← 不超过 512
    
  • 负向案例:同一次改动之前,同样的输入产生 1672 字符的实体名,直接导致 PostgreSQL StringDataRightTruncationError: value too long for type character varying(512)

  • 结果:Passed

主张 1:真实链路端到端(原来永久卡死的分块)

  • 直接证据:把修复部署到本地后重跑该知识库的图谱构建:

    修复前 修复后
    该分块 graph_indexed false(永久) true
    该分块在 PG 的实体数 0(整批被拒) 11
    构建日志 write_failed=1 remaining=1 write_failed=0 remaining=0
    全库 913/914 914/914
  • 结果:Passed

主张 3:丢弃可观察

  • 直接证据:test_normalize_extraction_result_keeps_metadata_clean_without_drops 断言无丢弃时 metadata 不变;有丢弃时写入两个计数;test_normalize_extraction_result_counts_dropped_entities_distinctly 断言实体数按去重计、关系数按条计。
  • 负向案例:去掉守卫后这 4 条用例失败(变异校验已跑)。
  • 结果:Passed

主张 4:常量与列定义一致

  • 直接证据:test_normalizer_limits_match_graph_column_lengths 直接比较 MAX_ENTITY_NAME_LENGTH == KnowledgeGraphEntity.name.type.length 等四处。
  • 结果:Passed

主张 5:不回归

  • 直接证据 / 命令:pytest test/unit/graphs -q43 项通过(新增 8 项);ruff check → All checks passed;git diff --check 干净。
  • 环境说明(Not run):pytest test/unit/knowledge 在本环境有 3 项失败,全部是 test_parser_facade.py 的 XLS 用例,原因是本环境镜像未安装 xlrdModuleNotFoundError: No module named 'xlrd'),与本改动无关(这些用例不导入图谱模块)。
  • 结果:Passed

简化 / 删除验收

不涉及(bug-fix)。

独立语义 Review

已执行(本改动伴随的一次自检中发现并修掉了一处自身缺口):

  • 初版只处理「越界实体以对象形式出现」的情况。补测试时发现:若关系只以裸字符串引用一个超长名(该名未出现在 entities[] 里),守卫看不到它,仍会抛错让整个分块的抽取失败。已补上这条路径(引用名本身超过实体名列长度即丢弃该关系)。
  • 同时把 dropped_entities 的计数从「出现次数」改为「按实体去重」——初版对同一个被两条关系引用的越界实体会报 2。

替代方案

  • 扩大列(512 → TEXT):治本,但只解决这一个字段;关系类型(256)、标签(128)同样没有上限,而且 normalized_name 上有 (kb_id, normalized_name, label) 唯一约束,放宽列会改变该约束的语义面。判为后续可选,不在本 PR。
  • 截断到列长度:比丢弃更保信息,但截断后前缀相同的两个实体会撞 (kb_id, normalized_name, label) 唯一约束 → 又是一次整批失败,等于没修干净。故取丢弃。
  • 抛异常让抽取失败:更"响亮",但结果是整个分块都建不了图谱,比只丢一条更糟。故选丢弃 + 计数可观察。
  • 在 Prompt/Schema 里约束实体粒度:治源头,但模型不保证遵守,且 Schema 是用户配置项(本 PR 不改默认行为)。与守卫互补而非替代。

未验证范围与风险

  • 跨存储不一致未修Not run):Neo4j 先写、PG 后写,PG 失败时 Neo4j 已落盘。本 PR 只消除了一个触发原因,没有改变这个顺序,也没有补偿/自动收敛。已有一条 open issue [Bug][数据一致性] 图谱写入跨 Neo4j 与 PostgreSQL 两个独立事务,Neo4j 成功而 PG 失败时靠重试收敛 #879 专门讨论这一点(含它建议的幂等补偿与顺序调整),本 PR 不与之重叠。目击的那次不一致(Neo4j 有 12 实体、PG 为 0)在修复后重跑时被 MERGE 收敛掉了。
  • 丢弃是静默降级的边界情况Passed,但值得注意):超长实体通常确实是抽取噪声(整段正文),本次样本里丢弃后同批 11 个实体完好;但若将来出现"合法的超长实体"(例如极长的机构全名),丢弃会损失信息。当前列长度下这是可接受的取舍。
  • 只在 llm 抽取器路径上验证Not inspected):目前 GraphExtractorFactory.supported_types() == ["llm"],其它抽取器尚未存在。
  • 未做真实并发/大批量回归Not run):端到端只在单个原失败分块上验证;全库 914 个分块此前已建完,未重跑整库。

界面变更

不涉及(后端规范化层,界面上只表现为不再有分块卡在「待构建」,以及该分块的节点数从 0 变为 11)。

关联事项

相关但不重叠:#879(图谱写入跨 Neo4j 与 PostgreSQL 两个独立事务,Neo4j 成功而 PG 失败时靠重试收敛)。本 PR 消除其中一个具体的 PG 失败原因,不改变该 issue 描述的跨存储语义。

补充说明

  • 兼容性:不改变任何既有配置或数据结构;只在规范化阶段丢弃越界值。已入库的图谱数据不受影响。
  • 为什么在规范化层而不是写入层normalize_extraction_result 是「模型输出 → 落库」之间唯一的收敛点,且它已经在做同一类事(校验非空、合并重复实体、解析引用)。放在这里,任何调用方都绕不过去。
  • 数据迁移:无。

LLM 抽取偶尔会把整段正文当成实体名。实测一篇 NASA 技术备忘录的表单封面页被抽出
一个 1672 字符的「实体」(内容是整段摘要),而 knowledge_graph_entities.name /
normalized_name 是 varchar(512)。

后果被放大是因为写入方式:upsert_chunk_graph 把一个分块的所有实体放进**一条多行
INSERT**,一个值超长整批被 PostgreSQL 拒绝——该分块图谱全部丢失、graph_indexed 不
置位,于是永远停在 pending;每次重试都要再烧一次模型调用,还照样失败。目击的那次
构建停在 913/914,日志只有 write_failed=1,界面上看不出原因。

改动:在 normalize_extraction_result(落库前的规范化层)加长度守卫,丢弃越界的单位
而不是抛异常(抛异常会让整个分块的抽取都失败,比丢弃更糟):

- 实体名/归一化名超过 512、标签超过 128 → 丢弃该实体,并一并丢弃引用它的关系
  (否则会留下悬空关系);关系以裸字符串引用超长名时同样丢弃该关系
- 关系类型超过 256 → 丢弃该关系
- 丢弃数记入 metadata.dropped_entities / dropped_relations,避免静默降级

常量与列定义的一致性由单测断言,防止将来漂移。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant