增量翻译的变更检测实现方法详解(GitHub 文档翻译工具项目)

全量翻译的路子,文档一多就走不通:每次仓库有 push,就把整站文档翻一遍。文档库上百页,一天几十次 push,模型调用量很快把成本顶到扛不住,翻译耗时也长。于是我决定改成增量翻译,只处理变化的部分。真正动手才发现难点不在”翻译”,而在”怎么准确判断哪些文件变了”。我先后试过只信 commit 信息、全量比对文件内容两种思路,前者漏文件,后者太耗网络,最后落地成 GitHub Contents API + 本地哈希表 + 任务状态机三层组合的方案。下面把变更检测的设计和踩坑过程讲清楚。

一、变更检测的三层结构

先看整体架构。变更检测要回答的是”仓库里哪些文件需要重新翻译”,单靠任何一层都做不完整,所以拆成三层各管一段。

层 数据 作用
GitHub commit、tree、blob SHA 仓库级变更
数据库 TranslationSegment.sourceHash 任务级缓存
内存 当前任务的 hash 集合 任务内去重

GitHub 层靠 commit 和 tree 的 SHA 回答”仓库里什么变了”;数据库层靠 TranslationSegment.sourceHash 回答”这份源文我翻过没有”;内存层靠 hash 集合回答”这次任务里有没有重复文件”。三层各司其职,缺一层就会出现重复翻译或漏翻。最初我只做了 GitHub 层,以为 commit 信息够用,结果同一个文件被多个 commit 命中,一次任务翻了两遍,成本直接翻倍,才补上内存层去重。

二、GitHub 层:从 commit 拿差异

WebHook 携带 push 事件的 commits[],每个 commit 含 added/modified/removed:

{
  "commits": [
    {
      "id": "abc123",
      "added": ["docs/zh/index.md"],
      "modified": ["docs/en/index.md"],
      "removed": []
    }
  ]
}

拿到目标分支最新 SHA head_sha,再调 GET /repos/{owner}/{repo}/compare/{base_sha}...{head_sha} 拿完整差异。

这段解决”本次 push 涉及哪些文件”的问题。commit 数组里的字段拼出第一版变更集合,compare 接口再从 base 到 head 拿完整 diff,两边的信息合并,才能覆盖”多个 commit 合并推送”的场景。这里有个坑:compare 接口有分页,超过限制要按 next 指针翻页。我最初漏了翻页,大 push 事件总是少拿文件,翻译任务偶尔缺漏,排查了挺久。

三、数据库层:判断源文是否变化

每个 TranslationSegment 存 sourceHash = sha256(content)。新一次任务执行前:

const seg = await prisma.translationSegment.findFirst({
  where: { path, jobId: previousJobId },
  orderBy: { createdAt: "desc" },
});

const newHash = sha256(fetchedContent);
if (seg && seg.sourceHash === newHash) {
  // 源文未变,跳过
} else {
  // 创建新片段或更新源文
}

用哈希而不是直接比内容,是为了省传输和比较开销:sha256 结果只有 32 字节,源文可能是几十 KB。这段代码的核心是拿”数据库里最近一次任务、同 path 的片段”当基准,哈希一致就跳过,否则重建片段。容易出错的是基准选择:必须按 createdAt 降序取最新一条,否则 revert 之后再修改的场景会拿旧片段比较,该翻译的没翻译。

四、内存层:任务内去重

任务内可能因合并推送、并发拉取导致同一 path 多次进入。处理:

const seen = new Set<string>();
for (const file of files) {
  if (seen.has(file.path)) continue;
  seen.add(file.path);
  enqueue(file);
}

这段解决”同一个文件在一次任务里只进队一次”。合并推送时多个 commit 可能都改了同一个文件,不去重的话,翻译、写库、推送全部重复执行,既浪费模型调用又污染数据库。Set 去重是开销很小的做法,内存放一个哈希集合,判断是 O(1)。

五、变更检测的判定流程

把三层串起来,完整流程如下。

  1. Webhook 解析出本批变更文件集合 changed;
  2. 拉取每个文件的最新内容;
  3. 计算 hash,与数据库最近一次任务的同 path 片段比较;
  4. 新增 / hash 变化 → 加入待翻译队列;
  5. 删除的文件 → 把数据库片段标记 stale,不删除以备审计。

