← 返回文章列表

为什么 Neo4j 不是事实源:TeachFlow 的学习证据、Current 与投影数据血缘

从一次正式作答出发,拆解 TeachFlow 如何保留不可变证据、确定性重算 Current、治理纠错,并通过 Outbox 把可重建视图投影到 Neo4j。

返回 TeachFlow 项目总览 · 交互图:LEARNING · 证据 · 投影 · 血缘

很多学习系统都有一张“知识图谱”和一个“掌握度”。真正难回答的问题却不是图画得多漂亮,而是:某个学生的 62% 从哪里来?一次订正后,为什么它应该变化?图库宕机时,作答还能不能成为正式事实?半年后能否重放出当时的判断?

TeachFlow 对这些问题采用了一个有意保守的答案:先让事实在 PostgreSQL 中成立,再计算 Current,最后把需要多跳查询的视图投影到 Neo4j。 图数据库可以失败、延迟或被重建,但不能成为学生历史唯一且不可恢复的持有者。

本文只讨论这一条数据链。它不评价模型回答质量,也不声称掌握度规则已经证明教学效果;重点是系统怎样区分事实、当前结论和查询投影,以及失败时究竟保住什么。

先定义三个不同的“真相”

TeachFlow 没有把所有数据都叫状态,而是把它们分成三个层次:

  1. 事实:学生做过什么、提交了什么、服务端怎样评分、谁提出和审核了纠错。这些记录追加保存,普通运行时不能覆盖。
  2. Current:在给定算法版本、图版本和证据截止时间下,当前应该向产品提供的个人或班级结论。它可以更新,但必须能指回 Snapshot 和输入事件。
  3. 查询投影:为了先修路径、误区路径和子图分析而写入 Neo4j 的结构。它服务查询,不拥有不可恢复的业务真相。

这个区分解决了一个常见混乱:事实不可变,不等于界面永远不变;Current 会变化,也不等于历史被改写;Neo4j 中有一条掌握关系,也不等于它是权威来源。

TeachFlow 学习证据、Current、Outbox 与 Neo4j 的完整数据血缘

上图从左到右展示主路径:正式来源先经过 Scored Evidence Gate,随后形成 attempt、response 和 event;计算器把合格事件写成不可变 Snapshot,个人 Current 指向最新 Snapshot,班级 Current 再由成员的个人 Current 聚合;同一 PostgreSQL 事务只追加 Outbox,异步 Worker 最后更新 Neo4j。下方纠错路径不会擦除原事件,而是追加提议与审核决定后触发重算。

打开交互版学习证据与投影数据血缘图

第一道门:不是“看起来像分数”就能成为证据

教师录入已批改成绩时,调用方可以提交学生、任务、原始答案、正误、是否独立、是否使用提示和带时区的发生时间。知识点、权重和图版本不能由浏览器任意指定,服务端会从班级当前图与正式 LearningTask 读取。

任务类型也受限制:只有 assessmentdiagnostic-probenear-transferfar-transfer 会映射为合格评分证据。普通 BoardEvent、OCR 识别结果、LLM 判断或“完成了一次干预”都不能直接变成掌握事实。

写入后的重算还会再做一次来源白名单过滤。SQL 只读取已发布 Quiz,或采用 scored-v1 且来源属于正式路径的事件;被教师接受纠错的源事件则从计算输入中排除。这意味着,即使有人构造了一个外形相似的 JSON,也不能仅凭字段名称把 AI 结论塞进 Current。

这里的设计压力来自教育数据的特殊性:模型可以提出“可能不会通分”,但“可能”不是已经发生的作答;教师可以录入分数,但不能顺便改写题目所评测的知识点;前端可以显示建议,却不能成为学情权威。

事实层:重试可以幂等,历史不能被覆盖

一次正式评分会形成 learning_attemptslearning_responseslearning_events。事件保存发生时间 occurred_at 与入库时间 recorded_at,并带上图版本、来源、正误、独立性和提示使用等信息。

为了处理网络重试,服务端先对证据 ID 加事务级 advisory lock,再检查同一事件是否已经存在:

  • ID 和内容完全一致,返回 replayed: true
  • 同一 ID 对应不同学生、题目、答案、评分或 payload,抛出冲突;
  • 不存在时才追加 attempt、response 和 event。

数据库触发器禁止更新这些学习事实。删除也不是普通后台权限:必须切换到专用 retention 角色,提供与目标表匹配的追加式保留决定,并且记录早于批准的 cutoff。这个设计并不意味着“永不删除”,而是让订正、合规清理和篡改成为三种可区分的行为。

Snapshot 与 Current:可重算不等于每次读请求都重放历史

