【Agent Harness】Gliding Horse 中 JSON‑LD 的深度设计:从“能用”到“可靠”的语义总线

Gliding Horse 中 JSON‑LD 的深度设计:从“能用”到“可靠”的语义总线

摘要:本文深入解析 Gliding Horse Agent OS 中 JSON‑LD 的六大核心设计决策——有意设计、统一上下文、IRI 注册表、聚焦展开、共享审计日志与命名空间管理。从 L0 存储的双层结构到轻量级 Expansion 算法,从消除上下文冲突到构建全系统可寻址的语义总线,揭示如何将 JSON‑LD 从数据格式升级为支撑多 Agent 编排、记忆分页与合规审计的可靠基础设施。适合对语义 Web、知识图谱与 Agent 系统架构感兴趣的开发者阅读。

关键字:JSON‑LD, Gliding Horse, Agent OS, 语义总线, IRI 注册表, 统一上下文, 聚焦展开, 审计日志, 命名空间管理, 知识图谱, 语义互操作, Oxigraph, RDF, 多 Agent 系统, 架构设计


在构建 AI Agent 操作系统的过程中,我们选择 JSON‑LD 作为贯穿所有模块的统一数据抽象。这不仅仅是为了让数据带上 @context@id,更是要把它打造成一条可寻址、可校验、可演进的语义总线。然而,要让 JSON‑LD 真正胜任这个角色,需要一系列精心的底层设计,而不是简单地把数据序列化成 JSON‑LD 格式。

本文将详细拆解 Gliding Horse 在 src/jsonld/src/memory/src/skill_graph/src/tools/ 等模块中有意为之的 JSON‑LD 设计决策,涵盖有意设计(不可误改)统一上下文IRI 注册表聚焦展开共享审计日志以及命名空间管理六个关键维度。这些设计共同将 JSON‑LD 从“数据格式”提升为支撑整个 Agent OS 可靠运行的“基础设施”。


1. 有意设计:不追求规范“完美”,而是追求系统“合适”

在实现 JSON‑LD 处理时,我们没有一味追求完整的 W3C 规范实现,而是基于封闭系统这一前提,做出了一系列轻量、高效的设计选择。以下是一些不可轻易更改的刻意简化

1.1 L0 存储:完整 JSON‑LD + 提取索引的双层结构

在持久层 L0 中,每个节点被保存为一个 L0Entry,它同时包含:

  • content:完整的原始 JSON‑LD 字符串(信息零丢失)
  • jsonld_types:展开后的 @type 数组(用于快速类型过滤)
  • jsonld_context@context 原文(惰性解析,写入 O(1))
flowchart LR Input["JSON‑LD 文档"] --> Extract["提取 @id, @type, @context"] Extract --> Store["L0Entry 结构"] Store --> Index["jsonld_types 索引"] Store --> Raw["content 原始文档"]

设计意图

  • content 保留全量文档,确保任何消费方都能重建完整 JSON‑LD,不被“摘录”的字段限制。
  • @context 存为字符串而不展开为 IRI 映射,是为了避免写放大——在封闭系统中,上下文字段固定,运行时并不需要每次读写都解析。
  • jsonld_types 以数组形式提供快速的多态检索,比如查找所有 Task 类型的节点。

禁止的行为:不要将 content 拆解为离散字段存储,那会丢失文档结构与类型注释;不要强制在写入时完全展开所有 IRI,这会显著增大存储并降低性能。

1.2 轻量级 Expansion:HashMap 查表代替递归遍历

context.rs 中的 map_field_to_iri() 函数采用了一个预构建的 HashMap,将常见 JSON 字段名(如 summarypriority)直接映射为 IRI。

