Files
worldmodel/plans/PRISM/02_architecture.md
T

20 KiB
Raw Blame History

Chapter 02 — 四层空间记忆架构

本章目标:把机器人大脑里的"空间记忆"拆成 L1L4 四层,逐层说清楚:存什么 / 怎么存 / 谁写 / 谁读 / 何时过期。


2.1 为什么是四层?而不是 1 层、3 层、7 层?

不能是 1 层的原因

单一表示(如纯点云)既不能高频更新(点云重写太慢),也不能直接被 LLM 查询(没有语义)。

不能是 3 层的原因

若合并 L3 拓扑与 L4 语义为一层,路径规划(需要拓扑边权)与问答(需要属性查询)会互相干扰。

不能更多(如 7 层)的原因

分层越多,层间同步成本越高。四层来自三个现实约束的交集:

flowchart TB
    subgraph C["认知科学"]
        direction LR
        C1["Working"] <--> C2["Short-term"]
        C2 <--> C3["Long-term"]
    end
    subgraph S["SLAM 工程"]
        direction LR
        S1["Metric"] <--> S2["Topological"]
        S2 <--> S3["Semantic"]
    end
    subgraph R["机器人控制"]
        direction LR
        R1["Reactive (Hz 级)"] <--> R2["Deliberative (秒级)"]
    end
    subgraph L["PRISM 四层(三家传统的最小公倍数)"]
        direction LR
        L1["L1<br/>感知缓冲"] --- L2["L2<br/>度量"] --- L3["L3<br/>拓扑"] --- L4["L4<br/>语义"]
    end
    C -. 分化 .-> S
    S -. 加感知缓冲 .-> R
    R -. 取并集 .-> L
    style L fill:#fff7d6,stroke:#c97a00,stroke-width:2px

四层是这三套传统在工程上的最小公倍数


2.2 四层全景图

flowchart LR
    subgraph PRISM["PRISM Spatial Memory(左:高频・易变・低抽象 → 右:低频・稳定・高抽象)"]
        direction LR
        L1["<b>L1 感知缓冲</b><br/>Perceptual<br/>──────────<br/>位姿环形缓冲<br/>深度环形缓冲<br/>──────────<br/>ZED 2i 主写<br/>更新:30 Hz<br/>寿命:ms 级"]
        L2["<b>L2 度量</b><br/>Metric<br/>──────────<br/>占据栅格<br/>TSDF<br/>3DGS<br/>──────────<br/>iPhone+ZED 共写<br/>更新:5 Hz<br/>寿命:秒级"]
        L3["<b>L3 拓扑</b><br/>Topological<br/>──────────<br/>房间节点 + 边<br/>锚点列表<br/>──────────<br/>iPhone 主写<br/>更新:低频<br/>寿命:分钟级"]
        L4["<b>L4 语义</b><br/>Semantic<br/>──────────<br/>场景图 Neo4j<br/>bbox 属性<br/>──────────<br/>iPhone+ZED 共写<br/>更新:极低频<br/>寿命:小时级"]
        L1 --> L2 --> L3 --> L4
    end
    style L1 fill:#fde2e2,stroke:#a33
    style L2 fill:#fff1c1,stroke:#a87a00
    style L3 fill:#d4f0d4,stroke:#2e7d32
    style L4 fill:#d8e4ff,stroke:#1565c0

记住三条直觉:

  1. 越往右越"抽象":L1 是像素和位姿,L4 是 "the lamp is on the nightstand"
  2. 越往右越"慢"L1 ms 级覆盖,L4 小时级更新
  3. 越往右越"小"L1 几 GB/小时,L4 几 MB/全场景

2.3 L1 — 感知缓冲 (Perceptual Buffer)

2.3.1 定位

"机器人最近几秒看到的所有东西"——纯粹的工作记忆,类似人的视觉残留。

2.3.2 存什么

@dataclass
class PerceptualFrame:
    timestamp: float
    pose: Pose                    # T_robot→map (ZED VIO 输出)
    rgb_left: np.ndarray          # (H,W,3) 可选保存
    depth: np.ndarray             # (H,W)
    imu_packet: List[IMUSample]   # 自上一帧以来的 IMU
    keypoints: Optional[np.ndarray]  # ORB/SuperPoint 关键点

class L1Buffer:
    capacity_seconds: float = 10.0          # 环形缓冲容量
    keyframe_interval: float = 0.5          # 关键帧采样间隔
    ring: Deque[PerceptualFrame]            # 满则覆盖
    keyframes: Deque[PerceptualFrame]       # 关键帧池(保留更久)

