首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >本体驱动ERP|大模型也会认错人:我是怎么用本体做实体消歧的

本体驱动ERP|大模型也会认错人:我是怎么用本体做实体消歧的

作者头像
本体与AI
发布于 2026-09-09 20:51:56
发布于 2026-09-09 20:51:56
1410
举报

本体驱动ERP系列 ·  一个让系统"认错人"的坑,和它背后的本体解法

先讲个真事。有段时间我总觉得供应链风险传导的结果不对劲——明明只是一家供应商出问题,系统却推导出五六个项目受影响。翻了半天才发现:系统把两家不同的"鑫达"当成同一家了。

一家是AAA鑫达合金材料有限公司(注册地深圳,统一社会信用代码 91440300MA5XXXXXX1),另一家是BBB鑫达精密制造有限公司(注册地东莞,代码 91441900MA5YYYYYY2)。名字里都有"鑫达",但八竿子打不着。

更要命的是反方向:同一家真公司,在三个系统里叫三个名——ERP 里是"深圳市鑫达合金材料有限公司",OA 报销系统里是"鑫达合金",一份扫描合同里 NLP 抽出来的是"鑫达"。系统不知道它们是同一家,风险传导、采购分析、合规检查全算错。

这就是实体消歧(Entity Disambiguation)要解决的问题。它看着小,实则是整个本体驱动系统的地基——地基歪了,上面跑再花哨的推理都是空中楼阁。

■ 先说清楚:实体消歧到底在解决什么

它其实管两件事,方向相反:

  • 异名同实:俩记录指同一个真实对象("深圳市鑫达合金材料有限公司" = "鑫达合金")。要认出来、合并、指向同一个规范实体。
  • 同名异实:俩记录名字一样但根本不是一回事(深圳鑫达 vs 东莞鑫达)。绝不能合并,合并了就是事故。

难点就在这:你不能简单地"名字像就合并"——那样会把深圳鑫达和东莞鑫达焊在一起;你也不能"名字不同就当不同"——那样会漏掉"鑫达合金"和"深圳市鑫达合金材料有限公司"。判断"是不是同一个",本身就是一门学问。

■ 我踩过的第一个坑:编辑距离自动合并

刚上手那会儿,我用了最朴素的办法——编辑距离(Levenshtein)+ 阈值自动合并。名字相似度超过 0.85 就自动认成同一家。代码十几行,跑得飞快。

然后就出了开头那个事。系统把深圳鑫达和东莞鑫达的相似度算成了 0.91——都叫"鑫达",后缀"有限公司/精密制造"差异被稀释了——直接合并。结果风险传导 case(第9篇那个)里,一家出事,另一家无辜躺枪,推导出一堆假风险。

# 我一开始写的"聪明"代码(后来证明是坑) def is_same(a, b):     return levenshtein(a, b) / max(len(a), len(b)) > 0.85  # "深圳市鑫达合金材料有限公司" vs "东莞市鑫达精密制造有限公司" # 相似度 0.91 → 误判为同一家 ← 事故根源

启示零(用一次事故换来的):字符串相似 ≠ 同一个实体。名字像,可能只是都叫"鑫达";名字不像,可能只是录入格式不同。相似度只能当线索,不能当判决。

■ 本体为什么适合干这个活

字符串匹配搞不定的根因是:它不知道"供应商"这个概念到底由什么定义。而本体干的就是这件事——定义概念、定义关系、定义"什么算同一个"。

我在本体里给 Supplier 定义了身份判据,不是"名字相等",而是一组优先级规则:

  • 第一优先级:统一社会信用代码。代码相同 → 100% 同一家(这是国家给的法定身份,比名字靠谱一万倍)。
  • 第二优先级:名称归一化 + 注册地 + 法人。三者同时命中才算同一家。
  • 冲突保护:同名同地但信用代码不同 → 绝不合并,标红人工确认(这就是深圳鑫达 vs 东莞鑫达的防火墙)。

本体里用 owl:sameAs 表达"等价",用规范实体(canonical entity)做 hub——所有别名都指向它,原始记录一个不动,只在上面挂关系。

代码语言:javascript
复制
# 本体定义(Turtle) 
:Supplier a owl:Class . 
:hasCreditCode  a owl:DatatypeProperty ;     
  rdfs:domain :Supplier ; 
  rdfs:range xsd:string . 
:normalizedName a owl:DatatypeProperty ;     
  rdfs:domain :Supplier ; 
  rdfs:range xsd:string . 
:registeredCity a owl:DatatypeProperty ;     
  rdfs:domain :Supplier ; 
  rdfs:range xsd:string . 
:aliasOf a owl:ObjectProperty .   
# 别名 → 规范实体

■ 实体消歧三步算法(呼应第7篇)

第7篇我提过"实体消歧三步算法",这里展开。整个流程跑在 SWRL 推理 + SPARQL 查询 + 应用层 merge 三层上。

Step 1:候选生成(Blocking)

别拿新记录跟全库几万家供应商逐一比对——太慢。本体定义了 blocking key:名称前4字归一化 + 注册省份。先按这个分桶,只在桶内比对。

