☰
Utopia 实体身份(Identity)设计解析:名字即证据,证据定身份
2026/9/25 15:07:12 网站建设 项目流程
  • 后端
  • 前端
  • 人工智能
  • RAG
  • 知识图谱
  • 知识管理
  • 搜索引擎

【免费下载链接】utopia

World's first open-source enterprise world model.

项目地址:https://gitcode.com/gh_mirrors/ont/utopia
点击查看免费下载

Utopia 是开源的企业级世界模型(enterprise world model)项目,本仓库为ont/utopia的镜像。本文以 docs/design/identity.md 为骨架,结合 0041 号决策记录 与utopia-store、utopia-server源码及迁移脚本,系统讲解 Utopia 如何判定「一个名字到底指谁」:名字如何作为事实进入账本、召回如何提出候选、证据如何决定归属、合并如何可撤销,以及这条设计线的评测基准与未竟事项。读完你将掌握 Utopia 身份解析(entity resolution)的完整链路:从抽取契约里的names字段,到known_as名字事实、name_vectors向量召回、shared_name裁决队列,再到可回滚的entity_merges,并能读懂 identity 基准 的评测口径。

核心结论先行:Utopia 对实体身份的答案只有一句话——身份由证据决定,而不是由字符串决定。名字是实体的一条事实(可以错、可以改、可以带时间),召回只是「报个到」地提出候选,证据(共享边、互斥值、类型不相交)才做决定;不确定时拆开并送审,绝不静默合并。


一、身份问题的两种失败方式,以及一个统一解释

设计文档开篇就给出了问题框架:以表面字符串为键的身份,会以两种方式失败:

  • 重名(namesake):一个字符串对应多个事物。同一家公司里两个「张伟」、两家公司各自的「技术中心」、探测器的「1 号」和「2 号」——字符串一样,人/物不同。
  • 多名(alias / 别名):多个字符串对应一个事物。海洋探测器 1 号在发射通告里写作「海洋探测器1号」,之后的每篇报告都只写「海探1」;一个名字有中文和英文两种写法。

老方案里,这两种失败会进一步被入库顺序放大:解析是贪心的、永不回头的——先到的文档给实体播种画像(profile),后到的文档要么贴上去、要么裂开。0041 记录里那句探针写得很直白:

今天探测仪会变成两个实体,Review 里什么也没有;两个张伟会变成一个实体,或者一个永远等不到人的平局——取决于哪篇文档先到。

而docs/design/identity.md的核心段落("What it does today")说明,这条线已经推进到了什么程度:名字已经是事实,召回有了双通道,合并可以在两个时钟上都撤销,裁决器以证据为先。下面逐节展开。


二、名字即事实:known_as值事实与账本化名字

2.1 名字进账本,不再进entities.aliases

0041 决定 1 的落地方式是迁移 0055_a_name_is_a_fact.sql:

  • 旧结构:entities.canonical_name一个标签 +entities.aliases一串别名。别名只有合并会写,没有出处、没有时间、撤不回单个;抽取读到的「简称海探1」根本进不来。
  • 新结构:每个名字都是内建属性known_as上的一条值事实(value fact):object_value = {"value": 名字},object_id为空——名字是值,不是节点。