2.3.3 写者 / 读者

角色 频率 操作
ZED 2i 驱动 30 Hz ring.append(frame)
:避障 / 局部规划 10 Hz 取最近 1 s 的 frames
L2 融合器 5 Hz 取一个关键帧融入 TSDF
:回环检测 1 Hz 与关键帧池做相似度匹配

2.3.4 过期规则

  • 超过 capacity_seconds 的非关键帧 → 直接丢弃
  • 关键帧:若已成功融入 L2 → 5 分钟后可丢弃
  • 全部仅在内存,不落盘(除非 debug)

2.3.5 失效与降级


2.4 L2 — 度量记忆 (Metric Memory)

⚠️ v1.5 更新:本节后续提及"L2 voxel 直接生成 L3/L4 节点内容"的写法已被 § 2.7b L2-L3 数据流:路由 vs 内容(v1.5 新原则) 取代。L2 几何在 v1.5 起仅用作路由信号,不再承担"决定节点内容"的职责。旧描述保留以备历史追溯。

2.4.1 定位

"3D 几何长什么样"——机器人能在上面规划路径、避障、渲染的稠密表示。

2.4.2 存什么(多重表示)

@dataclass
class L2Memory:
    # 用途 1:导航——2D/2.5D 占据栅格
    occupancy_grid: OctoMap           # 5 cm 分辨率,0=空 1=占 -1=未知

    # 用途 2:精细几何——TSDF / Mesh
    tsdf: VoxelBlockGrid              # 2 cm 分辨率
    mesh_uri: str                     # 离线烘焙的 .glb

    # 用途 3:渲染 / 视觉相似度——3DGS
    gaussians_uri: Optional[str]      # .ply,每房间一份

    # 用途 4:先验掩膜
    prior_mask: np.ndarray            # 0=ZED 可写  1=iPhone 静态保护
    no_update_zone: np.ndarray        # 1=镜面/玻璃,禁止写入

为什么要 4 套表示?

表示 谁用 为什么不能替代
OctoMap 路径规划器 路径规划只关心"能不能走",cm 级足够
TSDF 差异检测 / 抓取 需要带符号距离才能算 SDF 差
Mesh 可视化 / Unity 仿真 渲染流水线友好
3DGS 视觉重定位 / 新视角生成 比 mesh 真实得多

它们共享同一份原始点云,只是不同的"派生视图"。

2.4.3 写者 / 读者

区域 主写者 来源 频率
静态结构(墙、门、固定家具) iPhone RoomPlan mesh 1 次/场景
可变区域(家具间空隙、地面) ZED 2i TSDF 增量 5 Hz 局部
新发现区iPhone 没扫到) ZED 2i TSDF 新建 voxel 5 Hz
镜面/玻璃 都不写

2.4.4 关键规则:先验保护

def fuse_zed_to_l2(zed_tsdf_local, l2: L2Memory):
    for voxel in zed_tsdf_local:
        if l2.no_update_zone[voxel.idx]:
            continue                       # 镜面区,跳过
        if l2.prior_mask[voxel.idx]:
            # 先验区:只允许微调(低权重)
            l2.tsdf.update(voxel, weight=0.1)
        else:
            # 自由区:正常融合
            l2.tsdf.update(voxel, weight=1.0)

→ iPhone 提供的墙不会被一次 ZED 噪声毁掉,但小幅度(< 5 cm)的修正可累积起效。

2.4.5 过期规则

  • iPhone 写入的体素 → 长期保留,但有 last_seen 字段
  • ZED 写入的体素:30 天内未被再次确认 → 衰减 confidence
  • 若某区域被打上 delta(家具搬走)→ 在 Consolidator 跑后才物理移除

2.5 L3 — 拓扑记忆 (Topological Memory)

⚠️ v1.5 更新:本节中"由 L2 几何(TSDF/OctoMap)反推生成 L3 节点内容"的旧写法已被 § 2.7b L2-L3 数据流:路由 vs 内容(v1.5 新原则) 取代。L3 节点的 clip_embedding / polygon / 锚点描述等内容改为从 L1 高质量 keyframe 直接获取;L2 仅决定"写哪个节点"。旧描述保留以备历史追溯。

2.5.1 定位

"房间和走廊怎么连"——机器人长距离导航的骨架。

2.5.2 存什么

