第 1 篇:「Glass-Box 的骨架」——全景架构与六层数据流水线

第 1 篇:「Glass-Box 的骨架」------全景架构与六层数据流水线

阅读本文你将了解: Semantica 官方宣传的 27 个模块如何组织成六层数据流水线;一条"文档进、图谱出"的主链路在源码里长什么样(GraphBuilder 逐段拆解);双时态事实模型为何选择"包装器而非替换"的兼容设计;以及用 1.9GB 依赖面换来的"确定性"到底值不值。本篇是全系列的地基:先建立六层心智模型,后面 02-06 篇逐层打穿。

关键源码事实: 版本锚点 main @ 30fc644(v0.6.8)。所有 file:line 均为完整仓库相对路径,可在本地镜像 ..\semantica\ 直接定位。


1. 从一句官网话术说起

官网首页的口号是 "The Glass-Box Alternative to Black-Box Intelligence"------向量库回答"什么是相似的",Semantica 回答"什么是关联的、为什么、如何关联"。这句话在架构上的兑现方式,是把"知识"拆成三种一等公民

  1. 事实(Facts)------实体、关系、带双时态有效期的事实,存于知识图谱;
  2. 决策(Decisions)------AI 或人做出的每个决策本身也是图节点,带完整因果链;
  3. 溯源(Provenance)------每条事实到原始来源的 W3C PROV-O 血缘,带 SHA-256 哈希链审计。

这三种公民分别由 semantica.kgsemantica.contextsemantica.provenance 三个模块承载。理解了"三类一等公民",六层流水线的每一层就都有了明确的归宿:上层数据经过加工变成事实,事实之上沉淀决策,一切动作留下溯源。

2. 官方六层 ↔ 源码 29 个目录:一张对照表

官方文档 /modules 页把 27 个模块组织为六层。我们逐层对照源码目录(行数为 wc -l 实测),这张家地图是全系列的索引基座:

官方层 官方模块 源码目录(行数) 一句话职责
Input Layer ingest / parse / split / normalize semantica/ingest/(20,890) parse/(7,801) split/(4,694) normalize/(6,059) 把世界上的原始数据搬进来、洗干净、切好块
Core Processing semantic_extract / kg / ontology / reasoning semantic_extract/(11,614) kg/(13,343) ontology/(9,298) reasoning/(4,035) 抽取实体关系、建图、建模式、推新知
Storage embeddings / vector_store / graph_store / triplet_store embeddings/(3,054) vector_store/(12,067) graph_store/(7,829) triplet_store/(6,313) 向量、属性图、RDF 三元组三类持久化
Quality Assurance deduplication / conflicts deduplication/(4,634) conflicts/(4,951) 实体去重与事实冲突仲裁
Context & Memory context / provenance / change_management context/(21,541) provenance/(3,808) change_management/(2,115) Agent 记忆、决策留痕、PROV-O 溯源、版本管理
Output & Orchestration export / visualization / pipeline / explorer export/(11,602) visualization/(6,461) pipeline/(3,965) explorer/(9,691) 导出 12+ 格式、可视化、流水线 DSL、Web 工作台
Utilities(官方不单独分层) llms / mcp_server / seed / evals / core / utils llms/(1,571) mcp_server/(983) seed/(1,151) evals/(574) core/(3,604) utils/(5,172) cli.py(4,848) provider 门面、MCP 服务、编排器、CLI

这张表里第一个值得注意的信号是体量分布:Input 层合计约 3.9 万行、Storage 层约 2.9 万行------"把数据搬进来存下去"占了全仓三分之一;而宣传中被反复强调的 reasoning 只有 4,035 行。体量不会说谎:这个项目的工程重心在数据管道与存储抽象,推理引擎是"亮点功能"而非"主体结构"。

3. 六层流水线架构图

这张图在回答什么问题 :Semantica 的 29 个源码目录之间到底谁调用谁------它给出的是官方文档"六层分组"背后的真实数据流向 。关键节点有三处:其一,kg(GraphBuilder)是全图的枢纽,L1-L4 的加工成果最终都汇入它产出的"图谱字典";其二,context 并不直接依赖 L2 的抽取模块,而是通过 build_from_conversations 等入口自行组织,这是很多人误读的地方------决策智能是"平行于 KG 管道"的第二主干,而非管道的下游;其三,L6 的 pipeline DSL 是对 L1/L2 的编排外壳(虚线),不是数据流的必经之路------每个模块都可以独立 import 使用(官方页首句 "Every module works independently" 的源码依据见第 7 节的惰性导出机制)。三处 note 标出了后文将逐行拆解的两个枢纽类。

