搜索

iainfrastructure

我的 AI-Powered Markdown Translator 脚本更新(v1.9):新功能,以及在不做人审的情况下追求干净代码的方法

我的 AI-Powered Markdown Translator 脚本更新(v1.9):新功能,以及在不做人审的情况下追求干净代码的方法

ai-powered-markdown-translator

由 gpt-5.4-mini 将法语翻译成中文的文章。

在 GitHub 上查看项目 ↗

AI-Powered Markdown Translator 是一个我自 2024 年起一直维护的开源项目:这是一个 Python 脚本,可通过 4 家 AI 提供方(OpenAI、Mistral AI、Claude、Gemini)把任意 Markdown 文件翻译成 14 种语言。它为这个博客的每次发布提供支持——你在这里读到的每一页,只要不是法语,基本都经过了它——而且它在生产环境中已经驱动了将近 1,800 个已翻译版本

2026 年 5 月 8 日,我发布了 v1.9,它汇总了 75 次提交,也是自 2024 年 v1.5 以来最大的一次更新。三个产品新功能:

  1. 翻译后校验(防止静默失败)
  2. 多位置翻译说明(顶部、底部或两者都放)
  3. --news 模式,用于保留英文源引用

但这个 v1.9 有一个我想在这里讲清楚的特点:全部代码都是通过 AI 结对编写的。没有一行是手工输入的。所以除了这 3 个新功能之外,这篇文章还会讲“怎么做”:当你不亲自复查 AI 产出的内容时,如何设置护栏,去追求干净且安全的代码?

背景:一个每天都在用、但代码维护并不频繁的项目

从 2024 年 9 月到 2026 年 5 月:持续使用,间歇维护

我曾经发布过一篇详细介绍 2024 年 v1.5 源代码的文章。那时候,我是直接把脚本贴在文章里。如今角度变了:重要的不再是我写了什么代码,而是生成这些代码的工作流。

2024 年 9 月发布的 v1.52026 年 1 月 之间,这个项目一直在运行——它会翻译这个博客的每一篇新内容——但公开代码几乎没怎么动。2025 年只推过一个提交。期间,我一直在本地按自己的需求演进代码——主要是模型,我会随着新版本发布不断替换——但这些变化都停留在我的机器上。GitLab 上的公开版本仍然指向 v1.5 的默认值。

2026 年初,我做了第一次系统性升级:两个月内发布了三个版本(1 月初两天内的 v1.6 和 v1.7,以及 3 月的 v1.8),把功能层面更新到了最新——2026 年模型、Gemini 支持、--eco 模式、单文件、以及用于源引用的 --news 模式。但依然没有 CI,没有自动化测试,也没有质量门禁——这让我在让一个 AI 代理代我写代码时,无法继续放心往前推。

个人时间里的项目节奏

为什么会有这个时间差?因为这个项目是我用个人时间在维护。我有家庭,屏幕之外也有生活,所以项目只能在我挤出晚间和周末时一段一段地推进。我确实很投入,也会花不少时间在这些事情上——测试很多,指导代理,验证结果——但节奏终究不是职业项目那种。

AI 结对恰好改变了这一点。它让我能在两种约束之间推进——热情和屏幕之外生活的分配。没有 AI 结对,我显然不可能走到这么远,也不可能这么快。有了它,我就能在不把整个人生都投入进去的前提下,维护一个工业级开源项目。

最初目标:质量 + GitLab → GitHub 迁移

2026 年 4 月中旬,我终于决定认真处理这件事。两个简单目标:

  1. 增加一层质量保障(静态分析、测试、CI)
  2. 把仓库从 GitLab 迁移到 GitHub

仅此而已。只是,和 AI 结对写代码的代理一起工作时,你永远不会只做你原本计划的事情。最终这个 PR 变成了 75 次提交、9,837 次新增、1,982 次删除、58 个文件

