跳到正文
Hacker News· jakevoytko·· 6 天前AI 评分22

前 Google Docs 工程师用 Codex 和 GPT 5.6 自建博客编辑器

I built my own Google Docs for my blog

AI 导读

一位前 Google Docs 工程师用 Codex 完成 8 小时问答、在 200 美元/月套餐上重置四次 ChatGPT 后,为自己博客搭建了一个自定义 Google Docs 克隆编辑器。该编辑器拥有自研 operational transform 栈,支持 magic redo,并将改动快照存入 Postgres,发布时重新生成静态站点。

正文 · AI 翻译

我最近注意到,我的博客文章还不到 30 篇,但我已经维护它将近十年了。我敢肯定你能猜到我的下一个念头:

“这是因为我还没有自己超个性化的 Google Docs 版本可以用作编辑器。”[0]

于是我着手解决这个问题。而且我有一个很大的优势:2010 到 2015 年间,我是一名在 Google Docs 上工作的 Google 员工。虽然已经过去很久了,但我仍然记得一些让这位老姑娘运转起来的门道。

这花了与 Codex 长达 8 小时的问答。在我每月 200 美元的套餐上重置了四次 ChatGPT,才用 xhigh 的 Astra 完成。但它完成了。这是我第一篇完全由我自己定制的 Google Docs“克隆”产出的文章。它驱动着我博客的编辑器。它有自己的自定义操作变换栈。它慢得要死。我爱死它了。

A screenshot of an earlier draft of this post, being edited within the new blog editor

这就是我个人的 Google Docs,带着它所有的荣耀与恐怖,来自本文早期的一个草稿。

需要说明的是,你并不是直接从 Google Docs 编辑器的软件中阅读。面向公众的网站和编辑器是解耦的。公开网站只是静态内容。编辑器在我工作时累积更改和快照,把所有内容都记录在 Postgres 数据库中。每次我发布、重新发布、取消发布或删除时,都会启动一个任务。该任务会重新生成静态网站。这就是你现在正在阅读的内容。

来自地狱的问答

我是用 xhigh 的 GPT 5.6 Sol 规划这个项目的。Astra 正好在我快完成问答时发布。否则我也会用它来做问答。

我像所有人开始一项大任务时那样开始:写下一大段提示词,然后按下发送。

但我还多提了一个要求:Codex 不允许在规格中留下任何不一致或含糊之处。它必须不断问我问题,直到规格完全确定,从最重要的未回答问题开始。

它仔细思考了我最初的提示词,并在一个 Markdown 文件中缓存了大约 380 个问题。我的许多回答又促使 Codex 提出了几个后续问题。我不是一次性完成的。这只是一个副业项目,我还有家庭和工作。所以我用了四五天时间完成这次访谈。

其中一些问题很折磨人,需要对我想要的架构主要部分做长篇解释。有些需要大量思考。有些对正确性至关重要。有些则是我碰巧知道的关于实现文字处理器的实现细节。我确定了一套流程,让我能相当快速地掠过这些问题。

  • 我使用语音输入,并告诉它:“我正在使用语音输入,转录可能有误;请更努力地猜测我原本说了什么。”

  • 我要求它同时问 1 到 3 个问题,但只有在问题相关时才能一次问多个。

  • 如果它能想到一个明显的默认选项,它就会解释该选项,然后问:“同意吗?”所以如果三个都没问题,我可以说“全部同意”。通常我需要一种混合方式,比如“一:同意。二:改成这样做。三:可以,但要做一个小修改。”

等我做到最后大约50个问题时,我基本上对每个三选一都只是说“同意。同意。同意。”,因为所有决定都无关紧要。

我的需求

我希望这个静态网站能与我现有的博客功能对等。从历史背景来说,我的博客长期使用 WordPress。今年二月,我把它转换成了一个 Golang 服务器,托管在 DigitalOcean 上。现有博客只需要非常基础的功能就能完全对等:标题、文本样式、链接、列表、图片、行内代码、块代码、引用、脚注、嵌入的 YouTube 内容。没什么太复杂的。

我还想要 Google Docs 里我最喜欢的功能:魔法重做。

“什么是魔法重做?”

很高兴你问!打开 doc.new,把这篇帖子复制粘贴到编辑器里。选中一些文本并加粗。现在向下滚动,选中另一些文本(期间不要做任何其他操作!),然后按下 Redo 快捷键(就是撤销/重做那个)。新位置的文本现在也加粗了。再选中一些文本,按重做。那段文本也加粗了。这就是魔法重做!它会像在当前选择模型下执行一样,重复上一个操作。当你想要大量重复做同一件事时,比如给表格添加几列,它就很有用。魔法重做对后端设计有一些影响,因为这意味着编辑器所做的一切都需要作用于 f(selection)。