这条设计有四个刻意为之的性质:

  1. 不成节点,不产生边、不占计数:画布只画带object_id的边,名字事实与数量值走同一条通道(#586/#587)。因此名字不撑大图计数、不冒充证据。
  2. 时态引擎天然放行:temporal只对声明了 functional / inverse-functional 的状态关系收口、记冲突;known_as两者都不是(重名本来就存在),所以第二个名字不会关掉第一个,也不会打开一个冲突。
  3. 两个时钟俱全:名字事实带recorded_at/invalidated_at(记录轴,我们何时知道)与valid_from/valid_to(世界轴,该名字何时在世界上有效)——「2019 年它叫什么」「我们何时学到海探1 = 海洋探测器1号」都可回答。
  4. 可撤回、可合并搬动:名字事实随合并一起搬、撤回合并时一起搬回,不需要任何特判。

2.2canonical_name的保留与迁移的工程细节

canonical_name保留为界面显示的标签,同时它本身也是一条名字事实(迁移第 2 步为每个存活实体插入本名事实)。迁移脚本对已合并实体做了细致处理(第 3 步):用递归 CTEname_walk沿merged_into链走到存活者,把链上每个名字事实挂到存活者身上,并把事实 id 追加进链上每条未撤回合并的moved_subject_facts——这样撤回链上任何一环,名字都跟着那一环回去;同名去重(「Apple」并入「Apple」)则记入invalidated_facts,撤回时一并复活。脚本注释里还记录了性能教训:临时表无索引、逐行相关子查询的写法在 20 万实体、4.5 万合并的库上要跑八分钟以上,因此每一步都写成「一次扫描加连接」。

从 crates/utopia-store/src/names.rs 可以看到运行时对应物:

  • ensure_known_as:按需创建内建属性(与is_a同一做法:builtin = TRUE,建库时不铺,第一次用时建);
  • record:写名字事实 + 证据(chunk + 引文);同名同实体只有一行(按主语、谓词、值去重),再被提到只是多一条证据;
  • has_name_in/not_a_name:分别给召回与列事实用——实体面板、审阅卡、度数、规则这些地方,名字不算「一条事实」,不能冒充证据、不能把计数撑大。

2.3 抽取侧:模型报名字,服务端只核对「在不在原文里」

0041 决定 2 的契约是一份names: [{ref, name, quote}]。服务端的核对只做两件事,且没有词表:

  • 名字在它的引文里、引文在 chunk 里 → 保留;
  • 同一响应或本文档更早的 chunk 已把这个字符串声明给另一个实体 → 丢弃(name_claimed_by_one之外的name_claimed_by_another信号)——一个字符串不能同时命名两个东西,这不需要任何词汇表。

「简称」「又名」「aka」「formerly」这些词服务端一概不认,也不需要认:是否把某个词视为名字是模型的职责,写进契约;服务端的职责只是字符串在场校验。names.rs中is_name_attribute的注释点明了另一层防线:名字走自己的通道(抽取回复里的names),不能当一条普通属性被模型直接写进来——那样就绕过了核对。

设计文档还记录了一个已知缺口:模型把描述报成名字而不声明实体时仍会漏过(「海探1项目」在每轮 cut-1 运行中都成了海洋探测器1号的别名),这是 0041 的开放问题之一。


三、召回(Recall):两条通道,只出候选、永不决定

3.1 通道一:字面精确匹配(名字事实)

召回的第一条通道是精确字符串匹配:mention 的名字(及其去掉泛用后缀的键)与同类型实体的名字事实精确相等。注意三个细节:

  • 曾用名照样召回:names.rs的has_name_in只看记录轴(invalidated_at IS NULL),不看世界轴——更名之前的旧文档还在用旧名字,所以世界轴上已结束的名字仍然认得出它。
  • 只认现行名字事实:查询带上facts_value_text_idx(迁移 0055 末尾为(kb_id, lower(value)) WHERE invalidated_at IS NULL建的索引),names.rs注释特别强调要用不相关子查询 + 按库过滤,否则规划器用不上索引,每次召回都要扫全表 facts。
  • 同类才配:类型是候选的门槛(e.type_id IS NULL OR me.type_id IS NULL OR e.type_id = me.type_id之类的约束在共享名排队逻辑里)。

3.2 通道二:名字向量(name_vectors,cut 2 已建)

通道一有个结构性的洞:「海探1」在一篇从没写过全名的文档里谁也碰不上;一个名字写成两种文字也永远是两个实体(#709)。0041 决定 3 的通道二——名字字符串的向量——正是为此:在同类家族内取最近的几条名字事实,向裁决器提议一个name_vector|<cosine>对,从不自动挂接。短称、另一种文字的名字,通过一个问题而不是一个静默的第二实体,与它的实体相遇。

落地迁移是 0080_a_name_has_a_vector.sql:

  • 一张从表name_vectors:一条名字事实一行,embedding不定维(随所选嵌入模型,与chunks.embedding同一条规矩),HNSW 由vector_index按第一次写下的维度排任务去建;
  • 复合外键把「事实存在」和「事实同库」合成一条约束(0070 §1b),kb_id落定后不可改——但合并搬名字事实时entity_id要能改,所以触发器只禁改 kb;
  • 名字事实作废时向量留着,查询那头按事实是否现行过滤——作废是可撤的,向量不必重算。

从 crates/utopia-store/src/name_vectors.rs 的注释看,这条通道的相似度阈值设在 0.4 上下,「所以这条线比画像的SIM_ATTACH高。同样是待测量的临时值」——即阈值是显式标注的临时值,等待基准来校准。

3.3 通道三:共享显著邻居(cut 3,未建)

第三条通道是邻居:与 mention 在同一响应中解析出的事实共享一个显著邻居的实体——同谓词、同解析对象、且对象不被其他候选指向。这条通道在 0041 中是 cut 3 的内容,尚未实现,属于「Proposed and not built」。

3.4 第一通道才做决定:画像向量的三档阈值

设计文档的关键表述是:只有第一通道决定任何事情。这里的「决定」指上下文向量profile_embedding(实体被提及的 chunk 向量的运行均值)的阈值判断,实现在 crates/utopia-store/src/resolution.rs:

  • SIM_ATTACH = 0.55:候选余弦 ≥ 0.55 →挂接(attach);
  • SIM_NEW = 0.35:< 0.35 →新建实体;
  • 中间地带(0.35 ~ 0.55)→新建实体 + 送一对给裁决器;
  • 两个同名候选分数贴着最高分(差 ≤SIM_TIE_MARGIN = 0.02)→ 送人(#270),除非corroborating_candidate在 chunk 里找到某个候选的对象名(#331 让事实打破平局);
  • CONFUSABLE_TYPE_KEYS = ["organization", "project", "product"]:三键硬表,给没装包的库兜底;声明了disjointWith时它让位(见下节)。

注意设计文档的原话——「上下文向量只用来打破平局,不做别的事」——这是 0044 提议阶段的立场,当前实现中画像向量仍是通道一内的裁决依据。


四、类型与不相交:重名实体「先分开,后裁决」

两个重名实体能不能共存,取决于类型:

  • 声明的不相交优先:owl:disjointWith现在有表、有导入、有编辑端点(0016 B3),解析时declared_disjoint_from用一条递归查询收集与 mention 类或其祖先声明不相交的类,再展开到它们的后代——先于一切启发式,同名实体直接判Disjoint;
  • 亲属类送审:同名实体属于 kin 类(祖先、后代或共享非根祖先)→ 送 Review(#226);
  • 无声明时兜底:CONFUSABLE_TYPE_KEYS三键硬表;没有包时这层不命中,跨类型同名一律Disjoint——更严格,不会错并(0009);
  • 无类型可共存:entities.type_id自 0009 起可空,无类型同名实体允许共存——NULL ≠ NULL,唯一索引拦不住它们,而这是故意的:无类型时我们知道得更少,更没有理由合并。

这条线在 0001 里还有配套决策:名字冲突是提示而不是 409——改名/改类型时 UI 提示「已存在同名实体,是否合并?」,让用户继续;唯一约束或 409 会打破解析的地基(两个张伟正是「不确定就分开」的产品形态)。


五、合并:可撤销、有闸门、审计留痕

5.1 两个时钟上的可撤销

entity_merges记录合并把什么搬走了,带created_at/reverted_at;revert_merge把事实、名字事实、限定词搬回去。读取端带as_of(记录轴回放)时,合并发生之前的瞬间显示两个节点——这正是 0019 的「第二时钟可以被倒拨」。名字事实随合并走、随撤回回,靠的就是迁移 0055 里moved_subject_facts/invalidated_facts的追加机制,无需特判。

5.2 谁有资格合并

合并是人的动作、裁决器在 0.8 置信时的动作、或治理闸门(governor)放行后的动作。自动合并当它会让图「离开」时(某一时刻的 functional 冲突、一条推导、一个被引用的答案)会被扣住等人(0027):错误合并会把两个实体的事实混在一起,而撤回并不能召回此后在它之上建起的推导、违规与答案。

5.3 改名、改型与审计

entity.retyped/entity.renamed进审计账本,带前后快照;改名撞名是合并的提示,不是 409。归属裁决器 / 治理闸门详见 governance.md。


六、裁决器(Adjudicator):一对十二,证据线与机械规则

设计文档给出的裁决器工作方式:

  • 每次调用给12 对,每对带名字、类型、顶部事实、「又名」行与账本先例;未决的对会带工具再看一遍;
  • 身份规则(identity rules)只写一次、两个提示词共用:版本/版次/分部后缀是不同事物;含名字的短语不是名字;列表不是其成员;被丢弃的限定词、公司后缀、姓氏、缩写是同一事物;一份文档历经修订仍是同一事物;
  • 两条机械规则:name_shape(英文词缀与介词分类)与type_family(类型族)。

机械规则name_shape是 0041 决定 6 的退役对象:一旦基准证明闸门抓不到证据抓不到的东西,name_shape与CORPORATE_SUFFIX就离开闸门。它现在仍挡着Version/Phrase形状的合并——因为模型会对「Claude 和 Claude 4」「Sam Altman's efforts 和 Sam Altman」说same;0041 的证据规则预期会用 functional 冲突(两个发布日期)分开前者、用 span 校验(#582)让后者根本不成为实体。


七、先例:shared_name排队与「又名」行

0041 决定 5(修订版)解决的是到达顺序问题:cut 1 曾让倒序更糟——只有缩写的文档先建了「海探1」,写着「简称海探1」的文档最后到,把名字记在新建的全名实体上,结果没有任何东西把两者配成对。修订后的规则(在 crates/utopia-store/src/names.rs 的pair_shared_name中实现):

  • 一个名字,另一个兼容类型的实体已经持有 → 排队shared_name|<名字>对给裁决器,永不合并;
  • 类型一方为空或两边相同才配对;已经判过「不是一个」的对不再排队(否则同一个简称每出现一次就重排一次,等于没记住分开的决定);
  • 裁决器与 Review 卡片看到实体的非本名作为「also known as」行;本名不列出——两个重名者因此不会显得共享证据(没有那行时裁决器在 0.90–0.95 置信度下仍把对拆开)。

pair_shared_name的查询注释还写明了一个防御细节:同名候选路径在相似度 ≥SIM_ATTACH时只出候选、永不自动合并(resolution.rs 第 647 行附近的注释),排队时用resolution_reviews的kept状态做去重。


八、评测:identity 基准与双向 F1

docs/design/identity.md的 Bench 一节给出了评测口径,脚本在 scripts/bench/identity.mjs:

  • 语料(scripts/bench/corpora/identity/):双语,含一家公司里的两个重名、一篇文档里的两个重名、一个跨无关文档的人、全名+缩写(两种到达顺序)、带日期的更名、一个名字的中英文两种写法、必须分开的相似物(两家公司的技术中心、产品与其下一版本、公司与同名水果);
  • 真值(scripts/bench/truth/identity.json):锚点分组「这篇文档里、引文含这段话的事实、这个类型那一侧的实体是真实世界的哪一个」;
  • 打分:以锚点解析到的实体 id两两比较是否相同,计 pairwise precision / recall / F1(同实体对)。名字不参与打分——否则别名与重名会先把打分本身搅乱;
  • 到达顺序是被量之物:同一轮正序、倒序各建一个库(新建 → 种本体,冻结自动扩本体/类型消解/治理/物化 → 逐篇灌入并等任务停 → 打分),两边的判定差异作为数字报告;
  • 附属报告:split_in_doc(同一篇内拆开)、false_merges/false_splits、每个真实实体落成几个系统实体、lookalikes 是否被错误合并、pending_reviews(送审队列分布)。

历史数字(0041 记录头,均为一轮或数轮的实测,非承诺):cut 0 基线 forward F1 0.43、reverse 0.54;cut 1 后 forward 0.68、reverse 0.68,210 对两个顺序一致(各一轮;两轮 cut-1 运行 forward 差 0.07);在 DeepSeek-V3 上重测:dev 0.46 / 0.40(21 个锚点中 2 个未决),cut 1 为 0.61 / 0.44–0.53(三轮)。ai-timeline.duplicates.json(govern.mjs背后的真值)作为回归一并跑。


九、为什么这么设计:身份设计的原则清单

docs/design/identity.md的 Why 一节浓缩了整套设计动机,可作为阅读决策记录的索引:

  1. 表面字符串为键必败:重名与别名就是同一把键的两种失效方式;主题向量记录的是段落「关于什么」,不是「谁在里面」,且结果依赖到达顺序(0041)。
  2. 名字必须能错、能改、能带时间:这正是账本对一切其他事实做的事;单独建表等于把有效性、出处、撤回在账本旁边重建一遍(0041)。
  3. 服务端核对的是「文本在场」,不是词表:迄今每张词表都只覆盖作者想到的说法,且name_shape看不见中文名字的形状(0041、0044 d8)。
  4. 不确定就分开:错误合并把两个实体的事实混在一起,而撤回不召回此后建起的推导、违规与答案(0001、0027)。
  5. 两个同型张伟必须可分存:名字索引是普通索引,NULL 类型不冲突(0001、0009)。
  6. 共享名去裁决,绝不合并:倒序到达曾让全名落在新实体上、无人配对(0041 d5 修订)。
  7. 子句不是实体名:按词数与定式动词判定——57 字的法院名与 65 字的子句无法按长度区分(0012)。

十、现状边界与未竟事项(Proposed and not built)

设计文档明确区分了「已建」与「提议未建」:

  • 已建:名字即事实(cut 1,迁移 0055);name_vectors通道二(cut 2,迁移 0080,2026-09-23);shared_name先例排队;两个时钟上的合并撤销;disjointWith先于一切启发式。
  • 未建(0041 cut 2 通道三与 cut 3/4):邻居通道;证据决定(共享显著边 / 反函数值重叠有效 / 共享名不算证据 / 上下文向量只打破平局);响应解析后再解析 mention、删除doc_cache;名字或边到达时重评估(cut 4);name_shape与后缀词表的退役(等基准证明闸门抓不到证据抓不到的东西)。
  • 跨文档身份(0044 d6):文档内合并实体、以名字/类/属性/邻居/时间跨度为画像;跨文字脚本的候选;确定性打分与 cannot-links;只有未决对才送裁决器(裁决器看两份画像);约束聚类,使 A≈B、B≈C 永不违证并 A、C;更名是一个实体、名字在不同时间有效;角色不是实体。
  • #725 退役清单:写时解析(resolve_mention、阈值、containment_reviews、resolve_type_drift、doc_cache、无嵌入回退)、批裁决器与resolution_verdicts、词表;退役要等设计通过 identity 基准与 Linked-Re-DocRED 对。
  • 开放问题:被报为名字的描述(「海探1项目」)能否不用词表挡住(0041);两个无类型重名只靠画像相似度(0009);确定性证据在裁决器介入前能解决多少(0044);跨库重名先例(0025)。

附:深入阅读路径

  • 设计总览与领域索引:docs/design/README.md(identity 域的现状说明、相关决策记录状态表)
  • 决策记录:核心为 0041-a-name-is-a-claim-about-an-entity.md,配套 0001、0009、0019、0025、0027、0028
  • 迁移:0055_a_name_is_a_fact.sql、0080_a_name_has_a_vector.sql
  • 实现:crates/utopia-store/src/names.rs、crates/utopia-store/src/name_vectors.rs、crates/utopia-store/src/resolution.rs(阈值常量SIM_ATTACH/SIM_NEW/SIM_TIE_MARGIN、CONFUSABLE_TYPE_KEYS)
  • 基准:scripts/bench/identity.mjs 与 scripts/bench/truth/identity.json、scripts/bench/corpora/identity/ 语料
  • 治理侧细节:docs/design/governance.md;时间轴/双时钟:docs/design/time.md、docs/decisions/0019-the-second-clock-can-be-rewound.md
  • 后端
  • 前端
  • 人工智能
  • RAG
  • 知识图谱
  • 知识管理
  • 搜索引擎

【免费下载链接】utopia

World's first open-source enterprise world model.

项目地址:https://gitcode.com/gh_mirrors/ont/utopia
点击查看免费下载

相关推荐

上一篇:在 Cursor 中通过 Gong MCP 插件拉取账户摘要、交易洞察与通话简报
下一篇:HCCL Star 算法解析:星型拓扑一步完成有根通信的原理、适用场景与耗时模型

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询