4. 主链路解剖(一):GraphBuilder.build() 的三态输入

全链路的枢纽是 GraphBuilder.build()semantica/kg/graph_builder.py:422)。它接受"一态以上"的输入:原始文本字符串、预抽取的 Entity/Relation 对象、或者官方 quickstart 推荐的 {"entities": [...], "relationships": [...]} 字典。真实签名如下(节选,完整 docstring 见源码):

python 复制代码
# semantica/kg/graph_builder.py:422
def build(
    self,
    sources: Union[List[Any], Any],
    second_arg: Optional[Any] = None,
    pipeline_id: Optional[str] = None,
    **options,
) -> Dict[str, Any]:
    # options 关键项(源码 docstring 摘录):
    #   extract (bool): 传入原始文本时是否运行抽取,默认 True
    #   extract_triplets (bool): 文本抽取时是否抽三元组,默认 True
    #   ner_method (str): "ml" / "pattern" / "llm",默认 "ml"
    #   relation_method (str): 默认 "pattern"
    #   entity_resolver: 覆盖 builder 配置的 EntityResolver 实例
    # Returns: {"entities": [...], "relationships": [...], "metadata": {...}}

docstring 里有一句对生产环境极其关键的承诺:"Raw-text extraction uses local extractors by default and needs no provider or API key"semantica/kg/graph_builder.py:455 附近)------默认路径完全本地、确定性、零 API 成本。这正是它"确定性基础设施"卖点在主链路上的落点。分发的实现是 _process_item()semantica/kg/graph_builder.py:144),用鸭子类型探测逐一分派:

python 复制代码
# semantica/kg/graph_builder.py:144-186(节选)
def _process_item(self, item, all_entities, all_relationships, **options):
    if isinstance(item, str):
        self._extract_from_text(item, all_entities, all_relationships, **options)
        return
    if hasattr(item, "text") and (hasattr(item, "label") or hasattr(item, "type")):
        # Entity 对象 → dict
        entity_dict = {
            "id": getattr(item, "id", getattr(item, "entity_id", item.text)),
            "name": item.text,
            "type": getattr(item, "label", getattr(item, "type", "UNKNOWN")),
            "confidence": getattr(item, "confidence", 1.0),
        }
        all_entities.append(entity_dict)
    elif hasattr(item, "subject") and hasattr(item, "predicate") and hasattr(item, "object"):
        # Relation 对象 → dict
        ...
    elif isinstance(item, dict):
        if "source_id" in item and "source" not in item:
            item["source"] = item["source_id"]
        if "subject" in item and "source" not in item:
            item["source"] = item["subject"]
        ...

三个细节值得圈点。其一 ,dict 分支的字段归一化是"宽容输入"设计:source_id→sourcesubject→sourceobject→target,多套命名习惯都能喂进来------这与上游 semantic_extract 返回 Relation(subject=..., predicate=..., object=...) 对象、官方文档示例又用 source/target/type 字典的两套口径直接相关,_process_item 实际上承担了"API 方言翻译器"的角色。其二 ,文本分支 _extract_from_text()semantica/kg/graph_builder.py:352)通过 _get_extractor()semantica/kg/graph_builder.py:243)按 (kind, method) 缓存 extractor 实例------这是为了避免 NER 对象反复加载 spaCy 模型的经典优化。其三build() 返回的是纯字典 而非图对象:{"entities", "relationships", "metadata"}。持久化是可选的下一步(传 graph_store=store 才落库),这个"先内存字典、后可选落库"的两段式,是理解后面存储层设计(03 篇)的前提。

5. 主链路解剖(二):实体合并后的端点重写

GraphBuilder 里最能体现"图谱工程成熟度"的是 _remap_relationship_endpoints()semantica/kg/graph_builder.py:265)。问题背景:实体消歧会把 "Apple"、"Apple Inc."、"AAPL" 合并成一个规范实体,合并前收集的关系边仍然指向旧实体 ID------不重写就是一张悬空边遍布的坏图。

