"md 是真相,构件是皮肤:多环境数据同步架构实战"

2026-10-16

我在多个环境用 AI 助手,每个环境都有自己的知识库。问题来了:A 环境学到的东西,B 环境怎么知道?

我的解法很简单,一句话:md 是真相,构件是皮肤。多环境之间只同步 md 文件,构件在各环境按指南重建。

为什么做这个

三个约束:

  1. 环境异构:各环境的构件实现可能不同,不能直接复制数据库
  2. 数据主权:每个环境的数据是独立的,不能 A 环境直接读 B 环境的库
  3. 冲突不可避免:多环境同时写,必须有明确的冲突解决规则

传统方案(主从复制、CRDT)太重了。我要的是轻量、可审计、人工可干预的方案。

工具环境

工具 用途 说明
Git Database API 推送 md 到远端 Python 直调 api.github.com,走"现查 HEAD → 建 blob → 建 tree → 建 commit → 更新 ref"五步,禁盲推
Python(urllib) 推送与拉取脚本 不依赖第三方库;base64 编解码在 Python 内做,不要直接传 base64 文本给 API
水位线脚本 增量检测 对知识源生成基线 manifest,定期对比发现新增/修改/删除
Markdown 唯一正本格式 三段式结构,所有环境以 md 为准

怎么做的

核心设计

远端 md(唯一真相源)
    ↑ push / ↓ pull
本地 md(各环境正本)
    ↑ 双写 / ↓ 重建
构件(视图+行为层)

三层职责:

  • 远端 md:多环境共享的真相源,放私有仓库
  • 本地 md:各环境的正本,与远端 diff 确认一致再动手
  • 构件:纯视图+行为层,数据丢了可以重建

幂等键

跨环境关联不用自增 id(重建后会变),用业务键:

幂等键 = 标题 + 来源日期

灌数据时先查幂等键,已存在就跳过,天然防重复。

双写一致

写视图层的同时追加 md(写正本),两个动作在同一个流程里完成。每周复盘做条目列表 vs md 的 diff,发现漂移立即修复。

先拉后推

推送前先拉取远端最新,diff 确认无冲突再推。禁盲推——盲推曾误删过远端文件。

Git Database API 推送示例(Python):

import base64, json, urllib.request

def push_markdown(content, repo, path, message, token):
    base = f"https://api.github.com/repos/{repo}"
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/vnd.github+json",
    }
    # 1. 查 HEAD
    ref = api_get(f"{base}/git/ref/heads/main", headers)
    head_sha = ref["object"]["sha"]
    commit = api_get(f"{base}/git/commits/{head_sha}", headers)
    base_tree = commit["tree"]["sha"]
    # 2. 建 blob(base64 编解码在 Python 内做)
    blob = api_post(f"{base}/git/blobs", headers, {
        "content": base64.b64encode(content.encode()).decode(),
        "encoding": "base64",
    })
    # 3. 建 tree
    tree = api_post(f"{base}/git/trees", headers, {
        "base_tree": base_tree,
        "tree": [{"path": path, "mode": "100644",
                  "type": "blob", "sha": blob["sha"]}],
    })
    # 4. 建 commit
    new_commit = api_post(f"{base}/git/commits", headers, {
        "message": message, "tree": tree["sha"],
        "parents": [head_sha],
    })
    # 5. 更新 ref(注意用复数路径 /git/refs/)
    api_patch(f"{base}/git/refs/heads/main", headers,
              {"sha": new_commit["sha"]})

注意三个坑: - 更新 ref 用 PATCH /git/refs/heads/main(复数),单数只用于 GET - sha 传完整 40 位,截断的报 422 - 删文件要在 tree 里显式列出,不能只靠省略

四阶段构建

  • Phase A 首次构建:拉 md → 建构件 → 灌数据 → 行为对齐 → 部署检索 → 建水位线 → 校验
  • Phase B 日常增量:md 新增 → 写视图层 → 同步 md → 推送远端
  • Phase C 重建:环境丢失时重复 Phase A(计数从零开始是预期行为)
  • Phase D schema 变更:主环境改 → 修指南 → 各环境同步 → 校验 → 推送

踩过的坑

坑 1:双写漂移

现象:构件里改了,md 没改,两边对不上。

解法:入库 SOP 强制双写;每周 diff 检查。

坑 2:Git Database API 传 base64 被写成乱码

现象:用 create_or_update_file 传 base64,远端文件变成乱码。

解法:Python 内做 base64 编解码,走 api.github.com 直调;推完回读验证。

坑 3:更新分支 ref 报 404/422

现象:PATCH /git/ref/heads/main 报 404。

原因:更新要用复数路径 /git/refs/heads/main;sha 必须传完整 40 位;urllib.parse.quote 只编码路径部分。

坑 4:旧口径有同义表述

现象:grep "只存本地" 找不到,但语义相同的"不上网"还残留在某处。

解法:修正时按语义 grep,不能只按字面。

坑 5:运行时数据不跨环境

现象:重建后 helpful_count 归零,以为是 bug。

原因:计数、检索日志、反馈历史是各环境独立的运行时数据,设计上就不进 md。

教训:哪些数据同步、哪些不同步,要在设计时就明确。

注意事项

  1. 先拉后推,禁盲推:推送前必须先拉取远端新版做三方比对。盲推会覆盖别人的修改,甚至误删远端文件。
  2. 更新分支 ref 的三个坑:
  3. 更新用复数路径 PATCH /git/refs/heads/main(单数只用于 GET)
  4. sha 必须传完整 40 位,截断的报 422
  5. URL 编码只编码路径部分,query 参数拼在编码之后
  6. base64 不要直接传:某些 API 把 base64 文本原样入库变成乱码。正确做法是在代码里做编解码,文件内容保持原文。
  7. 删文件要显式声明:只传 base_tree + 省略文件,文件会被保留。删文件必须在 tree 数组里显式列出并标记删除。
  8. 幂等键设计:用"标题+来源日期"做幂等键,不要用自增 id(重建后 id 会变)。

长期维护

  • 定期同步检查:每周对比本地 md 与远端 md 的 diff,确认一致。发现漂移立即修复,不要攒。
  • 水位线基线更新:知识源结构变化时,重建基线 manifest,并记录变更原因。
  • 指南本身是活文档:同步机制、schema、工具链有变化时,同步修订重建指南并推送远端。
  • 复核周期:工具类 60 天、经验类 90 天、原则类 180 天复核一次。超期先复核,不直接删除;归档不等于删除。

总结

  1. 多环境同步的核心是"正本唯一+业务键",不是复制数据库
  2. 视图层应该是无状态的、可重建的——丢了能重建的才叫视图
  3. 推送前先拉取,盲推是万恶之源
  4. 运行时数据和业务数据要分开,前者不跨环境
  5. 冲突解决规则要在设计时定好,不能等冲突发生了再想

以上,全文完。

最后认识一下,我是 Liyu,一名 AI 实践爱好者,会不定期记录我的学习过程,期待你的关注和分享。

← 返回文章列表