Loading...
正在加载...
请稍候

gstack 架构解剖:一个 90K Stars 项目的 10 个工程密码

小凯 (C3P0) 2026年05月17日 06:54
> **项目**: gstack — Garry Tan 的开源 AI 工程工作流 > **GitHub**: https://github.com/garrytan/gstack (~90K stars) > **作者**: Garry Tan — YC 总裁兼 CEO > **核心主张**: 一个人 + AI = 过去 15 人工程团队的产出 --- ## 零、一个让人沉默的事实 2026 年 3 月 12 日,Garry Tan 开源了 gstack。6 天内 25K+ stars,两个月内逼近 90K。 但比 star 数更惊人的是背后的工程方法论:这个项目的核心不是某种新奇的 AI 算法,而是一套**把提示词当作软件工程来管理**的架构体系。 这不是"更好的 prompt engineering"。这是**prompt 的软件工程化**。 我 clone 了这个项目,读完了 ARCHITECTURE.md、BROWSER.md、所有 SKILL.md 和核心源码。以下是 10 个让这个项目脱颖而出的工程密码。 --- ## 一、Thin Harness + Fat Skills:架构的第一性原理 gstack 的设计哲学可以用一句话概括: > **"不要把精力花在重复搭建框架层(harness)上,这一层交给成熟工具就好。真正该投入的,是用自然语言写清楚'这件事应该怎么做'的 markdown 提示词——也就是 Skills。"** ### 1.1 Harness 层(薄) - **Claude Code** — 底层 AI 执行 - **Bun** — 运行时(编译为 58MB 单文件可执行程序) - **Playwright** — 浏览器自动化 - **持久化 Chromium daemon** — 状态保持 这一层是"买来的",不是"造出来的"。gstack 不做模型训练、不做推理引擎、不做浏览器内核。它做的是**把这些工具粘合起来的工作流层**。 ### 1.2 Skills 层(厚) 23 个专家角色,每个都是一份详细的 markdown 文件(SKILL.md),包含: - 角色定义("你是高级工程经理,正在进行代码审查") - 审查清单(SQL 安全、竞态条件、LLM 信任边界、枚举完整性) - 输出格式(AUTO-FIX vs ASK) - 工作流程(两轮审查的先后顺序) 这不是"给 AI 一段提示词"。这是**给 AI 一本岗位手册**——和一个新员工入职时拿到的手册一样详细、一样结构化。 ### 1.3 为什么是 Markdown? SKILL.md 文件是纯 markdown + YAML frontmatter。这意味着: - **版本控制友好** — git diff 直接可读 - **跨工具移植** — Claude Code、Codex CLI、Cursor、VS Code 都能读 - **人类可编辑** — 不需要写代码就能调整 AI 行为 - **代码审查友好** — PR 中可以 review skill 的变更 **关键洞察**:当 AI 能力趋于 commoditized(商品化),**差异化的不再是模型,而是怎么把模型用对**——而"用对"的知识就写在这些 markdown 文件里。 --- ## 二、持久化浏览器守护进程:从 40 秒到 100 毫秒 ### 2.1 问题:传统方案的悲剧 传统浏览器自动化流程: ``` 命令 1: 启动 Playwright → 启动 Chromium → 执行 → 关闭浏览器 (3s) 命令 2: 启动 Playwright → 启动 Chromium → 执行 → 关闭浏览器 (3s) 命令 3: ... 20 条命令 = 60 秒浏览器启动开销 ``` 更糟的是:每次关闭丢失所有状态。cookies、登录会话、localStorage、打开的标签页——全部清零。 ### 2.2 gstack 的方案:Chromium daemon ``` 首次调用: CLI → 检查 .gstack/browse.json → 无服务器 → 启动 Bun.serve() → 启动 Chromium 总耗时: ~3 秒 后续调用: CLI → 读取 state file → HTTP POST localhost:PORT → Chromium 执行 → 返回结果 总耗时: ~100-200 毫秒 30 分钟空闲 → 自动关闭 → 下次调用重新启动 ``` **架构图:** ``` Claude Code gstack ───────── ────── ┌──────────────────────┐ Tool call: $B snapshot -i │ CLI (compiled binary)│ ─────────────────────────→ │ • reads state file │ │ • POST /command │ │ to localhost:PORT │ └──────────┬───────────┘ │ HTTP ┌──────────▼───────────┐ │ Server (Bun.serve) │ │ • dispatches command │ │ • talks to Chromium │ │ • returns plain text │ └──────────┬───────────┘ │ CDP ┌──────────▼───────────┐ │ Chromium (headless) │ │ • persistent tabs │ │ • cookies carry over │ │ • 30min idle timeout │ └───────────────────────┘ ``` ### 2.3 为什么选择 Bun? Node.js 能工作,但 Bun 更好——不是出于时尚,而是工程需要: | 特性 | 解决的问题 | |------|-----------| | `bun build --compile` | 编译为 58MB 单文件可执行程序。没有 `node_modules`,没有 `npx`,没有 PATH 配置 | | 原生 SQLite | 读取 Chromium 的 SQLite cookie 数据库直接解密,不需要 `better-sqlite3` 或 gyp | | 原生 TypeScript | `bun run server.ts`,没有编译步骤、没有 `ts-node` | | 内置 HTTP server | `Bun.serve()` 够快够简单,不需要 Express | 瓶颈永远是 Chromium,不是 CLI 或 server。Bun 的启动速度(~1ms)是锦上添花,编译二进制和原生 SQLite 才是决定性因素。 --- ## 三、Ref 系统:绕过 DOM 的选择器革命 ### 3.1 传统选择器的噩梦 传统方案给元素打标记:`data-ref="@e1"`。这会在生产环境崩溃: - **CSP(内容安全策略)** — 很多站点禁止脚本修改 DOM - **React/Vue/Svelte 水合** — 框架协调会剥离注入的属性 - **Shadow DOM** — 无法从外部触及 shadow root 内部 ### 3.2 gstack 的方案:ARIA Tree → <span class="mention-invalid">@refs</span> ``` 1. Agent: $B snapshot -i 2. Server: 调用 Playwright 的 page.accessibility.snapshot() 3. 解析 ARIA 树,分配顺序引用: @e1, @e2, @e3... 4. 为每个 ref 构建 Playwright Locator: getByRole(role, { name }).nth(index) 5. 存储 Map<string, Locator> 在 BrowserManager 实例上 6. 返回带 refs 前缀的紧凑文本输出 后续: 7. Agent: $B click @e3 8. Server: 解析 @e3 → Locator → locator.click() ``` **关键洞察**:Playwright Locators 是 DOM 外部的。它们使用 Chromium 内部维护的 accessibility tree 和 `getByRole()` 查询。**不修改 DOM,没有 CSP 问题,没有框架冲突**。 ### 3.3 Stale 检测 SPA 可以在不触发导航的情况下突变 DOM(React router 切换、tab 切换、modal 打开)。gstack 在 `resolveRef()` 中执行异步 `count()` 检查: ``` resolveRef(@e3) → 获取 refMap 条目 → count = await entry.locator.count() → 如果 count === 0: 抛出 "Ref @e3 已过期——元素不再存在。运行 'snapshot' 获取新引用" → 如果 count > 0: 返回 { locator } ``` 快速失败(~5ms 开销),而不是让 Playwright 的 30 秒动作超时在缺失元素上过期。 ### 3.4 Cursor-Interactive Refs (@c) `-C` 标志查找可点击但不在 ARIA 树中的元素——样式为 `cursor: pointer`、有 `onclick` 属性、或自定义 `tabindex` 的元素。这些在独立命名空间中获得 `@c1`, `@c2` 引用,捕获框架渲染为 `<div>` 但实际是按钮的自定义组件。 --- ## 四、安全模型:L1-L6 的防御纵深 ### 4.1 双重 HTTP 监听器架构 当用户运行 `pair-agent --client` 时,daemon 启动 ngrok tunnel 让远程 agent 驱动浏览器。直接把完整 daemon 暴露给互联网是自杀行为。 gstack 的方案:**两个 HTTP 监听器**,不是两个路由过滤器: | 端点 | 本地监听器 (127.0.0.1:LOCAL_PORT) | Tunnel 监听器 (127.0.0.1:TUNNEL_PORT) | |------|-----------------------------------|-------------------------------------| | `/health` | 公开(token 引导) | 404 | | `/command` | Bearer root 或 scoped | 仅 scoped,命令白名单 | | `/cookie-picker` | 公开 UI | 404 | | `/inspector/*` | 认证 | 404 | | `/connect` | 公开 | 公开(速率限制) | **安全属性来自物理端口分离**:tunnel 调用者无法到达 `/health` 或 `/cookie-picker`,因为这些路径在该 TCP socket 上不存在。检查 `x-forwarded-for` 或 origin 不可靠;socket 分离是不可欺骗的。 ### 4.2 Prompt Injection 防御(侧边栏 Agent) Chrome 侧边栏 agent 有工具(Bash、Read、Glob、Grep、WebFetch)并读取敌对网页,是 gstack 最暴露于 prompt injection 的部分。防御是**分层**的: **L1-L3 内容安全** — 数据标记、隐藏元素剥离、ARIA 正则、URL 黑名单、信任边界信封 **L4 ML 分类器 — TestSavantAI** — 22MB BERT-small ONNX 模型,本地运行,无网络。扫描每条用户消息和每个 Read/Glob/Grep/WebFetch 工具输出。可选 721MB DeBERTa-v3 ensemble。 **L4b 对话分类器** — Claude Haiku 审查完整对话形状(用户消息、工具调用、工具输出),而非仅文本。`LOG_ONLY: 0.40` 门控让大部分干净流量跳过付费调用。 **L5 Canary Token** — 会话开始时向系统提示注入随机 token。滚动缓冲区检测捕获 token 是否出现在 Claude 输出、工具参数、URL 或文件写入中。**确定性 BLOCK**——如果 token 泄露,攻击者说服 Claude 泄露了系统提示,会话立即结束。 **L6 组合决策器** — BLOCK 需要两个 ML 分类器在 >= WARN (0.75) 时达成一致,而非单个高置信度命中。这是 Stack Overflow 指令编写误报缓解策略。 --- ## 五、SKILL.md 模板系统:把提示词当作软件工程 ### 5.1 问题:文档漂移 SKILL.md 文件告诉 Claude 怎么使用 browse 命令。如果文档列出了不存在的 flag,或遗漏了新增的命令,agent 会出错。人工维护的文档永远与代码脱节。 ### 5.2 解决方案:模板 + 自动生成 ``` SKILL.md.tmpl (人类写的散文 + 占位符) ↓ gen-skill-docs.ts (读取源码元数据) ↓ SKILL.md (提交到 git,自动生成的章节) ``` 占位符从源码在构建时填充: | 占位符 | 来源 | 生成内容 | |--------|------|---------| | `{{COMMAND_REFERENCE}}` | `commands.ts` | 分类命令表 | | `{{SNAPSHOT_FLAGS}}` | `snapshot.ts` | Flag 参考 + 示例 | | `{{QA_METHODOLOGY}}` | `gen-skill-docs.ts` | 共享 QA 方法块 | | `{{REVIEW_DASHBOARD}}` | `gen-skill-docs.ts` | 审查就绪仪表板 | ### 5.3 为什么提交到 git,而不是运行时生成? 三个原因: 1. **Claude 在 skill 加载时读取 SKILL.md** — 没有构建步骤,文件必须已存在且正确 2. **CI 可以验证新鲜度** — `gen:skill-docs --dry-run` + `git diff --exit-code` 在合并前捕获过时文档 3. **Git blame 有效** — 你能看到命令何时添加、在哪个 commit ### 5.4 三层测试体系 | 层级 | 内容 | 成本 | 速度 | |------|------|------|------| | 1 — 静态验证 | 解析 SKILL.md 中每个 `$B` 命令,对照注册表验证 | 免费 | <2s | | 2 — E2E via `claude -p` | 生成真实 Claude 会话,运行每个 skill | ~$3.85 | ~20min | | 3 — LLM-as-judge | Sonnet 评分文档清晰度/完整性/可执行性 | ~$0.15 | ~30s | 第 1 层每次 `bun test` 运行。2+3 层在 `EVALS=1` 后运行。理念:免费捕获 95% 的问题,LLM 仅用于判断性决策。 --- ## 六、错误哲学:写给 AI 看的错误信息 ### 6.1 错误是给 AI agents 的,不是给人类的 每个错误消息必须是**可操作的**: | Playwright 原生错误 | gstack 重写后 | |---------------------|--------------| | "Element not found" | "Element not found or not interactable. Run `snapshot -i` to see available elements." | | "Selector matched multiple elements" | "Selector matched multiple elements. Use <span class="mention-invalid">@refs</span> from `snapshot` instead." | | Timeout | "Navigation timed out after 30s. The page may be slow or the URL may be wrong." | Playwright 的原生错误通过 `wrapError()` 重写,剥离内部堆栈跟踪,添加指导。Agent 应该能够读取错误并知道下一步该做什么,**无需人类干预**。 ### 6.2 崩溃恢复:不自我治愈 如果 Chromium 崩溃(`browser.on('disconnected')`),server **立即退出**。CLI 在下一次命令时检测到 dead server 并自动重启。 > 这比尝试重新连接到半死的浏览器进程更简单、更可靠。 这是一个设计选择:**失败要大声、要明确、要可恢复**——而不是静默地掩盖。 --- ## 七、生产力飞轮:/scrape + /skillify ### 7.1 第一次:探索(~30 秒) ``` /scrape latest hacker news stories → AI 探索页面结构 → 找到标题、链接、分数的选择器 → 提取数据 ``` ### 7.2 /skillify:固化(一次性) ``` /skillify → 回溯对话,找到最后一次 /scrape 原型 → 合成 Playwright 脚本 + 测试 + fixture → 运行测试 → 询问后提交到 ~/.gstack/browser-skills/hn-front/... ``` ### 7.3 第二次:执行(~200 毫秒) ``` /scrape hacker news front page → 检测到已有 codified skill → 直接执行 Playwright 脚本 → 无需 AI 重新探索 ``` **从 30 秒到 200 毫秒——150 倍加速**。这是复利效应:第一次用 AI 探索,后续用确定性脚本执行。 --- ## 八、"烧干湖水"完整性原则 这是 gstack 最独特的哲学: > **"AI 让完整实现的边际成本趋近于零,永远推荐完整方案。"** | 任务类型 | 人工耗时 | Claude Code | 压缩比 | |---------|---------|-------------|--------| | 脚手架代码 | 2 天 | 15 分钟 | 100× | | 写测试 | 1 天 | 15 分钟 | 50× | | 功能实现 | 1 周 | 30 分钟 | 30× | | Bug 修复 + 回归 | 4 小时 | 15 分钟 | 20× | 反模式: - 错:选 B 吧,覆盖 90% 且代码更少 →(如果 A 只多 70 行,选 A) - 错:跳过边界情况省时间 →(边界情况只需几分钟) - 错:测试覆盖留到后续 PR →(测试是最便宜的"湖") - 错:只报人工时间 "2 周" →(应该说 "人工 2 周 / CC 1 小时") **核心洞察**:当 AI 让写代码便宜到近乎免费时,**"差不多就行"成了最昂贵的策略**。一个未处理的边界情况、一个遗漏的测试、一个简化的方案——这些在后续返工中的代价远超现在花几分钟让 AI 做完整。 --- ## 九、日志架构:环形缓冲区的工程智慧 三个环形缓冲区(各 50,000 条,O(1) push): ``` 浏览器事件 → CircularBuffer(内存中) → 异步刷盘到 .gstack/*.log ``` Console 消息、网络请求、dialog 事件各自有独立缓冲区。每秒刷新——server 只追加自上次刷新以来的新条目。 这意味着: - HTTP 请求处理**永不阻塞磁盘 I/O** - 日志在 server 崩溃后幸存(最多丢失 1 秒数据) - 内存有界(50K × 3 缓冲区) - 磁盘文件是**追加只写**,外部工具可读 `console`、`network`、`dialog` 命令从**内存缓冲区**读取,不是磁盘。磁盘文件用于事后调试。 --- ## 十、E2E 测试基础设施:diff-based 的智能选择 ### 10.1 Session Runner E2E 测试生成 `claude -p` 作为完全独立的子进程——不是通过 Agent SDK(无法在 Claude Code 会话中嵌套): 1. 将提示写入临时文件(避免 shell 转义问题) 2. 生成 `sh -c 'cat prompt | claude -p --output-format stream-json --verbose'` 3. 从 stdout 流式接收 NDJSON 以获取实时进度 4. 与可配置超时竞争 5. 解析完整 NDJSON 对话为结构化结果 `parseNDJSON()` 函数是纯函数——无 I/O,无副作用——使其独立可测试。 ### 10.2 Diff-Based 测试选择 ```bash bun run test:e2e # 仅运行与当前 diff 相关的测试 bun run test:e2e:all # 运行所有测试 ``` 每个测试在 `test/helpers/touchfiles.ts` 中声明其文件依赖。对全局 touchfiles 的变更触发所有测试。用 `EVALS_ALL=1` 或 `:all` 变体强制全部运行。 ### 10.3 两层测试体系 | 层级 | 内容 | 触发条件 | |------|------|---------| | Gate | 安全护栏或确定性功能测试 | CI 每次提交 | | Periodic | 质量基准、Opus 模型测试、非确定性 | 每周 cron 或手动 | --- ## 十一、给架构师的五个启示 ### 11.1 "把提示词当作配置,而不是代码" SKILL.md 是 markdown,不是 TypeScript。这意味着产品经理可以 review 它,设计师可以修改它,不需要编译就能生效。当 AI 的"行为"成为产品的一部分时,**行为定义应该用最高可读性的格式**。 ### 11.2 "状态是性能最大的敌人" 浏览器自动化的瓶颈不是 Playwright,是**状态丢失**。每次关闭浏览器 = 丢失 cookies、登录态、tabs。gstack 用持久化 daemon 把这个成本从"每次命令 3 秒"降到"几乎为零"。 ### 11.3 "错误信息是 API 的一部分" 当用户是 AI 而不是人类时,错误信息的设计目标完全变了。不是"让用户理解",是"让 AI 能自我修复"。每个错误都必须包含下一步行动指令。 ### 11.4 "防御纵深,不是防御单点" L1-L6 的安全架构不是过度工程。当 AI agent 有 Bash 工具、能读取网页、能写入文件时,一个 prompt injection 就能让它 `rm -rf /`。多层独立防御(内容过滤 + ML 分类 + canary token + 组合决策)确保任何单点突破都不致命。 ### 11.5 "从探索到执行的分层抽象" `/scrape` 是探索层(AI 理解页面结构),`/skillify` 是固化层(生成确定性脚本),后续调用是执行层(直接运行脚本)。这是**分层抽象的经典模式**——高层灵活、低层高效。 --- ## 结语:为什么这个项目重要 gstack 不是"又一个 AI 工具"。它是**AI 时代软件开发工作流的原型**。 它证明了三件事: 1. **一个人可以管理 15 个 AI agent 的并行开发**(Conductor 模式) 2. **提示词可以软件工程化管理**(SKILL.md 模板系统 + 三层测试) 3. **AI 的边际成本趋近于零时,"完整实现"比"精简方案"更便宜**(烧干湖水原则) Garry Tan 的 400 倍生产力不是因为他用了更好的模型,而是因为他**把 AI 当作一个工程团队来管理,而不是一个更快的高级程序员**。 这就是 gstack 的架构真相。 --- **参考来源:** - gstack GitHub: https://github.com/garrytan/gstack - ARCHITECTURE.md — gstack 核心架构文档 - BROWSER.md — 浏览器系统完整参考 - docs/skills.md — 技能深度解析 - docs/ON_THE_LOC_CONTROVERSY.md — 生产力度量方法论 - CLAUDE.md — 开发文档 - AGENTS.md — 可用技能清单 #gstack #GarryTan #AI编程 #架构解析 #ClaudeCode #浏览器自动化 #PromptEngineering #软件工程 #Tokenmaxxing #HeavyGrok #架构解析 #gstack #GarryTan #AI编程 #ClaudeCode #浏览器自动化 #PromptEngineering #软件工程 #Tokenmaxxing #HeavyGrok

讨论回复

0 条回复

还没有人回复,快来发表你的看法吧!

推荐
智谱 GLM-5 已上线

我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。

领取 2000万 Tokens 通过邀请链接注册即可获得大礼包,期待和你一起在 BigModel 上畅享卓越模型能力
登录