python 复制代码
# semantica/kg/graph_builder.py:265-308(节选)
def _remap_relationship_endpoints(self, entities, relationships) -> int:
    endpoint_map: Dict[Any, Any] = {}
    for entity in entities:
        if not isinstance(entity, dict):
            continue
        canonical_id = entity.get("id")
        if canonical_id is None:
            canonical_id = entity.get("entity_id")
        if canonical_id is None:
            continue
        # 规范 ID 自映射,merged_from 里的每个旧 ID 映射到规范 ID
        try:
            endpoint_map[canonical_id] = canonical_id
        except TypeError:
            continue
        merged_from = entity.get("merged_from") or []
        if isinstance(merged_from, (list, tuple, set)):
            for source_id in merged_from:
                if source_id is not None:
                    try:
                        endpoint_map[source_id] = canonical_id
                    except TypeError:
                        continue
    ...

注意那两个 try: ... except TypeError: continue------对不可哈希的 ID(比如调用方塞了个 list 当 ID)跳过而不是让整图构建失败 ,注释原话是 "Invalid/unhashable IDs are left for graph validation to report rather than making graph construction fail here"(semantica/kg/graph_builder.py:289)。这种"宽进严出"的防御姿态贯穿整个仓库:上层输入尽量容错,把严格性留给专门的校验模块(GraphValidator)。代价是调试时坏 ID 会静默丢失、直到校验阶段才浮出------对大海捞针式的数据质量排障并不友好,算是一个可辩护但有成本的取舍。

6. 主链路解剖(三):双时态事实的"包装器"设计

semantica/kg/temporal_model.py:28 定义了全项目时间智能的原子类型 BiTemporalFact。先看它携带的四个时间字段,这是读懂 04/05 篇时间相关逻辑的钥匙:

python 复制代码
# semantica/kg/temporal_model.py:17-64(节选)
class TemporalBound(Enum):
    """Sentinel bounds for open-ended temporal intervals."""
    OPEN = "OPEN"

@dataclass
class BiTemporalFact:
    valid_from: Optional[datetime]                       # 有效时间起点(事实在现实中成立)
    valid_until: Optional[datetime | TemporalBound]      # 有效时间终点(OPEN=仍然成立)
    recorded_at: datetime = field(default_factory=_default_recorded_at)   # 事务时间(我们何时得知)
    superseded_at: datetime | TemporalBound = TemporalBound.OPEN           # 被修订时间

    @classmethod
    def from_relationship(cls, relationship: Dict[str, Any]) -> "BiTemporalFact":
        valid_until_raw = relationship.get("valid_until", TemporalBound.OPEN)
        if valid_until_raw is None:
            valid_until_raw = TemporalBound.OPEN
        ...

设计文档里藏着一个重要的架构决策,类 docstring 写得坦白:"Facts continue to live as plain relationship dicts in the graph. This wrapper is only used internally for normalization"semantica/kg/temporal_model.py:31-35)。也就是说,图里的事实永远是普通 dict(兼容旧版调用方),BiTemporalFact 只在归一化时临时把 dict 包装成带类型的数据类,序列化时再拆回 dict 字段(to_relationship_fields()semantica/kg/temporal_model.py:61)。这个选择的收益是零迁移成本的向后兼容------代价是类型安全只在归一化瞬间成立,图里游荡的 dict 随时可能缺字段。与之配套,查询侧的 is_active()semantica/context/context_graph.py:430)把 at_time 和存储值统一归一到 tz-naive UTC 再比较,实测验证过:valid_from 在未来的节点在历史时点快照中被正确过滤(见 04 篇实测数据)。

一个实测发现的 API 陷阱要在此预警:ContextGraph.add_node(timestamp=...)semantica/context/context_graph.py:1124)里的 timestamp 只是普通属性,不参与时态判断 ;真正生效的是 valid_from/valid_untiladd_node docstring 有注明,但参数名极具误导性)。我们在隔离环境实测中第一版测试脚本就栽在这里------timestamp 传入历史时间后 state_at() 仍返回了"未来"节点。

7. 模块装配机制:为什么"每个模块可独立使用"是真的

官方反复强调 "Every module works independently: use only what you need"。源码依据是全仓库统一的**惰性导出(lazy export)**模式。包级 semantica/__init__.py:200 定义了模块级 __getattr__(PEP 562),子包 semantica/ingest/__init__.py:166 则维护一张显式导出表:

python 复制代码
# semantica/ingest/__init__.py:166-290(节选)
_LAZY_EXPORTS: Dict[str, Tuple[str, str]] = {
    "FileIngestor": (".file_ingestor", "FileIngestor"),
    "WebIngestor": (".web_ingestor", "WebIngestor"),
    ...
}