TeachFlow 的掌握度计算器是确定性规则 deterministic-state-v2。它先按截止时间过滤证据,再按发生时间与事件 ID 稳定排序;独立且无提示的正确作答计为 1,正确但依赖提示或不独立计为 0.6,错误计为 0,再按知识点权重汇总。

计算结果不只有一个百分比,还包括:

  • masteryLevelunknown / emerging / developing / secure
  • diagnosticConfidence:当前诊断置信度;
  • transfer:迁移类证据表现;
  • hintDependency:对提示或非独立完成的依赖;
  • inputEventIdshypothesisIds 和计算指纹。

每次计算先追加 learner_state_snapshots。Snapshot 记录算法版本、证据截止时间、输入事件和完整状态;随后 learner_knowledge_current 才在结果更新时推进版本,并指向对应 Snapshot。产品读取 Current,审计和重放读取 Snapshot 与事实。

这样做的理由很实际:每次打开首页都从全部事件重算会放大延迟和数据库压力;只保存一个可变百分比又无法解释来源。Snapshot + Current 同时保留了可追溯性和服务效率。

unknown 不是 0 分

如果没有合格证据,计算器会输出 masteryLevel: unknown。数值字段可能为了类型和聚合保持为 0,但产品语义必须是“证据不足”,不能渲染成学生不会,也不能拿它参与有证据节点的平均掌握度。

学生尚无正式证据时,界面引导留下第一条学习轨迹

这张图使用本地合成演示身份。它证明的是界面状态:系统明确说明不使用预设成绩,并把下一步指向一次正式学习;它不证明真实学生的学习表现。

随文执行了三个无外部依赖的确定性检查:

  • 事件输入顺序颠倒后,状态与计算指纹保持一致;
  • 截止时间之后的事件不会进入较早 Snapshot;
  • 无证据返回 unknown
  • 6 组专家设计的合成纵向边界样例、24 个检查点全部符合预期层级;
  • 结果汇总的样本门槛、层级精度和非法输入拒绝通过。

这些结果验证的是实现的一致性与回归边界,不是算法对真实教学结果的预测效度。

班级 Current:聚合读模型不能反推个体

个人 Current 推进后,投影器会读取当前班级的有效学生 Enrollment,用各学生的个人 Current 重建 class_knowledge_current。班级记录保存学生数、掌握层级计数、平均掌握度、平均诊断置信度和高频误区,并拥有独立版本号。

教师与学生随后读取不同 DTO:学生只得到本人已有证据节点与最近证据;教师得到班级观察覆盖、分布和优先关注节点,不返回个人作答。这个边界既是隐私控制,也防止产品把一个班的平均值冒充某个学生的结论。

教师知识图谱页面区分班级聚合、observed 与 unobserved

图中 student-a / student-b / student-c 是合成演示身份。observed 表示已有正式证据,unobserved 表示证据不足;它不能被解释为学生不会。班级 62% 也是当前样例的聚合结果,不是教学效果指标。

纠错:改变 Current,但不改写原作答

学生可以对进入本人 Current 的某条证据提出异议。提议写入 evidence_corrections,但状态为 proposed 时不会改变 Current。教师审核会追加一条后继决定:

  • rejected:保留原事件继续参与计算;
  • accepted:在重算查询中排除源事件,并以审核时间作为新的有效时间生成 Snapshot;
  • 排除后没有其他合格证据:状态回到 unknown

原事件、纠错提议和审核决定都继续存在。由被排除事件派生的 Agent 记忆和候选也会退出可信读取。这里最重要的不是“支持改分”,而是把“历史发生了什么”和“当前应该相信什么”分开保存。

为什么 Outbox 必须和 Current 在同一事务

个人 Current 更新、班级 Current 重建和 graph_projection_outbox 追加都使用同一个 PostgreSQL client 与事务。Outbox 的事件 ID 由聚合类型、主键和版本确定,重复追加使用 ON CONFLICT DO NOTHING 收敛。

这避免了经典双写问题:如果业务先提交 PostgreSQL,再直接请求 Neo4j,进程可能在两次写之间崩溃,系统既不知道图库缺了什么,也没有可靠重放依据。Transactional Outbox 把“应该投影什么”先变成 PostgreSQL 内的事实;Neo4j 是否当场可用不影响这次业务提交的原子性。

这里也没有宣称 exactly-once。Worker 可能在 Neo4j 已写成功、但 processed_at 尚未回写时崩溃,租约过期后同一任务会再次执行。正确目标是至少一次领取 + 幂等投影 + 版本单调收敛:Cypher 使用 MERGE,个人和班级状态还通过 source version 防止旧投影覆盖新状态。

Worker 失败时,系统究竟保住什么

Worker 使用 FOR UPDATE SKIP LOCKED 领取一条待处理 Outbox,并写入 locked_by / locked_until。成功后标记 processed_at;失败则增加 attempts、记录 last_error,并按指数退避安排下一次尝试。

