NEE's Blog

我如何用 LLM 写软件

March 16, 2026

本文翻译自 How I Write Software with LLMs,原载于 Hacker News。


我并不在乎编程的乐趣

最近我重新燃起了制作东西的热情,这主要归功于 LLM。我原本以为我喜欢的是编程,结果发现我真正喜欢的是创造,而编程只是实现这一目标的一种方式。既然 LLM 已经擅长编程,我就一直用它们不停地做东西——我们正处于又一个完全未被探索的前沿领域的起点,这令人非常兴奋。

目前关于 LLM 有很多争论,但一些朋友询问我的具体工作流程,所以我决定详细写下来,希望能帮助他们(以及你)更轻松、更快速、更高质量地制作东西。

文章末尾我还附上了一个真实的(带注释的)编程会话。如果你想跳过工作流程的细节,可以直接去那里看。

收益

从 Codex 5.2 发布(感觉像是一个世纪前)开始,尤其是最近的 Opus 4.6,我第一次惊喜地发现:我可以用 LLM 写软件,而且缺陷率非常低——可能比我手写代码还要低——同时我仍然完全了解整个系统是如何工作的。在此之前,代码在编程两三天后很快就会变得难以维护,但现在我已经连续几周开发几个项目,增长到数万行有用的代码,每次修改都和第一次一样可靠。

我还注意到我的工程技能并没有变得无用,只是转移了重心:我不再需要知道如何正确地写代码,但现在理解如何正确地架构系统以及做出正确的选择变得极其重要。

在我不了解底层技术的项目上(比如移动应用),代码仍然会很快变成一堆糟糕的选择。然而,在我熟悉所用技术的项目上(比如后端应用,不一定是 Python),这种情况还没发生过,即使到了数万行代码。这大部分是因为模型变得更好了,但我认为很大程度上也是因为我改进了与模型协作的方式。

我注意到不同的人使用 LLM 得到的结果差异很大,所以我怀疑你与它们交流的方式会影响结果。因此,我会在本文中深入细节,甚至发布实际的会话记录,让你可以看到我开发的所有细节。

另一点值得一提的是,我不知道模型未来会如何演变,但我注意到一个趋势:在 LLM 的早期(GPT-2 时代不算,因为它太有限了,但从 davinci 开始),我必须检查每一行代码确保它是正确的。随着 LLM 的升级,这个检查级别上升到函数级别,所以我不必检查代码,但必须检查函数是否正确。现在,这主要是在”整体架构”级别,也许明年连这都不需要了。不过目前,你仍然需要一个有良好编码技能的人类。

我用这种方式构建的东西

最近我构建了不少东西,我想在这里列出其中一些,因为对 LLM 的一个常见批评是人们只用它们写玩具脚本。这些项目从严肃的日常工具到艺术项目都有,但它们都是真实的、我在日常使用的项目:

Stavrobot

我最近构建的最大东西是 OpenClaw 的一个替代品,专注于安全性。我想要一个 LLM 个人助理已经多年了,我终于实现了。大多数人会说”但你不能让 LLM 安全啊!”,这是误解了安全就是关于权衡的,我的代理试图做的是在给定的可用性下最大化安全性。我认为它非常成功,我已经使用了一段时间,真的很喜欢我能够准确推理它能做什么和不能做什么。

它管理我的日历并智能地对我的可用性或任何冲突做出决策,为我做研究,通过编写代码扩展自己,提醒我所有我以前会忘记的事情,自主管理家务等等。助手的好处很难解释,因为它们没有一个杀手级功能,而是缓解了一千个小痛点,而每个人的痛点都不同。

我计划很快写一篇详细的文章,因为设计它时遇到了一些非常有趣的挑战,我喜欢我解决它们的方式。

Middle

也许我的命名最近不太好,但这是一个小挂件,可以录制语音笔记,转录它们,并可以选择将它们 POST 到你选择的 webhook。我让它把语音笔记发送给我的 LLM,随时从口袋里拿出来,按一个按钮,记录一个想法或问一个问题,知道下次我检查助手消息时答案或待办事项就在那里,感觉太棒了。

这是一个简单的东西,但它的有用性不在于它做什么,而在于它怎么做。它总是可用、总是可靠、零摩擦使用。

Sleight of hand

