【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))
设计意图:
content保留全量文档,确保任何消费方都能重建完整 JSON‑LD,不被“摘录”的字段限制。@context存为字符串而不展开为 IRI 映射,是为了避免写放大——在封闭系统中,上下文字段固定,运行时并不需要每次读写都解析。jsonld_types以数组形式提供快速的多态检索,比如查找所有Task类型的节点。
禁止的行为:不要将
content拆解为离散字段存储,那会丢失文档结构与类型注释;不要强制在写入时完全展开所有 IRI,这会显著增大存储并降低性能。
1.2 轻量级 Expansion:HashMap 查表代替递归遍历
在 context.rs 中的 map_field_to_iri() 函数采用了一个预构建的 HashMap,将常见 JSON 字段名(如 summary、priority)直接映射为 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 动态生成映射。
2.2 关键动作
- 扩展
context.json:将GLOBAL_CONTEXT中的映射全部迁移过来,并加上缺失的@type注解(如xsd:string、xsd:dateTime),提升类型安全性。 - 删除
GLOBAL_CONTEXT:移除 Rust 中的硬编码HashMap,所有模块统一从context.json派生映射。 - 添加
@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 的位置信息。
核心数据结构 EntityLocation 记录了:
iri:实体标识namespace:所属子系统(task、skills、memory 等)named_graph:存储的具体 Oxigraph Named Graphstorage_layer:L0/L2/L3 等entity_type:来自 JSON‑LD 的@typecreated_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 前缀(如 entity、statement、share、checkpoint 等),导致 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 编排、记忆分页、知识沉淀与质量门禁等全部高阶特性。
浙公网安备 33010602011771号