代码语言:javascript
复制
# blocking key:先把候选集从 10万 缩到 几十 
def blocking_key(name, province):     
    return (normalize(name)[:4], province)  
    # "深圳市鑫达合金材料有限公司" → ("鑫达", "广东") 
    # 只在 ("鑫达","广东") 桶里找候选,跳过其他

Step 2:特征匹配(SWRL / SPARQL)

在候选集内,用 SWRL 规则做判定推导。信用代码相同直接推 sameAs;名字+地+法人多维度命中推 aliasOf。

代码语言:javascript
复制
# SWRL 规则:信用代码相同 → 同一实体(最高优先级) 
Supplier(?s1) ^ Supplier(?s2) ^ hasCreditCode(?s1, ?c) ^ hasCreditCode(?s2, ?c) ^ differentFrom(?s1, ?s2) -> aliasOf(?s1, ?s2) 
 # 注:纯 SWRL 写 owl:sameAs 有推理机兼容坑, # 实际我在应用层监听新增三元组后做 merge(见第10篇 Action 层思路)

Step 3:决策与合并(阈值 + 人工)

打分:信用代码命中=100 分自动合并;名字+地+法人三命中=80 分自动挂别名;50~80 分=人工审核;低于 50=保持独立。中分段必须人确认——这就是第10篇说的 Human-in-the-Loop,在实体消歧里它救过我好几次。

合并时只挂关系、不改原始数据。这点后面踩坑会讲为什么。

代码语言:javascript
复制
# SPARQL:反向检测"同名异实"的误合并风险 
SELECT ?a ?b WHERE {   ?a a :Supplier ; :normalizedName ?n ; :registeredCity ?c .   ?b a :Supplier ; :normalizedName ?n ; :registeredCity ?c .   FILTER(?a != ?b)   ?a :hasCreditCode ?ca . ?b :hasCreditCode ?cb .   FILTER(?ca != ?cb)        
# 同名同市但代码不同 → 必须人工确认 }

■ 大模型在这个事里该站哪

现在人人聊"大模型 + 知识图谱"。实体消歧正好是看清两者分工的绝佳例子。

大模型擅长当"提取器":一份《供应商准入规范》PDF,LLM 能从中抽出"潜在供应商""资质材料"这些候选,还能把"深圳市鑫达合金材料有限公司"自动归一化成"鑫达合金材料"喂给 Step 1。这事它干得比正则强。

但大模型不能当"裁判":让它直接判断"这两家是不是同一家",它会基于概率编一个看似合理的答案,而且你没法审计它为什么这么判。在合规和风控场景,这是不能接受的。

启示一:大模型是好的提取器,不是好的裁判。裁判必须是可审计的规则 + 本体定义的身份判据。本体提供 ground truth,大模型提供候选——分工清清楚楚。

■ 三个踩坑(都是真金白银换来的)

踩坑 1:编辑距离自动合并翻车(前面讲过了)。一句话:相似度只能当线索。

踩坑 2:合并后删了原始记录。早期我以为合并就是"把重复的删掉"。结果有一次合并错了,想回滚发现原始数据没了,没法恢复。改成现在这样:原始记录永不删,只在上层挂 aliasOf 关系。合错了?断开关系就行,数据原样还在。

踩坑 3:信用代码字段有脏数据。有的录了全角字符,有的少一位,有的带了空格——直接相等判断全失效。加了归一化清洗层(去全角、去空格、补位校验)才稳。

代码语言:javascript
复制
# 归一化清洗(合并前必过) 
import re 
def normalize(name):     
    s = re.sub(r'[((].*?[))]', '', name)      
    # 去括号内容     
    s = re.sub(r'^(深圳市|广东省|东莞)', '', s)   
    # 去行政区划     
    s = s.replace('有限公司','').replace('有限责任公司','')     
    s = re.sub(r'\s+', '', s).strip()     
    return s 
    # normalize("深圳市鑫达合金材料有限公司") => "鑫达合金材料"

■ 写在最后

实体消歧的本质,是"定义什么是同一个"。而"定义概念和关系"恰恰是本体最擅长、大模型最不擅长的事。这俩不是替代关系——本体定规则、大模型提候选,合起来才是一个能上线、能审计、能回滚的企业级方案。

如果你也在做知识图谱落地、大模型 + 企业数据、或者任何"系统认人"的场景,实体消歧这道坎早晚要过。别信编辑距离,别让大模型当裁判,把"什么是同一个"写进本体里。

■ 互动时间

1. 你们系统里"认错人"最离谱的一次是什么?怎么发现的?

2. 实体消歧你用的是规则、向量相似度、还是大模型?效果咋样?

3. 如果让你给本体加一条"同名异实"防火墙规则,你会怎么写?

评论区聊聊。好的思路我会在下一篇 Agent 层的文章里接着聊——Agent 怎么调用这套消歧服务做决策。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-09,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 本体驱动ERP系列 ·  一个让系统"认错人"的坑,和它背后的本体解法
    • ■ 先说清楚:实体消歧到底在解决什么
    • ■ 我踩过的第一个坑:编辑距离自动合并
    • ■ 本体为什么适合干这个活
      • Step 1:候选生成(Blocking)
      • Step 2:特征匹配(SWRL / SPARQL)
      • Step 3:决策与合并(阈值 + 人工)
    • ■ 三个踩坑(都是真金白银换来的)
    • ■ 互动时间
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档