Dif.Sh 使用指南:以 Markdown 文件为载体的开源特性开关
特性开关属于你的代码库------所以它应该住在你的仓库里。每一个开关、每一场实验,就是一个 Markdown 文件,与它所控制的代码放在一起,在 PR 中被评审,其历史就是 Git 历史。没有要登录的仪表盘,没有会腐烂的控制台。
一、Dif.Sh 是什么?
1.1 一句话定义
Dif.Sh (简称 dif )是一个免费、开源、可自托管的特性开关(Feature Flags)与 A/B 测试工具,MIT 协议。它的核心设计是:每个特性开关都是一个 Markdown 文件,检入 Git,与它所控制的代码放在一起。
一条命令安装,无需注册账户,没有会腐烂的仪表盘(No dashboard to rot in)。
1.2 与传统特性开关平台的区别
| 维度 | 传统平台(LaunchDarkly 等) | Dif.Sh |
|---|---|---|
| 开关存放位置 | Web 仪表盘,与代码脱节 | 仓库内的 Markdown 文件,与代码同处 |
| 审批流程 | 独立后台权限体系 | PR 评审(Git 是审计日志) |
| 历史记录 | 平台自带数据库 | Git 历史 |
| 开关原因 | 没人记得为什么存在 | 文件里写明功能、原因、决策 |
| 赋值评估 | 数据库 + 网络请求 | 本地纯函数,无网络调用 |
| 实验结束 | 结果躺在 Slack 旧线程里 | 决策写进文件,学习沉淀到 surface 日志 |
1.3 核心哲学
- Flag 是代码库的一部分:它和代码一起被评审、一起被版本化、一起被清理
- 赋值是纯函数:没有赋值数据库、没有网络请求;同一用户在页面加载和设备间永远不会在不同变体间跳变
- 昨天的学习进入明天的草稿:每场实验的结论写回 surface 日志,下一个测试从上次学到的东西开始
- Git 即事实:schema 在 git 里,客户数据不在------受众属性在运行时由应用提供,从不提交客户名单
二、快速开始
2.1 安装
方式一:npm(推荐)
bash
npm install -g @dif.sh/cli
方式二:独立二进制(无 Node 环境)
bash
# macOS / Linux:单个静态二进制,无需 Node
curl -fsSL https://dif.sh/install.sh | sh
无需注册账户,安装即用。
2.2 初始化仓库
bash
dif init
生成 dif/ 目录结构:
your-app/
├── dif/
│ ├── experiments/
│ │ ├── active/ # 正在运行/起草的开关与实验
│ │ └── concluded/ # 已结束的实验(归档)
│ ├── surfaces/ # 每个页面的上下文 + 学习日志
│ │ ├── checkout.md
│ │ ├── pricing.md
│ │ └── signup.md
│ ├── config.yaml # 配置
│ ├── context.json # 供智能体读取的上下文(build 时生成)
│ └── generated/ # 生成的客户端(gitignored)
└── # ... 应用其余部分
2.3 创建第一个实验
bash
dif new home-hero-cta --surface home
dif new 会用你的 Git 邮箱作为 owner 起草文件。打开生成的文件,写上假设(hypothesis),把 status 改为 active:
bash
# 编辑 dif/experiments/active/home-hero-cta.md
dif validate # 校验一切是否正确
dif build # 生成 TS 客户端 + context.json
2.4 在代码中使用
bash
npm install @dif.sh/sdk
typescript
// 启动时导入一次生成的客户端
import "./dif/generated/client";
import { attributes } from "./dif/generated/audiences";
import { dif } from "@dif.sh/sdk";
dif.init({
userId: () => currentUser?.id ?? null,
attributes: () => attributes(),
});
// 在渲染处调用
const cta = dif("home-hero-cta", {
control: () => "Start free trial",
variant_a: () => "Try it free for 30 days",
});
button.textContent = cta();
2.5 别忘了构建钩子
dif init 已将 dif/generated/ 加入 .gitignore,所以 CI/部署必须先运行 dif build,否则应用会带着空客户端上线:
json
// package.json
{
"prebuild": "dif build"
}
三、八个命令速查
| 命令 | 作用 |
|---|---|
dif init |
在当前目录脚手架 dif.sh 约定(dif/ 目录、配置、agent 文件) |
dif connect |
用 publishable key 连接 dif.sh Cloud(可选) |
dif new |
起草新实验,自动读取该 surface 的既往学习 |
dif validate |
校验工作区:schema、owner、surface 引用、排除图 |
dif build |
将激活实验编译为类型化 TS 客户端 + context.json |
dif qa |
追踪某用户的分配链并输出预览 URL |
dif conclude |
将实验移到 concluded/、起草 Decision、追加到 surface 日志 |
dif scaffold-audiences |
幂等脚手架起步受众解析器(locale、device_type) |
四、文件格式:一个 Flag 就是一个实验
4.1 Flag 与实验是同一个格式
markdown
---
id: new-checkout
status: active
owner: sam@acme.com
surface: checkout
hypothesis: >
内联地址表单将提升移动端完成结账率,
且不推高退款率。
audience:
include:
- device_type: [mobile, tablet]
exclude:
- plan: free
variants:
- id: "off"
weight: 90
summary: 当前结账流程
- id: "on"
weight: 10
summary: 内联地址表单的新结账流程
metrics:
primary: completed_checkout
guardrails:
- refund_rate
exclusion_group: checkout
created: 2026-07-01
---
## Brief
护栏保持一周后,爬坡到 25%。
4.2 Flag vs 实验:唯一的区别是权重
| Flag | 实验 | |
|---|---|---|
| 形态 | 正在向 100% 爬坡的开关 | 保持拆分,等数字回答假设 |
| 权重 | 如 90/10 逐步上调 | 如 50/50 保持 |
| 结束方式 | 全量后删除死分支 | conclude 归档 |
实验赢了 → 变成 flag 爬坡;flag 不确定 → 变回实验拆分。 同一个 schema、同一个分桶数学、同一个校验器、同一个 SDK 调用,不用改一行应用代码。
4.3 exclusion_group:防止实验互相踩踏
两个激活实验在同一 surface 上必须满足其一:
- 共享
exclusion_group:保证每个用户最多被分到一个实验 - 受众可证明不重叠:dif 能证明分离
如果 dif 无法证明隔离,dif validate 直接失败。冲突在 CI 中断,而不是在生产爆炸。
五、CLI 深入使用
5.1 dif validate:实验的类型检查器
校验内容:
- 权重必须合计 100
- 引用的 surface 和受众属性必须存在
- 扫描应用源码中的
dif("...")调用点,警告指向仓库中不存在实验的代码 - 检测实验冲突(同 surface 无 exclusion_group 且受众可能重叠 → 失败)
在 CI 中运行,坏 flag 就像坏构建一样让 PR 失败。
5.2 dif qa:追踪用户分配
bash
dif qa --user u_8131 --attr device_type=mobile
输出该用户落在哪个变体、为什么:
trace u_8131:
• checkout-cta-v2 → variant_a (bucket 7142)
• pricing-headline → value (bucket 71)
• signup-headline ↛ audience miss
same user_id, same bucket, every time.
强制指定变体并生成预览链接:
bash
dif qa --user u_8131 --attr device_type=mobile --force checkout-cta-v2=variant_a
- 返回
?_dif=...预览链接,在浏览器中固定该变体 - 强制分配不触发曝光事件
5.3 dif conclude:结束而非放弃
bash
dif conclude checkout-cta-v2
-
记录决策和日期
-
将文件移到
dif/experiments/concluded/ -
在 surface 文件追加一行学习:
2026-05-28 checkout-cta-v2: "Get it today" 提升完成结账 2.1%(CI 0.6--3.5%)。已发布。仅回头客。
下一次 dif new 在该 surface 上会读取这些学习,避免两年后新人重复同样的失败实验。
六、与编程智能体协作
这是 Dif 相比仪表盘类工具的核心优势:flags 是文件,所以智能体像读其他源码一样读它们,也用同样的方式写它们。
6.1 安装 agent 文件
dif init 会自动:
- 合并管理块到
CLAUDE.md、AGENTS.md、.cursorrules - 安装 Claude Code skills 到
.claude/skills/
--agents 参数可只脚手架子集:
bash
dif init --agents claude # CLAUDE.md + .claude/skills/dif-* skills
dif init --agents general # 仅 AGENTS.md
dif init --agents cursor # 仅 .cursorrules
dif init --agents none # 不写任何 agent 文件
6.2 内置 Skills
| Skill | 用自然语言问 |
|---|---|
dif-generate-surfaces |
"为这个应用设置 dif surfaces"------读取路由和页面,提议 surface 集合,写入文件 |
dif-author-experiment |
"为新结账流程加个 flag,仅移动端"------起草 frontmatter、定权重、运行 dif validate |
dif-conclude-experiment |
"结束 checkout-cta-v2,变体胜出,发布它"------写决策、归档文件、记录学习 |
6.3 context.json:智能体的实验记忆
每次 dif build 重新生成 dif/context.json:
- 每个激活实验及变体
- 每个 surface 的最近学习
编码智能体会话启动时读取它,先前的学习随工作流动------像 CLAUDE.md 一样,但是给实验用的。
告诉智能体"给新结账加个 flag",它能起草文件、给代码路径加门控、运行
dif validate检查自己的工作。
七、分析(Analytics)
7.1 完全无分析也能跑
赋值是本地纯函数,flag 和爬坡不需要任何配置即可工作。Cloud 模式也是 opt-in 的:没有 publishable key 时,dif 不向 Cloud 记录任何东西------没有警告,就是沉默。
7.2 连接 Dif Cloud
bash
dif connect --key dif_pk_live_... # 写入 dif/config.yaml,开启 cloud 模式
# 新项目可一步到位:
dif init --key dif_pk_live_...
key 是 publishable key,安全可提交。dif build 把它烘焙进生成的客户端:
typescript
import { events } from "./dif/generated/events";
dif.init({
events,
userId: () => currentUser?.id ?? null,
});
7.3 自定义事件管道
已有自己的事件管线?用自定义模式:
bash
dif init --events custom
生成两个归你所有的处理器 dif/events/exposure.ts 和 dif/events/track.ts。把事件转发到 Segment、Amplitude、webhook 或你自己的数仓------dif 不在乎事件去哪。没有捆绑的第三方集成,只有那两个函数。
7.4 指标跟踪
typescript
dif.track("completed_checkout");
dif.track("revenue", { value: 49 });
八、Dif Cloud(可选托管层)
Dif Cloud 是可选托管层(cloud.dif.sh),核心不依赖它------仓库里的文件永远是事实来源。Cloud 读取你的仓库,给团队提供实时视图;它提议的每项变更都以 PR 形式落地,由你评审。
8.1 Pulse(实时脉搏)
实验运行期间直接阅读:lift(提升)、confidence(置信度)、exposures(曝光)、以及你写在文件里的假设------图表旁边就放着假设原文。
8.2 Suggestions(AI 建议)
dif 读取你的 surfaces、已结束实验和数据中的行为,起草下一个测试,附带假设和预期 lift。把 brief 直接复制进 dif new。
8.3 History(历史)
每个 surface 的每场已结束实验:结果、lift、谁批准的。与追加进 surface 文件的学习一致。
九、技术架构
9.1 单一事实来源的数学
赋值是纯函数,同一套分桶数学运行在:
- Rust CLI(解析、校验、分桶、代码生成)
- TypeScript SDK(运行时)
两者被共享的测试夹具锁定------如果两个实现在单个 bucket 上漂移,CI 在两侧都失败。
9.2 仓库结构
cli/
├── crates/dif-core/ # 解析器、校验器、分桶、代码生成(Rust)
├── crates/dif-cli/ # dif 二进制
└── packages/
├── cli/ # @dif.sh/cli(npm 包装器)
├── sdk/ # @dif.sh/sdk(运行时 SDK,TypeScript,零依赖)
├── react/ # @dif.sh/react
└── svelte/ # @dif.sh/svelte
dist/ # install.sh + Homebrew tap 模板
9.3 注意事项
@dif.sh/sdk、@dif.sh/react、@dif.sh/svelte均为 ESM-only ,没有require()入口- npm 包
@dif.sh/cli在内部从 GitHub Release 下载匹配版本的 Rust 二进制
十、最佳实践
10.1 工作流
- 初始化 :
dif init,设置"prebuild": "dif build" - 起草 :
dif new <id> --surface <surface>,写上假设和 brief - 激活 :设置
status: active,dif validate+dif build - 接入 :代码中调用
dif("id", {...}) - 追踪 :
dif qa验证分配,dif.track()记录指标 - 结束 :
dif conclude归档 + 写决策 + 沉淀学习 - CI:validate + build 进流水线,坏 flag 拦在 PR
10.2 团队协作
- Flag 像代码一样评审:每个开关都在 PR 里被评审,评审的就是决策本身
- surface 文件是制度记忆:每个页面的地雷和学习都在,新人先读 surface 再动手
- exclusion_group 是纪律:同页面实验要么共享组、要么受众可证明分离
- 客户数据永不入库:受众属性声明在配置里,值在运行时由应用提供
10.3 智能体协作
- 让编码智能体完成安装和日常操作(初始化、起草、校验、结束)
- context.json 让智能体"知道"哪些功能已上线、哪些方案试过
- 用自然语言驱动 skills:加 flag、结束实验、生成 surfaces
十一、常见问题
Q: 需要注册账户吗?
A: 不需要。一条命令安装即用,本地模式完整可用。Cloud 是可选层,opt-in 才连接。
Q: 没有 Cloud 能做什么?
A: 除了统计分析和 AI 建议之外的一切:创建、校验、构建、分桶、追踪、结束、学习沉淀,全部本地可用。分析也可以走自定义事件管道。
Q: 为什么会"烂在仪表盘里"?
A: 传统平台里,flag 存在 Web 仪表盘,与代码脱节,没人记得 new-checkout-v2 为什么存在、能否删除,于是它 100% 运行三年,后面跟着一个死分支。dif 把原因写进文件,历史就是 git 历史,结束时决策写回同一个文件。
Q: flag 和 A/B 实验有什么区别?
A: 文件格式相同,唯一的结构区别是权重。Flag 是向 100% 爬坡的实验,实验是保持拆分等数字回答假设。赢了就爬坡,不确定就拆分,不用改应用代码。
Q: 两个实验同时在同一个页面会冲突吗?
A: 会,但 dif 在构建时拦截。同一 surface 的激活实验必须共享 exclusion_group(保证每个用户最多看到一个),或受众可证明不重叠。无法证明隔离就校验失败。
Q: 用户会跨设备跳变吗?
A: 不会。赋值是纯函数,同一 user_id 永远落在同一个 bucket------"same user_id, same bucket, every time."没有数据库、没有网络请求。
Q: 支持哪些框架?
A: 官方 SDK 支持 TypeScript(零依赖)、React、Svelte。ESM-only。Web/Server/React/Svelte 渲染方式都用同一套文件和 CLI。
Q: 为什么说 agent 能"读完上下文文件就知道方案试过没有"?
A: dif build 生成 dif/context.json,包含每个激活实验和每个 surface 的最近学习。编码智能体会话启动时读取它;dif new 起草新实验时也会读取 surface 日志,把既往学习带进草稿。
参考资源
- 官方网站:https://dif.sh
- GitHub 仓库:https://github.com/dif-sh/dif
- 完整文档:https://www.dif.sh/docs
- Dif Cloud:https://cloud.dif.sh
- npm 包(CLI):https://www.npmjs.com/package/@dif.sh/cli
- npm 包(SDK):https://www.npmjs.com/package/@dif.sh/sdk
Dif.Sh 基于 MIT 协议开源,本文基于 2026 年 9 月公开资料整理。具体命令和配置以官方文档为准。