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

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
  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 参数可只脚手架子集:

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.tsdif/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 工作流

  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 月公开资料整理。具体命令和配置以官方文档为准。

相关推荐
hhzz1 小时前
【OpenCV 入门到精通 01】认识 OpenCV 与计算机视觉:从零建立全局认知
人工智能·python·opencv·计算机视觉·开源
m4Rk_1 小时前
【论文阅读】Agent 记忆机制(62):DCM-Agent——用双簇记忆化解优化问题的多范式冲突
论文阅读·人工智能·学习·开源·github
a1117766 小时前
图片转3D模型 img2threejs 开源
前端·开源
lunzi_08267 小时前
【无标题】
ai·金融·开源·供应链安全·ai agent·银行开源治理
ovO7 小时前
给 DeepSeek Harness 加一个异步 B 模型:dsh-second-opinion 使用指南
开源
XIE3928 小时前
TipKit:开源富文本编辑器套件,一套逻辑,任意风格!
前端·笔记·开源
zzzzzz31010 小时前
picoclaw:从“迷你部署代理”看轻量化项目该怎样被理解
人工智能·开源·github
jonyleek11 小时前
低代码平台的数据架构设计:动态表单存储与查询优化
低代码·postgresql·开源·数据架构·动态表单·jvs低代码平台·jvs低代码