
2026 年 3 月,Y Combinator 的 CEO Garry Tan 公布了一组数据:过去 60 天,他在全职运营 YC 的同时,用一个叫 gStack 的工具完成了 60 万行生产代码。日均 1-2 万行,一周内 14 万行代码、362 次提交。
一个人,干了一支小工程团队的活。
消息传开后,很多人去看了 gStack 的源码。28 个专业 Skill,覆盖产品讨论、代码审查、QA 测试、浏览器自动化。每个 Skill 背后都有一个 SKILL.md 文件——AI 执行任务时读取的操作手册。
但真正让这套系统跑起来的,不是 Skill 本身,而是生成这些 Skill 文档的模板系统。
这篇文章就拆解这个模板系统:它解决什么问题、怎么工作的、有哪些设计原则。最后我会用自己仿写 content-workflow 的真实经历,告诉你哪些坑可以避开。
一、为什么 AI Agent 需要"操作手册"
先想一个问题:你给新员工交代任务,可以说一遍,他不懂会追问。AI 不行。
AI 的特点是执行速度快但容错率低。你给它的文档写错一个命令,它不会说"这个命令好像不对"——它会直接用错误的命令执行下去,然后整个任务失败。

人读文档出了错,问一句就解决了。AI 读文档出了错,整个任务就废了。
所以 gStack 的做法是:给每个 Skill 写一份精确到每一步的操作手册,也就是 SKILL.md。AI 启动时读取这份手册,按步骤执行。手册里写什么,AI 就做什么。不多不少。
问题来了:gStack 有 28 个 Skill,每个 Skill 的手册还要区分 Claude Code 和 Codex 两个环境。手动维护 56 份文档?不现实。
这就是模板系统要解决的事。
二、模板系统的核心机制
2.1 三层架构
gStack 的模板系统只有三个角色:
SKILL.md.tmpl(人写)
↓
gen-skill-docs.ts(脚本处理)
↓
SKILL.md(AI 用)模板文件 SKILL.md.tmpl 是人写的,里面用 {{PLACEHOLDER}} 标记需要自动生成的部分。构建脚本 gen-skill-docs.ts 读取模板,找到占位符,调用对应的函数生成内容,最后输出 AI 实际使用的 SKILL.md。

人只需要维护模板,文档的生成和同步全部交给脚本。
2.2 占位符与 RESOLVER
模板里长这样:
{{PREAMBLE}}
# browse
{{COMMAND_REFERENCE}}{{PREAMBLE}} 和 {{COMMAND_REFERENCE}} 就是占位符。脚本处理时,用正则表达式 \{\{(\w+)\}\} 匹配所有占位符,然后查一个叫 RESOLVERS 的字典,找到对应的生成函数:
const RESOLVERS: Record<string, (ctx: TemplateContext) => string> = {
PREAMBLE: generatePreamble,
COMMAND_REFERENCE: generateCommandReference,
SNAPSHOT_FLAGS: generateSnapshotFlags,
QA_METHODOLOGY: generateQAMethodology,
// ... 共 21 个
};核心替换逻辑就几行:
let content = tmplContent.replace(/\{\{(\w+)\}\}/g, (match, name) => {
const resolver = RESOLVERS[name];
if (!resolver) throw new Error(`Unknown placeholder: {{${name}}}`);
return resolver(ctx);
});[数据来源:gStack/scripts/gen-skill-docs.ts]
注意这里的设计:遇到未知占位符直接报错,不会静默跳过。这就是"强制验证"——模板写了占位符但没注册 RESOLVER,构建就过不了。
2.3 PREAMBLE:最复杂的占位符
21 个占位符里,{{PREAMBLE}} 是最特殊的一个。它不是一段静态文字,而是由 10 个子函数组合生成的初始化脚本:
function generatePreamble(ctx: TemplateContext): string {
return [
generatePreambleBash(ctx), // bash 初始化
generateUpgradeCheck(ctx), // 升级检查
generateLakeIntro(), // "Boil the Lake" 原则
generateTelemetryPrompt(ctx), // 遥测提示
generateAskUserFormat(ctx), // 交互格式
generateCompletenessSection(), // 完整性原则
generateRepoModeSection(), // 仓库模式
generateSearchBeforeBuildingSection(), // 建造前搜索
generateContributorMode(), // 贡献者模式
generateSecurityPrompt(ctx), // 安全提示
].join('\n\n');
}这 10 个子函数覆盖了 AI 执行任务前需要知道的所有上下文:环境怎么初始化、遇到测试失败怎么办、该不该搜一下再动手、安全边界在哪。
[数据来源:gStack/scripts/gen-skill-docs.ts generatePreamble 函数]
说白了,PREAMBLE 就是 AI 的"开机自检流程"。每个 Skill 启动时都先跑一遍,确保环境正确、认知对齐。