当前实现只自动领取 attempts < 5 的任务。超过上限不会被无限重试,因此需要运维检查错误、修复依赖或执行全量重建。这是一条必须明确说明的运行边界:Outbox 让失败可见、可恢复,但并不自动提供生产级告警和值班体系。

在图库故障期间,PostgreSQL 中已经提交的学习事实、Snapshot、个人/班级 Current 和待处理 Outbox 仍然保留。依赖 Current 的角色化摘要可以继续拥有权威来源;依赖 Neo4j 多跳查询的图 Agent 则应显式降级或失败,不能用模型臆测替代结构化图结果。

为什么 Neo4j 可以被删掉重建

全量重建函数从 PostgreSQL 读取四类当前材料:已发布知识图、已批准教材映射、个人 Current 和班级 Current,再逐项执行同一套投影函数。Neo4j 节点和关系使用稳定业务键,重复执行通过 MERGE 与版本守卫收敛。

因此,Neo4j 的价值是把“这个薄弱知识点有哪些前置节点”“某个误区沿哪条路径传播”之类多跳查询做得自然,而不是保存唯一历史。真正不可替代的信息仍在 PostgreSQL:原作答、发生时间、纠错决定、Snapshot 输入和 Current 版本。

项目冻结基线记录过全量重建、二次重放、租约恢复、并行领取和图库不可用恢复,但本文的独立验证没有重新启动 PostgreSQL/Neo4j 执行这些集成检查。因此,本文把机制标为源码确认,把历史基线标为 partial,不把旧回执冒充当前 HEAD 的实跑结果。

被拒绝的三种更简单方案

1. 只保留一个可变掌握度字段

实现简单,却无法回答它由哪些事件、哪个算法版本和哪个图版本产生;订正只能覆盖旧值,历史解释链消失。

2. 让 Neo4j 同时承担事实与查询

可以少维护一套投影,但学习事件、事务纠错、角色化聚合和保留策略会与图查询耦合。图库故障会升级成正式证据不可提交,灾备也必须恢复一份不可替代的异构事实源。

3. 业务事务直接双写 PostgreSQL 与 Neo4j

表面上延迟更低,实际上无法获得跨数据库原子提交。任何一次网络超时都可能留下“数据库成功但图库未知”的灰色状态,而重试又容易制造倒序覆盖。

TeachFlow 选择的代价是更多表、版本和 Worker 运维;换来的不是抽象上的优雅,而是每一层都能说清谁负责、失败后剩什么、怎样重算。

常见问题与设计边界

为什么不直接称为 Event Sourcing?

关键学习事实以追加方式保存,Current 可由受控历史重算。课程 Current、班级关系和其他治理表仍有各自权威模型,并非所有业务状态都从统一事件日志派生。

Current 会不会与 Snapshot 不一致?

个人 Current 只保存指向 Snapshot 的外键与冗余 state,更新发生在同一事务;写入时还使用 advisory lock 和版本推进。需要进一步证明的是生产并发、迁移和灾备恢复下的长期一致性,本文没有做这部分负载验证。

Outbox 是 exactly-once 吗?

不是。领取和回写之间允许重复执行,所以投影使用稳定键、MERGE 和 source version 收敛。系统追求的是可检测、可重放、幂等的最终一致,而不是跨 PostgreSQL 与 Neo4j 的分布式 exactly-once。

超过 5 次失败怎么办?

任务仍留在 Outbox 并保留 last_error,但当前 Worker 不再自动领取。生产化需要积压指标、告警、人工重试/死信处置和容量基线;必要时也可以从 PostgreSQL 全量重建图库。

这套掌握度算法可靠吗?

随文证据只能证明规则确定、边界样例通过、输入可追溯。没有真实学校对照、长期标签和外部效度研究,不能声称它准确预测学生能力或提升教学效果。

当前边界

这篇文章已经由当前源码、数据库迁移、三项确定性检查、两张本地产品截图和一份通过多视口检查的 Archify 数据血缘图支持。可确认的是:

  • 合格证据经过服务端门禁并追加保存;
  • Snapshot 记录算法、截止时间和输入,Current 可重算;
  • 纠错追加决定而不覆盖原事件;
  • 个人、班级和图库投影拥有不同权威边界;
  • Outbox 让 Neo4j 故障不会抹掉 PostgreSQL 中已经成立的事实。

仍不能确认的是学校生产环境中的 IAM、TLS、隐私合规、容量、HA/DR、告警值班,以及掌握度对真实教学效果的有效性。更准确的项目表述是:TeachFlow 已建立一条可解释、可重算、可降级的本地学习证据纵切片;生产可信与教学有效仍需要独立验证。

← 返回文章列表