我也计划写一些关于这个东西的内容,但这更像是一件艺术品:这是一个会不规则地滴答作响的挂钟,但每分钟都是准确的(时间通过互联网同步)。它有多种模式,一种模式有可变的滴答时间,从 500 毫秒到 1500 毫秒,这令人愉悦地愤怒。另一种模式滴答得几乎察觉不到地快于一秒,然后随机暂停一秒,让毫无戒心的观察者质疑自己的理智。还有一种模式以双倍速度跑到 :59 然后在那里等三十秒,最后一种就是普通的时钟,因为所有不规则的滴答声让我发疯。

Pine Town

Pine Town 是一个异想天开的多玩家无限草地画布,你有自己的一小块土地可以画画。大多数人画……有问题的内容,但偶尔会有成年人来访并画一些不错的东西。有些画真的很棒,通常滚动看看人们做了什么很有趣。

所有这些项目我都是用 LLM 构建的,我甚至从未阅读过它们的大部分代码,但我仍然对每个项目的架构和内部工作原理了如指掌。这是怎么做到的:

工具框架(Harness)

对于工具框架,我使用 OpenCode。我非常喜欢它的功能,但显然有很多选择,我用 Pi 的体验也不错。但无论你使用什么框架,它需要让你能够:

  • 使用来自不同公司的多个模型。大多数第一方框架(Claude Code、Codex CLI、Gemini CLI)做不到这一点,因为公司只想让你使用他们的模型,但这是必要的。
  • 定义可以自主相互调用的自定义代理。

还有各种其他不错的功能,如会话支持、worktree 管理等,根据你的项目和技术栈你可能想要这些,但这些都由你决定。我会解释上面的两个要求,以及为什么它们是必要的。

多个模型

你可以把特定的模型(如 Claude Opus)看作一个人。当然,你可以用全新的上下文重新开始,但模型主要会有相同的意见/优势/弱点,而且很可能同意自己。这意味着让一个模型审查它刚刚写的代码基本上没用,因为它倾向于同意自己,但让一个不同的模型来审查代码会带来很大的改进。本质上,你是从第二双眼睛获得审查。

不同的模型在这里会有不同的优势和劣势。例如(这对今天的模型来说非常具体),我发现 Codex 5.4 相当挑剔和迂腐。这不是我写代码时想要的,但这绝对是我在审查时想要的。Opus 4.6 做出的决定与我会做出的决定非常一致,而 Gemini 3 Flash(是的,Flash!)甚至在想出其他模型没看到的解决方案方面非常出色。

每个人都有不同的意见,哪种模型适合哪个工作,而且模型往往会交替使用(例如,我在 11 月使用 Codex 作为我的主要模型,后来切换回 Opus)。为了获得最佳结果,你需要混合使用所有模型。

相互调用的代理

我使用的工作流程由不同的代理组成,如果框架没有能力让代理相互交流,你就要在 LLM 之间做很多烦人的信息传递。你可能想减少这种情况,所以这是一个非常有用的功能。

我的工作流程

我的工作流程包括一个架构师、一个开发者和一到三个审查者,取决于项目的重要性。这些代理被配置为 OpenCode 代理(基本上是技能文件,包含关于我希望每个代理如何行为的说明的文件)。我手写这些,因为我发现如果你让 LLM 写技能并没有真正帮助,这就像让某人写关于如何成为一名伟大工程师的说明,然后给他们自己的说明并说”这是如何成为一名伟大工程师,现在成为一位”。这显然不会让他们变得更好,所以我尝试自己写说明。

架构师

架构师(目前是 Claude Opus 4.6)是我唯一与之交互的代理。这需要是一个非常强大的模型,通常是我能访问的最强模型。这一步不消耗太多 token,因为主要是聊天,但你希望这是非常深思熟虑的。

我会告诉 LLM 我的主要目标(这将是一个非常具体的功能或错误修复,例如”我想在 Stavrobot 中添加带指数退避的重试,这样如果 LLM 提供商宕机它可以重试”),并与其交流直到我确定它理解了我想要什么。这一步花费最多时间,有时甚至长达半小时的来回讨论,直到我们最终确定所有目标、限制和方法的权衡,并同意最终架构应该是什么样子。它会产生一个相当低级别的计划,详细程度达到单个文件和函数。例如,任务可能是”我将在这个文件的这两个组件的这三个代码路径中添加指数退避,因为没有其他组件与 LLM 提供商通信”。