三、核心设计原则:"代码即文档"
模板系统背后有一个一以贯之的理念:代码即文档(Code as Documentation)。
唯一真相
gStack 的设计是:源代码是文档的唯一来源。不是先写代码再补文档,而是文档从代码自动生成。
最典型的例子是 commands.ts。这个文件定义了 gStack 所有的命令——名称、参数、用途。它同时被三个地方引用:
- •
server.ts运行时读它做命令分发 - •
gen-skill-docs.ts构建时读它生成文档 - •
skill-check.ts测试时读它做健康检查
命令定义只写一次,三个消费者各取所需。
构建时同步
gStack 不是"写完代码记得更新文档",而是"写完代码跑一下构建,文档自动更新"。
bun run gen:skill-docs一条命令,28 个 Skill 的 SKILL.md 全部重新生成。你改了 commands.ts 里的命令定义,跑完构建,browse 的 SKILL.md 里的命令参考部分就自动更新了。
强制验证
gStack 在 commands.ts 末尾加了一段验证代码:
const allCmds = new Set([...READ_COMMANDS, ...WRITE_COMMANDS, ...META_COMMANDS]);
const descKeys = new Set(Object.keys(COMMAND_DESCRIPTIONS));
for (const cmd of allCmds) {
if (!descKeys.has(cmd))
throw new Error(`COMMAND_DESCRIPTIONS missing: ${cmd}`);
}你加了新命令但忘了加描述?构建直接报错。不是提醒,是报错。
[数据来源:gStack/browse/src/commands.ts 验证代码段]
双环境兼容
gStack 同时支持 Claude Code 和 Codex 两个 AI 环境。两个环境的路径、frontmatter 格式、安全机制都不一样。
模板系统通过构建时的自动转换解决这个问题:
if (host === 'codex') {
content = content.replace(/~\/\.claude\/skills\/gstack/g,
'~/.codex/skills/gstack');
content = content.replace(/\.claude\/skills\/gstack/g,
'.agents/skills/gstack');
}一份模板,跑两次构建,输出两个版本。人只维护一份,不会出现"改了 Claude 版忘了改 Codex 版"的情况。
[案例位置:gStack-03 中 Claude vs Codex 路径差异的具体对比]
四、实战:从零搭一个内容创作工作流
理论讲完了,下面用我自己仿写 content-workflow 的过程,演示怎么把这套模板系统用到自己的项目里。
第一步:创建目录和模板
content-workflow/
├── SKILL.md.tmpl # 人写的模板
├── SKILL.md # 脚本生成的产物
├── references/
│ ├── content-angles.md # 内容角度库
│ ├── content-stages.md # 阶段定义
│ └── platform-specs.md # 平台规格
└── scripts/
└── gen-content-skill-docs.ts模板文件的 frontmatter 声明 Skill 的名称和允许使用的工具:
---
name: content-workflow
version: 1.0.0
description: |
端到端内容创作工作流。包含选题策划、大纲生成、初稿写作、
审核润色、发布准备全流程。
allowed-tools:
- Read
- Write
- Edit
- WebSearch
- WebFetch
- AskUserQuestion
---第二步:定义占位符和 RESOLVER
内容创作工作流有 5 个阶段,每个阶段对应一个占位符:
| 占位符 | 对应阶段 |
|---|---|
{{TOPIC_PLAN}} | 选题策划 |
{{OUTLINE_GEN}} | 大纲生成 |
{{DRAFT_WRITING}} | 初稿写作 |
{{REVIEW_STEPS}} | 审核润色 |
{{PUBLISH_PREP}} | 发布准备 |
在脚本里注册 RESOLVER:
const RESOLVERS: Record<string, (ctx: TemplateContext) => string> = {
PREAMBLE: () => generatePreamble(),
TOPIC_PLAN: generateTopicPlan,
OUTLINE_GEN: generateOutlineGen,
DRAFT_WRITING: generateDraftWriting,
REVIEW_STEPS: generateReviewSteps,
PUBLISH_PREP: generatePublishPrep,
};模板里只放占位符,具体内容全部由 RESOLVER 函数生成:
## Stage 1: 选题策划
{{TOPIC_PLAN}}
## Stage 2: 大纲生成
{{OUTLINE_GEN}}第三步:处理参考文件
内容角度库有 8 种内容类型、80 多个标题模板。平台规格覆盖 6 个平台。
错误做法:把这些内容直接塞进 SKILL.md(我一开始就是这么干的,文件膨胀到 20KB+)。
正确做法:放在 references/ 目录下,在 SKILL.md 里只放路径引用:
内容角度库详见 [references/content-angles.md](references/content-angles.md),
执行时读取该文件获取 8 种内容角度。AI 需要时按需读取,不会一股脑全加载。
第四步:构建验证
bun run scripts/gen-content-skill-docs.ts --dry-run先 dry-run 预览,确认没有未知占位符、没有格式问题,再正式生成。