这个顺序不能乱:先拉内容再算 hash,保证比较的是最新源文;先查数据库再决定入队,避免对已翻译内容重复调用模型。删除文件不直接删记录而是标 stale,是为了审计和历史追溯,这一点在回滚场景里特别关键。

六、与 GitHub 树 API 的配合

对于大仓库,commit diff 包含的列表可能不全。补一步:

const tree = await octokit.git.getTree({
  owner, repo, tree_sha: head_sha, recursive: "true",
});

把整棵树 hash 入库,只对差异文件做内容校验,效率与准确性兼顾。

tree 接口返回整棵文件树的 SHA,相当于仓库当前状态的快照指纹。用它对照差异列表,可以兜住 commit 信息里没体现的变更,比如某些合并场景下 diff 不完整的情况。但整棵树对超大仓库体积不小,所以只在 diff 列表不可信时才全量拉,平时仍以 commit 差异为主。

七、源文变化的边界

哈希比较解决了”变没变”,但”变了要不要翻”还需要定义边界,否则容易误翻。

  • Frontmatter 变化:只翻译指定字段,不重写其它部分;
  • 仅空白变化:hash 不变,跳过;
  • 链接变化:保留链接,更新链接文本;
  • 代码块变化:原样保留,不翻译。

这些边界是测试阶段逐渐总结出来的。文档库经常有”只改空行”的提交,按内容重翻的话,模型会把整段重写,产生一堆无谓 diff;代码块里的内容交给模型翻译几乎必错,所以一律原样保留。Frontmatter 只翻 title、description 这类指定字段,其余元数据不动。

八、回滚与恢复

当 push 是 revert 提交时,diff 列表会反向。处理:

  • 不再翻译 revert 删除的文件;
  • 翻译 revert 重新引入的文件;
  • 数据库里对历史片段保留 “stale” 标记,便于审计。

回滚的坑在于”删除的文件其实恢复了旧版本”,如果只按 diff 的 removed 字段处理,会把重新引入的文件漏掉。这里以最终 head 状态为准:文件存在于 head 就翻译,不存在就标 stale。数据库里所有历史片段保留,配合 sourceHash 可以随时追溯某个版本翻过没有。

九、性能与缓存

变更检测跑得够快,翻译任务才能及时开始。几个优化点:

  • 拉取文件用并发,但并发度按 Installation 限流调整;
  • 内容缓存到 Redis,TTL 5 分钟;
  • 批量拉取用 GraphQL 一次拿多文件元数据。

并发度这个参数我一开始拍脑袋设成 20,结果频繁触发 GitHub 限流,任务大量 403 失败。后来改成按 API 剩余配额动态调整并发,限流问题基本消失。Redis 缓存内容避免同一文件在同批任务里反复拉取,TTL 5 分钟足够覆盖一个任务周期。

十、可观测性

变更检测的正确性要靠观测兜底,没有指标就没法判断逻辑对不对。

  • 指标:扫描文件数、跳过数、待翻译数、翻译成本;
  • 日志:每个被翻译文件记录 path、hash、segment_id;
  • 告警:跳过率突然下降说明可能拉取异常。

“跳过率”是我后来补的指标。正常情况下,大部分 push 应该被哈希命中跳过,跳过率突然下降,大概率是 hash 逻辑出了问题或者拉取异常。光看翻译成功不够,看”该跳过的有没有跳过”才能暴露隐藏的 bug。

十一、回归测试

逻辑改完之后,靠回归测试保证变更检测不出错。测试覆盖四类场景:

  • 模拟无变化 push:任务 0 片段创建;
  • 模拟修改 push:仅修改文件入队;
  • 模拟大批量 push:分片处理;
  • 模拟 revert push:按反向规则处理。

这四条分别验证去重、哈希比对、分页和回滚逻辑。其中”无变化 push”是容易漏的用例——不写这条,某次把 hash 比较改坏时,全量翻译的 bug 可能一周后才在线上暴露出来。

到这里,增量翻译的变更检测链路就完整了:三层结构、哈希比对、边界定义、回滚处理、性能与可观测性,再到回归测试,每一环都有明确的职责和易踩的坑。

常见问题(FAQ)

Q1:仅空白变化要不要重翻译?

不要,hash 不变即可跳过。

Q2:翻译过的文件被删除怎么办?

数据库标记 stale,不删除,便于追溯。

Q3:增量翻译如何保证完整性?

每次执行前用最新 head_sha 全量校验一次任务范围。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
小码农的头像小码农认证作者

相关推荐

返回顶部