"md 是真相,构件是皮肤:多环境数据同步架构实战"
2026-10-16
我在多个环境用 AI 助手,每个环境都有自己的知识库。问题来了:A 环境学到的东西,B 环境怎么知道?
我的解法很简单,一句话:md 是真相,构件是皮肤。多环境之间只同步 md 文件,构件在各环境按指南重建。
为什么做这个
三个约束:
- 环境异构:各环境的构件实现可能不同,不能直接复制数据库
- 数据主权:每个环境的数据是独立的,不能 A 环境直接读 B 环境的库
- 冲突不可避免:多环境同时写,必须有明确的冲突解决规则
传统方案(主从复制、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。
教训:哪些数据同步、哪些不同步,要在设计时就明确。
注意事项
- 先拉后推,禁盲推:推送前必须先拉取远端新版做三方比对。盲推会覆盖别人的修改,甚至误删远端文件。
- 更新分支 ref 的三个坑:
- 更新用复数路径
PATCH /git/refs/heads/main(单数只用于 GET) - sha 必须传完整 40 位,截断的报 422
- URL 编码只编码路径部分,query 参数拼在编码之后
- base64 不要直接传:某些 API 把 base64 文本原样入库变成乱码。正确做法是在代码里做编解码,文件内容保持原文。
- 删文件要显式声明:只传 base_tree + 省略文件,文件会被保留。删文件必须在 tree 数组里显式列出并标记删除。
- 幂等键设计:用"标题+来源日期"做幂等键,不要用自增 id(重建后 id 会变)。
长期维护
- 定期同步检查:每周对比本地 md 与远端 md 的 diff,确认一致。发现漂移立即修复,不要攒。
- 水位线基线更新:知识源结构变化时,重建基线 manifest,并记录变更原因。
- 指南本身是活文档:同步机制、schema、工具链有变化时,同步修订重建指南并推送远端。
- 复核周期:工具类 60 天、经验类 90 天、原则类 180 天复核一次。超期先复核,不直接删除;归档不等于删除。
总结
- 多环境同步的核心是"正本唯一+业务键",不是复制数据库
- 视图层应该是无状态的、可重建的——丢了能重建的才叫视图
- 推送前先拉取,盲推是万恶之源
- 运行时数据和业务数据要分开,前者不跨环境
- 冲突解决规则要在设计时定好,不能等冲突发生了再想
以上,全文完。
最后认识一下,我是 Liyu,一名 AI 实践爱好者,会不定期记录我的学习过程,期待你的关注和分享。