Agent 工程学习站 做棵大树 出品 · beatree.cn 免费 · 无需登录 · 进度存本机
IV 版 · 知识引擎 第 01 章 Entity, Fact, Inference
IV · 知识引擎 01 / 22 进阶

知识建模:实体、事实、推断

Entity, Fact, Inference
预计阅读 30 分钟
难度 进阶
关键词 实体抽取 · 事实层
本机状态 未读

把业务知识塞进向量库就完事了?不是。检索找不到、找到了不敢信、信了发现过时——这三个问题的根因都相同:知识没有被建模。这一章讲清实体、事实、推断这三层为什么必须分开,混在一起会出什么事。

编辑部注 · 本章的「三层知识结构」是知识引擎部分的地基,后面三章(RAG、Embedding、混合检索)都建立在它之上。

先看一个典型的知识库事故:用户问「上个季度华东区的转化率是多少」。检索命中了一段文档,内容是「华东区转化率偏低,建议关注」。模型据此回答「华东区转化率偏低」。

问题出在哪?文档里那句话是半年前的某个人的判断,不是数据。而在向量库里,它和一条真实的季度数据看起来没有任何区别——都是「一段和华东区、转化率相关的文本」。

一、三层知识:为什么必须分

知识的三个层次:各自的来源、特征与用途
定义来源可验证性用途
实体层
Entity
可锚定的对象:人、部门、项目、指标、系统、文档 主数据、组织架构、元数据 高(有唯一标识) 解决「说的是同一个东西吗」——如「华东」和「华东区」是不是一个地区
事实层
Fact
可溯源的关系与数值:谁在什么时候是什么、某指标某期等于多少 数据库、系统记录、正式文档 最高(能回溯到源系统) 回答「是什么」——这是唯一可以直接作为结论依据的层
推断层
Inference
带依据链的衍生结论:分析、建议、判断、预测 人的分析、模型的生成 低(依赖依据链是否成立) 提供视角与方向——但必须带依据链,且不能当作事实

三层混在一起的后果,就是开头那个例子:半年前的「判断」被当成了「事实」,而模型无从分辨——因为它们在库里长得一模一样。

三层知识结构与依据链 ③ 推断层 带依据链的衍生结论 「Q3 华东转化率下滑主要因为新客占比上升」 依据链:fact#1024(Q3 新客占比 42%)+ fact#1025(新客转化率 1.1% vs 老客 4.3%) 规则:没有依据链的推断不允许入库;带推断标记展示,用户可见其证据 ② 事实层 可溯源的关系与数值(唯一可直接作为依据的层) fact#1024 指标=新客占比 维度=华东 期=2025Q3 值=42%    来源:bi.sales.daily_agg 口径版本:v3 更新时间:2025-10-08 fact#1025 指标=转化率 维度=华东·新客 期=2025Q3 值=1.1% 规则:每条事实必须带「来源 + 口径版本 + 更新时间」,三者缺一不可 ① 实体层 可锚定的对象与它们的同一性 ent:region/east 别名:华东、华东区、East China、东区 上级:ent:region/china ent:metric/conversion_rate 别名:转化率、CVR、成交转化率 口径表:v3(2025-09 变更) 作用:把用户的说法映射到系统里的唯一对象——这是检索准确的第一步 冲突消解:不同层产生分歧时,信谁 事实 vs 推断冲突 → 信事实,并把推断标记为「与最新事实不符,可能已过时」 新事实 vs 旧事实冲突 → 信新的,但旧事实保留并标记失效时间(可回溯口径变更历史)
图 1 三层结构自下而上支撑:实体提供「同一性」、事实提供「可溯源依据」、推断提供「视角与方向」。关键规则有两条:事实必须带来源 + 口径版本 + 更新时间;推断必须有依据链、必须带标记、且在与事实冲突时让步。

二、实体层:解决「说的是同一个东西吗」

这一层最容易被跳过,但它是检索准确率的第一道关。三个具体的必要性:

第一,别名归一。 用户说「华东」「东区」「East China」,系统里只有 east。不做归一,检索会漏掉大部分相关内容。

第二,歧义消解。 「转化率」在不同业务线可能指不同指标。实体层要能根据上下文(哪个业务线、哪个页面)确定具体指向哪一个——这正是办事型 Agent 里 NL2DSL 的核心难点之一。

第三,层级关系。 「华东区的销售额」是否包含「华东区下各城市的销售额」?这依赖实体层的层级定义。没有这层关系,聚合查询会算错。

实体类型必须维护的字段常见坑
地区id、标准名、别名列表、上级、层级深度别名不全导致检索漏召回;层级不清导致聚合范围错
指标id、标准名、别名、口径定义、口径版本、生效时间口径变更无记录——这是数据类争议的主要来源
部门/人id、名称、别名、所属、有效期组织架构调整后旧关系失效但未标记,导致引用错误
项目/系统id、名称、别名、负责人、状态同名不同项目(如多个「增长」项目)未区分

三、事实层:口径版本是灵魂

事实层的设计里,最重要的不是「存了什么值」,而是每个值都带着它被定义时的口径版本