/// 将 JSON 字段名映射为完整 IRI。
///
/// # 设计取舍
/// - 使用编译期嵌入的 `context.json` 生成静态 HashMap,查询 O(1)。
/// - 不处理嵌套字段、`@type` 值提升或 `@language` 标签——这些在封闭系统中
///   要么不需要,要么由调用方在更高层处理。
/// - 若字段不在映射表中,原样返回(视为已为 IRI 或外部字段),避免误拦截。
pub fn map_field_to_iri(field: &str) -> String {
    // 从编译期嵌入的上下文生成映射,O(1) 查表
    CONTEXT_MAP
        .get(field)
        .cloned()
        .unwrap_or_else(|| field.to_string())
}

这是有意为之的轻量 Expansion,而不是完整的 JSON‑LD 1.1 Expansion 算法。完整算法需要递归遍历、值类型提升等复杂逻辑,在封闭系统中完全是过度设计。

1.3 Framing 热路径不预 Expansion

apply_frame() 直接在原始 JSON key 上操作,因为数据和框架共享同一个 @context,key 名天然对齐。添加 expansion 步骤只会增加延迟而无收益。

/// 对原始 JSON 值应用 Frame,直接在未展开的 key 上匹配。
///
/// # 设计取舍
/// - 不预先调用 `expand_object_keys()`:数据和 Frame 共享同一 `@context`,
///   字段名天然对齐,预展开只会增加一次 HashMap 遍历而无任何语义收益。
/// - 仅匹配 Frame 中显式列出的属性:未列出的 key 会被过滤,这既是简化
///   也是安全措施——防止 Frame 未覆盖的字段意外泄露到输出中。
/// - 若未来需要跨上下文 Framing,应调用 `apply_frame_cross_context()`,
///   后者会在内部启用聚焦展开(见第 4 节),不影响此热路径。
pub fn apply_frame(value: &Value, frame: &FrameTemplate) -> Value {
    match value {
        Value::Object(map) => {
            let filtered: serde_json::Map<String, Value> = map
                .iter()
                .filter(|(key, _)| frame.properties.contains(key))
                .map(|(key, val)| (key.clone(), val.clone()))
                .collect();
            Value::Object(filtered)
        }
        other => other.clone(),
    }
}

这些"有意设计"共同保证了一条原则:在不牺牲正确性的前提下,用最简单的机制满足系统内部需求。当未来出现跨上下文场景时,我们再按需引入聚焦展开(见第 4 节),而不是推翻现有轻量实现。


2. 统一上下文:结束“两种方言”的时代

系统初期存在两套并行的上下文定义:

  • context.json:包含 26 条映射,使用 pdca: 前缀
  • GLOBAL_CONTEXT(Rust 代码内嵌):约 45 条映射,使用 task:agent: 等多种前缀

更严重的是,同一个字段名 status 在两套上下文中映射到了不同的 IRI,极易造成语义混乱。

2.1 设计目标

将所有上下文定义统一到 context.json 中,Rust 侧只保留 context_value() 作为唯一访问入口,map_field_to_iri() 直接从该 JSON 动态生成映射。

flowchart TB subgraph Before["之前:两套上下文"] A1["context.json (26条)"] A2["GLOBAL_CONTEXT (45条)"] A3["多个模块各自引用"] end subgraph After["之后:统一上下文"] B1["context.json (所有映射)"] B2["context_value() 唯一入口"] B3["map_field_to_iri() 自动生成"] end Before -->|"统一"| After

2.2 关键动作

  1. 扩展 context.json:将 GLOBAL_CONTEXT 中的映射全部迁移过来,并加上缺失的 @type 注解(如 xsd:stringxsd:dateTime),提升类型安全性。
  2. 删除 GLOBAL_CONTEXT:移除 Rust 中的硬编码 HashMap,所有模块统一从 context.json 派生映射。
  3. 添加 @type 注解:让 L3 投影和 Skill 注册能够自动进行数据类型校验,减少运行时错误。