我知道有些人在这一步更喜欢让 LLM 把计划写到一个文件中,然后他们把自己的反馈添加到那个文件中,而不是与 LLM 交流。这是个人偏好的问题,因为我可以看到两种方法同样有效,所以如果更适合你,请随意以那种方式进行审查。就我个人而言,我更喜欢与 LLM 聊天。

澄清一下,在这一步我不仅仅是在提示(prompting),我是在 LLM 的帮助下塑造计划。我仍然需要大量纠正 LLM,要么因为它错了,要么仅仅因为它做事的方式不是我会做的方式,这是我贡献的很大一部分,也是我从中获得乐趣的部分。这个方向让我可以把项目称为我的,因为使用相同 LLM 的其他人会想出不同的东西。

当我满意我们已经解决了所有问题(LLM 在这方面非常有帮助,会为它还不知道的事情提问并给我选项)时,我终于可以批准计划。我已经要求架构师在我真正说”approved”这个词之前不要开始任何工作,因为一些模型往往过于急切,当它们觉得理解了就开始实现,而我想确保有信心它理解了。

然后,架构师会将工作分解成任务,把每个任务写到一个计划文件中,通常比我们的聊天更详细(在更低的级别),并调用开发者开始工作。这给开发者具体的方向,并最大限度地减少开发者可以做的高级选择,因为这些选择已经为它做出了。

开发者

开发者可以是一个较弱、更省 token 的模型(我用 Sonnet 4.6)。计划不应该给它太多回旋余地,它的工作严格是实施计划中的更改。当它完成后,它调用审查者来审查它的工作。

审查者

每个审查者都会独立查看刚刚实现的功能计划和差异(diff),并对其进行批评。对于这一步,我总是会使用至少 Codex,有时我会添加 Gemini,在重要的项目上我也会添加 Opus。

这个反馈会回到开发者那里,如果审查者同意,开发者就会整合它;如果审查者不同意,就升级到架构师。我发现 Opus 非常擅长选择正确的反馈来实现,有时会忽略反馈因为它太迂腐(即难以实现且在实践中不太可能成为问题)。显然,当我使用客观评估如”非常好”时,我真正的意思是”我非常同意它”。

整体方法

这种工作方式意味着我仍然知道函数级别以上做出的每一个选择,并可以在后续运行中使用这些知识。我经常注意到 LLM 推荐的东西在另一个代码库中可能是好的,但在我的代码库中要么行不通要么是次优的,这表明 LLM 在研究代码时有一些盲点。我经常说”不,你应该用 Y 来做这个”,这时 LLM 意识到 Y 实际上存在于代码中,是一个比它推荐的更好的方式。

失败模式

另一方面,当我对技术不够熟悉以至于无法掌控架构时,我倾向于不发现 LLM 做出的糟糕决定。这导致 LLM 在这些糟糕决定之上构建越来越多的东西,最终陷入无法解开混乱的状态。当你不断告诉 LLM 代码不工作,它说”我知道原因!让我修复它”然后不断把事情搞得更糟时,你就知道发生了这种情况。

这是一个真实的失败模式,已经发生在我身上太多次了,这就是为什么我最终采用了这个工作流程。出于这个原因,我尝试在规划时尽可能多地理解事物,即使我不熟悉具体的技术。如果我能够很好地引导 LLM,它会省去很多后来的麻烦。

一个真实的会话

这是从一个真实会话中摘录的注释转录,我在其中为 Stavrobot 添加电子邮件支持。我删减了工具调用和冗长的部分,但对话和决策过程完全按照发生的情况呈现。

我开始告诉 LLM 我想实现什么,在非常高的层面上。有时我会给它更多细节,特别是如果我已经有了一个我想要如何实现的想法。

我: 我想给这个机器人添加电子邮件支持。让我们想想该怎么做。

机器人阅读代码并找到所有相关的部分,并提出一些问题。

LLM: 获得了良好的概览。这是我看到的情况和我们需要解决的问题:

当前模式

所有渠道都遵循相同的流程:入站 webhook/listener → enqueueMessage(message, source, sender, attachments) → LLM 处理 → LLM 调用 send_*_message 工具回复。渠道要么是进程内的(Telegram webhooks,通过 Baileys 的 WhatsApp),要么是独立的容器(Signal bridge)。电子邮件将是一个遵循相同模式的新渠道。

