GStack 的 SKILL.md 模板系统是怎么设计的?一文讲透"代|码|即文档"
栏目:行业资讯 发布时间:2025-01-10
GStack 的 SKILL.md 模板系统是怎么设计的?一文讲透"代|码|即文档"

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, '..', '..', '..') 定位项目根目录,换一台机器或改一层目录就崩。

解决:改为向上查找 .gitAGENTS.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. 1. 建目录SKILL.md.tmpl + scripts/gen-skill-docs.ts
  2. 2. 写占位符:识别哪些内容应该自动生成,用 {{PLACEHOLDER}} 标记
  3. 3. 跑构建:一条命令生成最终产物,验证无误后提交

gStack 开源在 GitHub(https://github.com/garrytan/gstack),MIT 协议。fork 下来,改成你自己的模板系统,让 AI 用上永远准确的操作手册。

                 


#   # GStack  # SKILL.md  # 模板系统是怎么设计的?一文讲透"代||码||即文档"  # 软件教程  # 文档  # 自己的  # 工作流  # 自动生成  # 这就是  # 一句  # 出了  # 报错  # 一遍  # 错了  # 这套  # 一个叫  # 仿写  # 改了  # 你给  # 写完  # 塞进  # 具体内容  # 启动时  # 自动更新 


相关栏目: 【 设计资讯7769 】 【 行业资讯35393


相关文章: 哇哈哈平面广告设计  宣传平面广告设计价格标准  平面广告设计风潮趋势  文案平面广告设计灵感  平面广告设计师文案伤感  吸尘器平面广告设计说明  平面广告设计宣传页尺寸  平面广告设计百度百科  平面广告设计大牌排名前十  艺术与平面广告设计制作  平面广告设计实践报告范文  平面广告设计日记  鹌鹑蛋平面广告设计  T恤平面广告设计:展示个性与时尚  平面广告设计字  平面广告设计的四大原则  电梯轿厢平面广告设计  铲车平面广告设计  经典平面广告设计师  手动剃须刀平面广告设计的行业文章  尽情联络平面广告设计  平面广告设计宣传册的行业  新产品平面广告设计平台  平面广告设计内容阐述  赴日平面广告设计师  蝴蝶平面广告设计  平面广告设计的总结  榄菊平面广告设计  包装平面广告设计价位  香水礼盒平面广告设计说明  立白平面广告设计  洗手盆创意平面广告设计  心理平面广告设计案例  奢侈品户外平面广告设计  婚宴平面广告设计  品牌平面广告设计内容  一站式平面广告设计理念  平面广告设计实体店  服装优秀平面广告设计  红点平面广告设计奖  现代主义平面广告设计  京剧元素的平面广告设计  平面广告设计技术  平面广告设计月薪百万  平面广告设计论文提纲  喜欢平面广告设计的理由  平面广告设计主要任务  平面广告设计的18招式  **平面广告设计风潮趋势分析**  平面广告设计导师