- 后端
- 前端
- 人工智能
- RAG
- 知识图谱
- 知识管理
- 搜索引擎
【免费下载链接】utopia
World's first open-source enterprise world model.
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为空——名字是值,不是节点。
这条设计有四个刻意为之的性质:
- 不成节点,不产生边、不占计数:画布只画带
object_id的边,名字事实与数量值走同一条通道(#586/#587)。因此名字不撑大图计数、不冒充证据。 - 时态引擎天然放行:
temporal只对声明了 functional / inverse-functional 的状态关系收口、记冲突;known_as两者都不是(重名本来就存在),所以第二个名字不会关掉第一个,也不会打开一个冲突。 - 两个时钟俱全:名字事实带
recorded_at/invalidated_at(记录轴,我们何时知道)与valid_from/valid_to(世界轴,该名字何时在世界上有效)——「2019 年它叫什么」「我们何时学到海探1 = 海洋探测器1号」都可回答。 - 可撤回、可合并搬动:名字事实随合并一起搬、撤回合并时一起搬回,不需要任何特判。
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 一节浓缩了整套设计动机,可作为阅读决策记录的索引:
- 表面字符串为键必败:重名与别名就是同一把键的两种失效方式;主题向量记录的是段落「关于什么」,不是「谁在里面」,且结果依赖到达顺序(0041)。
- 名字必须能错、能改、能带时间:这正是账本对一切其他事实做的事;单独建表等于把有效性、出处、撤回在账本旁边重建一遍(0041)。
- 服务端核对的是「文本在场」,不是词表:迄今每张词表都只覆盖作者想到的说法,且
name_shape看不见中文名字的形状(0041、0044 d8)。 - 不确定就分开:错误合并把两个实体的事实混在一起,而撤回不召回此后建起的推导、违规与答案(0001、0027)。
- 两个同型张伟必须可分存:名字索引是普通索引,NULL 类型不冲突(0001、0009)。
- 共享名去裁决,绝不合并:倒序到达曾让全名落在新实体上、无人配对(0041 d5 修订)。
- 子句不是实体名:按词数与定式动词判定——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.
相关推荐
Material-UI数字签名:数据完整性验证与身份认证
Material UI数字签名:数据完整性验证与身份认证 引言 在当今数字化时代,数据安全至关重要。数字签名(Digital Signature)作为一种重要的
前端UI组件设计系统Wasp 邮箱认证身份数据深度解析:email identity 字段、存储原理与访问实践
Wasp 邮箱认证身份数据深度解析:email identity 字段、存储原理与访问实践 导读 在 Wasp 全栈框架中启用邮箱(Email)认证后,每个用户
Web框架后端前端CLI开发工具mlx-lm深度解析:AI开发者必知的LLM本地部署新范式
mlx lm深度解析:AI开发者必知的LLM本地部署新范式 还在为LLM本地部署的高额硬件成本发愁?mlx lm正以革命性的方式改变这一现状。作为专为Apple
密码学网络通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考