事实与事件设计
Fact Layer 是四大真源之一,负责统一记录"发生了什么";本文同时涵盖外部事件如何统一抽象并进入 Fact Layer。 它不解释事实、不判断优先级、不维护议程,只保证每一条事实可追溯、不可变、保留原始上下文。 如本文与《产品与架构设计》冲突,以后者为准。 最后更新:2026-03-18
一、架构定位
在统一认知架构的六层体系中,Fact Layer 的位置如下:
text
Interaction Layer(接收消息、主动触达)
↓
Fact Layer ─── 统一记录事实输入与动作结果
↓
Person Layer ── 维护人物真源
Cognitive Core ─ 解释事实、统一决策
↓
Concern Layer ── 持续事项状态机
↓
Action Layer ── 渠道发送、工具执行
↓
动作结果回写 Fact Layer一句话定义:
Fact Layer 是系统对"世界发生了什么"的唯一记录层。所有判断、议程、人物更新都必须能追溯到某条 Fact。
二、与现有实现的映射
当前事实记录分散在三个集合中,迁移策略如下:
| 现有集合 | 当前职责 | 迁移方向 |
|---|---|---|
events (scripts/memory/event_store.py) | 通用事件流,含消息、审批、日程等 | 升级为 Fact 主存储,字段对齐 Fact 数据模型 |
person_events (scripts/person/store.py) | 按人索引的事件(task_signal、concern_signal、dialog_signal) | 保留为按人索引视图,person_refs 指向 Fact 主记录 |
decision_log (scripts/concern/decision_log.py) | 系统决策记录(discovery、action) | 保留,归类为 action_result 类 Fact |
关键变化
events集合新增fact_id、fact_type、person_refs、raw_context字段person_events不再独立存储事实本体,改为引用fact_iddecision_log的每条记录同步写入一条action_result类型的 Factlog_event()继续作为 Fact 写入 facade,内部直接对齐新模型
三、Fact 数据模型
统一结构
python
@dataclass
class Fact:
fact_id: str # 全局唯一,格式 fact_{date}_{seq}
fact_type: str # 见第四节分类
source: str # 来源标识(feishu / telegram / gitlab / system / scheduler)
person_refs: list[str] # 关联人物 person_id 列表
content: dict # 结构化事实内容
raw_context: dict # 原始上下文(保留完整 payload)
timestamp: str # 事实发生时间(ISO 8601)
metadata: dict # 扩展字段(trace_id、channel、session_type 等)JSON 示例
json
{
"fact_id": "fact_20260318_001",
"fact_type": "dialog",
"source": "feishu",
"person_refs": ["p_zhangsan"],
"content": {
"direction": "in",
"text": "支付接口联调卡住了,第三方那边没给回调地址",
"msg_type": "text"
},
"raw_context": {
"channel": "feishu",
"channel_user_id": "ou_xxx",
"msg_id": "om_xxx",
"chat_id": "oc_xxx"
},
"timestamp": "2026-03-18T10:30:00+08:00",
"metadata": {
"trace_id": "tr_abc123",
"session_type": "avatar"
}
}字段约束
| 字段 | 必填 | 说明 |
|---|---|---|
fact_id | 是 | 原子自增,格式 fact_{YYYYMMDD}_{seq:03d} |
fact_type | 是 | 枚举值,见第四节 |
source | 是 | 事实来源渠道或系统标识 |
person_refs | 否 | 允许为空(如系统级定时触发可能无关联人) |
content | 是 | 结构化内容,按 fact_type 不同有不同 schema |
raw_context | 否 | 原始 payload,用于审计和回溯 |
timestamp | 是 | 事实发生时间,不是写入时间 |
metadata | 否 | 扩展字段,不影响核心逻辑 |
四、fact_type 分类
| fact_type | 定义 | 典型场景 |
|---|---|---|
dialog | 对话事实,含用户消息和系统回复 | 飞书/TG 收到消息、系统发出回复 |
external_event | 外部系统事件 | 飞书审批状态变更、GitLab merge、任务状态变更、日程变化 |
action_result | 系统动作结果 | concern 发送消息、升级通知、工具查询结果、回流报告 |
schedule_trigger | 定时触发记录 | 定时 concern 到点、心跳扫描触发、日报生成 |
absence_detection | 缺席检测结果 | 预期提测未发生、日报超时未提交、审批超 SLA 未处理 |
各类型 content 示例
dialog
json
{
"direction": "in",
"text": "帮我催一下张三提测",
"msg_type": "text"
}external_event
json
{
"event_type": "task_status_change",
"task_id": "task_001",
"old_status": "开发中",
"new_status": "已提测",
"operator": "p_zhangsan"
}action_result
json
{
"action": "send_message",
"target_person_id": "p_zhangsan",
"channel": "feishu",
"message_preview": "老板想了解提测什么时候能给?",
"concern_id": "con_20260318_001"
}schedule_trigger
json
{
"schedule_id": "sch_daily_report",
"schedule_name": "每日简报",
"attempt": 1
}absence_detection
json
{
"expected_event": "gitlab_merge",
"expected_by": "2026-03-18T18:00:00+08:00",
"watch_id": "watch_001",
"description": "张三承诺今日提测,截至 18:00 未检测到 merge 事件"
}五、写入规则
谁写
| 写入方 | 写什么 | 示例 |
|---|---|---|
| 渠道入口(TG/飞书 Bot) | dialog | 用户消息、系统回复 |
| 事件适配器(EventAdapter) | external_event | 飞书 webhook、GitLab webhook |
| 动作执行器(Action Layer) | action_result | concern 发消息、升级、回流 |
| 调度器(Scheduler) | schedule_trigger | 定时 concern 触发 |
| 认知中枢(缺席检测) | absence_detection | 预期事件未到达 |
什么时候写
- 即时写入:消息收到时、事件到达时、动作执行完成时
- 不延迟:Fact 写入不应等待后续判断完成
- 先写 Fact,再做判断:这是整个链路的硬前提
不可变原则
Fact 一旦写入,不允许修改 content 和 raw_context。
允许的操作:
- 补充
metadata(如后续关联的 concern_id) - 标记
processed: true
禁止的操作:
- 修改事实内容
- 删除已写入的 Fact
- 用后续判断结论反写事实字段
原始上下文保留
raw_context 必须保留足够信息用于回溯:
- 渠道原始 payload(或关键字段)
- 原始消息 ID
- 原始用户标识
- 触发链路标识(trace_id)
六、查询接口
核心查询维度
| 查询方式 | 接口签名 | 用途 |
|---|---|---|
| 按人查询 | query_facts(person_ref=, limit=) | 查某人相关的所有事实 |
| 按时间范围 | query_facts(since=, before=, limit=) | 查某段时间内的事实 |
| 按类型 | query_facts(fact_type=, limit=) | 查某类事实(如所有缺席检测) |
| 按关联 concern | query_facts(concern_id=, limit=) | 查某个 concern 相关的所有事实 |
| 组合查询 | query_facts(person_ref=, fact_type=, since=) | 多条件组合 |
| 单条查询 | get_fact(fact_id) | 精确查询 |
接口约束
- 默认按
timestamp倒序 - 返回结果不含 MongoDB
_id,统一用fact_id标识 raw_context默认不返回,需显式请求(减少传输量)- 聊天历史查询继续通过
get_chat_history()facade 提供
七、与其他层的关系
text
Fact Layer
├─→ Perception(感知层输入)
│ 事实到达 → 感知层理解 → 形成 Signal
│
├─→ Cognitive Core(判断依据)
│ Signal + Fact + 当前状态 → 统一裁决
│
├─→ Person Layer(更新证据)
│ 对话/事件中的客观状态 → 更新 work_state 的事实依据
│
├─→ Concern Layer(推进证据)
│ 回复、业务数据变化 → advance_concern 的证据来源
│
└─→ Memory(提炼来源)
值得长期保留的事实 → 记忆提炼链路 → 经验/规则/偏好各层与 Fact 的关系
| 层 | 读 Fact | 写 Fact | 说明 |
|---|---|---|---|
| Interaction | 否 | 是 | 消息到达即写入 dialog |
| Perception | 是 | 否 | 读取事实用于理解,不改写事实 |
| Cognitive Core | 是 | 否 | 读取事实用于判断,决策记录由 Action 回写 |
| Person | 是 | 否 | 读取事实作为状态更新依据 |
| Concern | 是 | 否 | 读取事实作为推进证据 |
| Action | 否 | 是 | 动作结果回写 action_result |
| Memory | 是 | 否 | 读取事实用于长期提炼 |
八、存储策略
MongoDB 集合设计
主集合:facts(从现有 events 集合升级)
json
{
"fact_id": "fact_20260318_001",
"fact_type": "dialog",
"source": "feishu",
"person_refs": ["p_zhangsan"],
"content": { ... },
"raw_context": { ... },
"timestamp": "2026-03-18T10:30:00+08:00",
"metadata": { ... },
"created_at": "2026-03-18T10:30:01+08:00"
}索引建议
| 索引 | 字段 | 用途 |
|---|---|---|
idx_facts_id | fact_id (unique) | 精确查询 |
idx_facts_timestamp | timestamp DESC | 时间范围查询 |
idx_facts_type_ts | fact_type + timestamp DESC | 按类型查询 |
idx_facts_person_ts | person_refs + timestamp DESC | 按人查询 |
idx_facts_source_ts | source + timestamp DESC | 按来源查询 |
idx_facts_concern | metadata.concern_id | 按关联 concern 查询 |
idx_facts_trace | metadata.trace_id | 链路追踪 |
TTL / 归档策略
| 策略 | 规则 | 说明 |
|---|---|---|
| 热数据 | 最近 30 天 | 保留完整 content + raw_context |
| 温数据 | 30-90 天 | 保留 content,raw_context 压缩归档 |
| 冷数据 | 90 天以上 | 仅保留 fact_id / fact_type / person_refs / timestamp 索引记录,全文归档到对象存储 |
dialog 类特殊处理 | 与 owner 的对话永不归档 | 老板对话是长期决策依据 |
归档操作由离线任务执行,不影响在线写入。
九、事件接入
外部系统事件如何统一抽象、路由,并最终进入 Fact Layer,供认知中枢进一步判断。 事件系统只负责事实进入系统,不直接拥有议程决策权。
9.1 目标
事件系统负责把外部变化转成系统内部可处理的统一事件。
典型来源:
- 飞书 webhook
- GitLab webhook
- 业务系统推送
统一事件进入后,再由认知链路决定:
- 更新人物状态
- 写入记忆
- 进入议程并创建 concern
- 直接通知
9.2 统一结构
python
UnifiedEvent(
source="feishu" | "gitlab" | "...",
event_type="...",
target_person_id="p_xxx" | None,
payload={...},
timestamp=...,
event_id="...",
)统一事件的关键不是平台字段,而是:
- 事件是什么
- 影响了谁
- 系统要如何继续处理
9.3 处理流程
text
Webhook / 事件推送
↓
EventAdapter
├─ 验签
├─ 去重
└─ 转为 UnifiedEvent
↓
EventRouter
↓
Handler
├─ 补详情
├─ 找到相关 person_id
├─ 写入 Fact Layer(事实记录)
└─ 由调用方显式 dispatch 到 awareness → 认知中枢链路9.4 与人物档案的关系
人员相关事件进入后,统一写入人物体系:
- 自动补齐或创建
people - 更新
person_work_state - 写入
person_events
这保证事件处理和对话识别人使用的是同一份人物主档。
9.5 与 Concern 的关系
以下事件通常会形成"值得进入议程"的强信号:
- 任务逾期
- 审批长时间无进展
- 明确的阻塞、异常、升级信号
原则应收口为:
- 事件只负责把事实送进系统
- 是否持续跟进,交给统一认知中枢判断
- 一旦正式进入议程,再由 Concern 承接持续推进
9.6 事件接入原则
- 外部事件统一转成
UnifiedEvent - 事件影响到人时,先定位
person_id - 需要持续推进的事件先进入议程判断,再由 Concern 承接
- 需要长期回忆的结论进入 Memory
- 需要实时响应但不持续跟进的事件直接通知或静默处理
十、当前原则
- 先写 Fact,再做判断 -- 所有输入先进入 Fact Layer,不跳过
- 事实不可变 -- 写入后不修改
content和raw_context - 保留原始上下文 --
raw_context保留足够信息用于回溯和审计 - 每条事实可追溯 -- 有明确的
source、timestamp、trace_id - 不解释、不判断 -- Fact Layer 只记录发生了什么,不决定它意味着什么
- 统一模型 -- 所有类型的事实用同一个 Fact 结构,通过
fact_type区分 - 统一入口 --
log_event()/query_events()/get_chat_history()只是 Fact Layer facade,不再代表独立旧模型 - 按人可查 --
person_refs确保任何事实都能按人检索
Fact Layer 的职责可以概括成一句话:
让系统的每一个判断都能追溯到"到底发生了什么"。