五、踩过的坑和实际教训
仿写过程中踩了几个坑,值得提前知道。
坑一:内容重复冗余
现象:SKILL.md 膨胀到 500+ 行,每个阶段的内容出现了 2-3 遍。
原因:模板里硬编码了一版内容,RESOLVER 函数里又生成了一版。两份内容同时出现在最终产物里。
解决:模板里只放占位符,具体内容全部交给 RESOLVER。一个内容只有一个来源。
坑二:参考文件内联导致膨胀
现象:content-angles.md 和 platform-specs.md 的全部内容被读出来塞进 SKILL.md,文件超过 20KB 被截断。
原因:RESOLVER 里用 fs.readFileSync 把参考文件内容原样注入。
解决:改为路径引用,AI 按需读取。SKILL.md 体积从 20KB 降到 4KB。
坑三:占位符是空壳
现象:{{PREAMBLE}} 和 {{COMPLETION_STATUS}} 注入的只是一句"使用时需替换"。
原因:占位符注册了但函数没实现,占位符变成了无意义的占位文字。
解决:要么实现真正的功能(PREAMBLE 改为执行前检查指令),要么直接删掉(COMPLETION_STATUS 当前不需要就删)。
坑四:路径硬编码
现象:脚本里用 path.join(ROOT, '..', '..', '..') 定位项目根目录,换一台机器或改一层目录就崩。
解决:改为向上查找 .git 或 AGENTS.md 来动态定位项目根:
function findProjectRoot(startDir: string): string {
let dir = startDir;
while (dir !== path.dirname(dir)) {
if (fs.existsSync(path.join(dir, '.git'))) return dir;
dir = path.dirname(dir);
}
throw new Error('Project root not found');
}结语
gStack 的模板系统看起来简单——模板加占位符加构建脚本——但它解决的是 AI 工程化中一个基础问题:怎么让 AI 的"知识"永远准确、永远最新。
对于人来说,文档不准确可以追问。对于 AI 来说,文档就是它的全部认知。文档错了,AI 就错了。模板系统保证了文档从代码自动生成,不会和代码脱节。
如果你想给自己的 AI Agent 搭一套类似的系统,核心就三步:
- 1. 建目录:
SKILL.md.tmpl+scripts/gen-skill-docs.ts - 2. 写占位符:识别哪些内容应该自动生成,用
{{PLACEHOLDER}}标记 - 3. 跑构建:一条命令生成最终产物,验证无误后提交
gStack 开源在 GitHub(https://github.com/garrytan/gstack),MIT 协议。fork 下来,改成你自己的模板系统,让 AI 用上永远准确的操作手册。

夜雨聆风