关键设计问题

  1. 入站 — 电子邮件如何到达?
    • IMAP 轮询(最简单 — 按时轮询邮箱,适用于任何提供商)
    • 来自事务性电子邮件服务的 Webhook(SendGrid/Mailbird 入站解析 — 需要该服务)
    • 运行 SMTP 服务器(最复杂,需要 DNS/MX 设置)
  2. 出站 — 我们如何发送?
    • SMTP 客户端(适用于任何提供商 — Gmail、Fastmail、自托管)
    • 事务性电子邮件 API(SendGrid/Mailgun API)
  3. 你的实际用例是什么?这会影响几个决定:
    • 完全双向对话(回复电子邮件,维护线程)?
    • 来自机器人的单向通知?
    • 处理传入的电子邮件(例如,转发的电子邮件、摘要)?
  4. 架构 — 独立容器还是进程内? Signal 使用 bridge 容器是因为 signal-cli 是一个重量级的 Java 进程。电子邮件库很轻量,所以进程内(像 Telegram)是可行的。但是关于不向应用容器添加运行时的 AGENTS.md 规则在这里不适用,因为它只是 TypeScript/Node。

  5. 电子邮件特有的问题:
    • 如何处理 HTML 电子邮件?剥离为纯文本?传递 markdown 转换给 LLM?
    • 线程跟踪 — 通过 In-Reply-To/References 头匹配回复到对话,还是将每封电子邮件独立处理?
    • 附件 — 需要吗?

你设想的场景是什么?这会快速缩小范围。

在这个会话中,我只是带着一个还没有仔细思考过的想法。LLM 通过提出具体问题来帮助,我决定我想要实现走哪条路:

我:

  1. 电子邮件将通过 webhook 到达。是的。我会创建一个 Cloudflare Email worker,它将通过 webhook 转发电子邮件的整个正文。它会上传消息的 JSON 版本:{“from”:”me@mydomain.com”, etc}
  2. SMTP 客户端
  3. 是的,完全双向对话。这将与其他传输的 interlocutor 工作方式相同,如果有人向机器人发送电子邮件,除非他们被设置为 interlocutor,否则将被忽略。所有者也可以发送电子邮件或转发电子邮件进行处理。
  4. 进程内,因为这将是 webhook。
  5. 是的,Markdown 转换。让我们暂时将每封电子邮件独立处理,然后如果必要我们可以更改。需要附件,它们应该像任何其他平台一样工作,代理将作为文件接收它们。

我设想的场景是既能够通过子代理(像机器人目前用 Signal/Telegram/Whatsapp 做的那样)通过电子邮件与人通信,也能够让所有者转发电子邮件(例如预订的确认电子邮件)让机器人处理和摄取信息。这有意义吗?你能预见到什么边缘情况,或者你担心什么?

LLM 塑造计划,给它更多细节:

LLM: 这很有意义,并且干净地适应现有的渠道模式。以下是我的观察和我看到的边缘情况:

关注点和边缘情况

  1. MIME 解析非琐碎。纯文本电子邮件很简单,但现实世界的电子邮件是多部分 MIME(HTML+text 替代方案、内联图像、附件、嵌套的多部分、各种字符编码)。我们应该使用 mailparser(来自 Nodemailer 项目)在服务器端解析原始 RFC 2822 消息,而不是尝试在 worker 中处理这个。这意味着 worker 负载可以最小化 — 只是 from、to 和 raw。

  2. Webhook 认证。端点必须是公开的(Cloudflare 不能做浏览器登录),但我们需要防止任何人 POST 假电子邮件。匹配 Telegram 模式的最简单方法:在启动时生成随机密钥,但由于我们无法回调 Cloudflare 注册它,我们会在 config.toml 中配置一个共享密钥,让 worker 将其作为头发送。端点验证它。

  3. 出站电子邮件需要一个主题行。每个其他渠道都是无主题的,但电子邮件需要一个。send_email 工具需要一个 LLM 提供的主题参数。这是代理需要处理的新事物。

  4. 仅 HTML 的电子邮件。有些电子邮件根本没有纯文本部分。mailparser 可以提取 textAsHtml,我们会将其转换为 markdown。html-to-text 库处理得很好,或者 mailparser 本身提供一个做基本 HTML 剥离的 .text 属性。

  5. From 地址身份。对于出站 SMTP,from 地址必须与 SMTP 服务器允许的匹配。这是一个配置值。如果有人给 bot@mydomain.com 发电子邮件,回复应该来自那个地址。

  6. 转发电子邮件处理。当所有者转发电子邮件时,原始发件人的地址将在转发的正文中,而不是在 from 字段(将是所有者的地址)。LLM 需要在上下文中理解这一点 — 不需要特殊代码,转发的内容只是会在消息正文中。这应该自然工作。

  7. 出站附件。现有的 send_*_message 工具通过文件路径支持附件。send_email 也需要相同的,使用 nodemailer 附加文件。这很简单。