我妥协的地方

最大的妥协在视图层。我在 Google Docs 工作时,它做了很多巧妙的事情来尽可能提高编辑器效率。他们还对每一次更改进行测量,以防止性能退化。我离开后,他们后来把渲染切换到了 canvas,据我理解这是一次巨大的性能提升。

这些我都没做。我只是更新正在编辑的段落的 DOM 节点。我没有做任何巧妙的技巧。我还没有性能护栏。

我本来也大可以把 LLM 指向它混淆后的源码,然后说“逆向工程出这个编辑器的每一个功能”,最后得到一份相当完整的功能列表。但那样它生成的编辑器就不会适合我博客的需求。

我没有任何无障碍或从右到左文本支持。如果哪天我失明了或者搬到约旦,我会一直冲 Claudex 嚷嚷,直到它把它做出来。

我没有离线支持。没有建议更改。

我加入 Google 时,Docs 缺少一个我现在认为必不可少的 OT 命令:“移动文本”。Docs 中的很多操作都是用“删除这里的文本,再在这里重新添加”来实现的,这在面对传入命令时可能转换得很糟糕,并把它们冲掉(因为它们会被应用到被删除的范围上)。我目前没有使用它,但我计划做一个“大纲”功能,让我能非常快速地重新排序列表,我打算用 move 来实现它。

“但是 Jake,你是唯一的编辑者。你为什么需要关心东西怎么 OT?” 兄弟,我为了实现自己的 Google Docs,而不是去编辑一个 Markdown 文件。我们这里已经超越逻辑了。

我还在试验一个“人类证明”功能,实际上就是一个只读的 Etherpad,我会从我的帖子中链接过去。由于我有完整的命令历史记录,每篇文章都可以有完整的回放,以证明是我写的。我对推出它并不完全放心,因为我的写作非常具有探索性。我会写很多我并非本意的东西,之后又会删掉。我不想让人们能够断章取义地截图。

实现过程

我在 xhigh 上使用 GPT-6 Astra 实现了这个项目。

我有一个每月 200 美元的 ChatGPT 账户,用于我的副项目编码,实现它需要四次重置。但我有一个优势:我实现它的那一周是重置额度非常充裕的一周。所以非常感谢 Tibo。我在每周配额刷新后立即开始,当时已经有三次存下的重置可用。还有一次未存入的重置。最初的实现用掉了四个完整的每周配额,几乎用完了第五个才最终完成。

我在提示词上犯了一些基本错误,后来反噬了我。大量时间浪费在测试和验证上。我不记得我具体是怎么提示模型的,因为问答是一个月前做的。但我确定我说过它需要验证一切,需要测试所有情况之类的话。我想我还建议它对编辑器进行模糊测试,以测试 OT 栈。

我犯了一个错误,表现得像是在向一位资深工程师描述设计。但不对,Astra 不是人类,当我表达不精确时,它并不具备人类共有的理解。所以它以最机械的方式处理了这些。“模糊测试”变成了一系列 10,000 多个用例的集成测试,每次运行需要五个多小时。每天我都会告诉它删除一个庞大的测试,替换为实现同等用例覆盖所需的最少测试用例,而每天晚上它都会再生成另一个庞大的 10,000 用例集成测试,运行需要 5 小时。我确信这消耗了大量 token。最终我禁止它生成超过几百个用例的测试。它仍然会在完全不相关的情况下提到这件事。这总是让我想,“……但你当时真的以为你可能会那么做吗?”

告诉它“验证”东西也是个错误。这个应用一开始慢得无法使用。它添加了运行时检查,把应用卡死了。

作为背景,当你有一个完整的操作转换栈时,许多功能都需要针对传入的更改进行转换。如果你有撤销和重做栈,撤销和重做栈中的所有内容都需要被修改,以便它们仍然能够干净地应用。如果一条命令将字母 'b' 加粗,而在它之前插入了一个字母,那么命令中引用的索引需要向前移动,以便 'b' 在被应用时仍然加粗。

在这种情况下,我要求字素簇在应用编辑时始终有效。对我来说,这意味着“在应用编辑之后、将命令发送到服务器之前,检查周围的模型上下文直到安全边界,以确保操作没有导致无效的字符序列。”但生成的应用程序会在每次按键时通过复制文档、应用编辑,然后运行全文字素簇检查来“验证”撤销和重做栈。所以只要在编辑器中输入几个字符,就足以让它卡顿和停顿。

我已经在不同粒度上做了几轮性能优化,比如“带我了解按下按键时发生的一切”和“优化输入 1000 个字符”,我想我已经消除了大部分不必要的工作。输入相对流畅,考虑到我并没有为性能优化设计,这很不错。

切换

