Dif.Sh 使用指南:以 Markdown 文件为载体的开源特性开关

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(推荐)

npm install -g @dif.sh/cli

方式二:独立二进制(无 Node 环境)

# macOS / Linux:单个静态二进制,无需 Node
curl -fsSL https://dif.sh/install.sh | sh

无需注册账户,安装即用。

2.2 初始化仓库

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 创建第一个实验

dif new home-hero-cta --surface home

dif new 会用你的 Git 邮箱作为 owner 起草文件。打开生成的文件,写上假设(hypothesis),把 status 改为 active

# 编辑 dif/experiments/active/home-hero-cta.md
dif validate   # 校验一切是否正确
dif build      # 生成 TS 客户端 + context.json

2.4 在代码中使用

npm install @dif.sh/sdk
// 启动时导入一次生成的客户端
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,否则应用会带着空客户端上线:

// 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 与实验是同一个格式

---
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:追踪用户分配

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.

强制指定变体并生成预览链接:

dif qa --user u_8131 --attr device_type=mobile --force checkout-cta-v2=variant_a
  • 返回 ?_dif=... 预览链接,在浏览器中固定该变体
  • 强制分配不触发曝光事件

5.3 dif conclude:结束而非放弃

dif conclude checkout-cta-v2
  1. 记录决策和日期
  2. 将文件移到 dif/experiments/concluded/
  3. 在 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.mdAGENTS.md.cursorrules
  • 安装 Claude Code skills 到 .claude/skills/

--agents 参数可只脚手架子集:

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

dif connect --key dif_pk_live_...   # 写入 dif/config.yaml,开启 cloud 模式
# 新项目可一步到位:
dif init --key dif_pk_live_...

key 是 publishable key,安全可提交。dif build 把它烘焙进生成的客户端:

import { events } from "./dif/generated/events";
dif.init({
  events,
  userId: () => currentUser?.id ?? null,
});

7.3 自定义事件管道

已有自己的事件管线?用自定义模式:

dif init --events custom

生成两个归你所有的处理器 dif/events/exposure.tsdif/events/track.ts。把事件转发到 Segment、Amplitude、webhook 或你自己的数仓——dif 不在乎事件去哪。没有捆绑的第三方集成,只有那两个函数。

7.4 指标跟踪

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 工作流

  1. 初始化dif init,设置 "prebuild": "dif build"
  2. 起草dif new <id> --surface <surface>,写上假设和 brief
  3. 激活:设置 status: activedif validate + dif build
  4. 接入:代码中调用 dif("id", {...})
  5. 追踪dif qa 验证分配,dif.track() 记录指标
  6. 结束dif conclude 归档 + 写决策 + 沉淀学习
  7. 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 日志,把既往学习带进草稿。


参考资源


Dif.Sh 基于 MIT 协议开源,本文基于 2026 年 9 月公开资料整理。具体命令和配置以官方文档为准。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Htr_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值