把业务知识塞进向量库就完事了?不是。检索找不到、找到了不敢信、信了发现过时——这三个问题的根因都相同:知识没有被建模。这一章讲清实体、事实、推断这三层为什么必须分开,混在一起会出什么事。
编辑部注 · 本章的「三层知识结构」是知识引擎部分的地基,后面三章(RAG、Embedding、混合检索)都建立在它之上。
先看一个典型的知识库事故:用户问「上个季度华东区的转化率是多少」。检索命中了一段文档,内容是「华东区转化率偏低,建议关注」。模型据此回答「华东区转化率偏低」。
问题出在哪?文档里那句话是半年前的某个人的判断,不是数据。而在向量库里,它和一条真实的季度数据看起来没有任何区别——都是「一段和华东区、转化率相关的文本」。
一、三层知识:为什么必须分
| 层 | 定义 | 来源 | 可验证性 | 用途 |
|---|---|---|---|---|
| 实体层 Entity |
可锚定的对象:人、部门、项目、指标、系统、文档 | 主数据、组织架构、元数据 | 高(有唯一标识) | 解决「说的是同一个东西吗」——如「华东」和「华东区」是不是一个地区 |
| 事实层 Fact |
可溯源的关系与数值:谁在什么时候是什么、某指标某期等于多少 | 数据库、系统记录、正式文档 | 最高(能回溯到源系统) | 回答「是什么」——这是唯一可以直接作为结论依据的层 |
| 推断层 Inference |
带依据链的衍生结论:分析、建议、判断、预测 | 人的分析、模型的生成 | 低(依赖依据链是否成立) | 提供视角与方向——但必须带依据链,且不能当作事实 |
三层混在一起的后果,就是开头那个例子:半年前的「判断」被当成了事实(Fact),而模型无从分辨——因为它们在库里长得一模一样。更隐蔽的是:一段推断(Inference)如果没带依据链(Evidence Chain),和一条事实在库里也毫无区别,却可能被当成结论直接给出。
二、实体层:解决「说的是同一个东西吗」
实体层(Entity)这一层最容易被跳过,但它是检索准确率的第一道关。三个具体的必要性:
第一,别名归一。 用户说「华东」「东区」「East China」,系统里只有 east。不做归一(Alias Normalization),检索会漏掉大部分相关内容。
第二,歧义消解。 「转化率」在不同业务线可能指不同指标。歧义消解(Disambiguation)要能根据上下文确定具体指向哪一个——这正是办事型 Agent 里NL2DSL的核心难点之一。
第三,层级关系。 实体层本质是主数据(Master Data)(组织架构、元数据等权威基础数据)。「华东区的销售额」是否包含「华东区下各城市的销售额」?这依赖实体层的层级定义。没有这层关系,聚合查询会算错。
| 实体类型 | 必须维护的字段 | 常见坑 |
|---|---|---|
| 地区 | id、标准名、别名列表、上级、层级深度 | 别名不全导致检索漏召回;层级不清导致聚合范围错 |
| 指标 | id、标准名、别名、口径定义、口径版本、生效时间 | 口径变更无记录——这是数据类争议的主要来源 |
| 部门/人 | id、名称、别名、所属、有效期 | 组织架构调整后旧关系失效但未标记,导致引用错误 |
| 项目/系统 | id、名称、别名、负责人、状态 | 同名不同项目(如多个「增长」项目)未区分 |
三、事实层:口径版本是灵魂
事实层的设计里,最重要的不是「存了什么值」,而是每个值都带着它被定义时的口径版本。
同一个「转化率」,2025 年 9 月前后可能是两个不同的计算方式(比如是否包含退款订单)。如果系统不记录口径版本,就会出现这种情况:
- 用户看到 2025Q2 转化率 4.2%、2025Q4 转化率 3.1%,得出「下滑明显」的结论;
- 实际上 Q4 换了更严格的口径(剔除退款),两个数字不可比。
这不是检索问题,是建模问题。 而它造成的后果比检索错更严重——用户会基于错误的对比做出决策。
from dataclasses import dataclass, field
from typing import Literal
from datetime import date
@dataclass
class Entity:
"""实体层:解决「说的是同一个东西吗」"""
id: str
canonical: str # 标准名
aliases: list[str] = field(default_factory=list)
kind: Literal["region", "metric", "org", "project", "system"] = "region"
parent: str | None = None # 上级实体,用于层级聚合
attrs: dict = field(default_factory=dict)
def match(self, mention: str) -> bool:
m = mention.strip().lower()
return m == self.canonical.lower() or any(m == a.lower() for a in self.aliases)
@dataclass
class Fact:
"""
事实层:可溯源的关系与数值。
三个字段缺一不可 —— 缺来源则不可信,缺口径版本则不可比,缺时间则不知时效。
"""
id: str
subject: str # 实体 id
predicate: str # 关系/指标名:指标=转化率
value: str # 值
period: str # 期:2025Q3
source: str # 来源系统:bi.sales.daily_agg
caliber_version: str # 口径版本:v3 ← 最容易被漏掉的一个
updated_at: str # 更新时间
dims: dict = field(default_factory=dict) # 附加维度:{"客群": "新客"}
def comparable_with(self, other: "Fact") -> bool:
"""两个事实能不能直接比较:同指标、同口径版本、同维度才可比"""
return (self.predicate == other.predicate
and self.caliber_version == other.caliber_version
and self.dims == other.dims)
def render(self) -> str:
dims = "·".join(f"{k}={v}" for k, v in self.dims.items())
return (f"{self.subject} {self.predicate} {self.value}"
f"({self.period}{'·' + dims if dims else ''})"
f" [来源 {self.source}|口径 {self.caliber_version}|更新 {self.updated_at}]")
@dataclass
class Inference:
"""推断层:必须带依据链,且永远不能替代事实"""
id: str
conclusion: str
based_on: list[str] # 依据:事实 id 列表 —— 没有它就不允许存在
producer: str # 谁产出的:人 / 模型 + 版本
produced_at: str
confidence: float = 0.7
def is_valid(self) -> bool:
return len(self.based_on) > 0 and 0.0 <= self.confidence <= 1.0
def render(self) -> str:
return (f"[推断·{self.confidence:.1f}] {self.conclusion}\n"
f" 依据:{', '.join(self.based_on) or '(缺失,不应展示)'}\n"
f" 产出:{self.producer} {self.produced_at}")
def resolve_conflict(a: Fact, b: Fact) -> tuple[Fact | None, str]:
"""
两层之间的冲突消解原则。
返回 (应采用的事实, 说明)。选择 None 表示无法裁决,应暴露冲突。
"""
if a.predicate != b.predicate:
return None, "两个事实不是同一指标,不可比较,不应裁决"
# 口径版本不同 → 不可比,必须显式暴露
if a.caliber_version != b.caliber_version:
return None, (f"口径版本不同({a.caliber_version} vs {b.caliber_version}),"
f"数值不可直接比较。请确认应使用哪个口径。")
# 口径相同、期相同、值不同 → 数据源冲突
if a.period == b.period and a.value != b.value:
return None, (f"同期同口径出现两个值({a.value} vs {b.value}),"
f"来源分别为 {a.source} / {b.source},需要人工确认以哪个为准。")
# 期不同 → 取更新的
newer, older = (a, b) if a.updated_at > b.updated_at else (b, a)
return newer, f"取更新的一条({newer.updated_at} > {older.updated_at}),旧值保留为历史"
def render_for_prompt(facts: list[Fact], inferences: list[Inference],
budget_chars: int = 5000) -> str:
"""
回喂给模型的形态:事实在前、推断在后,推断必须带依据,
并且整体带「来源/口径/更新」标签 —— 让模型自己判断该信谁。
"""
blocks = []
for f in facts:
blocks.append(f"[事实] {f.render()}")
for i in inferences:
if i.is_valid(): # 无效推断(无依据链)直接不展示
blocks.append(f"{i.render()}")
text, used = [], 0
for b in blocks:
if used + len(b) > budget_chars:
break
text.append(b)
used += len(b)
head = ("以下为可核验材料。【事实】可直接作为结论依据;"
"【推断】仅代表某一时期的分析观点,需判断其依据是否仍然成立。\n\n")
return head + "\n\n".join(text)resolve_conflict),就不会因为「新的看起来更可信」而误判。四、推断层:结论必须带着它的依据
实体层讲「同一性」、事实层讲「可溯源」,但真正最容易出错的是推断层——因为它长得最像「有用的答案」,却最不可信。这一节把它单独讲透。
推断是什么:从事实出发、经过推理得到的衍生结论——「华东转化率下滑,主要因为新客占比上升」就是一条推断,它本身不是一个可直接查询的数值,而是对几个事实的解释。
它和事实的本质区别只有一条:事实可以直接作为结论依据(因为它可回溯到源系统),推断必须带着依据链,且永远不能替代事实。这条区别决定了推断层的三条硬规则:
- 没有依据链的推断不允许入库。 一条裸结论(「华东转化率偏低,建议关注」)没有任何可验证的锚点,和事实放在一起,模型无从分辨它到底是数据还是某人的判断——这正是开头那个事故的根因。依据链就是把结论钉回它来自哪些事实的那根绳子。
- 推断必须带「产出者」和「产出时间」。 依据链只能说明「它从这些事实推出来」,不能说明「这推理现在还成立」。一个 Q3 的分析结论,到了 Q4 可能已经过时;一个模型生成的推断和一个资深分析师写的推断,可信度也完全不同。所以推断要记
producer(人/模型 + 版本)和produced_at。 - 依据链成立 ≠ 结论可信。 依据链只证明「推理有出处」,不证明「推理本身正确」——前提可能选错了、可能遗漏了关键因素。所以推断即使带全了依据,也只能作为视角呈现,不能作为结论呈现。
| 字段 | 作用 | 缺失的后果 |
|---|---|---|
based_on(依据链) | 钉回它来自哪些事实 | 变成无法验证的裸结论,与事实无法区分 |
producer(产出者) | 区分人/模型、及版本 | 无法判断可信度等级 |
produced_at(产出时间) | 判断是否过时 | 过期推断被当成现行结论 |
confidence(置信度) | 给下游一个「可不可信」的量化信号 | 下游只能全信或全不信 |
推断层的生命周期比事实短得多,所以要单独管「失效」。 事实会因口径变更而「不可比」,推断会因依据失效而「作废」:某条推断引用了 fact#1024(Q3 新客占比 42%),到了 Q4 这条事实更新了、或者口径变了,那条推断的结论就不再成立。所以推断层要有一个「复核时间」字段,并在每次口径版本变更时,批量扫描所有引用了该指标的推断,标记为「依据已变更,待复核」。没有这一步,系统会长期沉淀一批「看起来有依据」的历史结论——它们比没有依据更危险,因为读者会默认「带了依据就是可信的」。
一句话记住三层的关系:实体决定「说的是不是同一个东西」,事实决定「这个值能不能信、能不能比」,推断决定「这个结论有没有出处、还新不新」。 三者各管一段可信度,混在一起就全塌了。
五、常见误区与追问
这一章的结论很容易被点头同意,然后在工程里被忘掉。下面五条,都是「听起来对、做起来错」的典型。
5.1 误区:把「知识库」当成「文档库 + 向量化」
错在哪:以为把文档切块、嵌入、入库,知识就建好了。为什么自然:几乎所有 RAG 教程都从「切块—嵌入—检索」讲起,看起来那就是全部工作。但向量库只回答「哪段文字语义相近」,不回答「这条信息是不是事实、值不值得信」。 判据:随机抽 10 条检索结果,如果你不能在 10 秒内为每条说出「它是事实还是推断 / 来自哪个源系统 / 口径版本是什么」,那么你缺的是建模,不是更好的嵌入模型。一条不带来源的数值和一条带来源的数值,在上下文里长得完全一样,前者的错误要等到用户拿它做决策时才暴露。
5.2 误区:口径版本是数据团队的事,与检索和 Agent 无关
错在哪:把它当成数仓内部的元数据约定。为什么自然:口径确实由业务方定义,落库时也常常只写在表注释里。但对 Agent 来说,口径版本是判断「两个数能不能比」的唯一依据,必须一路带到上下文里。 判据(可操作):把两个口径不同的季度值同时放进上下文,看模型会不会主动做对比、甚至算出「下滑 26%」——会,就说明 口径版本(Caliber Version) 没有跟着知识块进上下文,或者提示词里没有写「口径不同的数值不可直接比较」这条硬规则。这类错误不需要模型犯错,只需要它足够勤快地做对比。
5.3 误区:有依据链,推断就可以当事实用
错在哪:把「可溯源」直接当成「可信」。为什么自然:依据链确实提升了可验证性,看起来比一条裸结论可靠得多。但它只证明「结论是从这些事实推出来的」,不证明「推理本身成立」。 判据两条:依据链是否覆盖了全部关键前提;前提是否仍然有效(未过期、未因口径变更而失效)。缺任何一条,这条推断就只能作为视角呈现,不能作为结论依据。这也是 可溯源(Provenance) 的边界——它保证你能回到源头,不保证源头支持你的结论。
5.4 误区:别名表一次建好就够
错在哪:把别名归一当成一次性数据清洗。为什么自然:别名看起来是静态词典,建表时穷举一遍就完了。但它会持续增长:新简称、新外文写法、错别字变体、组织改名。 判据可操作:每周从检索日志里捞出「出现过但没有映射到任何实体 id」的名词提及,做成未命中列表;累计超过 50 条就补一次表。同时给每条别名加「生效期」与「上下级」两个字段——改过的别名不要删除,只标记失效时间,否则历史问题会莫名其妙地对不上实体。
5.5 误区:三层分完就一劳永逸
错在哪:以为分好实体、事实、推断,建好表这件事就结束了。为什么自然:结构定下来之后,日常只剩往里写数据。但三层的边界会被时间侵蚀:一份 Q3 的分析报告,到 Q4 变成「过时推断」,新口径发布后又变成「口径不同的事实」。判据:给推断层加一个「复核时间」字段,并在每次口径版本变更时扫描所有引用了该指标的推断,批量标记为「依据已变更,待复核」。没有这一步,系统会长期沉淀一批「看起来有依据」的历史结论,而它们比没有依据更危险。
六、自测
七、小结
| 层 | 必须带什么 | 用途 | 冲突时 |
|---|---|---|---|
| 实体 | id、标准名、别名、层级 | 解决同一性与歧义 | 提供裁决的上位依据 |
| 事实 | 来源 + 口径版本 + 更新时间 | 唯一可直接作为依据的层 | 优先于推断 |
| 推断 | 依据链 + 产出者 + 时间 + 置信度 | 提供视角与方向 | 与事实冲突时让步并标注 |
其中三条硬规则:没有依据链的推断不允许入库;口径版本不同的数值不可直接比较;同级冲突不能裁决时必须暴露冲突。
知识建模的本质,是把「可信度」这个属性从人的判断变成数据结构。做成了,系统才可能「有据可查」。本刊编辑部
建模清楚了,下一章看检索的完整链路——以及那条链上六个环节各自能怎样把结果做错。
八、参考与延伸
这一章的方法论没有单一一手论文可读,所以下面按「先对齐概念、再看工业实现、最后读规范」排了三份材料。全站不做原文转载,这里只登记链接与「为什么值得读」。
先看概念(把「指标口径」想清楚)
- dbt 官方文档 · About MetricFlow —— 工业界怎么把「指标」定义成可版本化、可复用的声明式对象。读本章第三节卡住时来这儿最快:它把「同一个指标名 + 不同定义」当成一等公民处理,而不是把它塞进表注释。
再看实现(别人把校验放在哪一层)
- Eugene Yan · Patterns for Building LLM-based Systems & Products —— 从评测、RAG 到 guardrails 的工程模式总览。重点看「数据校验与输出约束」落在流水线的哪一段——本章的实体/事实/推断分层,正是这类校验的依据来源。
最后读规范(对齐一手定义)
- W3C · PROV-O: The PROV Ontology —— 「来源(provenance)」这件事的官方本体定义:Entity、Activity、Agent 三类核心对象及其派生关系。只需要看核心类与
prov:wasDerivedFrom这一个关系——本章说的「依据链」,在规范里就是它的传递闭包。
◇ 面试官会怎么问
共 6 条 · 其中 2 条高频 · 先自己答一遍,再展开对照
为什么要把知识拆成实体、事实、推断三层? 高频
实体层(人、项目、组织、产品)——可锚定的对象,有唯一标识,相对稳定,来自主数据,可信度最高。
事实层(参与了某项目、在某时间发生了某事)——可溯源的关系与事件,来自业务系统记录或结构化抽取,每条都能回指来源,可校验。
推断层(基于实体与事实得出的结论,如"该成员适配某类岗位")——由模型生成,必然带不确定性,必须标注依据链与置信度,且可被新事实推翻后重算。
分开的核心价值是:事实层可溯源,所以敢拿去支撑结论;推断层带依据,所以敢展示但知道要谨慎。如果把三者混成一个"知识库",你无法回答"这条结论到底站不站得住"。
追问链
- 推断层怎么保证质量?
- 实体对齐怎么做?
推断的质量怎么保证?
① 生成时强制给依据——要求推断必须附带它引用了哪些事实(事实 ID 列表),没有依据的推断直接丢弃。这既是质量控制,也是可解释性。
② 置信度与呈现策略——按依据数量、事实新鲜度、以及是否有冲突事实计算置信度;低置信度的推断不以结论性语气呈现,而是作为"可能的观察"提示,避免误导用户做出重要决策。
③ 抽样人审 + 回流——定期抽样人工核验,把误判案例回流入评测集与规则库。审的结果要反哺到生成策略(比如某类推断总是错,就要收紧触发条件)。
还要有失效机制:推断所依赖的事实发生变化时(人员转岗、项目结束),相关推断必须被标记为待重算而不是继续留着——"过期但看起来很确定"的推断比没有推断更危险。
实体对齐是什么?不做会怎样?
不做对齐有三类后果:
① 信息碎片化——同一个人被拆成多个实体,各自的记录都不完整,检索时召回不全,看起来像"知识缺失",实际是对齐没做。
② 事实冲突——同一个实体的属性在不同来源里被存成两份,取值不同且无法判断谁新谁旧。
③ 统计口径失真——"参与过几个项目"这类聚合会失真。
常用手段是用强标识优先(工号、唯一 ID)→ 弱标识兜底(姓名 + 部门 + 时间窗)→ 冲突时人工裁决,并且对齐结果要可回溯(记录合并了哪些来源),否则出错了无法拆开。
一条事实里如果缺了「口径版本」这个字段,最严重的后果是什么? 高频
例如 2025Q2 转化率 4.2%(含退款订单)与 2025Q4 转化率 3.1%(剔除退款),界面上看起来是「一年下滑 26%」,实际两个数字的分母定义不同,根本不可比。
缺来源则不可信,缺更新时间则不知时效,但只有缺口径版本会产生「看起来完全正常的错误结论」——系统不报错,用户据此排期、复盘、写进汇报。
判据:把两个数放进上下文,模型会不会主动算出下降幅度;会,就说明口径版本没有跟着事实进入上下文,或者提示词里没有写「口径不同的数值不可直接比较」。
追问链
- 历史数据已经按旧口径入库,怎么补口径版本?
- 口径变更时,引用过该指标的历史推断应该怎么处理?
两条知识冲突:一条是上个月生成的分析结论,一条是本周的事实记录。应该信哪个,为什么不能按时间裁决?
裁决顺序不能颠倒:先判层次(事实优先于推断),再判可比性(同指标、同口径版本、同维度),最后才轮到比时间。
按时间裁决会出错,因为推断的「新」只代表它被生成得晚,不代表它更可信——一条刚生成的分析完全可能建立在过时前提上。
正确处理:采用事实作为依据,并把该推断标记为「与最新事实不符,可能已过时」;如果它仍有视角价值,可以呈现,但必须标明它只是推断。
追问链
- 同一指标、同一口径、同一期出现两个不同的值,属于哪一类冲突?怎么处理?
- 「可溯源」是否足以让一条推断当作事实使用?
业务方说「华东区销售额」时,系统至少要能回答哪两个问题,聚合查询才不会算错?
二是层级范围:「华东区销售额」是否包含其下各城市的销售额,这由实体层的上下级定义决定。
缺别名归一会漏召回;缺层级定义会让聚合范围直接算错——而这类错误很难从结果里看出来,因为数字看起来很正常。
工程做法:实体表同时维护「标准名、别名列表、上级、层级深度」,并给每条别名加生效期;改别名不要删除,只标记失效时间。
追问链
- 别名表为什么不能只建一次?你用什么信号判断该补表了?
- 「转化率」在不同业务线含义不同,这属于实体层的哪类问题?
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。