由于该项目涉及从静态站点迁移到混合站点(动态后端、静态前端),切换成了对 Astra 具有宗教意义的 Epoch 事件。Astra 对切换非常痴迷。我的 Makefile 和 Ansible 实现中充斥着大量额外的命令和检查。不仅支持旧版本和新版本,还有大量验证检查,以确保切换的每个中间步骤中站点完全正常。Astra 需要验证,它通过哈希许多不同的东西并称之为证明来获得验证。所以整个事情都用证明进行了验证,这非常令人安心。

由于我的站点大约有 30 个 Markdown 文件,备份在 GitHub 上,风险不能再低了。我决定直接尝试 Astra 的切换。如果失败,我就重新创建 droplet 并让它从头开始。但它做得很好。迁移成功。站点运行良好。

但随后切换继续存在。每个更改都需要测试与切换前、切换后和中间步骤的兼容性。在描述新功能时,它仍然问我希望如何处理遗留的 Markdown 站点。

所以显然我根除了切换。我让 Sol 负责清理。它删除了旧的 Markdown 静态站点以及为其提供服务的 Go 代码。它还消除了所有 LLMslop 护栏:切换提及、验证检查、所有证明和验证证据。全部消失。作为额外收获,我已经有一段时间没看到它提到切换了。Codex 真的忘记了切换。

现在它上线了,我该如何编辑它?

首先,我有一堆技能来管理流程的每个阶段。提交工单(并就任何歧义给我问答),启动演示服务器,清理并提交,等等。

我有一个用 vibe coding 做出来的工作区切换器,我亲切地称它为 ngmi,它管理着我所有的 agent 以及它们如何与项目生命周期交互。当它接到一个工单时,会自动创建一个 agent,在未合并的依赖上阻塞,还有一个合并队列来串行化已完成工单的提交,等等。它运行在一个单独的 DigitalOcean droplet 上,可以同时处理 3 到 4 个 agent 运行而不太吃力。这对我的日常工作来说太受限了,但对一个副业项目来说没问题。我把它运行在 DigitalOcean droplet 上,因为我同时在我的 Windows 游戏 PC 和 MacBook M1 上工作。

ngmi 可以生成自己的非任务 agent,并且有自己的 MCP,所以我也可以从 ngmi 中管理的 agent 内部命令它做事。

因为我喜欢把副业项目做得太过火,我有自己的工单跟踪程序,它有自己的 MCP,地址是 bitlogwork.com。每当我想改点什么,我就提交一个工单,ngmi 会自动接单。它会检测我正在处理技术栈的哪一部分(工单跟踪、分析、博客),并在完成任务时启动一个演示服务器。它可以生成并提供解释性 HTML 页面,来突出显示我好奇的这次变更的不同方面。我可以切换到该 agent 并要求修改。

过去一个月我一直在做性能优化和添加功能,因为我有精力做副业项目。它终于到了写一篇博文不那么痛苦的地步,所以就有了这篇!这是来自我自己个人 Google Docs 克隆的首篇博文。

教训:现实有无限的细节

一次性搞定是失败者的做法。找到方法去揭示所有重要的隐藏规格,同时不要陷入无关紧要的枝节中。

如果你告诉一个 LLM“做一个带有 OK/Cancel 选项的模态对话框,在执行删除操作前询问你是否确定要删除这个”,你可能以为你已经非常精确地指定了它的行为。你大概已经能想象出它长什么样了。但 LLM 会在幕后猜测数量惊人的规格。它放在哪里?角有 border-radius 吗?有边框吗?有投影吗?内边距是多少?需要单独的标题,还是原始的问题文本就够了?按钮的顺序是什么?按钮是什么颜色?确切的文案是什么?没完没了。规格可以详细到无限。

但当你构建某个东西并且在意输出时,有无限多的可能细节都很重要。有很多类型的实现,你可以直接关掉大脑,让 LLM 替你选一个。但如果你真的想做出一个出色的产品,你仍然需要有远见和品味,并掌控输出。听我的,我就是那个没有远见也没有品味,但真的很想掌控输出的人。

教训:软件不是护城河

你任何一位老员工只要有心,都能在几天内复刻出你产品的大部分功能(如果只是业余随便搞搞,那一两个月也够了)。真正关键的是生态系统、合作伙伴关系,以及所有那些需要你亲自跑腿、打电话、动用关系网、一砖一瓦地在世界上建立自己地位的苦活累活。

脚注

[0]

你可能会说:“但是 Jake,你自己也有一份 Google Docs 的副本啊。Google Docs 是免费的。你直接用它复制粘贴内容不就行了。”但我反过来问你:“它真的是我的吗?我有自由把真正的 Google Docs 做得这么功能残缺、性能低下吗?”

来源:Hacker News · bitlog.com