> 项目: 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 — 状态保持
1.2 Skills 层(厚)
23 个专家角色,每个都是一份详细的 markdown 文件(SKILL.md),包含:
- 角色定义("你是高级工程经理,正在进行代码审查")
- 审查清单(SQL 安全、竞态条件、LLM 信任边界、枚举完整性)
- 输出格式(AUTO-FIX vs ASK)
- 工作流程(两轮审查的先后顺序)
1.3 为什么是 Markdown?
SKILL.md 文件是纯 markdown + YAML frontmatter。这意味着:
- 版本控制友好 — git diff 直接可读
- 跨工具移植 — Claude Code、Codex CLI、Cursor、VS Code 都能读
- 人类可编辑 — 不需要写代码就能调整 AI 行为
- 代码审查友好 — PR 中可以 review skill 的变更
---
二、持久化浏览器守护进程:从 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 |
---
三、Ref 系统:绕过 DOM 的选择器革命
3.1 传统选择器的噩梦
传统方案给元素打标记:data-ref="@e1"。这会在生产环境崩溃:
- CSP(内容安全策略) — 很多站点禁止脚本修改 DOM
- React/Vue/Svelte 水合 — 框架协调会剥离注入的属性
- Shadow DOM — 无法从外部触及 shadow root 内部
3.2 gstack 的方案:ARIA Tree → @refs
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)
--- 当用户运行 gstack 的方案:两个 HTTP 监听器,不是两个路由过滤器: 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 审查完整对话形状(用户消息、工具调用、工具输出),而非仅文本。 L5 Canary Token — 会话开始时向系统提示注入随机 token。滚动缓冲区检测捕获 token 是否出现在 Claude 输出、工具参数、URL 或文件写入中。确定性 BLOCK——如果 token 泄露,攻击者说服 Claude 泄露了系统提示,会话立即结束。 L6 组合决策器 — BLOCK 需要两个 ML 分类器在 >= WARN (0.75) 时达成一致,而非单个高置信度命中。这是 Stack Overflow 指令编写误报缓解策略。 --- SKILL.md 文件告诉 Claude 怎么使用 browse 命令。如果文档列出了不存在的 flag,或遗漏了新增的命令,agent 会出错。人工维护的文档永远与代码脱节。 占位符从源码在构建时填充: 三个原因:
1. Claude 在 skill 加载时读取 SKILL.md — 没有构建步骤,文件必须已存在且正确
2. CI 可以验证新鲜度 — --- 每个错误消息必须是可操作的: 如果 Chromium 崩溃( > 这比尝试重新连接到半死的浏览器进程更简单、更可靠。 这是一个设计选择:失败要大声、要明确、要可恢复——而不是静默地掩盖。 --- 从 30 秒到 200 毫秒——150 倍加速。这是复利效应:第一次用 AI 探索,后续用确定性脚本执行。 --- 这是 gstack 最独特的哲学: > "AI 让完整实现的边际成本趋近于零,永远推荐完整方案。" --- 三个环形缓冲区(各 50,000 条,O(1) push): Console 消息、网络请求、dialog 事件各自有独立缓冲区。每秒刷新——server 只追加自上次刷新以来的新条目。 这意味着:
--- E2E 测试生成 1. 将提示写入临时文件(避免 shell 转义问题)
2. 生成 每个测试在 SKILL.md 是 markdown,不是 TypeScript。这意味着产品经理可以 review 它,设计师可以修改它,不需要编译就能生效。当 AI 的"行为"成为产品的一部分时,行为定义应该用最高可读性的格式。 浏览器自动化的瓶颈不是 Playwright,是状态丢失。每次关闭浏览器 = 丢失 cookies、登录态、tabs。gstack 用持久化 daemon 把这个成本从"每次命令 3 秒"降到"几乎为零"。 当用户是 AI 而不是人类时,错误信息的设计目标完全变了。不是"让用户理解",是"让 AI 能自我修复"。每个错误都必须包含下一步行动指令。 L1-L6 的安全架构不是过度工程。当 AI agent 有 Bash 工具、能读取网页、能写入文件时,一个 prompt injection 就能让它 --- gstack 不是"又一个 AI 工具"。它是AI 时代软件开发工作流的原型。 它证明了三件事:
1. 一个人可以管理 15 个 AI agent 的并行开发(Conductor 模式)
2. 提示词可以软件工程化管理(SKILL.md 模板系统 + 三层测试)
3. AI 的边际成本趋近于零时,"完整实现"比"精简方案"更便宜(烧干湖水原则) Garry Tan 的 400 倍生产力不是因为他用了更好的模型,而是因为他把 AI 当作一个工程团队来管理,而不是一个更快的高级程序员。 这就是 gstack 的架构真相。 --- 参考来源:
#架构解析 #gstack #GarryTan #AI编程 #ClaudeCode #浏览器自动化 #PromptEngineering #软件工程 #Tokenmaxxing #HeavyGrok-C 标志查找可点击但不在 ARIA 树中的元素——样式为 cursor: pointer、有 onclick 属性、或自定义 tabindex 的元素。这些在独立命名空间中获得 @c1, @c2 引用,捕获框架渲染为 四、安全模型:L1-L6 的防御纵深
4.1 双重 HTTP 监听器架构
pair-agent --client 时,daemon 启动 ngrok tunnel 让远程 agent 驱动浏览器。直接把完整 daemon 暴露给互联网是自杀行为。
安全属性来自物理端口分离:tunnel 调用者无法到达 端点 本地监听器 (127.0.0.1:LOCAL_PORT) Tunnel 监听器 (127.0.0.1:TUNNEL_PORT) /health公开(token 引导) 404 /commandBearer root 或 scoped 仅 scoped,命令白名单 /cookie-picker公开 UI 404 /inspector/*认证 404 /connect公开 公开(速率限制) /health 或 /cookie-picker,因为这些路径在该 TCP socket 上不存在。检查 x-forwarded-for 或 origin 不可靠;socket 分离是不可欺骗的。4.2 Prompt Injection 防御(侧边栏 Agent)
LOG_ONLY: 0.40 门控让大部分干净流量跳过付费调用。五、SKILL.md 模板系统:把提示词当作软件工程
5.1 问题:文档漂移
5.2 解决方案:模板 + 自动生成
SKILL.md.tmpl (人类写的散文 + 占位符)
↓
gen-skill-docs.ts (读取源码元数据)
↓
SKILL.md (提交到 git,自动生成的章节)
占位符 来源 生成内容 {{COMMAND_REFERENCE}}commands.ts分类命令表 {{SNAPSHOT_FLAGS}}snapshot.tsFlag 参考 + 示例 {{QA_METHODOLOGY}}gen-skill-docs.ts共享 QA 方法块 {{REVIEW_DASHBOARD}}gen-skill-docs.ts审查就绪仪表板 5.3 为什么提交到 git,而不是运行时生成?
gen:skill-docs --dry-run + git diff --exit-code 在合并前捕获过时文档
3. Git blame 有效 — 你能看到命令何时添加、在哪个 commit5.4 三层测试体系
第 1 层每次 层级 内容 成本 速度 1 — 静态验证 解析 SKILL.md 中每个 $B 命令,对照注册表验证免费 <2s 2 — E2E via claude -p生成真实 Claude 会话,运行每个 skill ~$3.85 ~20min 3 — LLM-as-judge Sonnet 评分文档清晰度/完整性/可执行性 ~$0.15 ~30s bun test 运行。2+3 层在 EVALS=1 后运行。理念:免费捕获 95% 的问题,LLM 仅用于判断性决策。六、错误哲学:写给 AI 看的错误信息
6.1 错误是给 AI agents 的,不是给人类的
Playwright 的原生错误通过 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 @refs from snapshot instead."Timeout "Navigation timed out after 30s. The page may be slow or the URL may be wrong." wrapError() 重写,剥离内部堆栈跟踪,添加指导。Agent 应该能够读取错误并知道下一步该做什么,无需人类干预。6.2 崩溃恢复:不自我治愈
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 重新探索
八、"烧干湖水"完整性原则
反模式:
任务类型 人工耗时 Claude Code 压缩比 脚手架代码 2 天 15 分钟 100× 写测试 1 天 15 分钟 50× 功能实现 1 周 30 分钟 30× Bug 修复 + 回归 4 小时 15 分钟 20×
核心洞察:当 AI 让写代码便宜到近乎免费时,"差不多就行"成了最昂贵的策略。一个未处理的边界情况、一个遗漏的测试、一个简化的方案——这些在后续返工中的代价远超现在花几分钟让 AI 做完整。九、日志架构:环形缓冲区的工程智慧
浏览器事件 → CircularBuffer(内存中) → 异步刷盘到 .gstack/*.log
console、network、dialog 命令从内存缓冲区读取,不是磁盘。磁盘文件用于事后调试。十、E2E 测试基础设施:diff-based 的智能选择
10.1 Session Runner
claude -p 作为完全独立的子进程——不是通过 Agent SDK(无法在 Claude Code 会话中嵌套):sh -c 'cat prompt | claude -p --output-format stream-json --verbose'
3. 从 stdout 流式接收 NDJSON 以获取实时进度
4. 与可配置超时竞争
5. 解析完整 NDJSON 对话为结构化结果parseNDJSON() 函数是纯函数——无 I/O,无副作用——使其独立可测试。10.2 Diff-Based 测试选择
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 "把提示词当作配置,而不是代码"
11.2 "状态是性能最大的敌人"
11.3 "错误信息是 API 的一部分"
11.4 "防御纵深,不是防御单点"
rm -rf /。多层独立防御(内容过滤 + ML 分类 + canary token + 组合决策)确保任何单点突破都不致命。11.5 "从探索到执行的分层抽象"
/scrape 是探索层(AI 理解页面结构),/skillify 是固化层(生成确定性脚本),后续调用是执行层(直接运行脚本)。这是分层抽象的经典模式——高层灵活、低层高效。结语:为什么这个项目重要
#gstack #GarryTan #AI编程 #架构解析 #ClaudeCode #浏览器自动化 #PromptEngineering #软件工程 #Tokenmaxxing #HeavyGrok