收益

  • 单一数据源:修改上下文只需编辑一个 JSON 文件,无需改动 Rust 代码。
  • 类型安全@type 注解让工具链(如 JSON Schema 校验)可以直接消费。
  • 无冲突:彻底消除了同名字段映射到不同 IRI 的隐患。

3. IRI 注册表:让每一个 @id 都可以被“找到”

Gliding Horse 中,不同子系统各自生成 iri:// 格式的唯一标识:

  • Task:iri://task/{uuid}
  • Skill:iri://skills/{name}
  • Knowledge Graph:iri://entity/{id}
  • Sharing:iri://share/{uuid}
  • Memory:iri://memory/{uuid}

目前一共有多达 20+ 种 IRI 模式,但缺少一个中心化设施来记录每个 IRI 归属于哪个子系统、存储在哪个 Named Graph 中。这导致:

  • 无法快速回答“iri://task/abc 现在在哪一层?”
  • 跨子系统查询依赖硬编码的前缀推断,容易出错
  • 同一 IRI 可能被多个命名图无意中重复写入,难以去重

3.1 中心注册架构

我们设计了一个 IriRegistry,基于共享的 Oxigraph Store,使用专用命名图 graph:registry 存储所有 IRI 的位置信息。

flowchart TB subgraph Registry["IriRegistry"] Store["Oxigraph Store (graph:registry)"] Cache["DashMap 本地读缓存"] end subgraph Producers["生产者"] Task["Task 创建"] Skill["Skill 注册"] KG["知识图谱"] Share["共享"] Memory["L0 存储"] end Producers -- "register_iri()" --> Registry Registry -- "resolve_iri()" --> Consumers["任意子系统 (如 CA 审计)"]

核心数据结构 EntityLocation 记录了:

  • iri:实体标识
  • namespace:所属子系统(task、skills、memory 等)
  • named_graph:存储的具体 Oxigraph Named Graph
  • storage_layer:L0/L2/L3 等
  • entity_type:来自 JSON‑LD 的 @type
  • created_at:创建时间戳

3.2 核心 API

  • register(iri, location):写入 Oxigraph 并更新本地缓存,自动检测跨图重复。
  • resolve(iri):优先走 DashMap 缓存,未命中则查询 SPARQL,返回该 IRI 的所有已知位置。
  • find_duplicates():主动发现被多个命名图重复存储的 IRI。
  • resolve_by_namespace / resolve_by_type:支持按命名空间或实体类型批量查询。

3.3 集成方式

最关键的是在 L0 的 store_jsonld_node() 方法中植入注册逻辑。因为几乎所有子系统最终都会通过 L0 持久化,这是一个天然的集中接入点。注册操作设计为非阻塞,失败不会阻止正常写入。

收益

  • 可发现性:任意 Agent(尤其是 CA 审计时)可以通过 IRI 瞬间找到数据的物理位置。
  • 去重与一致性:自动检测同名 IRI 的多次写入,防止数据冗余。
  • 跨模块解耦:调用方不再需要猜测前缀规则,直接查询注册表即可获得权威位置信息。

4. 聚焦展开(Focused expand):为未来互操作留好“后门”

当前系统基于封闭世界的假设,但未来难免需要处理来自外部的 JSON‑LD 数据,其 @context 可能与系统内部不同。如果直接使用完整 JSON‑LD Expansion 算法,会引入不必要的复杂度。

我们定义了一组最小化 API,只做“key 解析”——将对象顶层的 key 从缩写映射到完整 IRI,而不递归处理值或嵌套对象。

// 展开单个 key
pub fn expand_key(key: &str, context: &Value) -> String;
// 展开对象的所有顶层 key
pub fn expand_object_keys(value: &Value, context: &Value) -> Value;
// 展开 Frame 的属性列表
pub fn expand_frame_properties(frame: &FrameTemplate, context: &Value) -> Vec<String>;

同时提供对称的 compact_object_keys() 用于将展开后的 key 压回缩写形式。