def __getattr__(name: str) -> Any:
    if name not in _LAZY_EXPORTS:
        raise AttributeError(...)
    module_name, attr_name = _LAZY_EXPORTS[name]
    ...  # 首次访问才 import 对应模块

这个模式在三处产生了实际影响。第一pip install semantica 的重型依赖(torch/transformers/spacy)虽然声明在核心依赖里装了 1.9GB(实测),但 import semantica.ingest 这样的轻量使用并不触发 torch 加载------安装重、运行可以很轻,两者被解耦了。第二 ,官方文档承认有 6 个 ingestor(DuckDB/Elastic/GDrive/HuggingFace/Mongo/Pandas)"ship but aren't re-exported"------就是导出表没登记,必须深路径导入,这是惰性模式的维护成本实证。第三 ,MCP server 专门加了 SEMANTICA_DISABLE_PROGRESS=1 环境变量防进度条污染 JSON-RPC stdout(semantica/mcp_server/__init__.py,注释记录了真实故障),说明"打印噪音"已经真实咬过他们一口。

8. 生产视角:1.9GB 的入场费与 doctor 深探针

我们在 Windows + Python 3.13.12 隔离环境实测了完整安装(PyPI 0.6.8,清华镜像):36 分 36 秒、site-packages 1,900 MB、200+ 包。除了宣传中提到的 torch/transformers/spacy,实测还拖进了 opencv-python、librosa、soundfile、onnxruntime------后三者来自 fastembed 的传递依赖,对只需要"文本+图"的团队纯属被动负重。