建议的设计

Worker 简化:

// 只发送我们需要的 — 从 raw 服务器端解析所有内容
const payload = {
  from: message.from,
  to: message.to,
  raw: rawEmail
};
// 添加认证头
headers: {
  "Authorization": "Bearer <secret>",
  "Content-Type": "application/json"
}

配置(config.toml):

[email]
smtp_host = "smtp.example.com"
smtp_port = 465
smtp_user = "..."
smtp_password = "..."
from_address = "bot@mydomain.com"
webhook_secret = "some-random-secret"

入站流程:

  • POST /email/webhook(公共路由,由 webhook_secret 头验证)
  • 用 mailparser 解析 raw → 提取主题、文本正文(HTML 回退 → markdown)、附件
  • 格式化消息给 LLM 为:Subject: …\n\n
  • 通过 saveAttachment() 保存附件
  • enqueueMessage(message, “email”, senderEmailAddress, attachments)
  • 将 “email” 添加到 GATED_SOURCES,添加电子邮件处理到 isInAllowlist()

出站流程:

  • 新的 send_email 工具带参数:to、subject、body(markdown)、attachments(可选文件路径)
  • 将 markdown 正文转换为电子邮件的 HTML(带纯文本回退)
  • 通过 nodemailer SMTP 传输发送
  • Interlocutor 身份服务:”email”,标识符:电子邮件地址

新依赖:mailparser、nodemailer、html-to-text(或者我们可以依赖 mailparser 的内置文本提取并最初跳过 html-to-text — 如果质量不够我们可以添加它)。

涉及的文件:

  • src/email.ts — 新文件:webhook 处理程序、MIME 解析、入站处理
  • src/email-api.ts — 新文件:通过 nodemailer 的 SMTP 发送
  • src/agent.ts — 添加 send_email 工具定义
  • src/config.ts — 添加 EmailConfig 接口并加载它
  • src/index.ts — 添加 /email/webhook 路由,在 isPublicRoute() 中白名单它
  • src/queue.ts — 将 “email” 添加到 GATED_SOURCES
  • src/allowlist.ts — 添加电子邮件渠道处理
  • config.example.toml — 添加 [email] 部分

明确不做(YAGNI):

  • 电子邮件线程 / In-Reply-To 头(你说独立处理)
  • CC/BCC 处理
  • 退信处理
  • 已读回执
  • 速率限制
  • 电子邮件签名剥离(LLM 可以在上下文中处理这个)

这看起来对吗?在我把它写成任务之前有什么要改变的吗?

我回复了一些我看到它遗漏的东西。这需要我很好地了解架构,遵循这个过程让我保持对所有变化的中等低级别的最新了解。

我: 确保也更新 README.md 和 config.example.toml。还要记得更改电子邮件白名单页面以不对这些验证 E.164。

LLM 更新计划并询问任何最后的问题。

LLM: 好的,现在我有了完整的图景。这是更新的协议:

需求

  1. 通过 Cloudflare Email Worker webhook 的入站电子邮件 → POST /email/webhook(公共,由共享密钥认证)
  2. 通过 SMTP(nodemailer)的出站电子邮件通过 send_email 工具(带 to、subject、body、可选 attachments)
  3. 门控渠道 — 电子邮件添加到 GATED_SOURCES、白名单和 interlocutor 身份系统(服务:”email”,标识符:电子邮件地址)
  4. 白名单 UI — 设置页面中新的”电子邮件白名单”部分;验证电子邮件格式(不是 E.164)
  5. 白名单数据 — Allowlist 接口获得一个 email: string[] 字段;后端验证接受电子邮件地址
  6. 所有者配置 — config.toml 中的 owner.email,像其他渠道一样自动种子到白名单
  7. MIME 解析 — 使用 mailparser 从原始 RFC 2822 消息中提取主题、文本正文(HTML 回退)和附件
  8. 给 LLM 的消息格式 — Subject: \n\n,以便代理看到电子邮件是关于什么的
  9. 配置 — 新的 [email] 部分:smtp_host、smtp_port、smtp_user、smtp_password、from_address、webhook_secret
  10. 更新 config.example.toml、README.md

