第4章 三层架构——Perception / Cognition / Memory 的工程分工
把空间记忆放进感知-认知-记忆三层架构里讲清工程分工;展开两条核心工程纪律——简约性(parsimony)与自洽性(self-consistency);用容器推理和跨会话重定位两个对照案例把抽象原则落到代码级
1. 为什么这一章是整模块的脊柱
前三章的工作可以浓缩为三句话:
- 空间记忆和对话记忆是不同的工程对象(第 1 章)
- 空间能力是一种独立的基础计算,背后有跨物种保守的设计原则(第 2 章)
- 三股应用压力同时撞上同一堵墙,这堵墙是状态层缺位(第 3 章)
到这里,“为什么”已经讲完了。这一章是”怎么做”——也是整个模块的工程脊柱。所有后面的章节(世界模型 M 层 / 神经符号路线 / 评测体系 / 工程落地)都将以这一章建立的架构作为公共参照。
一句话概括这一章要做的事:
把空间智能拆成感知 / 认知 / 记忆三层 + 给出两条贯穿始终的工程纪律(简约性 / 自洽性) + 用两个具体案例(容器推理 / 跨会话重定位)说明这套架构在代码级长什么样。
2. 三层架构:从问题类型出发的工程分工
2.1 先把三件事的”问题类型”说死
第 3 章已经分清过感知 / 认知 / 记忆的失败模式不同。这里我们把它们的问题类型给出更精确的形式化描述。
| 层 | 问题类型 | 输入 | 输出 | 时间观 |
|---|---|---|---|---|
| 感知 (Perception) | 函数 | 当前帧 / 当前点云 / 当前 IMU 等 | 结构化观测(对象 / 关系 / 区域 / 几何) | 无状态——只看当前 |
| 认知 (Cognition) | 函数 | 感知输出 + 当前记忆状态 | 经过对齐 / 归属 / 关系推断后的结构化场景 | 当前 + 当前记忆——一次性融合 |
| 记忆 (Memory) | 函数 | 旧记忆状态 + 当前对齐后的观测 | 新记忆状态 | 跨时间——这是它的全部价值 |
注意三层在”时间观”上的差别:
- 感知层完全无状态——它每次只回答”现在我看到什么”
- 认知层有限有状态——它需要记忆来做对齐 / 归属 / 关系推断,但本身不维护历史
- 记忆层完全跨时间——它的核心价值就是在 和 之间做有原则的融合
这个分工最关键的点在于:记忆层不是感知层的缓存,也不是 LLM 上下文的延伸。它有自己的状态结构、自己的更新规则、自己的查询接口。把它和前两层混在一起,是当前大量”空间智能”原型最常见的设计错误。
2.2 一张可以贴在白板上的架构图
把三层的接口画清楚,能给整个工程团队一个最小公共参照:
┌────────────────────────────────────────────────────────────┐
│ 应用层(规划 / 决策 / 对话 / UI) │
└──────────────────────┬─────────────────────────────────────┘
│ 查询接口 Q : Memory → Answer
│ - last_seen(obj)
│ - is_in(obj, container)
│ - changes(region, t0, t1)
│ - state_at(time, attribute)
┌──────────────────────▼─────────────────────────────────────┐
│ Memory 层 │
│ - 状态结构 M : { ObjectRecord, RelationGraph, │
│ ContainerLog, EvidenceIndex } │
│ - 更新规则 f_mem(M_{t-1}, Õ_t) → M_t │
│ · Bayesian belief update │
│ · 容器推理 (在 / 不在) │
│ · 时间衰减 + 负观测 │
│ · 漂移 vs 物理变化的区分 │
│ - 校准接口 calibrate(observed_truth) │
└──────────────────────▲─────────────────────────────────────┘
│ 对齐后的观测 Õ_t │
│ - 对象身份已对齐到 ObjectRecord │
│ - 关系归属已显式化 │
│ - 不确定性已标注 │
┌──────────────────────┴─────────────────────────────────────┐
│ Cognition 层 │
│ - 对象抽取 / 关系推断 / 区域归属 / 拓扑组织 │
│ - 跨帧身份对齐 │
│ - 简约性压缩(高维观测 → 低维结构状态) │
└──────────────────────▲─────────────────────────────────────┘
│ 结构化观测 O_t │
┌──────────────────────┴─────────────────────────────────────┐
│ Perception 层 │
│ - 视觉 / 雷达 / IMU / 触觉 … │
│ - 对象检测、深度、分割、特征提取 │
│ - 完全无状态:只回答 "now I see X" │
└────────────────────────────────────────────────────────────┘
这张图传达三件事:
- 每两层之间的接口是结构化的——不是裸像素,不是 token 流,是带语义和不确定性标注的结构对象
- Memory 层有独立的查询接口——应用层不直接看记忆数据结构,只通过 4 类查询访问(这正好对应第 1 章的 last-seen / containment / change / state-audit)
- 应用层从来不看感知层——这是和当前许多”VLM 直接做规划”的方案最大的不同。应用层只看记忆层提供的结构化状态。
2.3 三层接口的具体形态
为了让这张图能落到代码,我们给每一层的接口画一份最小可运行的数据契约:
# Layer 1 → Layer 2: Perception output
class Detection:
object_class: str # "key", "drawer", "person", ...
bbox_3d: BBox3D # in camera frame
feature_vec: np.ndarray # for re-id
score: float # detection confidence
timestamp: float
# Layer 2 → Layer 3: Cognition output (aligned observation)
class AlignedObservation:
object_id: str # aligned to existing ObjectRecord, or new
pose_in_world: Pose # transformed to allocentric frame
region: str # which room / area
relations: list[Relation] # on(table), inside(drawer), near(door)
perception_uncertainty: float # from Layer 1
alignment_uncertainty: float # from Layer 2
# Layer 3 internal state
class ObjectRecord:
object_id: str
class_name: str
last_observed_pose: Pose
last_observed_time: float
container: str | None # "drawer_42", or None if free
belief: float # P(currently_at_pose | evidence)
evidence_pointers: list[str] # IDs of observations supporting this state
# Layer 3 query API
def last_seen(obj_query: str) -> tuple[Pose, float, list[str]]:
"""returns (pose, timestamp, evidence_pointers)"""
def is_in(obj_query: str, container: str) -> tuple[float, list[str]]:
"""returns (probability, evidence_pointers)"""
这套数据契约不复杂,但它的完整性是关键——每一层的输出都包含对象身份、位置、不确定性、证据指针。这正是第 1 章给出的”五维结构”在代码上的样子。
3. 简约性 (Parsimony):认知层的核心纪律
三层架构本身只是骨架。真正决定一个空间记忆系统是否能长期跑得动的,是两条贯穿始终的工程纪律。第一条是简约性——它约束的是认知层。
3.1 简约性要解决的问题
回到第 3 章的” 倍带宽差”:感官输入 bits/s,认知决策 bits/s。这件事说明,任何想长期运行的空间智能系统,都必须在认知层做出激进的压缩——把高维观测变成低维、可组合、可查询的状态。
不做简约性压缩,会发生三件事:
- 存储爆炸:原始观测以 GB / 小时的速度堆积,几天内就把硬盘吃光
- 查询无效:用户问”钥匙在哪”,系统去回放过去 24 小时视频找钥匙——这不是智能,是搜索
- 隐私失控:保留全部原始视频意味着保留全部隐私信号,远超”知道钥匙在哪”所需
3.2 简约性的四条具体表现
把这条纪律落到工程上,至少有四个具体表现:
1. 对象中心 vs 帧中心
不要按”帧”组织记忆,要按”对象”组织记忆。
| 方式 | 单位 | 一份每天 1 小时使用的家庭机器人 |
|---|---|---|
| 帧中心 | 帧 / 视频片段 | 100K+ 条记录,按时间堆叠 |
| 对象中心 | 对象(带历史观测列表) | 数百个 ObjectRecord,每个对象的历史 ≤ 几十次观测 |
存储差异是 1000x 量级。更关键的是查询差异:对象中心的”钥匙在哪”是 O(1) 查找,帧中心的”钥匙在哪”是 O(N) 遍历。
2. 关系优先于几何
很多空间记忆原型的默认输出是”对象 + 三维坐标”。但用户问的问题大多数不是”它的 (x, y, z) 是多少”,而是:
- “它在哪个房间?”
- “它在桌上还是抽屉里?”
- “它在窗户附近吗?”
这些问题用关系(on / inside / near)回答比用坐标回答更自然、更稳健、更可解释。一个工程经验:把对象的几何精度做到 cm 级是浪费的——把对象的关系归属做对(哪个房间 / 哪个容器 / 哪个区域)才是真正影响下游决策的部分。
3. 显著性 + 稳定性双门槛
并不是每一个被感知到的对象都值得进入记忆层。一个干净的简约性策略至少要过两道门槛:
- 显著性 (Salience):这个对象对当前应用场景有意义吗?一只跑过摄像头的飞虫不该进记忆层——除非这是个昆虫研究系统。
- 稳定性 (Persistence):这个对象在多次观测中反复出现了吗?只看到一次的对象在融合到记忆前应该被标记为 transient,多次出现后再升级为 stable。
这两道门槛过滤掉的”噪声对象”,往往占感知输出的 80% 以上。简约性纪律的价值,很大程度上就来自这层过滤。
4. 证据指针 vs 证据本身
一个常见的取舍是:是把证据(原始帧 / 点云)存进记忆层,还是只存证据指针(外部对象存储的 ID)?
简约性纪律的答案是后者:
- 记忆层只保留指针 + 摘要(关键帧 ID、传感器源、时间戳、本地特征向量)
- 原始证据放在专门的对象存储里,按需访问、按隐私策略可删
- 这样既保证了”答得出”的可追溯性,又避免了把状态层撑成一个视频仓库
3.3 简约性的反例:把 NeRF / 3DGS 当作记忆主体
工程师容易掉的一个坑是把高保真重建当作空间记忆主体。NeRF / 3DGS 在重建质量上确实惊艳,但它们承担”状态层”角色时简约性纪律全盘破坏:
- 每个场景一个 100MB+ 的 3DGS 模型 → 跨房间 / 跨天就崩
- 没有对象身份 → 用户问”钥匙在哪”无法回答
- 没有关系结构 → “在抽屉里”这种关系无法表达
- 没有置信度衰减 → 重建出的场景”看起来都很真”,实际上可能是几天前的状态
第 6 章会详细展开这件事——这里只是用它做简约性纪律的反例。
4. 自洽性 (Self-Consistency):记忆层的核心纪律
第二条纪律约束的是记忆层。如果说简约性回答”存什么”,那自洽性回答”如何在噪声、缺观测和真实变化中持续相信、怀疑或修正某个状态”。
4.1 自洽性要解决的问题
物理世界中的观测不是绝对真理:
- 光照变化让同一个对象看起来不同
- 遮挡让对象”消失”
- 漂移让坐标系慢慢偏移
- 相似物体让身份匹配出错
- 传感器漏检让对象”凭空消失”
- 真实变化让旧状态突然失效
一个长期运行的系统不能每来一帧就完全覆盖旧状态,也不能因为某一帧没看到就立刻删除对象。它需要用当前内部状态生成预期,再用新观测去校验这个预期;只有当差异超过可解释范围时,才以受控方式更新信念。
4.2 自洽性的核心数据结构:贝叶斯信念
工程上把自洽性落地,最自然的方式是给每个 ObjectRecord 维护一个贝叶斯信念:
class ObjectBelief:
object_id: str
state: str # "at(table_42)", "in(drawer_7)", "missing"
p_state: float # P(state | all evidence so far)
last_evidence_time: float # when did we last update this belief
last_evidence_type: str # "positive_observation" | "negative_scan" | ...
def update_belief(belief: ObjectBelief, new_obs: Observation) -> ObjectBelief:
# P(state | obs) ∝ P(obs | state) · P(state)
likelihood = sensor_model(new_obs, belief.state)
prior = belief.p_state
posterior = likelihood * prior / normalizer(...)
return belief._replace(
p_state=posterior,
last_evidence_time=new_obs.timestamp,
last_evidence_type=type_of(new_obs),
)
这套结构的关键是把”看到了 / 没看到 / 看到但模糊 / 看到但相似”都建模成同一类操作——update_belief(belief, observation)——只是观测的 likelihood 不同。
4.3 自洽性的四种典型情形
把抽象的贝叶斯更新落到具体场景,有四种最值得关注的情形。
情形 1:正向观测 (positive observation)
最简单的情形:在合理位置看到了对象。
- 信念被强化到接近 1
last_evidence_time更新
情形 2:负观测 (negative observation) —— 这是关键
更微妙的情形:在应该看到的地方扫描后没看到。
很多原型在这里就崩了——它们要么把”没看到”当成”对象消失”(删除记录),要么完全忽略(不更新信念)。两个都错。正确做法:
- 如果对象应该可见(容器开着 / 该位置在视野内 / 没有遮挡):负观测应该显著降低信念
- 如果对象不应该可见(容器关着 / 不在视野内 / 被遮挡):负观测几乎不更新信念——因为这本来就是预期
形式化:
这个区分极其重要。它直接对应第 1 章 last-seen / containment 两类查询的核心。
情形 3:时间衰减 (temporal decay)
即使没有任何新观测,旧信念也应该随时间衰减——因为世界本身在变。
- 简单衰减:
- 不同对象有不同 :钥匙这种”经常被移动”的对象 短(几小时),墙上的画这种”几乎不动”的 长(几个月)
- 容器关闭时 显著拉长——容器是”对置信度的保护壳”
情形 4:突变检测 (change detection)
最难的情形:观测和当前信念显著矛盾。
- 信念:“钥匙在书桌上”
- 新观测:“书桌位置看到的是手机,钥匙不在”
两种可能解释:
- 真实变化:钥匙被移走了,应该更新信念
- 感知错误:模型把手机识别成了别的,钥匙其实还在
自洽性纪律要求系统不立即覆盖——而是累积多次观测:
- 第一次矛盾观测 → 信念略降,标记为 “to-verify”
- 第二次独立观测确认 → 信念明显下降,状态变更为 “moved”
- 第三次确认 → 提升为 “high confidence: moved”,并触发”哪里去了”的搜索
这套累积机制是自洽性的核心——它让系统对单次噪声鲁棒,又对真实变化敏感。
4.4 自洽性的反例:每帧覆盖更新
最常见的反例就是”每来一帧观测,就直接用新的状态覆盖旧的状态”。这种做法等价于完全不维护信念,只维护”最近一次观测”。它的失败模式:
- 单次误检 → 立刻进入错误状态
- 容器关闭 → 立刻判定对象消失
- 遮挡 → 立刻判定对象消失
- 用户问 last-seen → 永远只能回答”最近一次看到的位置”,无法处理”看不见但应该还在”
很多看起来”已经在做空间记忆”的系统,本质上就是在做这个反例。区分的判据非常简单:问它”螺丝刀放进抽屉关上后还在吗”——能正确回答,自洽性纪律就到位了;不能回答(说”我看不到所以不在”),就还没到位。
5. 案例 1:容器推理 (Containment Reasoning)
把上面的纪律落到具体代码,最经典的案例是容器推理——也是空间记忆和”普通感知系统”最容易被一题分清楚的地方。
5.1 任务描述
用户行为序列:
- :用户把螺丝刀放在桌上
- :用户把螺丝刀放进抽屉
- :用户关上抽屉
- :用户问:“螺丝刀现在在哪?“
5.2 普通感知系统的回答
只依赖当前帧的系统:
- 时刻看不到螺丝刀(在关闭的抽屉里)
- 系统:「我没看到螺丝刀。」或更糟:「螺丝刀不在了。」
这个回答是功能性失败——它没回答用户真正问的问题。
5.3 自洽性 + 三层架构的回答
按本章的架构,事件会这样在三层里流动:
(看到桌上)
- Perception:检测到 ObjectClass=screwdriver, BBox, Pose
- Cognition:身份对齐到 ObjectRecord(screwdriver_1),关系推断 → on(table_3)
- Memory:更新信念 P(at(table_3)) ≈ 0.95,evidence_pointer = obs_t0
(看到放进抽屉)
- Perception:检测到 screwdriver, drawer,运动轨迹 screwdriver → drawer
- Cognition:检测到容器进入事件 (object enters container)
- Memory:状态从 on(table_3) 切换为 inside(drawer_7),P ≈ 0.95,且记录 container_log
(抽屉关闭)
- Perception:drawer 状态变为 closed
- Cognition:更新 ContainerState(drawer_7).is_open = False
- Memory:标记 screwdriver_1 处于 protected 状态——后续负观测不会显著降低信念
(用户提问)
- 应用层调用
last_seen("screwdriver")和is_in("screwdriver", "drawer_7") - Memory 层:
- last_seen 返回 (Pose@drawer_7, , evidence_pointer)
- is_in 返回 (P ≈ 0.92, evidence_pointers)
- 应用层组装回答:「螺丝刀在抽屉里。我大概一小时前看到您把它放进去,之后抽屉一直关着,所以我相信它仍然在那里——置信度 92%。如果您打开抽屉后没看到,请告诉我,我会重新校准。」
5.4 这套回答的工程含义
这一段对话看起来普通,但它体现了本章三个核心要素:
- 三层分工:感知不知道”放进抽屉”是事件,认知层做的;认知不知道”容器关闭后信念应该被保护”,记忆层做的
- 简约性:记忆里没存任何视频帧——只存了 ObjectRecord + ContainerState + evidence_pointers
- 自洽性:抽屉关闭后,系统主动忽略后续负观测——这正是第 4.3 节情形 2 的工程化
最后一句”如果您打开抽屉后没看到,请告诉我,我会重新校准” 是自洽性纪律最优雅的体现——系统知道自己何时该不相信自己,并主动暴露校准接口。
6. 案例 2:跨会话重定位与漂移区分
第二个案例同样经典:用户重新进入一个房间,系统要回答”这还是同一个空间吗?里面有什么变了?“
6.1 任务描述
事件序列:
- 第 1 天:用户在房间里完成了一次完整扫描,记忆建立了 N 个 ObjectRecord
- 用户离开
- 第 2 天:用户回来,戴上头显 / 启动机器人 / 上车——开始新一轮扫描
第 2 天的扫描结果可能呈现以下几种情况:
- 大部分对象位置一致——可能是同一房间,没什么变化
- 大部分对象位置略偏(5-20cm 系统性偏移)——很可能是坐标系漂移
- 部分对象位置变了——可能是真实物理变化
- 完全不一致——可能用户在另一个相似房间里
6.2 错误做法:直接覆盖
最朴素的实现是”用第 2 天的扫描结果直接覆盖第 1 天的记录”。这种实现的灾难:
- 漂移被当成变化 → 每次重启都”看起来房间变了”,记忆失去意义
- 真实变化被当成漂移 → 用户搬动家具后系统不察觉,下游决策出错
- 跨设备 / 跨用户场景下,每个人的记忆完全独立——无法协作
6.3 自洽性的做法:三步联合解算
正确做法是把”对齐”和”变化检测”作为联合解算问题处理:
Step 1:粗对齐
- 用一组稳定锚点(不太可能被移动的特征:墙角、固定家具、明显地标)做初步配准
- 估算一个全局刚性变换 ,把第 2 天的坐标对齐到第 1 天的坐标
Step 2:评估对齐质量
- 用一些预期不动的对象(重型家具、墙上挂画)作为对齐验证集
- 计算这些对象在 变换后的残差
- 残差小 → 对齐质量高,可以信任
- 残差大 → 对齐失败,可能根本不是同一空间,进入”新房间”模式
Step 3:变化推断
- 在对齐质量良好的前提下,对每个对象做差分:
- 在 变换后位置接近原 ObjectRecord → 状态保持
- 位置显著变化 → 真实物理变化,记录到
change_log - 完全不见 → 触发 last_seen + 容器推理,判断是被收纳了还是真的丢了
- 新出现的对象 → 添加为新 ObjectRecord,关联到当前会话
Step 4:可解释回答
- 应用层调用
changes(room_id, t0=yesterday, t1=now) - 系统返回结构化变化摘要:「书桌上多了一个杯子;椅子朝向变了 30 度;那本之前在沙发上的书现在不见了——是您收起来了吗?」
6.4 这套做法的工程含义
跨会话重定位是三层架构 + 两条纪律的最复杂组合:
- 简约性让系统能在第 1 天只存 ObjectRecord 而不是全部点云——所以第 2 天的对齐成本可控
- 自洽性让系统不轻易把漂移当变化——它先做对齐,再做差分;先怀疑自己,再怀疑世界
- 三层接口让应用层永远只看到”结构化变化摘要”——而不是被点云差分图淹没
这两个案例(容器推理 / 跨会话重定位)会贯穿后面所有章节——第 5 章讨论世界模型 M 层时,会拿它们来对照”latent dynamics 解不了什么”;第 7 章设计评测时,会把它们做成基准任务;第 8 章工程落地时,会给它们一份最小可运行的代码骨架。
7. 三层架构 + 两条纪律:放在一起看
把本章铺开的所有内容压缩成一张”挂在白板上一直看着”的图:
| 层 | 主要纪律 | 工程载体 | 失败模式 |
|---|---|---|---|
| Perception | 完整性 + 不确定性显式化 | 检测器 / 分割器 / VIO / SLAM | 漏检、误检、不确定性被吞掉 |
| Cognition | 简约性 (Parsimony) | 对象表 / 关系图 / 区域归属 | 把高维观测原样放进记忆 |
| Memory | 自洽性 (Self-Consistency) | 信念 + 证据链 + 衰减 + 突变检测 | 每帧覆盖、忽略负观测、漂移当变化 |
简约性约束存什么,自洽性约束怎么更新。这两条纪律加在一起,是”空间记忆能不能长期跑得动”的全部秘密。
8. 章节小结
本章核心结论:
- 三层架构是按”问题类型”拆的——感知是无状态函数,认知是融合函数,记忆是跨时间状态机。这三件事的工程实现不能混在一层里。
- 每两层之间的接口必须是结构化的——不是裸像素、不是 token 流。结构化接口的核心字段是:对象身份、位置、关系、不确定性、证据指针。
- 简约性 (Parsimony) 是认知层的核心纪律——四条具体表现:对象中心、关系优先于几何、显著性 + 稳定性双门槛、证据指针 vs 证据本身。它的反例是”把高保真重建当作记忆主体”。
- 自洽性 (Self-Consistency) 是记忆层的核心纪律——四种典型情形:正向观测、负观测、时间衰减、突变检测。它的反例是”每帧覆盖更新”。
- 容器推理是检验空间记忆系统是否到位的最简单 litmus test——能正确回答”螺丝刀放进关闭的抽屉后还在吗”,整个状态层的设计基本就到位了。
- 跨会话重定位是检验空间记忆系统是否能长期跑得动的关键场景——核心是先怀疑自己再怀疑世界:先做对齐,再做差分;先验证漂移,再判定变化。
- 下一章预告:我们会把本章建立的三层架构放在 World Model 的视角里讨论——经典 V/M/C 架构的 M 层和我们的 Memory 层是什么关系?为什么 latent dynamics 不够?为什么必须把状态对象化、证据化、可查询化?
思考题
- 拿你目前的系统,画一张和本章一样的”四层接口图”——应用 / 记忆 / 认知 / 感知。把每两层之间实际流动的数据写下来。你会发现:很多团队在”应用 ↔ 感知”之间是直连的,没有真正的记忆层——这就是后面所有问题的根源。
- 把第 4.3 节的”四种自洽性情形”套到你的系统:哪几种已经实现了?哪几种完全没处理?尤其是”负观测在容器关闭时不更新信念”这件事——这是最容易没做、做了之后效果立竿见影的一件事。
- 容器推理案例里,最后那句「如果您打开抽屉后没看到,请告诉我,我会重新校准」是关键的工程动作。你的系统里有”主动暴露校准接口”的机制吗?还是说所有错误只能等到下次失败才被动发现?
- 把跨会话重定位的”先对齐 - 评估对齐质量 - 再判定变化”三步法套到你的系统:你目前在第 2 天的扫描和第 1 天的记忆之间,是直接覆盖、还是有联合解算?后者是把空间记忆做”可长期跑”的分水岭。
下一章我们把本章的三层架构与 Ha-Schmidhuber 经典 World Model 架构做严肃对照——指出 M 层和 Memory 层的对应与差异,并解释为什么”latent dynamics”无法替代结构化、对象化、可查询的空间记忆。