版本日期主要贡献
1.0–1.42024OpenAI,随后是 Mistral,再随后是 Claude
1.52024 年 9 月重构客户端,2024 年模型(gpt-4o、claude-3.5-sonnet)
1.62026 年 1 月2026 年模型(gpt-5、claude-sonnet-4-5、gemini-3-pro)、Gemini、--eco 模式、单文件(--file
1.72026 年 1 月--keep_filename.env、保留内联代码
1.82026 年 3 月默认 GPT-5.4 模型、带引用占位符的 --news 模式
1.92026 年 5 月翻译后校验、多位置说明、14 个 hooks + 229 个测试 + AI 审查的质量栈

雪球效应

每增加一个质量工具,都会暴露新的问题。Codacy 报出了重复代码。SonarCloud 提出了若干 code smells(提示代码将来会难以维护的信号:函数过长、参数未使用、结构绕弯等)。/pr-review-toolkit 指出了隐藏 bug。每次出现问题,代理都会修复,有时还会顺手把相邻部分一起改好。

项目范围就这样自然地膨胀了。这正是我想要的——让项目现代化——但工作量的节奏却由工具决定,而不是由我决定。对于一个 vibe coding 项目来说,这一点很关键:质量工具既在验证工作,也在引导工作。

新功能 1:翻译后校验(防止静默失败)

事故:是 AI 在测试期间发现了 bug

在不同公共仓库的 README 上测试这个 PR 时——这类场景任何 fixture 都没有覆盖——AI 指出了我漏掉的问题:在某些语言中(尤其是印地语,ISO 代码 hi),有些片段在翻译结果里仍然保持源语言。API 已经返回了 200,脚本也把文件写出去了,但内容却只翻译了一半。而这还能穿过现有的单元测试套件——因为那些测试并没有覆盖这个真实的多语言场景。

这正是 vibe coding 可能产生、而且没人会注意到的那类 bug。代码看起来很合理,测试 fixture 没覆盖到这个场景,人类也不会复查结果。可就在这里,当我用真实案例(多仓库)测试脚本时,AI 自己做了 fixture 没做到的事。

我的结论是:真实的多仓库测试能发现单元测试漏掉的问题。而且,AI 也能用来发现前一批 AI 代理留下的 bug——前提是你要把它放到真实且多样的场景里去。

也就是在那一刻,我意识到必须增加真正的翻译后校验。下面我就来讲这个第一项新功能:双层校验。

双层校验

步骤操作如果失败
1️⃣调用 provider 的 API网络异常 → ❌ failure
2️⃣按 provider 对 finish_reason(或 Claude 的 stop_reason)做白名单校验不在白名单内 → ❌ failure
3️⃣防泄漏:输出中不能出现任何与源文本完全一致、长度 ≥ 120 字符的源窗口找到源窗口 → ❌ failure
4️⃣langdetect.detect_langs(源语言与目标语言概率)源语言 > 0.80 且目标语言 < 0.20 → ❌ failure
5️⃣空内容 + 输出/源内容比例(若源内容 ≥ 500 字符)为空或输出 < max(50, source/20) → ❌ failure
SUCCESSexit code 0

第 1 层(确定性)——第一道保险:检查 API 返回的状态。每个 provider 都会暴露一个 finish_reason 字段(或 Claude 的 stop_reason),说明 LLM 为什么停止生成。脚本会维护一份按 provider 区分的可接受状态白名单——命名因平台而异(OpenAI/Mistral 用 stop,Gemini 用 STOPFINISH_REASON_STOP,Claude 用 end_turnstop_sequence)。代码还会出于安全考虑容忍 None,尤其是在 SDK 没有返回这个字段时。除此之外的任何状态——例如某些 provider 下的 lengthmax_tokensMAX_TOKENS,它们表示响应被 token 上限截断——都会立即触发 RuntimeError,不会尝试恢复。

第二道更细的确定性保险:检查源文本中是否有任何片段被原样带入了翻译输出。具体做法是从源文本中提取长度 120 字符或以上的窗口;如果其中任何一个窗口在输出中被完整找到,说明它根本没被翻译——failure。正是这个检查把印地语的案例拦了下来:LLM 返回的是 stop(也就是 API 侧“自然结束”),但输出里仍然保留了法语段落——它们没被 finish_reason 这道保险抓到,却被逐字泄漏检测抓了出来。

第 2 层(概率性)——langdetect.detect_langs 会分析输出语言,并返回对多种候选语言的概率分布。我们取出源语言概率和目标语言概率,然后仅当源语言概率高于 0.80 且目标语言概率低于 0.20 时才拒绝——这个阈值故意设得比较保守,以免把技术性 code-switching 误判为失败(比如法语翻译里合法出现的英文词)。对于非拉丁脚本语言,这一层会直接短路(印地语 hi、阿拉伯语 ar、中文 zh、日语 ja、韩语 ko),因为脚本信号本身就足以验证输出。而且它只会在清理后的输出至少有 100 个字符时才运行,以避免对过短文本产生误报。

定量护栏

在这两层之上,还有两个更朴素但必要的检查:

  • 空内容护栏:如果 provider 返回空输出,而 finish_reasonstop,就立刻拒绝(否则会把一个空文件写成 success)
  • 合理比例检查:只有当源内容至少 500 个字符时,才检查输出是否异常地短(典型情况是 < max(50, source/20))。这是一种用于发现隐性截断的检测,不是通用长度规则

在 Claude 上,max_tokens 在 v1.9 中从 4,096 提升到了 32,768(这个改动是在我确认症状并要求调查后,由 Claude 在代码侧完成的)。CHANGELOG 里记录的原因是:避免在 16 k 字符片段上出现潜在截断,并为非拉丁脚本语言(FR → JA、ZH、KO、AR、HI)留出额外余量,因为这些语言的输出往往比等长的拉丁脚本消耗更多 token。

按显式状态回传

文件流水线(translate_markdown_file())现在会返回一个显式状态——successfailureskipped。CLI 会聚合这些状态,只要有任意一个文件失败,就以非零退出码结束——这样无论是调用脚本还是 v1.9 新增的 CI,都能直接利用这个失败信号。在这个 v1.9 之前,一些错误只是在日志里打印,或者被当作翻译成功处理:进程甚至可能以 0 结束,而文件却根本不存在、内容不完整,或者校验不正确。skipped 本身也变成了一个可读信号(“故意忽略”),与 success(“翻译已正确写入”)区分开来。

📄 Python 片段:翻译后双重校验(translate.py)
def _check_passthrough_excerpt(segment, stripped, args):
    """Couche 1 : vérifie qu'aucune fenêtre source ≥120 chars (cleaned) n'apparaît
    verbatim dans la sortie (bug silent-failure typique : LLM renvoie le source brut)."""
    out_norm = re.sub(r"\s+", " ", stripped).casefold()
    for window in _extract_source_windows(segment, ignore_blockquotes=args.news):
        if _looks_like_proper_noun_list(window):
            continue
        window_norm = re.sub(r"\s+", " ", window).casefold()
        if window_norm in out_norm:
            raise RuntimeError(
                f"Output contains untranslated source excerpt "
                f"(model={args.model}, target={args.target_lang}, "
                f"matched window: {window_norm[:100]!r})"
            )


def _check_output_language(stripped, args):
    """Couche 2 : langdetect probabiliste sur la langue de sortie. Court-circuite
    si target script (HI/AR/ZH/JA/KO) déjà détecté en quantité suffisante (le
    code-switching technique fait que langdetect peut sous-estimer la cible).
    """
    if _has_target_script_signal(stripped, args.target_lang):
        return
    langdetect_text = _clean_for_language_detection(stripped)
    if len(langdetect_text) < 100:
        return
    probas = {p.lang: p.prob for p in detect_langs(langdetect_text)}
    # ... seuils source/target appliqués pour décider si on rejette

新功能 2:多位置翻译说明

需求:一个低调但有信息量的说明

当 AI-Powered Markdown Translator 写出译文时,它会附上一段翻译说明,注明所用模型和日期。在 v1.9 之前,这段说明总是固定放在文件底部,使用一种继承下来的(legacy)格式,并带有可见分隔符。

这种固定放在底部的格式对我的使用场景有两个问题。首先,读者只有到最后才知道内容是由 AI 翻译的——更好的做法是从一开始就提醒,这样对内容的预期会更准确。其次,页脚说明并没有凸显支撑这一切的翻译项目:读文章时,驱动多语言输出的来源并不显眼。所以我希望可以把说明移到顶部,同时保留可追溯性——还不能破坏已有用法。v1.9 增加了两个不会破坏兼容性的 flag:

  • --note_position {top,bottom,both}:顶部、底部或两者都放
  • --note_format {legacy,marker}:传统格式或标记格式(marker format

向后兼容的默认值legacy + bottom。现有任何翻译链默认行为都不会改变——只有在需要时才显式启用新 flag。

标记格式:一个干净的 GitHub 嵌入卡片(embed card

标记格式利用了 Markdown 在 GitHub 上的一个细节:未被使用的 link reference definitions 在渲染结果中是不可见的。因此我们可以把元数据(模型、日期、来源)编码到位于文件顶部的一个注释标记里——浏览器里看不见,但在复制纯文本时会原样保留。

另外,GitHub 在分享指向译文文件的链接时还会生成一张嵌入卡片embed card),这张卡片会正确显示文档标题,而且不会被多余文本污染。

下面是一个使用顶部 top 标记格式的原始 Markdown 示例:

[//]: # 'translation-marker: model=claude-sonnet-4-5 date=2026-05-08 source=fr target=en'

# Title of the article in target language

Body of the translated content...

从视觉上看,读者只会看到标题后接正文。这个标记既不会污染 HTML 渲染,也不会污染嵌入卡片。

前置元数据的有意识插入(frontmatter-aware

一个技术上细节却至关重要的点:在 top 中插入一条注释,并不意味着“插到文件第 1 行”。如果文件带有 YAML frontmatter(这个博客就是如此),就必须在 frontmatter 之后 插入——否则这条注释会把 YAML 弄坏。

我把需求交给 Claude(“把注释插到 frontmatter 后面,不要放前面——否则你会把 YAML 弄坏”),它产出了一个 _split_frontmatter helper,能检测打开/关闭的 --- fence。如果文件有一条未闭合的 YAML fence(格式错误的情况),这个 helper 会抛出一个 RuntimeError,而不是悄悄生成一个损坏的文件。从一个单体函数拆成 7 个纯 helper(彼此分离且可测试),正是指导得当的 pair-IA 擅长快速完成的典型工作。我在这里的角色:需求引导、测试者、最终验收结果的客户。不是写代码。在这个项目里,我身兼多职——唯独不写代码,那部分交给 Claude。

位置格式典型使用场景
topmarker博客文章(低调注释、干净的嵌入卡片)
toplegacy内部文档,强调可见追溯性
bottommarker开源 README(与页脚一致)
bottomlegacy默认值 — 向后兼容
bothmarker长篇文章,顶部 + 底部更让人放心
bothlegacy具有双重可追溯性要求的遗留场景
📄 Python 片段:helper _split_frontmatter (translate.py)
def _split_frontmatter(content):
    lines = content.splitlines(keepends=True)
    if not lines or lines[0].strip() != "---":
        return "", content
    for index in range(1, len(lines)):
        if lines[index].strip() == "---":
            frontmatter = "".join(lines[: index + 1]).rstrip("\n")
            body = "".join(lines[index + 1 :]).lstrip("\n")
            return frontmatter, body
    # Opening `---` sans fence de fermeture : insérer la note sans erreur
    # produirait un fichier mal formé. On préfère faire échouer le fichier
    # (failed_files dans translate_markdown_file) plutôt qu'écrire un output cassé.
    raise RuntimeError("malformed frontmatter: opening '---' without closing fence")

新功能 3:使用 --news 模式来保留英文来源引文

问题:翻译时不能破坏引文

当我为这个博客撰写 ia-actualites 文章时(AI 多源新闻的日更/周更),我经常引用英文推文、博客文章、版本发布公告——通常每篇都会有好几条。如果翻译碰到这些引文,它们就会失真。

被翻译过的引文,就是被篡改过的引文。在所有语言版本(EN、DE、JA 等)里,我们都想保留引文的英文原文——这是对来源忠实性的要求——同时附上目标语言的国旗,并提供斜体译文以便阅读。

解决方案:占位符 <NEWSQUOTE id="N"/>

步骤操作
1️⃣输入为带有英文引文的 FR Markdown 源文件
2️⃣预处理:提取英文引文,用占位符 <NEWSQUOTE id="0"/><NEWSQUOTE id="1"/> 等替换
3️⃣API 翻译(FR → target_lang)——原始英文引文从不发送给 LLM,发送的只有占位符(原样保留)
4️⃣后处理:用原始英文引文完整恢复占位符,并插入目标语言的国旗
5️⃣翻译后校验:所有占位符是否都已恢复?
目标输出,保留英文引文
若占位符未恢复或引文被篡改,则失败

--news 模式依赖于这个原则:先做预处理,提取所有英文引文,替换成类似 <NEWSQUOTE id="0"/> 的占位符,翻译其余内容,再把占位符原封不动地恢复回来。

映射 LANG_FLAGS 会根据 target_lang 调整国旗(覆盖 15 种语言):🇬🇧 代表英语,🇩🇪 代表德语,🇪🇸 代表西班牙语,🇮🇹 代表意大利语,🇵🇹 代表葡萄牙语,🇳🇱 代表荷兰语,🇵🇱 代表波兰语,🇸🇪 代表瑞典语,🇷🇴 代表罗马尼亚语,🇸🇦 代表阿拉伯语,🇮🇳 代表印地语,🇯🇵 代表日语,🇰🇷 代表韩语,🇨🇳 代表中文,🇫🇷 代表法语。

翻译后校验会检查所有占位符是否都被完整恢复。错误不是“EN 泄漏”——EN 本来就是有意保留的——而是占位符未恢复引文被篡改

当前用例与展望

今天,我只在博客的 --news 文章上使用 ia-actualites。从长远看,它还可以扩展到任何混合了法语正文和英文来源引文的文章——访谈、引用英文研究论文的经验总结、会议演讲逐字稿。

无需重读代码:为什么必须加倍护栏

「我不读代码。」

我什么都不重读。有时我会快速看一下 diff——这很少,而且只有在 Claude 在某个点上自己搞不定时才会这样。下面就是我日常使用、并且生成了 v1.9 的流程:Claude Code(仅限 Opus)负责写代码。Codex 在 Opus 卡住或用量窗口已满时接棒。GPT-5.5 在 reasoning extra-high 模式下会在执行前审查这些方案。/pr-review-toolkit:review-pr 会在每次 merge 前复查 PR。我的角色到此为止:确认方向并定义护栏。

这种开发方式——完全凭感觉开发(vibe coding)——并不是不严谨。它是一种明确的权衡:更少的人类复审,更多的工具化验证。刚刚介绍的 3 个 v1.9 新功能,全部都是在这个流程里产出的。而也正因为我们不重读代码,才必须加倍技术护栏——不是删掉它们。

下面这两道护栏,就是为了让这种开发方式能够在生产环境中可行:一套自动化质量栈(护栏 1)和一条多模型 AI 辅助审查流程(护栏 2)。

护栏 1:自动化质量栈(14 个 hook + 实践测试)

概览

防护网工具典型耗时失败时阻断
pre-commitshellcheck, ruff, prettier, pre-commit-hooks (8 sub-hooks), detect-secrets, Lizard CCN< 10 s
pre-pushmypy, Opengrep SAST, pip-audit + audit_verdict, unittest (229)~ 30 s是,除了 pip-audit 处于初始报告模式
CI 外部SonarCloud, Codacy, CodeFactor并行本地不阻断,PR 徽章

v1.9 数据:14 个 hook229 个 unittest stdlib 测试新代码约 98 % 覆盖率11 个 SonarCloud 徽章3 个外部平台

pre-commit:快速防护网

#工具版本作用
1shellcheck-py0.10.0.1Shell 代码检查
2ruff (lint)0.8.6Python 代码检查
3ruff (format)0.8.6Python 格式化
4prettier3.1.0Markdown / JSON / YAML 格式化
5trailing-whitespace5.0.0删除行尾空格
6end-of-file-fixer5.0.0强制末尾换行
7check-yaml5.0.0YAML 语法校验
8check-toml5.0.0TOML 语法校验
9check-added-large-files5.0.0阻止意外添加的大型二进制文件
10check-merge-conflict5.0.0检测 Git 冲突标记
11check-executables-have-shebangs5.0.0检查可执行文件是否有 shebang
12check-shebang-scripts-are-executable5.0.0检查带 shebang 的脚本是否可执行
13detect-secrets1.5.0检测 API 密钥和机密信息
14check-complexity (Lizard)local对新代码设置循环复杂度上限

实测总耗时:整个 repo 约 2 到 3 秒(热缓存情况下,pre-commit run --all-files 计时约 2.4 秒)。对于只改动少量文件的普通提交,会更快。我的经验法则是:超过 10 秒,开发者就会绕过(pair-IA 也是如此)——所以这个快速防护网必须始终保留。

pre-push:重型防护网

  • mypy:宽松模式;不是完全 strict(历史代码的 translate.py 过不了),而是对新代码做进展性检查
  • Opengrep SASTp/security-audit p/default p/python —— 大约 30 秒,用于扫描注入、eval、不安全反序列化
  • pip-auditscripts/check-pip-audit.sh 包装:捕获 JSON 输出,在 shell 侧对传输错误(网络、PyPI 挂掉)进行分类,避免把漏洞和不可用性混为一谈,并报告漏洞。v1.9 采用初始报告模式(warn + exit 0)——等依赖过期项的 bump PR 之后再加严,改为阻断。
  • unittest discovery:先 python -m unittest discover,再在 tests/ 上跑 scripts/tests/ —— 229 个测试,本地约 8 秒

外部 CI:SonarCloud + Codacy + CodeFactor

工作流 .github/workflows/sonarcloud.yml(project key jls42_ai-powered-markdown-translator)会在每个 PR 上运行。README 上展示了 11 个 SonarCloud 徽章:质量门禁、安全性/可靠性/可维护性评级、覆盖率、漏洞、缺陷、代码异味、重复代码、技术债务、LOC。

为什么要让 Codacy + SonarCloud + CodeFactor 互相冗余?因为它们各自看到的东西不同。Codacy 报出了 SonarCloud 没提示的重复代码。SonarCloud 报出了 Codacy 放过的低质量信号(也就是那些所谓的 code smells)。CodeFactor 报出了另外两个都忽略的复杂度问题。单靠任何一个都不够。额外增加一个平台的边际成本几乎为零(免费徽章、5 分钟集成),所以就多角度覆盖。

测试:stdlib 的 unittest(不是 pytest)

229 个测试,PR 的 6 个月内 0 回归,v1.9 新代码约 98 % 覆盖率。

典型细分:

  • test_silent_failure.py97 个测试,针对双重校验
  • test_orchestration.py79 个测试,针对编排流水线
  • test_translation_note_position.py38 个测试,针对 position × format 矩阵
  • test_audit_verdict.py15 个测试,针对 pip-audit wrapper(在 scripts/tests/ 中)

诚信说明:约 98 % 的覆盖率指的是 v1.9 的新代码——不是整个 translate.py 的历史代码库,后者仍然包含一些遗留函数,新测试套件对它们覆盖得还不够。我之所以明确说明这一点,是因为如果对整个项目宣称“98 % 覆盖率”,那就会误导人。

一个有争议但我认同的选择:使用 unittest 测试执行器(stdlib),不是 pytesttest_ 前缀只是沿用习惯,但真正执行的是 unittest。为什么?在一个 vibe coding 项目里,每新增一个依赖 = IA 就可能多误用一个依赖。简洁是目标。unittest 已经在 Python 标准库里了,不需要安装,也不需要插件。

实践测试:多仓库 + 产品内部自用(dogfooding)+ 可视化渲染校验

229 个 unittest 还不够。我还加了三层实践测试:

1. 多仓库——在多个带有不同格式 README 的公共仓库上测试脚本。这会暴露出 fixtures 覆盖不到的边界情况——一个有 8 级标题的 README、另一个带有遗留 shortcodes 的 README、第三个带有奇特内嵌代码的 README。正是在这个阶段,Nouveauté 1 的 silent-failure 事故被发现了。

2. 在博客上自用(dogfooding)——jls42.org 由脚本自身翻译。每一篇发布的文章都是生产环境中的实时测试。如果某个边界情况逃过了单元测试,它就会在这里出现,在你正在阅读的这一页上。这是终极测试——线上呈现的,就是项目真正产出的东西。

3. 可视化渲染测试——我会检查渲染后的翻译是否正确显示,要么在浏览器里看最终网页,要么直接通过 VSCode 的 Markdown 预览插件查看。核心思想:不只满足于语法上有效的 Markdown,而是直接看真实渲染效果。可视化渲染会暴露文本测试看不到的外观问题(表格损坏、代码块格式错误、frontmatter 解析错误)。

这些测试也有 IA 参与。/pr-review-toolkit 会在测试环境里执行代码,而 pair-IA 的使用流程里会系统性地加入可视化校验步骤(“检查 X 页面德语翻译是否正常显示”)。

📄 .pre-commit-config.yaml 片段(主要的 pre-commit hooks)
repos:
  - repo: https://github.com/shellcheck-py/shellcheck-py
    rev: v0.10.0.1
    hooks:
      - id: shellcheck
        args: ['-x']

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.6
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v3.1.0
    hooks:
      - id: prettier
        files: \.(json|yaml|yml|md)$

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-toml
      - id: check-added-large-files
        args: [--maxkb=1000]
      - id: check-merge-conflict
      - id: check-executables-have-shebangs
      - id: check-shebang-scripts-are-executable

  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.5.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

  - repo: local
    hooks:
      - id: check-complexity
        name: Lizard cyclomatic complexity (CCN <= 12)
        entry: scripts/check-complexity.sh
        language: system
        pass_filenames: false
        stages: [pre-commit]
📄 scripts/check-security-sast.sh 片段
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."

# Skip gracieusement si opengrep absent en local ; fail-closed en CI.
if ! command -v opengrep >/dev/null 2>&1; then
  if [[ -n "${CI:-}" || -n "${GITHUB_ACTIONS:-}" ]]; then
    echo "opengrep introuvable en CI → fail-closed" >&2
    exit 1
  fi
  exit 0
fi

exec opengrep scan \
  --config=p/security-audit \
  --config=p/default \
  --config=p/python \
  --severity=ERROR \
  --error \
  --exclude=venv \
  --exclude=tests/fixtures \
  translate.py scripts/
📄 .github/workflows/sonarcloud.yml 片段
name: SonarCloud

on:
  push:
    branches: [main]
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  sonarqube:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - uses: actions/setup-python@v6
        with:
          python-version: '3.12'
          cache: pip

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install -r requirements-dev.txt
          pip install coverage

      - name: Run tests with coverage
        run: |
          coverage run --source=translate,scripts -m unittest discover tests
          coverage run --append --source=translate,scripts -m unittest discover scripts/tests
          coverage xml -o coverage.xml

      - name: SonarQube Scan
        uses: SonarSource/sonarqube-scan-action@v8
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

护栏 2:AI 辅助审查 + 多模型流程

术语说明:当我在这一节里说 Claude Opus 时,我指的是我用来开发 v1.9 的模型——不是 AI-Powered Markdown Translator 用来翻译的模型。这个项目本身支持 4 个 provider(OpenAI、Mistral AI、Claude、Gemini),以及任意模型(Sonnet、Haiku、Mistral Large、Gemini 3 Pro 等)。在开发侧,我锁定为 Opus;在实际运行侧,项目保持模型无关。

概念图:一个人类编排者被四个职责不同的 IA 代理包围,并通过协调箭头连接

真实的工作流:4 个模型,4 个角色(用于开发)

  • Claude Code 中的 Opus,且仅限于 Opus(anthropic):主要执行者。读取上下文、编写代码、应用修正。不要 Sonnet,不要 Haiku,不要 fast mode。在这个项目里,我每次都要用最高端的模型——思路很简单:确保拿到最好的,去争取尽可能好的结果。
  • OpenAI Codex 作为后备方案(fallback:在两个明确场景下使用:
    • 当 Opus 在某个问题上卡住时(虽然少见,但确实会发生——例如外部代理如 Codacy 或 SonarCloud 提出的修正,Claude 有时无法收敛,这时我会把问题切到 Codex 来解锁)
    • 当 Anthropic 的使用额度窗口已满时。Codex 可以让我在等待配额重置期间不丢掉推进节奏。
  • GPT-5.5 reasoning extra-high (xhigh):在执行之前挑战方案。在让 Claude Code 开始处理某个问题之前,我会先把计划交给 GPT-5.5 的 reasoning extra-high。它会提出正确的问题,指出盲点。这能避免一开始就走错方向,之后还得返工。
  • /pr-review-toolkit:review-pr(Claude Code 的 skill plugin):在合并前进行复审,由专门代理负责(安全、质量、测试、注释、类型设计)。这个 skill 会在 PR 上运行,等我确认后再 merge——这是代码进入 main 之前,最后一道 AI 防线。

没有任何一个模型能单独完成全部工作。每个模型都承担不同角色——高端执行者、容量替补、计划挑战者、多角度复审者。

/pr-review-toolkit:我本来不会看到的东西

全部。我不看代码。这个 skill 会把一切都提出来——隐藏的 bug、安全问题、测试不一致、看起来通过了但实际上什么也没测到的测试。

在 PR #2(75 个 commits、9 837 次新增、1 982 次删除、58 个文件)上,单靠人类自己,疲劳之下会跳过 80% 的 PR。这个 skill 不会跳过任何东西。它会逐个阅读每个 diff、每个测试、每条评论。更重要的是,它会主动挑战——凡是被它识别为不好的模式,它都会拒绝,并给出替代方案。

人类是指挥,而不是演奏者

我的角色覆盖整条链路——除了写代码。我扮演 产品负责人(思考功能、排优先级、做取舍)、QA(在真实场景中测试、目视验证渲染效果)、tech lead(用 GPT-5.5 reasoning extra-high 挑战方案)、最终客户(基于我在博客上的日常使用体验来判断结果)。我唯一不扮演的角色,就是写代码。其余的,都是我。

我已经成了制作人,而不是音乐家。

为博客服务:它自己给自己做翻译(近 1 800 次翻译)

AI-Powered Markdown Translator 会用 14 种语言生成自己的 README,而它也负责生成 jls42.org 上所有外语内容。具体来说:近 1 800 个翻译版本在为博客供稿(25 篇文章 + 4 个项目 + 98 条 AI 新闻 × 14 种语言,不含法语源文——写这段时总计 1 778 个版本)。你在这里看到的任何非法语页面,都是经过这个项目处理的。

这是一种极致的产品内部使用(dogfooding)——同时也在对“翻译一篇讲翻译的文章”进行压力测试。如果你在 arhiko 中读到的内容是连贯的,那就说明新特性 1(翻译后验证)的安全网是有效的;如果翻译说明正确显示在顶部,那就说明新特性 2(多位置说明)工作正常;如果英文引文在各语言版本中被保留,那就说明新特性 3(--news 模式)也生效了。

结论:严谨的 AI 搭档,而不是粗糙的 AI 搭档

靠感觉开发之所以口碑不好,是有充分理由的。我正是针对这些问题在做事。这一版 v1.9 提炼出四条具体经验:

  1. 静默失败是头号敌人。 AI 生成的代码看起来没问题,也能通过单元测试。必须在客户端做系统性验证。并且要用另一套 AI 来复审真实产出,而不只是代码本身。

  2. pre-commit 钩子若超过 10 秒就会被绕过;pre-push 可以接受 30 秒以上。 AI 很乐意加工具,却不会考虑成本。需要手动设定边界,放在计划里或者事后补救都可以——关键是最后这些钩子要真正调好,并且在日常里确实被使用。

  3. 没有强断言的覆盖率就是表演。 AI 能生成 200 个测试,它们都能通过,但什么也没测到。unittest + 精确断言 > 大量 mock 的 pytest。要验证返回值,而不只是确认代码没崩。

  4. AI 的(PR review)不是可选项。 当人类作者没有复审时,AI 复审不是花架子——它是被委托的眼睛。

把 vibe coding 做好,也意味着接受自己不会读代码,并把批判性阅读委托给那些真正会读的其他 AI。

这个项目揭示了什么

这个 v1.9 体现了我工作方式的几个方面:

  • 人的角色覆盖整条链路,唯独不写代码:产品(思考功能、排优先级)、QA(在真实场景中测试、目视验证)、tech lead(用 reasoning extra-high 的 LLM 挑战方案)、最终客户(基于真实使用来判断)。我唯一不做的事,就是写代码。
  • 是增加安全网,而不是取消安全网:人工复审更少 = 工具化验证更多。这是有意为之的折中,不是严谨性不足。如果我取消复审,就必须把安全网加倍,而不是盲目相信 AI。
  • 用 AI 发现 AI 的 bug:这个静默失败是在多仓库的实测中被 Claude 发现的。完全委托也意味着:我们也可以把批判性复审委托出去。
  • 把 pair-IA 当作业余时间的放大器:我是在晚上和周末推进这个项目的。没有 pair-IA,我显然不可能走得这么远、这么快。有了它,我就能在其他职责之外,把一个开源项目维持在工业级水平。这就是 vibe coding 的意义——不是替代开发者,而是让开发者做成一个人做不到的事。
  • 迭代,而不是推倒重来:9 个版本,渐进式重构(1 个函数 → 7 个 helper),并保留向后兼容。pair-IA 帮助我快速迭代,而不用把一切全部重写。

资源


如果你想把 AI-Powered Markdown Translator 用在你自己的 Markdown 上——开源 README、博客文章、技术文档——,代码在 GitHub 上。几分钟即可安装,支持 4 个 provider,--eco 模式可降低成本,--news 模式可保留源引文,如今还配有你可以在自己的 pair-IA 项目中直接复用的 v1.9 质量栈模板。

如果你用 vibe coding 来开发个人项目,不要在质量上走最省事的路。可靠性就是速度的代价——把两者一起承担起来。