同一个「转化率」,2025 年 9 月前后可能是两个不同的计算方式(比如是否包含退款订单)。如果系统不记录口径版本,就会出现这种情况:

  • 用户看到 2025Q2 转化率 4.2%、2025Q4 转化率 3.1%,得出「下滑明显」的结论;
  • 实际上 Q4 换了更严格的口径(剔除退款),两个数字不可比。

这不是检索问题,是建模问题。 而它造成的后果比检索错更严重——用户会基于错误的对比做出决策。

knowledge_model.pypython
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)

四、自测

本章自测第 1、3 题是核心考点
Q1为什么知识要分成实体、事实、推断三层?
C。这是知识建模的核心动机。开头那个案例就是典型:半年前的「判断」被当作「事实」使用。三层的本质区别是可信度与可验证性等级不同,而不是存储方式不同。分开之后,系统才能做到「用事实作结论、用推断提供视角、并且始终标注哪个是哪个」。
Q2事实层的三个必备字段是什么?
B。缺来源则不可信,缺口径版本则不可比,缺更新时间则不知时效。其中「口径版本」最容易被漏掉,而它造成的后果最严重——用户会拿两个不同口径的数字做对比,得出错误结论,而这在界面上看起来完全正常。
Q3检索到一条事实和一条推断,两者结论相反,应该怎么处理?
D。这是三层结构存在的直接价值:当层次不同的两条知识冲突时,层级本身就提供了裁决依据——不需要额外判断。C 看起来合理但会出错:一条刚生成的推断可能比事实「更新」,但可信度低得多。

五、小结

必须带什么用途冲突时
实体id、标准名、别名、层级解决同一性与歧义提供裁决的上位依据
事实来源 + 口径版本 + 更新时间唯一可直接作为依据的层优先于推断
推断依据链 + 产出者 + 时间 + 置信度提供视角与方向与事实冲突时让步并标注

其中三条硬规则:没有依据链的推断不允许入库口径版本不同的数值不可直接比较同级冲突不能裁决时必须暴露冲突

知识建模的本质,是把「可信度」这个属性从人的判断变成数据结构。做成了,系统才可能「有据可查」。本刊编辑部

建模清楚了,下一章看检索的完整链路——以及那条链上六个环节各自能怎样把结果做错。

面试官会怎么问

共 3 条 · 其中 1 条高频 · 先自己答一遍,再展开对照

为什么要把知识拆成实体、事实、推断三层? 高频
因为三层的可信度与生命周期完全不同,混在一起就无法治理:
实体层(人、项目、组织、产品)——可锚定的对象,有唯一标识,相对稳定,来自主数据,可信度最高。
事实层(参与了某项目、在某时间发生了某事)——可溯源的关系与事件,来自业务系统记录或结构化抽取,每条都能回指来源,可校验
推断层(基于实体与事实得出的结论,如"该成员适配某类岗位")——由模型生成,必然带不确定性,必须标注依据链与置信度,且可被新事实推翻后重算。
分开的核心价值是:事实层可溯源,所以敢拿去支撑结论;推断层带依据,所以敢展示但知道要谨慎。如果把三者混成一个"知识库",你无法回答"这条结论到底站不站得住"。

追问链

  1. 推断层怎么保证质量?
  2. 实体对齐怎么做?
推断的质量怎么保证?
三道闸,缺一不可:
① 生成时强制给依据——要求推断必须附带它引用了哪些事实(事实 ID 列表),没有依据的推断直接丢弃。这既是质量控制,也是可解释性。
② 置信度与呈现策略——按依据数量、事实新鲜度、以及是否有冲突事实计算置信度;低置信度的推断不以结论性语气呈现,而是作为"可能的观察"提示,避免误导用户做出重要决策。
③ 抽样人审 + 回流——定期抽样人工核验,把误判案例回流入评测集与规则库。审的结果要反哺到生成策略(比如某类推断总是错,就要收紧触发条件)。
还要有失效机制:推断所依赖的事实发生变化时(人员转岗、项目结束),相关推断必须被标记为待重算而不是继续留着——"过期但看起来很确定"的推断比没有推断更危险。
加分点:"过期推断比没有推断更危险"是一句很有分量的话,能体现对知识时效性的理解。
实体对齐是什么?不做会怎样?
实体对齐是把来自不同数据源的"同一个对象"识别并合并成唯一实体——比如 HR 系统里的"张三(工号 001)"、项目系统里的"zhangsan"、邮件签名里的"三哥",需要判定为同一个人。
不做对齐有三类后果:
① 信息碎片化——同一个人被拆成多个实体,各自的记录都不完整,检索时召回不全,看起来像"知识缺失",实际是对齐没做。
② 事实冲突——同一个实体的属性在不同来源里被存成两份,取值不同且无法判断谁新谁旧。
③ 统计口径失真——"参与过几个项目"这类聚合会失真。
常用手段是用强标识优先(工号、唯一 ID)→ 弱标识兜底(姓名 + 部门 + 时间窗)→ 冲突时人工裁决,并且对齐结果要可回溯(记录合并了哪些来源),否则出错了无法拆开。
答这类题的通用结构

结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。

本章收尾 · Wrap up
掌握度自评
点一下给自己打分;低于 3 分建议加入复习队列
个人笔记 · Notes