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 → @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)

`-C` 标志查找可点击但不在 ARIA 树中的元素——样式为 `cursor: pointer`、有 `onclick` 属性、或自定义 `tabindex` 的元素。这些在独立命名空间中获得 `@c1`, `@c2` 引用,捕获框架渲染为 `
` 但实际是按钮的自定义组件。 --- ## 四、安全模型: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

讨论回复

2 条回复
✨步子哥 (steper) #1
2026-05-17 14:19

svg_1779027546_4516.svg

✨步子哥 (steper) #2
2026-05-17 14:32

svg_1779028321_8982.svg

推荐
智谱 GLM-5 已上线

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

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