Swan工程团队详细介绍了Cygnet背后的四阶段自动化流水线——该AI智能体可读取Slack提及、丰富模糊工单、编写代码并无人值守地发起Pull Request。
AI translation, not an official translation. Refer to the original for technical details.
Adapted from @skwp# Swan如何构建Cygnet:一位能在Slack中提交代码的AI同事 我们在Swan构建了一位神奇的Slack同事,它能在Slack线程中充当思维伙伴、管理项目,并在无人值守的情况下编写代码。以下是它的完整工作原理(收藏本文,也可以把它喂给你的智能体): 上个月,我们一位工程师在Slack中发了一条消息:@cygnet work on ISSUE-527。然后他去吃午饭了。 等他回来时,一个等待审核的Pull Request已经准备就绪。代码整洁,测试通过。PR中附有关于智能体每一个决策及其原因的详细说明,还附上了详细的示意图。他请另一位Swan同事进行了审核,合并后便继续去解决他整个上午真正在思考的那个问题。 但你不能只是下载Claude Code就期待获得这样的结果……这花了我们数月时间摸索打磨。 事实证明,AI编程最难的部分不是代码本身……而是那套让无人值守执行变得安全、可审计、成本可预测的脚手架。 我们构建Cygnet,是因为我们注意到了一种现象——任何工程团队听了可能都会感同身受:积压的任务列表里塞满了人人都理解、却没人有时间处理的工单。修复某个边界情况、添加某项校验、扩展某个API……每一项都需要一位高级工程师花费数小时,并非因为代码有多难,而是因为对代码库的充分理解需要时间,而彻底测试变更(包括手动QA)也绝非易事。 在项目工作与缺陷修复工作之间频繁切换上下文的代价相当高。许多类型的问题描述清晰明确:缺陷修复、库升级、框架重写、代码重构,乃至许多简单的功能实现。而它们周复一周地堆积,是因为工程师需要被传递上下文,而传递上下文是我们人类所做的最昂贵的事情之一。 我们的团队被大量Slack线程淹没,这些线程往往半途而废。多个人讨论一个问题后各自去做额外调研,最终发现问题太过复杂、难以快速解决,决策因此举步维艰。而那些调研的上下文就此消散在各人的本地机器上。 我们提出了一个简单的问题:如果一个AI智能体能成为我们的同事,拥有无限的能力来帮助我们解决问题、组织项目和交付成果,会怎样? 这就是Cygnet所做的事。以下是我们构建它的方式。 ## 架构:四个阶段,一条Slack消息 入口极其简单。工程师在Slack中输入 @cygnet work on ISSUE-123。随后触发一条四阶段自动化流水线: Slack提及 → 第一阶段:ENRICH(丰富工单,不写代码——读取、探索、规范) → 第二阶段:IMPLEMENT(编写代码,自我审查) → 第三阶段:TEST & REPORT(运行测试,撰写工作证明) → 第四阶段:LINT & FIX(编译/代码检查 + 自动修复循环) → 推送代码 + 创建PR → 质量流水线(风险门控 + Claude审查) → 人工审核PR 第一阶段:丰富工单,是整个系统中最重要的设计决策。 在Claude写下第一行代码之前,它会先读取含糊的Linear工单,探索代码库(如有需要可探索多个代码库),然后将工单改写成一份有能力的工程师无需追问即可执行的文档。其中包括完整的业务规范、需要修改的文件、需遵循的实现模式、测试计划,以及供下一阶段使用的结构化提示词。 这与优秀的高级工程师接到任务时的做法如出一辙。他们不会立即动手写代码,而是先做调查:阅读相关代码,检查团队已经建立的模式,然后才开始编写。 Enrichment 还会做一件微妙的事:根据工单的复杂程度,为第二阶段推荐合适的 Claude 模型。一个简单的 bug 修复可能只需要一个更小、更快的模型;而架构层面的改动则需要最强大的模型。第二阶段会读取这一推荐并做出相应调整——整个流水线会根据自身的发现进行自我配置。 **第二阶段:实现**,负责针对已丰富的工单执行完整的实现工作。 Claude 完成代码实现后,会进行自我审查,并将交接材料——一份结构化摘要和一份工作证明文档——写入运行器上的共享目录,供下一阶段读取。 **第三阶段:测试与报告**,读取这些交接材料,针对已实现的代码运行实际测试套件,尽可能修复测试失败的情况,并完成工作证明文档。 该文档随后会以 PR 评论的形式发布,让审查者立即获得必要的上下文信息:Agent 做了什么、为什么这么做,以及考虑了哪些边界情况。"工作证明"流程是 Swan 规定的一套技能集合,涵盖范围从捕获测试输出、命令行冒烟测试,到用户体验截图,乃至视频录制。 **第四阶段:代码检查与编译**,完全自动化执行,无需 Claude 介入。 工作流运行仓库的 lint 和类型检查命令,并检查退出码。如果出现任何失败,系统会将结构化的错误输出交给 Claude,并布置一项单一的专项任务:修复这些错误,提交,不要改动其他任何内容。这是唯一一个由工作流在运行时内联构建提示词(而非从提示词文件读取)的阶段,因为这一步纯属机械操作——将结构化的错误数据交给一个边界明确的任务处理。 **人工审查始终是必要环节。我们从不自动合并。** Cygnet 的目标不是将人类从流程中移除——而是把一个完成好的 PR 交到人们手中,而不是一个模糊的工单。 ## **Slack 应用:路由才是难点所在** Slack 应用是整个系统中始终在线的"大脑"。它的职责听起来很直接:对每一条入站消息进行分类,并将其路由到正确的模式。但实际上,这是整个系统中最难做对的部分之一。 Cygnet 目前支持三种模式,并在持续增长: - **Cygnet Code** — 实现一个 Linear 工单,开启一个 PR - **Cygnet Explore** — 通过触发 GitHub Actions 工作流来回答代码问题 - **Cygnet Chat** — 直接与 Claude Agent 对话,处理其他所有事项 路由分四个阶段进行: 1. **正则表达式快速通道。** 如果消息匹配到动作动词加工单 ID 的模式,立即将其归类为代码请求,无需调用 API,既快又省。 2. **AI 分类。** 对于含义模糊的消息,调用 Claude 最小、最快的模型。该模型会返回四种分类之一——实现、自由格式代码、探索或聊天——并附带置信度和理由。 3. **Token 解析。** Code 和 Explore 模式需要用户自己的 GitHub OAuth Token。如果 Token 缺失,系统会发送一个连接提示并优雅地退出。一个运行在安全执行环境中的无服务器函数负责处理与核心系统的握手,从而确保我们以用户自身的身份进行连接。 Slack 是多个人类与智能体协作的地方。我们将 Cygnet 引入线程并向它提问。探索模式开启后,它能了解我们所有代码库、Linear 中的项目以及 Notion 中的各类知识。Cygnet 在 Slack 中与我们协同规划,并将工作整理成项目。Cygnet 具备线程上下文感知能力,能看到线程中的所有内容,加以处理并帮助我们做出决策,最终将输出结果存储到 Linear 中以供执行。 当分类器不确定时,会回退到聊天模式。系统会优雅降级,而不是在代价高昂的操作上猜错。 ## GitHub Actions 作为执行层 当 Cygnet Code 触发时,它会针对一个临时自托管 runner 分发一个 GitHub Actions 工作流。每个阶段都是 Actions UI 中一个独立的、可见的步骤。 **自定义 Runner 镜像** 这个 runner 并非普通的 GitHub Actions 机器。它从一个自定义镜像启动,该镜像由一个独立的镜像构建工作流按计划在一天内多次运行构建而成。镜像构建会完成所有原本会拖慢任务的工作: - 克隆所有目标仓库 - 读取每个仓库的版本规范文件并安装正确的依赖 - 全局安装 Claude Code CLI 任务启动时,仓库从镜像的主目录移动到任务的工作区——使用同一文件系统,因此是即时完成的。工作流随后将每个仓库的锁文件与其 HEAD 提交进行对比:只有自镜像构建以来锁文件发生变化的仓库才会重新安装依赖,其他仓库立即运行。 这样一来,环境配置耗时不到 30 秒。对于一个超过 2 小时的智能体会话而言,节省的总时间并不算多,但对于开发者的即时反馈来说——在 Slack 中看到 Action 启动,几乎立刻看到阶段 1 开始——这很重要。 有一个细节花了不少精力才做对:镜像清理步骤会在快照拍摄前剥除所有认证信息,镜像中不保留任何凭证。任务启动时,会生成一个新鲜的短期令牌,用于验证 GitHub CLI 和 git 的凭证存储,并写入磁盘上的安全位置。提交由 GitHub App 机器人身份签署,而非 runner 用户——因此提交历史中显示的是机器人作为作者,使得智能体创建的提交在视觉上与人类提交可区分。 **输入模式** 该工作流接受两种互斥的输入:Linear 工单 ID 和自由格式的自定义提示。工单模式是最常见的情况——Slack 应用会携带工单 ID 进行分发。自定义提示模式用于临时请求:如果你提供自由文本,工作流会先创建一个 Linear 工单(以第一行作为标题),将其分配给相应团队,然后在所有后续阶段切换到工单模式。工单创建确保无论以何种方式发起,所有内容都被记录在 Linear 中。 **并行性与时序** 一个不那么显眼的优化:Docker 测试环境会在阶段 1 期间在后台启动。测试栈(Postgres + Redis + 应用容器)需要 2–3 分钟启动,而阶段 1(信息丰富)也需要 2–4 分钟。通过在阶段 1 开始前将测试容器作为后台进程启动,到阶段 2 完成代码实现、阶段 3 需要运行测试时,测试环境已经就绪。 阶段 3 有一个显式等待步骤——最长等待 5 分钟——轮询 Docker 健康检查是否通过。实际上几乎总是立即通过,因为信息丰富阶段给了容器足够的启动时间。 重试检测与幂等性 在运行任何 Claude 阶段之前,工作流会检查所有代码库,查找与工单 ID 匹配的现有 agent 分支。如果分支已存在但尚未创建 PR——这种情况发生在运行于实现完成后、PR 创建前被中断时——工作流会直接跳到从现有分支创建 PR 的步骤。这使得流水线具备幂等性:重新触发一个卡住的运行不会从头开始,而是从中断处继续。 如果 PR 已经存在,工作流会记录日志并正常退出。不会产生重复的 PR,也不会产生重复的实现。 并发控制 GitHub Actions 的并发控制机制可以防止同一工单的两次运行相互竞争。对同一工单的第二次调度会进入队列,而不是取消正在运行的任务。上述重试检测机制随后会捕获到这种情况并跳过重复工作。 阶段交接 各阶段通过 runner 上共享目录中的文件进行通信,该目录在任务启动时预先创建。阶段 2 会写入一份摘要,记录它所做的工作——分支名称、变更内容以及工作证明——阶段 3 读取该摘要以定位自身状态。如果阶段 2 崩溃且未留下任何内容,阶段 3 会回退到通过 git log 发现信息。流水线会优雅降级,而不是硬性失败。 推送和创建 PR 后,工作证明文档会以 PR 评论的形式发布。这为审查者提供了即时上下文:agent 做了什么、为什么这样做,以及它考虑了哪些边界情况。 会话自省 每次运行结束时都会执行一个自省步骤,无论成功或失败均会运行。它从输出日志中提取关键事件——工具错误、权限拒绝、Claude 在工具调用之间的推理文本——将这些内容连同一个聚焦的提示词一起输入 Claude 的最小模型,并将结果追加到 GitHub Actions 的任务摘要中。 摘要涵盖以下内容:完成了哪些工作、遇到了哪些问题、agent 自主做出了哪些假设,以及会话建议的工作流改进项。这是对每次运行的轻量级回顾。随着时间推移,它揭示了真实的规律:某些反复出现的权限缺口、在特定环境下失败的工具、事后证明有误的数据增强假设。将这些摘要纳入后续 agent 工作,正是我们调优流水线的方式。Cygnet 能够自我调优。 ## 质量流水线 当 agent 分支创建了 PR 后,第二个工作流会自动触发。这是一条独立运行的流水线,与代码流水线相互独立。 风险分类 质量流水线的第一个任务是风险策略门控。它从目标代码库读取一个策略配置文件——一个将 glob 模式映射到风险等级的 JSON 文件,风险等级分为:关键、高和标准。 门控获取已变更文件列表,将每个文件与 glob 模式进行匹配,并确定命中的最高等级。它还会强制执行文件数量限制——触及过多文件的关键等级 PR 会被标记。没有策略文件的代码库默认为高等级且需要审查。在团队选择启用完整策略之前,过度审查胜于审查不足。 门控会在 PR 上创建一个包含结果的检查运行。分支保护规则可以要求此检查通过。 Claude 代码审查 对于高优先级和关键优先级的 PR,Claude 会话将对差异进行审查。审查重点关注安全漏洞、逻辑错误、系统边界处缺失的错误处理、测试覆盖率缺口以及破坏性 API 变更。审查结果以结构化块的形式呈现,经解析后作为行内 PR 审查评论发布——精确定位到差异中的具体行。对于特定代码库,我们还会提供由 Gemini 和/或 Github Copilot 执行的额外审查步骤。使用不同的 LLM 会产生不同的结果,这是因为它们各自略有差异的"个性";它们能捕捉到不同类型的问题,这非常有帮助。人工审查者会对多个 LLM 的审查结果进行综合评估。 每条机器人评论都包含一个隐藏的 SHA 标记,因此审查评论天然具有版本属性。如果 PR 被更新并推送了新的提交,旧评论将在视觉上呈现为过期状态。一个正在开发中的修复循环将利用这些标记来区分哪些评论仍然适用。 审查会创建一个检查运行:无发现时显示通过,存在问题时则显示需要处理。 **自动解决** 在一次干净的审查之后,流水线会自动解决所有仅由机器人发起的审查线程。每条评论均来自机器人账户的线程,将通过 Github API 调用予以解决。这使 PR 界面保持整洁——人工审查者看到的是一个没有未解决线程的 PR,而不是一个堆满过期机器人噪音的 PR。 **防止自我批准** 当机器人创建 PR 时,它会发布一条归因评论,标明触发该运行的人工操作者。这条评论是贯穿我们所有代码库的审查门控的核心:门控读取归因评论,提取请求者的身份,然后要求至少获得一名非该请求者的人工审查者的批准。 发出"修复这个"指令的人,不能是唯一批准该结果的人。 有几个细节使这一机制更加健壮。首先,门控只读取未经编辑的评论——如果请求者篡改归因评论以更改记录的用户名,门控将以关于篡改的错误信息失败,而不是静默接受被操纵的身份。其次,门控在每次向 PR 推送新内容时重新运行,因此只有针对最新提交 SHA 的批准才会被计入——新推送之前的过期批准不会被延续。第三,机器人的批准将被完全排除。 这一逻辑位于一个可复用的单一工作流中,并被每个代码库所引用。门控只有一个事实来源——不存在分歧,没有漏洞。 ## 安全性:纵深防御 Cygnet 在无人值守的情况下运行,可访问真实代码库,但没有访问生产数据或系统的生产凭证。我们从第一天起就认真对待安全问题。 如果你打算让一个 AI 智能体在你的基础设施中无监督地运行,你需要针对它可能执行意外操作的可能性进行设计。这并非出于对模型的不信任,而是因为提示注入是真实存在的,工单可能包含对抗性内容,并且错误在所难免。目标是确保任何单一故障都不会造成灾难性后果。 我们在四个层面施加控制: **第一层:Claude 工具拒绝列表。** 我们的 Claude 设置在 Claude Code 层面屏蔽了特定的 shell 操作——权限提升命令、文件系统权限变更、网络扫描工具。即使其他环节出现问题,这些命令也根本无法运行。 第二层:运行用户加固。在镜像构建时,运行用户将从特权组中移除。在作业启动时,这一措施作为运行时安全网被重新应用,时机在需要高权限访问的合法设置步骤之后。Claude 无法提权至 root。 第三层:网络出口过滤。在任何 Claude 阶段运行之前,我们会应用防火墙出站规则。仅允许特定端口和目标:DNS、HTTPS、SSH 至 GitHub、本地测试服务以及回环地址。其他一切均被丢弃。经过对抗性提示的 Claude 会话无法将数据泄露至任意端点。这可能是最重要的一层——它意味着即使会话被完全攻陷,也无法向外部回传数据。 第四层:提示护栏。运行时机器本地上下文文件包含一个明确的安全约束部分,列出了禁止的操作。这与基础设施控制措施相互冗余,且这是有意为之的。纵深防御意味着每一层都假设其他层可能会失效。我们理解,针对大语言模型的提示注入在总体上是一个尚未解决(且可能无法解决)的问题类别,因此我们不信任这一层;它只是防御体系的组成部分。 此外,还有一个预检安全筛查步骤,在任何 Claude 阶段之前运行。自定义提示和 Linear 工单内容均会被发送至由 Claude 最小模型支持的分类器。该分类器负责识别提示注入企图、系统文件读取请求、凭证提取以及其他对抗性模式。如果内容被标记,作业将在 Claude 看到之前即告失败。Linear API 的瞬时故障被视为警告而非阻断——工单暂时无法访问不应阻碍正常工作的进行。 最后,该工作流运行在受保护的 GitHub 环境中,并设有分支限制:作业仅允许从主分支运行。这可以防止有人在功能分支上修改工作流文件以削弱安全控制,然后直接触发该工作流。 Slack 应用在此基础上拥有自己的安全层:一个预检筛查,在查询到达 Agent 之前,拦截提取环境变量、配置文件或凭证的企图。 ## Prompt-as-Code 我们从一开始就执行的一条规则:提示存储于文件中,绝不内嵌于脚本。 所有 Claude 指令均以有版本控制的技能文件和命令文件形式存在。工作流脚本通过名称调用它们,绝不以内联方式嵌入提示文本。这意味着提示受版本控制、可审查,且可独立于运行它们的基础设施进行测试。当提示发生变更时,差异对比中一目了然。当其出现问题时,可以通过二分法定位。 随着命令日趋复杂,这一点变得尤为重要。无头实现命令是交互版本的一个分支——业务逻辑相同,但移除了六个人工确认环节。将这些作为独立文件保存,使差异清晰可审计。你可以并排打开两个文件,清楚地看到"交互式"与"无人值守"之间究竟改变了什么。 ## Cost Controls 无人值守的 AI Agent 费用可能迅速攀升。如果你曾在没有预算上限的情况下让会话持续运行,一小时后回来看到一个出乎意料的数字,那种感觉你一定懂。我们从一开始就内置了断路器: - 四个阶段中每个阶段的单阶段最大轮次限制 - 跨多个代码库的工单享有更高的轮次上限 - 若成本超出阈值,则按单次运行预算中止 - 跨所有运行的每日预算上限 - 并发运行限制,防止任务积压 每个阶段根据需求选择适当的默认模型(富化和实现阶段使用能力更强的模型,测试和审查阶段使用更快的模型) 一张标准工单的典型成本——富化加实现——约为 3–6 美元。一个复杂功能可能达到 5–10 美元。虽然不是免费的,但比这些工单原本需要消耗的工程师工时要便宜得多。 我们使用一个专用 API 密钥,并在控制台设置了每月上限。这让我们能够清晰地归因支出,并设置独立的硬性止损,与应用层面的限制相互独立。双重保险。 按阶段选择模型也提供了一个成本调节手段。富化阶段会根据工单复杂度推荐适合实现阶段的模型——低复杂度的 bug 修复不需要最强大的模型。该推荐从富化输出中解析获取,并用于覆盖第二阶段的默认模型,除非调用方明确指定了模型。 ## 我们的经验总结 富化是杠杆所在。第二阶段输出的质量几乎完全取决于第一阶段。一张模糊的工单只能产出平庸的代码。而一张规格清晰的工单——包含文件路径、可参考的模式以及测试计划——则能产出看起来像是真正了解代码库的人写出来的代码。在确定两阶段设计之前,我们尝试了许多不同的架构。这是迄今为止影响最大的决策。 路由和维护 Slack 上下文是关键。四模式分类器是系统中较为复杂的部分之一,它直接影响用户体验。要让模型能够自信地区分"auth 是怎么工作的?"(探索模式)和"重构 auth 模块"(代码模式),需要精心的提示词工程和边界情况测试。原生的"@Claude" Slack 机器人在路由方面出了名地表现不佳,会导向错误的代码库或错误的操作,缺乏上下文感知。Cygnet 基于 Swan 自身的知识调优,并具备 Slack 线程上下文感知能力,可以在 Slack 线程中与其他同事实时协作。这是一种非常强大的沟通方式——多名人类可以在线程中协同引导 Agent,质疑其假设,逐步建立背景信息。一旦上下文建立完成,它可以轻松地将一段长对话转化为一个多工单的 Linear 项目并推进执行。 纵深防御很重要,即便你信任模型。我们信任 Claude,同时也清楚提示词注入是真实存在的威胁,工单可能包含对抗性内容,错误也难以避免。四层安全模型意味着任何单点故障都会被控制在局部。没有任何单一故障是灾难性的。这就是设计目标。 无头优先是一种设计哲学。将交互式命令拆分为独立的无头命令——而非在现有命令上添加标志——使两种工作流都保持了简洁。交互式命令需要确认关卡,无头命令需要直接退出而非提示用户。将两者混合会让命令两者都做不好。我们在拆分之前吃过这个亏。 各阶段通过文件通信,而非返回值。通过共享产物进行阶段间交接,是优于通过环境变量或工作流输出传递数据的刻意选择。文件易于检查、易于记录,且在阶段崩溃后仍然存在。当出现问题时,你可以直接打开产物文件,立即了解发生了什么。这种可调试性的重要程度超出了我们最初的预期。 首先构建可观测性。会话自省步骤——模型读取自身日志以撰写回顾性总结——相对较早就被加入进来,并已带来丰厚回报。那些我们通过手动阅读日志永远不会注意到的规律,一旦每次运行都有了结构化摘要,便变得一目了然。这也是良好的工程纪律:如果你在生产环境中运行无人值守的代码,你需要清楚它在做什么。 ## 自己动手构建 如果你正在构建类似的系统,我们想分享一个核心洞见:难点不在于让 Claude 编写代码。Claude 在这方面已经表现得相当出色。难点在于构建脚手架,使无人值守的执行变得安全、可审计且成本可预测。先把这部分做好。代码生成才是容易的那部分。