@dataclass
class L3Node:
    uid: str                          # "room_301"
    label: str                        # "Bedroom" / "Hallway" / "Lobby"
    center: np.ndarray                # (3,) 房间几何中心
    polygon: np.ndarray               # (N,2) 房间地面多边形
    anchors: List[str]                # 引用 L4 中的家具 uid(如 'bed_301'
    clip_embedding: np.ndarray        # (512,) 房间整体视觉指纹

@dataclass
class L3Edge:
    src: str                          # "room_301"
    dst: str                          # "hallway_3F"
    via: str                          # 连接介质:'door_301a' / 'open_passage'
    cost: float                       # 通行成本(距离 + 难度)
    bidirectional: bool = True

class L3Memory:
    nodes: Dict[str, L3Node]
    edges: List[L3Edge]
    anchor_index: Dict[str, str]      # furniture_uid -> room_uid 反向索引

2.5.3 写者 / 读者

角色 何时 操作
iPhone 解析器 离线一次 按 RoomPlan 的 room 切分自动生成节点;门窗作为边
ZED 2i + 巡逻 机器人实际穿过门时 确认/新增边,更新 cost
:高层规划器 任务下发时 在 L3 图上跑 A* / Dijkstra 得"房间序列"
:重定位器 上电 / 跟丢时 clip_embedding 做粗匹配

2.5.4 过期规则

  • 房间节点:除非装修,否则永久保留
  • 边:连续 N 次(默认 3 次)巡逻都走不通 → 标记 deprecated,路径规划器跳过
  • 锚点:被 ZED 检测到原家具消失 → 从 anchors 中移除,但节点不删

2.5.5 与 L4 的区别(容易混淆!)

  • L3 是"地理":节点是地方(房间、走廊),边是通行关系
  • L4 是"物品":节点是东西(床、灯、遥控器),边是支撑/包含/邻接关系
  • L3 节点 room_301 通过 anchors 字段指向 L4 节点 bed_301, tv_301...

2.6 L4 — 语义记忆 (Semantic Memory)

2.6.1 定位

"哪个东西在哪儿、长啥样、跟谁挨着"——机器人理解任务与人对话的底座。

2.6.2 存什么

@dataclass
class L4Node:
    uid: str                          # 'bed_301', 'lamp_301_a'
    label: str                        # 'bed' / 'lamp' / 'remote_control'
    category: str                     # 'furniture' / 'appliance' / 'small_item'
    pose: Pose
    bbox_3d: np.ndarray               # (8,3) OBB
    mesh_uri: Optional[str]
    clip_embedding: np.ndarray        # (512,)
    attributes: Dict                  # {color, material, state(on/off/open/closed),
                                      #  mobile: bool, fragile: bool, ...}
    parent_room: str                  # L3 房间 uid
    source: Literal['iphone','zed2i','vlm','fused']
    first_seen: float
    last_seen: float
    observation_count: int
    confidence: float

@dataclass
class L4Edge:
    src_uid: str
    dst_uid: str
    relation: Literal['on','under','in','next_to',
                      'inside_drawer','plugged_into',...]
    confidence: float

class L4Memory:
    nodes: Dict[str, L4Node]
    edges: List[L4Edge]
    spatial_index: Optional[KDTree]   # 加速"附近的东西"查询
    semantic_index: Optional[Faiss]   # CLIP 向量库,加速文本查物体

2.6.3 写者 / 读者

角色 频率 操作
iPhone 解析器 离线一次 16 类家具直接落库(高 confidence)
ZED + VLM 2 Hz 检出物品,与现有节点匹配或新建
Consolidator 充电时 确认 delta 并固化
LLM Agent 按需 find("遥控器在哪") → CLIP 检索 + 关系遍历
:抓取规划器 任务时 取目标 bbox_3d + attributes.fragile
:渲染器 可视化 取所有 mesh_uri

2.6.4 三类节点的不同生命周期

类型 例子 mobile 谁写 多久过期
固定结构 墙、门、内嵌衣柜 false iPhone 永不
大件家具 床、沙发、书桌 half iPhone 主,ZED 校准 季度级
小物品 遥控器、水杯、毛巾 true ZED + VLM 主 小时级

mobile 标签直接影响重定位是否能用它当 anchor(见 05_pipeline_B_relocalization.md)。

2.6.5 关系(Edge)的两种来源

# 1. 几何派生:通过 bbox 相对位置自动得出
def derive_edge_from_geometry(a: L4Node, b: L4Node) -> Optional[L4Edge]:
    if a.bbox_3d.contains(b.bbox_3d.center):
        return L4Edge(b.uid, a.uid, 'in', confidence=0.9)
    if abs(b.bbox_min_z - a.bbox_max_z) < 0.05:
        return L4Edge(b.uid, a.uid, 'on', confidence=0.85)
    if a.distance_to(b) < 0.5:
        return L4Edge(a.uid, b.uid, 'next_to', confidence=0.7)
    return None