安装后的第一道防线是 semantica doctorsemantica/cli.py:791 起,约 150 行)。它检查 Python/rich/图存储连通性/向量库可 import 性/LLM key/配置与日志目录,亮点是一个 --deep-embeddings 深探针:真正实例化 TextEmbedder 并 embed 一句探针文本,专治"import 成功但模型下载失败"这类环境病(CLI 注释引用 issue #994)。这与第 10 节要讲的"静默降级"直接相关------doctor 是官方给出的对抗手段,但需要你主动跑。

部署形态上,仓库提供了多阶段 Dockerfile(node:26-alpine 构建 Explorer 前端 → python:3.13-slim 运行时,digest pin + --require-hashes 供应链锁定 + 非 root + HEALTHCHECK),以及覆盖 helm/azure/gcp/fly/railway/render 七平台的 deploy/ 模板。docker-compose.yml 会附带启动一个 FalkorDB 容器------但截至 v0.6.8,后端 semantica/explorer/app.py:47-51 的注释明说 GraphSession 不连接外部图库,compose 里的 FalkorDB 目前是摆设,别被部署清单误导。

9. 进阶视角:两条技术路线的分野

把 Semantica 放回"LLM 时代知识图谱"的坐标系里,它代表了与 Microsoft GraphRAG / LightRAG 相反的一条路线。三条关键差异,每条都能锚定到源码:

维度 Semantica(确定性路线) GraphRAG/LightRAG(LLM 摘要路线)
图怎么来 规则/spaCy/本地 ML 抽取,默认零 API key(semantica/kg/graph_builder.py:455 docstring 承诺;semantica/semantic_extract/methods.py:755 spaCy 路径) LLM 逐块摘要+抽取,token 成本与语料线性相关
一致性 确定性函数,同输入同输出;冲突检测是精确比较(semantica/conflicts/conflict_detector.py LLM 输出有方差,同一语料两次构建图不同
审计能力 PROV-O 哈希链(semantica/provenance/manager.py:1450 实测 verify_chain valid=True) 摘要文本无法回溯到结构化血缘

代价同样锚定得住:确定性抽取的召回上限受制于规则与本地小模型------官方 quickstart 自己都要靠 NERExtractor(method="llm") 分支补精度;而 semantic_extract/methods.py 里 spaCy 路径的 confidence 恒为 1.0(无置信度可用,hasattr 检查形同虚设),下游如果按置信度过滤会全部放行。选型判据因此可以一句话说清:监管审计优先 → Semantica;问答质量优先 → GraphRAG 路线;两者都要 → 用 Semantica 做底座、把 LLM 抽取接到 ner_method="llm" 的插槽上。

10. 小结与下一篇

本篇建立了三层地基:六层心智模型 (官方分组 = 真实数据流)、枢纽类地图 (GraphBuilder 汇聚 L1-L4,ContextGraph 平行第二主干)、装配机制 (PEP 562 惰性导出让"模块独立使用"成为源码事实而非口号)。同时埋下两个贯穿全系列的伏笔:timestamp 参数名陷阱与 embeddings 静默降级------它们分别会在 04 篇(实测数据)和 03 篇(semantica/embeddings/text_embedder.py 源码)展开。

下一篇进入 Input 层的内部:31 个文件 2.1 万行的 ingest/ 里,企业连接器是真材实料还是空壳?官方宣传的 13 种分块策略中,为什么我们审读判定"5 种宣传里只有 2 个半是真的"?------semantica/split/methods.py:1345 的一行存根代码将给出铁证。


关键源码事实表

# 事实 锚点
1 build() 主入口接受文本/对象/dict 三态输入,默认本地抽取零 API key semantica/kg/graph_builder.py:422
2 鸭子类型分派:str→抽取、Entity 对象→dict、Relation 对象→dict、dict→字段归一 semantica/kg/graph_builder.py:144
3 extractor 按 (kind, method) 缓存,避免重复加载 spaCy 模型 semantica/kg/graph_builder.py:243
4 实体合并后按 merged_from 重写关系端点,防悬空边;不可哈希 ID 静默跳过 semantica/kg/graph_builder.py:265
5 BiTemporalFact 四字段:valid_from/valid_until(有效时间)+ recorded_at/superseded_at(事务时间) semantica/kg/temporal_model.py:28
6 事实在图中永远是普通 dict,BiTemporalFact 仅归一化时临时包装(零迁移兼容设计) semantica/kg/temporal_model.py:31
7 时态判断只认 valid_from/valid_until,add_node(timestamp=...) 是误导参数 semantica/context/context_graph.py:1124
8 is_active() 把 at_time 与存储值归一为 tz-naive UTC 再比较 semantica/context/context_graph.py:430
9 全仓惰性导出:PEP 562 getattr + _LAZY_EXPORTS 显式表;6 个 ingestor 未登记需深路径导入 semantica/ingest/__init__.py:166
10 doctor --deep-embeddings 真实实例化 TextEmbedder 做探针(对抗静默降级) semantica/cli.py:791
11 实测安装:36m36s / 1,900MB / 200+ 包(含 opencv/librosa 被动拖入) 复现脚本 semantica-test/test_quickstart.py
12 docker-compose 附带 FalkorDB 但 Explorer 后端不连接(预留) semantica/explorer/app.py:47

常见误区 FAQ

Q1:pip install semanticaimport semantica 会不会加载 torch?

不会立即加载。依赖安装了但 import 靠惰性导出(第 7 节),轻量使用 semantica.ingest 等不触发 torch。但要生成嵌入时(fastembed/sentence-transformers 路径)就会加载,首次还有模型下载。

Q2:build() 传入的 dict 关系字段到底该叫 source/target 还是 subject/object?

都可以。_process_item 的 dict 分支做了 source_id→sourcesubject→sourceobject→target 等多套归一(第 4 节)。但团队内部建议固定一套(与官方文档一致的 source/target/type),减少代码阅读时的方言切换。

Q3:为什么 state_at() 返回了我还没"创建"的节点?

大概率是给 add_node(timestamp=...) 传了历史时间并期望它影响时态------timestamp 只是普通属性。改用 valid_from="2024-01-01T00:00:00+00:00"(ISO 字符串)。

Q4:这张六层图和官方模块页有什么区别?

官方 /modules 页是"按层分类的模块清单";本文图 3 补上了官方没画的真实数据流向(尤其是 context 作为平行主干、pipeline 作为编排外壳这两点),并以 note 标注了两个枢纽类的源码行号。

相关推荐
Csvn2 小时前
第 22 章 安全、合规与治理
人工智能·aigc·agent
怕浪猫2 小时前
长会话不崩盘:DeepSeek Harness 的上下文压缩与目标管理策略
面试·github·agent
奔跑的架构师2 小时前
[A-50]ARMv9/v8-DVFS系统架构
linux·arm开发·架构·系统架构·arm
闲云自留地3 小时前
动手玩 Nova:Hypervisor、主机聚合、可用分区、虚拟机生命周期实操
运维·架构·openstack
一航jason3 小时前
Android平台推理框架及试用场景模型对比
android·人工智能·ai·架构·ai编程·llama
DeepAgent3 小时前
AI Agent 开发实战(14):AI Agent 开发工具推荐
人工智能·agent
Leo.yuan4 小时前
2026年多数据库实时同步的四种架构方案及工具推荐
数据库·架构