约束

  • 进程内(无新容器)
  • 电子邮件是可选的 — 如果 config 中没有 [email],不加载电子邮件功能(像其他可选渠道一样)
  • 完全遵循现有模式(webhook 处理像 Telegram,白名单像 WhatsApp/Signal,工具定义像其他发送工具)

成功标准

  • 可以通过 webhook 接收电子邮件,由 LLM 处理,LLM 可以通过 send_email 回复
  • 白名单 UI 适用于电子邮件地址
  • 所有者电子邮件自动种子到白名单
  • 构建并通过现有测试;webhook 处理程序和白名单验证的新测试

非目标 / 超出范围

  • 电子邮件线程(In-Reply-To / References 头)
  • CC/BCC 处理
  • 退信/投递状态处理
  • 已读回执
  • 电子邮件签名剥离
  • 速率限制
  • 出站正文的 Markdown 到 HTML 转换(目前纯文本就可以 — 我们以后可以添加这个)

在我写出任务之前有一个问题:对于出站,正文应该只作为纯文本发送,还是你想要 HTML 电子邮件(将 LLM 的 markdown 转换为 HTML)?纯文本更简单并且到处都工作,但 HTML 看起来更好。我现在倾向于纯文本,以后如果需要再添加 HTML。

另外 — worker 负载。我会将其简化为只是 { from, to, raw } 并从 raw 服务器端解析所有内容。这样你不需要在 worker 中维护解析逻辑。听起来好吗?

有时我会在阅读 LLM 的消息之前记住一些事情并告诉它:

我: 在 config.example.toml 中添加设置说明,包括实际使用的 Cloudflare worker。

它适应并整合我的关注点,重复它的消息。

LLM: 明白 — config.example.toml 将在注释中包含 worker 代码和设置说明。让我用那个更新计划并呈现完整的任务分解。

关于你没有解决的我的两个开放问题:

  1. 出站正文格式 — 我现在会用纯文本(YAGNI)。我们以后可以添加 markdown→HTML 转换。
  2. Worker 负载 — 我会简化为 { from, to, raw } 带认证头,并从 raw 服务器端解析所有内容。

建议的计划目录

misc/coding-team/email-support/

任务分解

  1. 配置和依赖 — 将 EmailConfig 接口添加到 config.ts,[email] 部分到 config.example.toml(带 Cloudflare worker 设置说明),安装 mailparser 和 nodemailer(以及它们的 @types/ 包)。将 owner.email 添加到 OwnerConfig。

  2. 白名单 — 将 email: string[] 添加到 Allowlist 接口,更新 validateAllowlist、loadAllowlist(所有者电子邮件自动种子)、getAllowlist、isInAllowlist、getOwnerIdentities。将 “email” 添加到 queue.ts 中的 GATED_SOURCES。

  3. 白名单 UI 和后端验证 — 将”电子邮件白名单”部分添加到设置页面 HTML/JS(带电子邮件格式验证而不是 E.164),更新 handlePutAllowlistRequest 以验证和接受电子邮件数组,更新 saveAllowlist 序列化(包括 notes 键修剪和所有者身份合并)。

  4. 入站电子邮件 — 新 src/email.ts:webhook 处理程序,验证密钥,用 mailparser 解析原始电子邮件,提取主题 + 文本正文 + 附件,保存附件,并调用 enqueueMessage。将 /email/webhook 路由添加到 index.ts 并在 isPublicRoute 中白名单它。

  5. 出站电子邮件 — 新 src/email-api.ts:nodemailer SMTP 传输设置和发送函数。将 send_email 工具添加到 agent.ts(参数:recipient、subject、body、可选 attachments),遵循与其他发送工具相同的模式(interlocutor 查找、白名单检查)。

  6. README 和测试 — 更新 README.md 以记录电子邮件支持。在现有测试文件中添加 webhook 处理程序验证和白名单电子邮件验证的测试。

这看起来对吗?说”approved”继续。

我确实回去阅读它的消息,因为它通常有好问题,我不想错过回答任何一个。

我: 关于你的问题,1. 纯文本。2. 是的,尽可能简化。

LLM 需要我说”approved”这个词才能继续。

LLM: 注意。计划如上所述。说”approved”继续。

我: Approved。

我在 LLM 工作时记住了什么并停下来问:

我: 顺便问一下,电子邮件对机器人来说会是什么样子?它能看到主题和正文吗?

LLM: 是的 — 计划是将入站消息格式化为 LLM 为:

Subject: Hello!

Hi, this is the body of the email.

所以主题作为前缀可见,由空行与正文分隔。这反映了你自然阅读电子邮件的方式,并给 LLM 足够的上下文来理解电子邮件是关于什么的(对于转发的电子邮件特别有用,其中主题行带有意义)。

对于出站,send_email 工具有一个主题参数,所以 LLM 在回复时会编写主题行。

我: 太棒了,请继续。

LLM 编写计划,调用开发者、审查者,最终完成。为了简洁起见,我在这里省略了所有后台工具/代理调用。

LLM: 所有 6 个任务都已实现。这是完成的工作:

新文件:

  • src/email.ts — 入站 webhook 处理程序(验证 Bearer token 认证,用 mailparser 解析原始 RFC 2822 电子邮件,提取主题/正文/附件,检查白名单,入队消息)
  • src/email-api.ts — 出站 SMTP 传输(nodemailer,带可选附件的纯文本电子邮件)
  • src/email.test.ts — webhook 认证测试

修改的文件:

  • src/config.ts — EmailConfig 接口,Config 和 OwnerConfig 上的 email 字段
  • src/allowlist.ts — Allowlist 上的 email: string[],不区分大小写匹配,所有者种子
  • src/allowlist.test.ts — 12 个新的电子邮件白名单测试
  • src/settings.ts — 电子邮件白名单 UI 部分 + 后端验证(电子邮件格式,不是 E.164)
  • src/settings.test.ts — 9 个新的电子邮件后端验证测试
  • src/queue.ts — “email” 添加到 GATED_SOURCES
  • src/agent.ts — createSendEmailTool(收件人解析、白名单门控、SMTP 发送)
  • src/agent-send-tools.test.ts — 电子邮件发送工具测试
  • src/index.ts — /email/webhook 路由 + isPublicRoute 条目 + 启动时 initializeEmailTransport
  • config.example.toml — 完整的 [email] 部分,带设置说明和 Cloudflare Worker 代码
  • README.md — 电子邮件设置部分,更新的功能列表和架构简介
  • package.json / package-lock.json — mailparser、nodemailer 和它们的类型包

测试结果:421 个测试通过,TypeScript 类型检查干净。

你这边需要的 Worker 更改:将你的 Cloudflare Worker 更新到 config.example.toml 中的简化版本 — 发送 { from, to, raw } 带 Authorization: Bearer 头,并将其指向 https:///email/webhook。

接下来你想做什么?


会话继续进行了更多 QA 轮次、添加电子邮件地址的通配符匹配、关于 SQL 注入的问题以及捕获子代理白名单中的遗漏条目。对话大致如上所述,我或者是发现错误或提出改进,与 LLM 一起完善它,并实现它。

整个功能从头到尾花了大约一个小时,我在那里结束了会话,因为我满意功能运行良好。

尾声

这就是我设置的基本概述。它没有什么极其花哨的东西,但对我来说非常有效,我一直对整个过程的可靠性非常满意。我已经 24/7 运行 Stavrobot 接近一个月了,它一直非常可靠。

如果你有任何反馈或者只是想聊聊,可以在 Bluesky 上联系我,或者直接给我发电子邮件。


总结

这篇文章的核心要点:

  1. 多模型协作:不要只用一个模型。使用强大的模型(如 Opus)做架构设计,用效率更高的模型(如 Sonnet)做实现,用挑剔的模型(如 Codex)做代码审查。

  2. 明确的角色分工:架构师负责理解和规划,开发者负责实现,审查者负责把关。每个角色有明确的职责边界。

  3. 人类仍然是核心:你不需要知道如何写每一行代码,但你需要知道如何架构系统。你的工程经验没有过时,只是转移了重心。

  4. 详细的过程文档:作者展示了从需求讨论到最终实现的完整过程,包括多次来回澄清、中途修正和 QA。这不是一次性提示就能搞定的。

  5. 实用主义:使用 YAGNI 原则,明确不做的事情,专注于当前需要的功能。

这种工作方式让我想起了传统的软件工程最佳实践,只是把其中一些执行工作交给了 LLM。核心的设计决策、架构思考和最终把关仍然需要人类工程师的参与。

comments powered by Disqus