# 2. VLM 派生:让 GPT-4V / Qwen-VL 看图给关系
def derive_edge_from_vlm(rgb_image, detections) -> List[L4Edge]:
    prompt = f"Given these detected objects {detections}, " \
             f"list spatial relations as (a, relation, b)."
    return vlm.parse_relations(rgb_image, prompt)

两路边都允许存在,按 confidence 加权融合。


2.7 层间交互规则

2.7.1 写入顺序(数据如何流入大脑)

flowchart TB
    ZED["ZED 2i<br/>原始数据"]
    VLM["VLM<br/>(在线感知)"]
    L1["L1 感知缓冲"]
    L2["L2 度量"]
    DELTA[("delta/<br/>差异目录")]
    L4["L4 语义"]
    L3["L3 拓扑"]
    CON["Consolidator<br/>(充电时跑)"]
    IPHONE["iPhone<br/>(离线一次)"]
    LTM[("L2 + L3 + L4<br/>永久保留")]

    ZED -- "30 Hz 写入" --> L1
    ZED -- "5 Hz 关键帧" --> L2
    ZED -- "2 Hz" --> VLM
    L2 -- "差异检测" --> DELTA
    VLM -- "关联" --> L4
    DELTA -- "充电时确认" --> CON
    CON --> L4
    L4 -- "更新 anchors" --> L3
    IPHONE -- "离线一次" --> LTM

    style L1 fill:#fde2e2,stroke:#a33
    style L2 fill:#fff1c1,stroke:#a87a00
    style L3 fill:#d4f0d4,stroke:#2e7d32
    style L4 fill:#d8e4ff,stroke:#1565c0
    style CON fill:#ffe9b3,stroke:#c97a00
    style DELTA fill:#f5e1ff,stroke:#7b1fa2

2.7.2 查询顺序(Agent 怎么读大脑)

最常见的查询是 "找东西 + 怎么去",标准流程:

def query_find_and_navigate(text: str, memory):
    # 1. L4 语义搜索:文本 → 物品 uid
    obj_uid = memory.l4.semantic_search(text)             # CLIP
    obj = memory.l4.nodes[obj_uid]

    # 2. L3 拓扑规划:当前房间 → 物品所在房间
    cur_room = memory.l3.locate(memory.l1.current_pose)
    path_rooms = memory.l3.astar(cur_room, obj.parent_room)

    # 3. L2 度量规划:物品所在房间内的精确路径
    metric_path = memory.l2.plan_path(
        start=memory.l1.current_pose,
        goal=obj.pose,
        room_sequence=path_rooms)

    # 4. L1 实时跟随 + 避障
    follow_path(metric_path)

    return obj

从右向左下钻:抽象 → 具体,是 PRISM 查询的标准范式。

2.7.3 写入冲突的仲裁

当 iPhone 和 ZED 对同一区域有不同观测:

情况 仲裁规则
iPhone 说有墙,ZED 说没墙 单次→忽略 ZED;连续 N 帧→写 deltaConsolidator 确认才删墙
iPhone 没标家具,ZED 检到家具 立即写 L4 新节点,但 confidence 起始 0.3,多次确认才上升
iPhone 标了 lampZED 检到 lamp 但位置偏 30 cm ZED 微调 poseconfidence 加权平均
iPhone 标了 chairmobile=true),ZED 没看到 不立即删,标记 state='moved'consolidate 决定

总原则:iPhone 写入的内容"假设正确直到证据充分相反"


2.7b L2-L3 数据流:路由 vs 内容(v1.5 新原则)

v1.5 起 PRISM 借鉴 Lyra 2.0 的核心思想——"几何只做路由,不做合成"——重新厘清 L2 与 L3 之间的职责边界。详细动机与 Lyra 2.0 的对照参见 18_lyra_inspirations.md(尤其是 §18.2)。在此之前,PRISM 的 L2 既负责几何稠密表示,又承担"把 voxel 内容写入 L3 节点"的合成职责,导致 L3 节点的视觉指纹(CLIP 嵌入)、bounding box、文字描述等都受 L2 voxel 量化精度限制;新原则把这两件事彻底解耦。

2.7b.1 路由 vs 内容:两条数据流分工

