2024 年 3 月,我们在《使用 Fabric.js 实现实时水印预览》的最后给自己挖了一个坑。
那篇文章原本在讲一件很具体的事情:怎样把 Dashboard 里只能靠滑块调整的水印,改造成一个可以直接拖动、缩放并实时预览的编辑器。前端用 Fabric.js 描述水印的位置、大小、文字和颜色,后端再用 libvips 生成最终图片。等到前后端的结果终于能够做到基本所见即所得时,我们又发现了另一个更大的问题:网上有关 libvips 的资料并不多,有关 govips 的资料更少,中文内容尤其零散。
于是,我们在文章结尾写下了这样的计划:
我们计划写一篇 libvips 的操作指南。指南将包括 libvips 的基础操作,并提供 Go、Python 和 JavaScript 三种语言的演示代码。由于任务复杂,而且团队成员都有全职工作,它可能会花上很长时间。
当时我们还说初版准备在两周内上线。现在回头看,这显然过于乐观了。两周过去之后,它没有变成一份完整的指南;两年过去之后,我们也仍然只是陆续写过一些有关 govips、裁剪、渐变、滤镜、CGO 和图片格式的独立文章。
好在直到两年半后的 2026 年 10 月,这个生态依然空缺,于是我们终于完成了 VIPS Handbook——一份同时提供中文和英文、覆盖 govips、sharp、pyvips 与 vips CLI 的 libvips 生态实践手册。
真正有意思的并不是「AI 帮我们把拖延了两年半的文章写完了」。如果事情只是把一份提纲交给模型,再复制出几十篇 Markdown,那么结果不会比互联网上已经存在的大量 AI 内容更有价值。
这次变化更接近于:我们在这个 AI 极速发展的年代重新思考了一个文档站可能需要包含的内容,以及 AI 应该如何在这里替代我们的部分劳动(而非思考),并尝试在实际工作中找到不同模型在不同阶段的价值。
我们也没有找到一个从头做到尾都最好的模型。最后真正工作起来的,是一条由 5.6 Sol High、DeepSeek v4.1 flash、Gemini 4、OpenCode、自动检查和人工判断共同组成的流水线。
2024 年的难点不是 Markdown
如果只看最终页面,技术文档似乎就是一组标题、说明、代码块和图片。真正动手之后,却会发现最费时间的部分全部藏在页面后面。
以一个最常见的 resize 为例,我们首先要确认 libvips 底层有哪些相关操作,然后分别寻找 govips、sharp、pyvips 中暴露出来的入口。它们可能名称不同、参数单位不同,也可能把几个底层操作合并成了一个更高层的接口。某个 binding 没有现成封装时,还要继续翻源码,判断它是真的不支持,还是仅仅没有提供方便的 helper。
接下来才轮到写代码。每种语言的示例都要读取同一张 input.jpg,执行尽可能相同的产品意图,再输出可以比较的图片。输出尺寸、通道数量、Alpha、色彩空间、元数据和编码参数都可能影响结果。肉眼看上去差不多,并不能说明行为等价;代码能够运行,也不能说明默认值一致。
最后还要整理截图、解释差异、补充错误处理、翻译另一种语言,并确保文章里的代码和真正运行过的代码没有逐渐变成两份东西。
因此,2024 年最直接的工作方式大概是:
- 找出几个 binding 支持的操作并取并集;
- 为每个操作查文档、源码和已有示例;
- 分别写 Go、Python 和 JavaScript 代码;
- 针对同一张输入图片运行实验;
- 比对输出并记录差异;
- 把过程整理成文章,再制作另一个语言版本。
这个流程很清楚,甚至没有什么特别困难的单一步骤。问题在于它极其消耗连续时间:每写一个主题,都要重复搜索、编码、运行、截图、解释和翻译。只要其中一个 binding 的行为与预期不同,当前章节就可能变成一次持续数小时的源码考古。
旧方法并不是错误的方法。恰恰相反,我们后来为 VIPS Handbook 设计的许多规则,都来自过去这些手工实验留下的教训。比如,图片处理文档不能只有一段看似合理的代码;展示出来的结果必须真的由这段代码生成;不同 binding 达成相同产品目标,并不意味着它们的 API 和默认语义完全相同。
真正变化的是:到了 2026 年,这些经验终于可以先被写成规则,再交给模型重复执行,极大的解放了劳动力,当然,我们会遇到一些新的挑战。
先让 5.6 Sol High 设计轨道
这次我们没有从第一篇正文开始,也没有先让模型「写一份完整的 libvips 教程」。第一步是让 5.6 Sol High 规划整个项目。
这次规划解决的是一组互相牵连的问题:站点使用什么前端框架,中英文路由如何保持对称,主题怎样分类,代码和生成图片放在哪里,多个 binding 怎样出现在同一个页面里,实验结果怎样进入正文,以及什么检查可以阻止一个半成品页面被当作已完成内容。
最终站点使用 Astro 和 Starlight。中文与英文分别放在 /zh/ 和 /en/ 下,拥有相同的内容标识和页面结构;正文通过组件读取真正执行过的 Recipe,而不是重新复制一遍代码;每个可观察的图像处理行为,都尽量关联输入、程序、输出和结果元数据。
与页面同时确定的,还有内容契约。原来一句「写一篇 resize 教程」,在新的流水线里会被拆成更接近下面这样的任务:
- 这页准备解决谁的什么问题;
- 读者需要先知道哪些概念;
- 必须回答哪些问题,哪些结论不能在没有证据时写出;
- 可以使用哪些官方文档、源码、Issue 和版本信息;
- govips、sharp、pyvips、vips CLI 分别提供什么入口;
- 使用哪张输入图,预期生成什么格式、尺寸和通道的输出;
- 哪些结果可以逐像素比较,哪些只能比较产品意图;
- 中英文页面需要保持哪些结构、警告和示例一致;
- 某个 binding 不支持时,是给出替代方法,还是明确写出
Not directly supported。
这一步看起来不像在「写内容」,却是后面所有内容能够批量展开的前提。模型面对一个宽泛目标时,很容易写出一篇结构完整、事实似是而非的说明文;面对输入、来源、输出和验收条件都明确的任务时,它才更像一个可以被检查的工程执行者。
换句话说,我们首先规定怎样才算完成,然后才开始生成。
DeepSeek v4.1 flash:先把工程展开
项目的第一轮大规模执行交给了部署在内部 NVIDIA RTX 6000D 集群上的 DeepSeek v4.1 flash,并通过 OpenCode 让它直接在仓库中工作。
这里的「工作」不是让模型在聊天窗口里输出一篇文章,然后由人复制进项目。它需要读取现有目录和约束,搜索文档与源码,创建或修改示例,运行命令,观察输出,生成中文和英文页面,并根据检查结果继续修正。
这也是这次项目中一个很直观的感受:模型未必需要在所有维度上都非常强,只要任务边界足够清楚、相关上下文在仓库里可以获得、每一步又有可运行的反馈,一个相对普通的模型同样可以完成大量具体工作。
例如,它不需要凭记忆保证一个 Recipe 的四个入口全部正确。它可以先找到对应版本的接口,写出程序,执行程序,再让 Schema、Go 测试、输出元数据和站点构建告诉它哪些地方没有满足约束。下一轮继续读取错误,修改仓库,重新运行。
这和过去最大的区别,并不是单次生成速度,而是搜索、写代码、运行和修正可以连续发生在同一个任务里。过去需要人频繁在浏览器、编辑器、终端和文章之间切换的工作,现在可以被组织成相对稳定的执行单元。
但我们发现一个很大的问题—— DeepSeek 生成的第一轮文字也有一个很明显的问题:AI 味很浓。
单独看一个段落,语法没有问题,信息似乎也都在;把几十个页面连续读下来,模式就会非常明显。章节开头喜欢先宣布这一节的重要性,结尾再次总结刚刚说过的内容;警告与边界被用相似的句式反复表达;中文里混着翻译腔和不自然的分类名称;每一页都很工整,但这种工整反而让整站像同一份模板反复填空。
下面是一段真实的修订。初稿描述手册覆盖范围时写道:
手册以 govips、sharp、pyvips 和 vips CLI 四种常用接口对照讲解同一套 libvips 心智模型,涵盖快速入门、基础、实践配方、binding 指南、生产注意事项、故障排查与参考页面,全部页面同时提供简体中文与英文。
经过 Gemini 4 重写后,它变成:
手册以 govips、sharp、pyvips 和 vips CLI 四种常用入口对照讲解同一套 libvips 心智模型,涵盖快速开始、基础知识、场景实战、生产实践、语言绑定、故障排查与参考资料,全部页面同时提供简体中文与英文。
这不是一个惊天动地的改写,事实也几乎没有变化。但「接口」换成了更适合同时容纳库和 CLI 的「入口」,「实践配方」「生产注意事项」「参考页面」被改成更自然的栏目名称,整句话不再像从英文信息架构逐项翻译过来。
当这种调整发生在大量页面的标题、引言、段落和提示中时,效果就很明显了。所谓去掉 AI 味,并不是再加一句「请写得像人」,而是需要一个真正的编辑阶段:从全站的阅读节奏出发,判断哪些内容在重复,哪些术语不自然,哪些段落虽然正确却没有帮助读者更快理解问题。
Gemini 4:从文字编辑到维修角色
Gemini 4 最初进入这个项目,主要就是为了解决上面的文字问题。
它会直接修改仓库中的中英文页面,不只替换几个形容词,而是重新组织段落,删除模板化的开场和结尾,统一栏目名称,同时尽量保留代码、警告、来源和版本边界。修改之后,它也需要运行相应检查,避免一次看似无害的润色破坏中英文标题层级、Recipe 引用或内部链接。
随着项目继续推进,Gemini 的职责逐渐从编辑扩展到维修。它开始处理复查阶段发现的代码与内容问题,重新生成输出,补充测试,并维护相关页面。也就是说,它并不是一个只在流程末尾做语言抛光的模型,而成为了返工链路里的主要执行者。
这也让我们放弃了一个很容易产生的想法:先选择「最强模型」,再让它从项目规划一路做到最终检查。(那样太贵了,我们钱包会穿孔,而且还会频繁装上 Codex 的 5hr 限制,此外还不提 Codex 喜欢干一些悄悄换底层模型的勾当)
在这次实践中,5.6 Sol High 更适合搭建整体结构和规则;DeepSeek v4.1 flash 适合沿着既定轨道批量展开任务,也能够在后续审查中寻找问题;Gemini 4 更适合重写、整合以及根据明确的问题单进行维修。它们并不是一条简单的能力排名,而是在不同工作阶段表现出不同的价值。
人:定义目标与验收标准
│
▼
5.6 Sol High:整体规划
框架、路由、内容契约、实验与检查
│
▼
DeepSeek v4.1 flash:第一轮执行
找资料、写代码、跑实验、生成双语页面
│
▼
Gemini 4:编辑
重写、整合,并运行相关检查
│
▼
DeepSeek + OpenCode:全站复查
检查页面、代码、生成物与技术语义
│
▼
人:确认问题、判断优先级、安排返工
│
▼
Gemini 4:维修
修代码、改说明、重生成、补测试
│
▼
全量自动检查 + 人工终审
更重要的是,流程并没有变成三个模型互相自动派活。模型可以提出问题,但是否成立、严重程度如何、应该由谁处理,仍然由人来判断。我们保留了任务调度和最终发布权,因为只有人知道这次发布的目标是什么,也只有人对公开结果负责。
交叉复查比单模型自检更重要
让生成内容的模型检查自己的输出当然有价值,但它很容易沿用生成时的假设。某段代码之所以被写成现在这样,往往正是因为模型已经接受了某个前提;让它在同一个上下文中立刻复查,未必能意识到前提本身有问题。
因此,在第一轮内容和 Gemini 的大量重写完成之后,我们又安排 DeepSeek 通过 OpenCode 进行了一次专门的全站复查。它不再以「完成这一章」为目标,而是把页面、代码、生成物和相互之间的对应关系当成检查对象。
这种角色变化很有意思。前面负责批量生成初稿的模型,在这里变成了审查者;前面负责润色正文的 Gemini,则在问题经过人工确认后接手维修。模型的角色不是固定的,更不是生成模型天然比编辑模型低一级。
复查结果也不会自动流向 Gemini。DeepSeek 先提交发现,人再判断它是真的技术问题、文档表达问题,还是模型误报。确认之后,我们才把范围、预期结果和验收方式整理成返工任务交给 Gemini。Gemini 修改代码和说明、重新生成结果并运行检查,最后再由人查看整体结果。
在这条链路中,人的工作不再是逐字输入正文,而是持续回答五个问题:
- 这项工作怎样才算完成?
- 当前阶段更适合交给哪个模型?
- 审查意见有事实依据吗?
- 返工之后用什么证据证明问题已经消失?
- 现在的结果是否可以公开并由我们负责?
这些问题无法通过「构建成功」自动回答。下面这个 premultiply 问题,就是最典型的例子。
一个真实案例:双重预乘为什么通过了第一轮检查
VIPS Handbook 的水印与图层合成示例需要处理带 Alpha 通道的半透明图层。最初的实现中,代码在调用 Composite 之前主动执行了 PremultiplyAlpha():
if err := textImg.PremultiplyAlpha(); err != nil {
return err
}
if err := pill.Composite(textImg, vips.BlendModeOver, 26, 16); err != nil {
return err
}
从直觉上看,这段代码很合理。图像合成经常涉及 Alpha 预乘;很多资料也会提醒在混合之前正确处理颜色通道与透明度。程序能够编译,Recipe 能够运行,WebP 文件可以导出,页面上的水印看起来也确实存在。
问题在于,govips 的 Composite 最终调用 vips_composite2,而这里使用的默认语义是 premultiplied=false。这并不是说它不处理预乘,而是告诉 libvips 当前输入是 straight,也就是未关联 Alpha。算子会在内部预乘输入,在预乘空间里混合,然后再反预乘输出。
如果调用者事先已经执行过一次 PremultiplyAlpha(),却仍然让 Composite 按 premultiplied=false 处理,同一批半透明像素就会被再乘一次 Alpha,结果因此变暗。
DeepSeek 在全站复查时发现了这个问题。经过人工确认,我们把维修任务交给 Gemini:删除合成前多余的预乘,修正 govips 与 pyvips 示例中的说明,重新生成参考输出,并增加一个不依赖肉眼判断的回归测试。
实际页面里的差异并不总是非常醒目,这恰好说明只看截图为什么不够。新的测试构造了一张纯白背景和一张 50% Alpha 的纯白叠加层。正确地把半透明白色合成到白色上,结果仍然应该接近 255;如果发生双重预乘,结果则会被压暗到约 191。
if err := base.Composite(overlay, vips.BlendModeOver, 0, 0); err != nil {
t.Fatal(err)
}
if err := base.Flatten(&vips.Color{R: 255, G: 255, B: 255}); err != nil {
t.Fatal(err)
}
raw, err := base.ToBytes()
if err != nil {
t.Fatal(err)
}
if len(raw) == 0 || raw[0] < 254 {
t.Fatalf("expected straight-RGBA composite of 50%% white over white to stay ~255, got %d", raw[0])
}
这个案例没有证明某个模型一定比另一个模型更懂 libvips。它证明的是另一件事:生成、编辑和审查使用不同的任务上下文,再由人确认并安排维修,比让同一个模型在一次会话中宣布自己已经完成更可靠。
它也给出了一个很朴素的边界:能编译、能构建、能显示,都不等于技术语义正确。视觉结果可以帮助人发现异常,而针对语义的最小测试才能把一次修复变成以后持续有效的约束。
最终得到的不是一批 Markdown
不到一周之后,VIPS Handbook 已经拥有中文和英文各 67 个公开页面,以及 13 组可执行 Recipe。内容不再局限于 2024 年设想的基础操作,而是分成快速开始、基础知识、场景实战、生产实践、语言绑定、故障排查、参考资料和项目说明等栏目。
同一个主题会对照 govips、sharp、pyvips 和 vips CLI 四种常用入口。govips 是主要参考实现,但页面不会因此假设其他 binding 具有完全相同的 helper、默认值或生命周期。无法直接支持的能力应当明确说明,仍待完成的跨 binding 比较也不应该被伪装成已经验证的等价实现。
每个 Recipe 的页面结果来自实际运行的程序。输入图有稳定标识和用途说明,程序输出原始图片与元数据,站点组件再把执行过的代码和生成结果放到正文中。这避免了文档代码与真正运行代码各自演化:如果示例改了,输出和页面必须一起重新验证。
围绕这些内容,项目还建立了一组比「Markdown 是否存在」更严格的检查:
- 工具链版本与依赖必须符合锁定配置;
- Recipe Manifest 必须通过 Schema 校验,并指向真实存在的入口文件;
- 中英文页面必须拥有相同的
contentId、标题层级、警告顺序和 Recipe 关联; - Fixture 与生成素材必须通过来源和路径审计;
- Go 示例需要运行测试,并检查图像引用是否正确关闭;
- 可复现基准与生成结果需要检查漂移;
- Astro 类型检查与完整构建必须通过。
这些检查不能证明每一句技术结论永远正确,也不能替代维护者审校,但它们能够把许多常见错误从「上线后靠读者发现」提前到模型仍在修改仓库的时候。
我们把现在的 VIPS Handbook 视为一个已经完成并可用的项目,而不是一个等待所有未来主题都写完才敢出现的预告页。与此同时,手册也不会被冻结。新的 libvips 版本、binding 能力和生产问题仍会继续出现,内容 Registry 中也保留了后续扩展的位置。完成一个站点与停止维护一份手册,本来就是两件不同的事。
站点已经上线,仓库为什么还没有公开
VIPS Handbook 已经可以使用,但承载它的 GitHub 仓库目前仍然是私有的。这并不意味着我们只准备公开最后生成的页面。我们已经在 libvips Discussions #5255 中说明,最终目标是开放整个仓库,包括文档源码、可运行示例和验证流水线。
我们在讨论中写道:
我们的最终目标是将整个仓库开源,其中包括文档、可运行示例和验证流水线。目前项目仍处于高强度开发阶段,仓库里还有不少凌乱的内部脚手架、草稿笔记和正在开发的工具。等结构稳定下来,并完成内部杂项的清理后,我们会公开 GitHub 仓库。
这里的「清理」不是要抹去 AI 参与过的痕迹(当然,我们抹掉许多 update,bump 之类的随意 commit 信息),也不是等到每一个未来主题都完成才公开。网站可以作为一份完整手册发布,维护仓库却还需要再达到另一组标准:目录和命令应该让项目之外的人看得懂,过期草稿不应与有效设计混在一起,内部开发约定需要与贡献者真正需要的说明分开,验证流程也应当能够在干净环境中复现。
这件事对我们尤其重要,因为公开页面只能让读者看到结果;公开可运行示例和验证流水线,才让其他人有机会复现实验、质疑结论、定位回归,并判断这些页面究竟是怎样生成和检查出来的。我们不希望只是把一个内部工作目录原样推到 GitHub,然后把理解它的成本交给社区。
在仓库公开之前,上面的 Discussion 也是一个公开的反馈入口。等整理完成后,文档、实验代码和检查规则会作为同一个工程被开放,而不是只发布一份脱离生产过程的 Markdown 快照。
AI 时代,做文档站的工作到底变了什么
如果用一句话概括,过去我们把时间花在逐项生产内容,现在我们把更多时间花在设计生产内容的系统。
过去的主要成本是搜索、编码、试错、截图、翻译和逐页展开。现在,这些工作并没有消失,只是其中相当一部分可以由模型在仓库和反馈循环中连续完成。人的时间转向了另外几件事情:定义内容与实验契约,为不同阶段选择合适的模型,建立可重复的检查,判断哪些发现值得相信,以及把问题重新描述成可以验收的维修任务。
这次项目也让我们对「使用 AI」有了几条更具体的认识。
第一,不存在一个模型天然适合所有阶段。能够做出漂亮整体规划的模型,未必是最适合连续处理几十个章节的执行者;能够快速展开代码和内容的模型,文字可能具有明显的模板感;擅长自然重写的模型,也仍然需要明确的事实边界和自动检查。
第二,模型的角色可以变化。DeepSeek 既生成过第一轮内容,也在后续作为全站审查者发现技术问题;Gemini 先做文字编辑,随后又承担代码维修和维护。把模型固定地分成「聪明的规划者」和「便宜的执行者」,反而会错过这种变化。
第三,AI 味不能只靠提示词消除。如果几十个页面使用相似结构、相似过渡句和相似总结,单页继续要求「自然一点」很难解决问题。需要有人或另一个模型从全站尺度阅读,重组信息,删除重复,并建立真正的编辑过程。
第四,交叉复查比自我确认更可靠。换一个模型、换一个任务目标、换一段上下文,常常比让原模型重复说「请仔细检查」更容易暴露假设。premultiply 问题正是如此。
第五,自动检查不是为了取代人工审校,而是把人工注意力留给机器不容易判断的地方。路径、Schema、双语结构、尺寸、通道和测试结果适合自动检查;一段解释是否诚实、一个比较是否公平、一个主题是否真正解决了读者的问题,仍然需要人负责。
因此,这并不是一个「三个人的工作被几个模型取代」的故事。我们仍然需要决定为什么做这份手册、它应该服务谁、哪些内容不能在证据不足时公开,以及发现问题后是否值得推迟发布。变化在于,同样规模的团队现在可以搭建一条更长、更密集、也更容易重复执行的生产与复查链路。
回到两年半前的那句话
2024 年,我们说这份指南可能会花上很长时间。这个判断并没有错。如果仍然沿用当时的工作方式,逐个寻找 API、手写四套实现、运行每个实验、整理两种语言的页面,它确实很可能继续躺在某个待办列表里。
两年半后,目标没有变:我们仍然希望为 libvips,尤其是 govips,提供更容易使用和核对的中文资料;代码仍然需要运行,图片仍然需要真实生成,不同 binding 的差异仍然需要被诚实说明。
改变的是完成目标的方法。我们先用 5.6 Sol High 设计框架和规则,让 DeepSeek v4.1 flash 在内部 6000D 集群上通过 OpenCode 命令打工人干活,再让 Gemini 4 编辑和维修;随后让 DeepSeek 换一个角色进行全站复查,由人确认问题、安排返工并承担最终责任。
最后出现的也不再是当初设想的 100% Markdown 的「libvips 操作指南」,而是一套能够继续执行、检查、返工和维护的实践手册。整个流程都可以通过自动化来完成,但每一步都需要有人来决策和承担责任。
如果你正在使用 libvips、govips、sharp 或 pyvips,欢迎访问 VIPS Handbook。如果页面中的解释、代码或实验结果仍有问题,也欢迎告诉我们。比起假装一份技术文档已经写完并且永远正确,我们更希望它能够在下一次复查和维修中继续变得可靠。