为什么不是完整 Expansion?
完整算法需要处理 @type 映射、@language@list@reverse 等复杂语义,而当前跨上下文场景只需要对齐 key 名即可。保留轻量实现,既避免性能退化,又为将来留下干净的扩展接口。

使用时机:只有当调用 apply_frame_cross_context() 时,即数据和框架使用不同 @context 时,才启用聚焦展开。现有热路径完全不受影响。


5. 共享审计日志:给内存 HashMap 加上“黑匣子”

Sharing 模块使用内存 HashMap 管理 Agent 间的临时数据共享(带有 TTL)。这是正确的性能选择,但缺少持久化的审计追踪——CA 无法查询“PA 在什么时间向 DA 共享了某个节点”。

我们增加了一个可选的 SharingAuditLog,将每一次共享创建、解析、撤销、过期事件以 RDF 三元组形式写入专用的 Oxigraph 命名图 graph:sharing-audit

GRAPH <graph:sharing-audit> {
    <iri://share/uuid> a share:AuditEntry ;
        share:eventType "Created" ;
        share:sourceAgent <iri://agent/pa> ;
        share:targetAgent <iri://agent/da> ;
        share:nodeIri <iri://task/123/plan> ;
        share:timestamp "..."^^xsd:dateTime .
}

设计原则

  • 主数据源不变:HashMap 依然是权威的共享关系存储,审计日志仅为追加写。
  • 非阻塞:审计写入失败不影响共享操作本身。
  • 按需启用:通过 with_audit_log() 注入,无审计需求时零开销。

收益

  • 合规审计:CA 可以精确追溯任何数据在 Agent 间的流转历史。
  • 调试辅助:当出现“数据过期”“共享丢失”等问题时,审计日志提供了完整的时序证据。

6. 统一命名空间注册表:让 20+ 个名字不再是“黑户”

系统初期只定义了 11 个命名空间,但实际代码中已经使用了 20+ 个不同的 IRI 前缀(如 entitystatementsharecheckpoint 等),导致 is_valid_namespace() 函数对大部分合法 IRI 返回 false,失去了校验意义。

我们将所有实际存在的命名空间整理为一个统一常量数组 KNOWN_NAMESPACES,并让 is_valid_namespace() 基于此进行校验。如果 IriRegistry 已部署,则进一步改为查询注册表动态判断。

pub fn is_valid_namespace(namespace: &str) -> bool {
    if let Some(registry) = IRI_REGISTRY.get() {
        registry.namespace_exists(namespace)
    } else {
        KNOWN_NAMESPACES.contains(&namespace)
    }
}

收益

  • 避免新模块因为命名空间不在白名单中而被错误拒绝。
  • 为 IRI 注册表提供了可靠的命名空间合法性验证基础。

7. 总结:JSON‑LD 作为可靠语义总线的支柱

通过这些精心设计,Gliding Horse 中的 JSON‑LD 不再是简单的序列化格式,而是一套完整的语义基础设施

设计项 解决的问题 架构收益
有意设计 防止过度工程化 保持系统轻量、高性能,避免不必要的复杂度
统一上下文 多套映射冲突 单一数据源,类型安全,消除字段歧义
IRI 注册表 IRI 散落难以发现 全系统可寻址,支持审计与去重
聚焦展开 跨上下文互操作 预留扩展性,不影响热路径性能
共享审计日志 临时共享无法追溯 提供完整流转记录,满足合规要求
命名空间注册 命名空间校验失效 动态、准确的合法性校验

这些设计让 Agent OS 的每一个数据都具有明确的身份、可查询的位置、可校验的类型、可追溯的变迁历史。正是这一层扎实的 JSON‑LD 总线,支撑起了 Gliding Horse 的多 Agent 编排、记忆分页、知识沉淀与质量门禁等全部高阶特性。

posted @ 2026-06-23 17:41  doiito  阅读(3)  评论(0)    收藏  举报