flowchart LR
    L1KF["<b>L1 keyframe</b><br/>full-res RGB-D<br/>+ CLIP embedding<br/>(高保真原始观测)"]
    L2GEO["<b>L2 几何</b><br/>TSDF / OctoMap<br/>(低精度,可量化)"]
    L3NODE["<b>L3 节点写入</b><br/>clip_embedding<br/>polygon / anchors<br/>(高保真内容)"]
    L1KF -- "内容来源<br/>(高保真)" --> L3NODE
    L2GEO -. "仅作路由信号<br/>(决定写哪个节点)" .-> L3NODE
    style L1KF fill:#fde2e2,stroke:#a33
    style L2GEO fill:#fff1c1,stroke:#a87a00
    style L3NODE fill:#d4f0d4,stroke:#2e7d32
  • L2 几何 = 路由信号:仅用来判断"当前观测的 3D 位置应该归属到 L3 图里的哪个节点"。即使 L2 的 voxel 是 2 cm 量化、含噪声、甚至局部缺失,只要它能指向正确的 L3 节点 uid就够了。
  • L1 keyframe = 内容源:节点的 clip_embedding、视觉证据、bounding box、属性描述等实际内容必须来自 L1 缓存里那一张未经量化的高分辨率 RGB-D keyframe(或它的特征),而非从 L2 voxel 反投影回来。

一句话总结:几何精度只需要够"指向哪个节点",不需要"决定节点内容"

2.7b.2 路由式写入伪代码

def write_to_l3(observation, l2_geometry, l3_graph):
    # ① L2 只参与"路由":根据观测的 3D 位置定位 L3 节点 uid
    target_node = route_via_l2(observation.position, l2_geometry)
    # ② 内容来源是 L1 高保真 keyframe,而非 L2 voxel
    keyframe_evidence = get_best_keyframe(observation, l3_graph[target_node])
    # ③ 用 keyframe 的原始 CLIP / bbox / patch 更新节点内容
    l3_graph[target_node].update(keyframe_evidence)

四行核心逻辑里,L2 只出现在第一步(route_via_l2)且只读;真正塑造 L3 节点内容的是第二步从 L1 取回的 keyframe_evidence。这一拆分让 L2 即使在 OctoMap 5 cm + TSDF 噪声的精度下仍然足以胜任,而 L3 的语义保真度由 L1 决定。

2.7b.3 对原有章节的影响一览

原章节 旧写法 v1.5 新原则下的对应
§2.4 L2 度量 L2 既存几何又"派生"L3 内容 L2 仅做几何 + 路由;不再向 L3 派生内容
§2.5 L3 拓扑 clip_embedding 由 voxel 颜色聚合 由 L1 keyframe 直接编码
§2.7.1 写入顺序图 L2 --差异检测--> DELTA 仍然成立 但 L3 节点内容更新的箭头改为来自 L1,L2 只贡献节点 uid 路由
06_pipeline_C §6.6 TSDF 体素直接重投影生成 patch § 6.6b keyframe-based 内容更新

2.8 整体存储预算

以一个 10 房间酒店楼层为例:

数据 大小
L1 10 s 环形缓冲(深度+RGB ~ 2 GB 内存
L2 10 房间 × (OctoMap 50 MB + TSDF 100 MB + 3DGS 200 MB) ~ 3.5 GB 磁盘
L3 10 节点 + 边 + CLIP 向量 ~ 100 KB
L4 ~ 500 nodes (10 房 × 50 件) + 关系 ~ 50 MB
~ 4 GB 磁盘 + 2 GB 内存

→ 完全可以在 Jetson Orin (3264 GB) 上跑。


2.9 本章小结

一句话 主写者 平均寿命
L1 感知缓冲 最近几秒的位姿+深度 ZED 2i 10 秒
L2 度量 3D 几何(导航/渲染用) iPhone 静态 + ZED 增量 月级
L3 拓扑 房间-走廊连通图 iPhone 永久
L4 语义 物品-关系场景图 iPhone 大件 + ZED+VLM 小件 小时~季度

四层不是平行的,而是沿"频率×抽象"轴展开的连续谱

读完本章你应能:

  • 解释每一层"是什么"
  • 解释每一层"由谁主写、谁主读"
  • 理解层间写入冲突的仲裁规则
  • 估算一个 10 房间场景的存储预算

下一章 03_data_schema.md 给出可直接复制运行的 Python dataclass schema + 序列化格式。


章节版本v1.0 估计阅读时间18 分钟 关键收获:四层结构的"存什么/谁写谁读/何时过期"完整规则