把业务知识塞进向量库就完事了?不是。检索找不到、找到了不敢信、信了发现过时——这三个问题的根因都相同:知识没有被建模。这一章讲清实体、事实、推断这三层为什么必须分开,混在一起会出什么事。
编辑部注 · 本章的「三层知识结构」是知识引擎部分的地基,后面三章(RAG、Embedding、混合检索)都建立在它之上。
先看一个典型的知识库事故:用户问「上个季度华东区的转化率是多少」。检索命中了一段文档,内容是「华东区转化率偏低,建议关注」。模型据此回答「华东区转化率偏低」。
问题出在哪?文档里那句话是半年前的某个人的判断,不是数据。而在向量库里,它和一条真实的季度数据看起来没有任何区别——都是「一段和华东区、转化率相关的文本」。
一、三层知识:为什么必须分
| 层 | 定义 | 来源 | 可验证性 | 用途 |
|---|---|---|---|---|
| 实体层 Entity |
可锚定的对象:人、部门、项目、指标、系统、文档 | 主数据、组织架构、元数据 | 高(有唯一标识) | 解决「说的是同一个东西吗」——如「华东」和「华东区」是不是一个地区 |
| 事实层 Fact |
可溯源的关系与数值:谁在什么时候是什么、某指标某期等于多少 | 数据库、系统记录、正式文档 | 最高(能回溯到源系统) | 回答「是什么」——这是唯一可以直接作为结论依据的层 |
| 推断层 Inference |
带依据链的衍生结论:分析、建议、判断、预测 | 人的分析、模型的生成 | 低(依赖依据链是否成立) | 提供视角与方向——但必须带依据链,且不能当作事实 |
三层混在一起的后果,就是开头那个例子:半年前的「判断」被当成了「事实」,而模型无从分辨——因为它们在库里长得一模一样。
二、实体层:解决「说的是同一个东西吗」
这一层最容易被跳过,但它是检索准确率的第一道关。三个具体的必要性:
第一,别名归一。 用户说「华东」「东区」「East China」,系统里只有 east。不做归一,检索会漏掉大部分相关内容。
第二,歧义消解。 「转化率」在不同业务线可能指不同指标。实体层要能根据上下文(哪个业务线、哪个页面)确定具体指向哪一个——这正是办事型 Agent 里 NL2DSL 的核心难点之一。
第三,层级关系。 「华东区的销售额」是否包含「华东区下各城市的销售额」?这依赖实体层的层级定义。没有这层关系,聚合查询会算错。
| 实体类型 | 必须维护的字段 | 常见坑 |
|---|---|---|
| 地区 | 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)四、自测
五、小结
| 层 | 必须带什么 | 用途 | 冲突时 |
|---|---|---|---|
| 实体 | id、标准名、别名、层级 | 解决同一性与歧义 | 提供裁决的上位依据 |
| 事实 | 来源 + 口径版本 + 更新时间 | 唯一可直接作为依据的层 | 优先于推断 |
| 推断 | 依据链 + 产出者 + 时间 + 置信度 | 提供视角与方向 | 与事实冲突时让步并标注 |
其中三条硬规则:没有依据链的推断不允许入库;口径版本不同的数值不可直接比较;同级冲突不能裁决时必须暴露冲突。
知识建模的本质,是把「可信度」这个属性从人的判断变成数据结构。做成了,系统才可能「有据可查」。本刊编辑部
建模清楚了,下一章看检索的完整链路——以及那条链上六个环节各自能怎样把结果做错。
◇ 面试官会怎么问
共 3 条 · 其中 1 条高频 · 先自己答一遍,再展开对照
为什么要把知识拆成实体、事实、推断三层? 高频
实体层(人、项目、组织、产品)——可锚定的对象,有唯一标识,相对稳定,来自主数据,可信度最高。
事实层(参与了某项目、在某时间发生了某事)——可溯源的关系与事件,来自业务系统记录或结构化抽取,每条都能回指来源,可校验。
推断层(基于实体与事实得出的结论,如"该成员适配某类岗位")——由模型生成,必然带不确定性,必须标注依据链与置信度,且可被新事实推翻后重算。
分开的核心价值是:事实层可溯源,所以敢拿去支撑结论;推断层带依据,所以敢展示但知道要谨慎。如果把三者混成一个"知识库",你无法回答"这条结论到底站不站得住"。
追问链
- 推断层怎么保证质量?
- 实体对齐怎么做?
推断的质量怎么保证?
① 生成时强制给依据——要求推断必须附带它引用了哪些事实(事实 ID 列表),没有依据的推断直接丢弃。这既是质量控制,也是可解释性。
② 置信度与呈现策略——按依据数量、事实新鲜度、以及是否有冲突事实计算置信度;低置信度的推断不以结论性语气呈现,而是作为"可能的观察"提示,避免误导用户做出重要决策。
③ 抽样人审 + 回流——定期抽样人工核验,把误判案例回流入评测集与规则库。审的结果要反哺到生成策略(比如某类推断总是错,就要收紧触发条件)。
还要有失效机制:推断所依赖的事实发生变化时(人员转岗、项目结束),相关推断必须被标记为待重算而不是继续留着——"过期但看起来很确定"的推断比没有推断更危险。
实体对齐是什么?不做会怎样?
不做对齐有三类后果:
① 信息碎片化——同一个人被拆成多个实体,各自的记录都不完整,检索时召回不全,看起来像"知识缺失",实际是对齐没做。
② 事实冲突——同一个实体的属性在不同来源里被存成两份,取值不同且无法判断谁新谁旧。
③ 统计口径失真——"参与过几个项目"这类聚合会失真。
常用手段是用强标识优先(工号、唯一 ID)→ 弱标识兜底(姓名 + 部门 + 时间窗)→ 冲突时人工裁决,并且对齐结果要可回溯(记录合并了哪些来源),否则出错了无法拆开。
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。