prompt-gray-release 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prompt_gray_release-0.2.0/LICENSE +21 -0
- prompt_gray_release-0.2.0/PKG-INFO +275 -0
- prompt_gray_release-0.2.0/README.md +240 -0
- prompt_gray_release-0.2.0/pyproject.toml +70 -0
- prompt_gray_release-0.2.0/setup.cfg +4 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/__init__.py +67 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/cli.py +186 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/envs.py +29 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/evaluators.py +86 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/exceptions.py +44 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/gate.py +134 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/py.typed +0 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/recorder.py +200 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/registry.py +87 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/render.py +25 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/rollout.py +174 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/router.py +71 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release/tracing.py +78 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/PKG-INFO +275 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/SOURCES.txt +30 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/dependency_links.txt +1 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/entry_points.txt +2 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/requires.txt +16 -0
- prompt_gray_release-0.2.0/src/prompt_gray_release.egg-info/top_level.txt +1 -0
- prompt_gray_release-0.2.0/tests/test_cli.py +206 -0
- prompt_gray_release-0.2.0/tests/test_evaluators.py +110 -0
- prompt_gray_release-0.2.0/tests/test_gate.py +110 -0
- prompt_gray_release-0.2.0/tests/test_recorder.py +134 -0
- prompt_gray_release-0.2.0/tests/test_registry.py +93 -0
- prompt_gray_release-0.2.0/tests/test_render.py +42 -0
- prompt_gray_release-0.2.0/tests/test_rollout.py +146 -0
- prompt_gray_release-0.2.0/tests/test_router.py +89 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 helloworldtang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prompt-gray-release
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 把 Prompt 当生产代码发布:版本化、灰度放量、评测门禁、一键回滚、事故重演。Prompt release engineering: versioning, canary rollout, eval gates, rollback and incident replay.
|
|
5
|
+
Author: helloworldtang
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/helloworldtang/prompt-gray-release
|
|
8
|
+
Project-URL: Issues, https://github.com/helloworldtang/prompt-gray-release/issues
|
|
9
|
+
Keywords: prompt,llm,gray-release,canary,prompt-engineering,llmops,evaluation
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Text Processing
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: PyYAML>=6.0
|
|
22
|
+
Provides-Extra: schema
|
|
23
|
+
Requires-Dist: jsonschema>=4.0; extra == "schema"
|
|
24
|
+
Provides-Extra: otel
|
|
25
|
+
Requires-Dist: opentelemetry-api>=1.20; extra == "otel"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
30
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
31
|
+
Requires-Dist: jsonschema>=4.0; extra == "dev"
|
|
32
|
+
Requires-Dist: build; extra == "dev"
|
|
33
|
+
Requires-Dist: twine; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# prompt-gray-release
|
|
37
|
+
|
|
38
|
+
**把 Prompt 当生产代码发布。** 轻量、可嵌入、零平台依赖的 Prompt 发布工程库:
|
|
39
|
+
版本化 → 评测门禁 → 灰度放量 → 一键回滚 → 事故确定性重演。
|
|
40
|
+
|
|
41
|
+
> Prompt release engineering: versioning, canary rollout, eval gates,
|
|
42
|
+
> one-click rollback and deterministic incident replay. Lightweight, embeddable, no platform required.
|
|
43
|
+
>
|
|
44
|
+
> [English README](README.en.md)
|
|
45
|
+
|
|
46
|
+
## 为什么是它:与平台方案的根本差异
|
|
47
|
+
|
|
48
|
+
LangSmith / Langfuse 是**评测与观测平台**——它们管"效果好不好",管不住**上线那一刻**。
|
|
49
|
+
prompt-gray-release 补的就是上线那一刻,并且不要求你搭建或迁移到任何平台:
|
|
50
|
+
|
|
51
|
+
| | LangSmith / Langfuse 等平台 | prompt-gray-release |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| 形态 | SaaS / 自托管服务,数据要出域或进它们的存储 | **纯库**,`pip install` 即用,数据不出你的进程 |
|
|
54
|
+
| 私有化与审计 | 自托管版要搭整套服务 | **JSONL 审计事件内建**,append-only,合规环境直接对接 |
|
|
55
|
+
| CI 门禁 | 依赖平台 CI 集成 | `prompt-gray check` 本地/CI 原生跑,**模板漂移直接挡合并** |
|
|
56
|
+
| 事故重演 | 数据集快照(要再调模型) | **确定性重演**:录下当时的响应,重放零调用、零成本、结果恒定 |
|
|
57
|
+
| 依赖 | 平台 SDK + 账号 + 网络 | **唯一依赖 PyYAML** |
|
|
58
|
+
|
|
59
|
+
一句话:**用平台看效果,用 prompt-gray-release 管上线。** 没有平台的小团队,它单独工作。
|
|
60
|
+
|
|
61
|
+
## 项目目标
|
|
62
|
+
|
|
63
|
+
**让任何团队——无需搭建平台——把 Prompt 变更纳入与代码同级的发布纪律。**
|
|
64
|
+
|
|
65
|
+
拆成三个可验收的子目标:
|
|
66
|
+
|
|
67
|
+
1. **安全上线**:Prompt 变更先过评测门禁,再按流量灰度,异常秒级回滚(对应 Registry / EvalGate / RolloutController);
|
|
68
|
+
2. **行为可验证**:Prompt 的每次行为变化都能被确定性重演并自动检出(对应 Recorder / Replayer / `prompt-gray check`);
|
|
69
|
+
3. **过程可审计**:谁、何时、把哪个版本、放了多少流量,全程 JSONL 可追溯。
|
|
70
|
+
|
|
71
|
+
**目标用户**:没有 MLOps 平台、也不打算搭平台的 LLM 应用团队;需要私有化与审计的强合规企业;个人开发者。
|
|
72
|
+
**非目标**:不做模型路由、不做评测平台、不做 Web 控制台——只做「发布链路」这一层。
|
|
73
|
+
|
|
74
|
+
## 为什么需要它
|
|
75
|
+
|
|
76
|
+
LLM 应用里,**Prompt 就是生产代码**——但它的变更方式却像在改配置:
|
|
77
|
+
|
|
78
|
+
- 改一句措辞就可能让格式崩坏、答非所问、成本飙升,而且是**全量瞬间生效**;
|
|
79
|
+
- 没有 diff 审查,没有回归测试,没有发布批次,出事了只能全网搜"刚才谁改的 Prompt";
|
|
80
|
+
- 观测平台能告诉你效果好不好,但**管不住上线那一刻**。
|
|
81
|
+
|
|
82
|
+
prompt-gray-release 补的就是"上线那一刻":**新 Prompt 先过评测门禁,再按 1% → 10% → 50% → 100% 灰度放量,出问题秒级回滚,历史事故随时确定性重演。**
|
|
83
|
+
|
|
84
|
+
## 安装
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install prompt-gray-release
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Python >= 3.10。唯一运行时依赖 PyYAML。
|
|
91
|
+
|
|
92
|
+
## 快速开始
|
|
93
|
+
|
|
94
|
+
完整可运行示例见 [examples/quickstart.py](examples/quickstart.py)(零 API key,
|
|
95
|
+
模拟模型 5 分钟跑通)与 [examples/openai_demo.py](examples/openai_demo.py)(真实模型全流程,
|
|
96
|
+
任意 OpenAI 兼容端点)。
|
|
97
|
+
|
|
98
|
+
### 1. 定义 Prompt 版本(prompts.yaml)
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
prompts:
|
|
102
|
+
support_reply:
|
|
103
|
+
v1:
|
|
104
|
+
template: |
|
|
105
|
+
你好 $name,这是 v1 的回复模板。关于 $topic 的问题我们会尽快处理。
|
|
106
|
+
metadata:
|
|
107
|
+
model: gpt-4o-mini
|
|
108
|
+
temperature: 0.2
|
|
109
|
+
v2:
|
|
110
|
+
template: |
|
|
111
|
+
你好 $name!这是 v2 的全新回复模板,专治 $topic 相关问题。
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 2. 灰度路由:同一用户永远同一版本
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from prompt_gray_release import GrayRouter, PromptRegistry, RolloutController
|
|
118
|
+
|
|
119
|
+
registry = PromptRegistry.from_yaml("prompts.yaml")
|
|
120
|
+
rollout = RolloutController()
|
|
121
|
+
rollout.set_stable("support_reply", "v1")
|
|
122
|
+
router = GrayRouter(registry, rollout)
|
|
123
|
+
|
|
124
|
+
rollout.start_rollout("support_reply", "v2", percent=10) # 先放 10% 流量
|
|
125
|
+
r = router.render("support_reply", {"name": "张三", "topic": "发票"}, user_key="user-42")
|
|
126
|
+
print(r.version, "->", r.text) # user-42 落在哪个版本,以后就一直在哪个版本
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
规则:`percent >= 100` 全量走 candidate;`0 < percent < 100` 按稳定 hash 分桶;
|
|
130
|
+
**没有 user_key 时不猜**——非全量一律走 stable(宁可不灰度,不可不确定)。
|
|
131
|
+
|
|
132
|
+
### 3. 评测门禁:不达标就拦截
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from prompt_gray_release import EvalCase, EvalGate
|
|
136
|
+
|
|
137
|
+
gate = EvalGate(registry)
|
|
138
|
+
cases = [EvalCase({"name": "张三", "topic": "发票"}, "期望的标准答案……")]
|
|
139
|
+
report = gate.evaluate("support_reply", candidate="v2", baseline="v1", cases=cases)
|
|
140
|
+
print(report.summary()) # [support_reply] v2=0.000 vs 基线 v1=1.000(要求 >= +0.000)→ 拦截
|
|
141
|
+
|
|
142
|
+
if report.passed:
|
|
143
|
+
rollout.set_percent("support_reply", 50) # 过门禁才放量
|
|
144
|
+
else:
|
|
145
|
+
rollout.rollback("support_reply") # 秒级回滚
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
评测函数可插拔:内置 `exact_match` / `contains`,也可以传自定义 `evaluator(output, expected) -> float`
|
|
149
|
+
(包括把真实模型当裁判——见 [examples/openai_demo.py](examples/openai_demo.py))。
|
|
150
|
+
`gate.enforce(...)` 在不达标时直接抛 `EvalGateBlocked`(fail-closed),适合放进 CI。
|
|
151
|
+
|
|
152
|
+
### 4. 放量、回滚与审计
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
rollout.set_percent("support_reply", 10) # 1% -> 10%
|
|
156
|
+
rollout.set_percent("support_reply", 50) # 观察指标没问题,继续放大
|
|
157
|
+
rollout.promote_full("support_reply") # 全量,candidate 升级为 stable
|
|
158
|
+
# 或发现异常:
|
|
159
|
+
rollout.rollback("support_reply") # 秒级回到 stable
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
传入 `state_path` 时,每次变更都会**原子落盘**并追加 **JSONL 审计事件**
|
|
163
|
+
(谁在什么时候把哪个版本放到了多少流量),满足企业合规的可追溯要求。
|
|
164
|
+
|
|
165
|
+
### 5. 录制与确定性重演
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from prompt_gray_release import Cassette, Recorder, Replayer
|
|
169
|
+
|
|
170
|
+
recorder = Recorder("traffic.jsonl")
|
|
171
|
+
model = recorder.wrap(real_model) # 调用即录制(输入 + 模型响应)
|
|
172
|
+
|
|
173
|
+
# 事后:确定性重演
|
|
174
|
+
cassette = Cassette.load("traffic.jsonl")
|
|
175
|
+
for item in Replayer(cassette).replay_registry(registry):
|
|
176
|
+
print(item.matches, item.rendered_now) # False = 当前模板与事故时行为已漂移
|
|
177
|
+
|
|
178
|
+
fake_model = Replayer(cassette).deterministic_model("support_reply", "v2")
|
|
179
|
+
fake_model("任意文本", {"name": "张三", "topic": "发票"}) # 永远返回当时录制的响应
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
确定性重演的意义:LLM 输出不可复现,但把"输入 + 当时的响应"录下来之后,
|
|
183
|
+
**同一批输入永远得到同一批输出**——下游解析逻辑、路由逻辑、降级逻辑的回归测试从此有了稳定靶子;
|
|
184
|
+
`matches=False` 则直接告诉你"事故之后有人改过模板"。
|
|
185
|
+
|
|
186
|
+
## CLI:不写 Python 也能用
|
|
187
|
+
|
|
188
|
+
安装后自带 `prompt-gray` 命令。运维子命令全部走唯一写入口,动作自动审计:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
prompt-gray set-stable support_reply v1 --state rollout.json
|
|
192
|
+
prompt-gray start-rollout support_reply v2 --percent 10 --state rollout.json
|
|
193
|
+
prompt-gray status support_reply --state rollout.json
|
|
194
|
+
prompt-gray set-percent support_reply 50 --state rollout.json
|
|
195
|
+
prompt-gray rollback support_reply --state rollout.json # 秒级回滚
|
|
196
|
+
prompt-gray promote-full support_reply --state rollout.json
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## CI 重演门禁:改一行模板,CI 变红
|
|
200
|
+
|
|
201
|
+
`prompt-gray check` 把"确定性重演"变成 CI 门禁——拿录制的真实流量对当前注册表逐条重演,
|
|
202
|
+
模板漂移 / 版本缺失 / 渲染失败任一出现即退出码非 0:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
prompt-gray check --cassette traffic.jsonl --registry prompts.yaml --strict
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`--strict` 下空 cassette 也算失败(防止"没录制所以全过"的假绿)。GitHub Actions 用法:
|
|
209
|
+
|
|
210
|
+
```yaml
|
|
211
|
+
# .github/workflows/prompt-drift.yml
|
|
212
|
+
on: [pull_request]
|
|
213
|
+
jobs:
|
|
214
|
+
prompt-drift:
|
|
215
|
+
runs-on: ubuntu-latest
|
|
216
|
+
steps:
|
|
217
|
+
- uses: actions/checkout@v4
|
|
218
|
+
- uses: actions/setup-python@v5
|
|
219
|
+
with: { python-version: "3.12" }
|
|
220
|
+
- run: pip install prompt-gray-release
|
|
221
|
+
- run: prompt-gray check --cassette traffic.jsonl --registry prompts.yaml --strict
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
效果:**PR 里改一行模板 → CI 拦截合并;还原 → CI 恢复绿色。**
|
|
225
|
+
录制文件(traffic.jsonl)跟着仓库走,门禁在本地与 CI 行为完全一致,不依赖任何外部服务。
|
|
226
|
+
|
|
227
|
+
## 用真实模型 5 分钟跑通
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
pip install openai prompt-gray-release
|
|
231
|
+
export OPENAI_API_KEY=sk-...
|
|
232
|
+
export OPENAI_BASE_URL=https://... # 可选:任意 OpenAI 兼容端点
|
|
233
|
+
python examples/openai_demo.py
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
示例覆盖:灰度路由选中候选 → 真实模型调用并录制 → 真实模型当裁判的评测门禁
|
|
237
|
+
(不达标自动回滚)→ 零成本确定性重演。库本身不引入 openai 依赖。
|
|
238
|
+
|
|
239
|
+
## 集成
|
|
240
|
+
|
|
241
|
+
- FastAPI:[examples/fastapi_app.py](examples/fastapi_app.py)——`router.render` 进请求处理,
|
|
242
|
+
附带回滚端点;
|
|
243
|
+
- pytest:[examples/test_with_cassette.py](examples/test_with_cassette.py)——用 cassette
|
|
244
|
+
的确定性假模型做下游逻辑回归;
|
|
245
|
+
- OpenTelemetry(可选 `[otel]` extra):`traced(router)` / `traced_model(model)` 导出 span;
|
|
246
|
+
- 多环境:`RolloutController.for_env("rollout", "prod")` → `rollout.prod.json`,环境间状态天然隔离。
|
|
247
|
+
|
|
248
|
+
## 设计原则
|
|
249
|
+
|
|
250
|
+
1. **库,不是平台**:一个 `pip install` 就能嵌入现有业务,不要求你迁移到任何系统;
|
|
251
|
+
2. **fail-fast / fail-closed**:缺变量即报错、评测不达标即拦截、重演找不到即抛错——发布链路上不玩默认静默;
|
|
252
|
+
3. **确定性优先**:没有 user_key 不做随机灰度;重演靠录制响应而不是"再问一次模型";
|
|
253
|
+
4. **审计内建**:状态每次变更原子落盘 + JSONL 事件流,合规环境可直接对接审计。
|
|
254
|
+
|
|
255
|
+
## 路线图
|
|
256
|
+
|
|
257
|
+
详细计划与进度见 [TODO.md](TODO.md)。
|
|
258
|
+
|
|
259
|
+
- [x] CLI:`prompt-gray status / set-stable / start-rollout / set-percent / promote-full / rollback / check`
|
|
260
|
+
- [x] 评测函数注册表(正则 / 关键词集合 / JSON 合法性 / JSON Schema)
|
|
261
|
+
- [x] 与 OpenTelemetry 对接的调用追踪导出(`traced` / `traced_model`,可选 extra)
|
|
262
|
+
- [x] 多环境(dev / staging / prod)状态隔离(`for_env`)
|
|
263
|
+
|
|
264
|
+
## 开发
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
pip install -e ".[dev]"
|
|
268
|
+
pytest # 109 个测试
|
|
269
|
+
ruff check src tests examples && ruff format --check src tests examples
|
|
270
|
+
mypy src # strict
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
[MIT](LICENSE) © helloworldtang
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# prompt-gray-release
|
|
2
|
+
|
|
3
|
+
**把 Prompt 当生产代码发布。** 轻量、可嵌入、零平台依赖的 Prompt 发布工程库:
|
|
4
|
+
版本化 → 评测门禁 → 灰度放量 → 一键回滚 → 事故确定性重演。
|
|
5
|
+
|
|
6
|
+
> Prompt release engineering: versioning, canary rollout, eval gates,
|
|
7
|
+
> one-click rollback and deterministic incident replay. Lightweight, embeddable, no platform required.
|
|
8
|
+
>
|
|
9
|
+
> [English README](README.en.md)
|
|
10
|
+
|
|
11
|
+
## 为什么是它:与平台方案的根本差异
|
|
12
|
+
|
|
13
|
+
LangSmith / Langfuse 是**评测与观测平台**——它们管"效果好不好",管不住**上线那一刻**。
|
|
14
|
+
prompt-gray-release 补的就是上线那一刻,并且不要求你搭建或迁移到任何平台:
|
|
15
|
+
|
|
16
|
+
| | LangSmith / Langfuse 等平台 | prompt-gray-release |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| 形态 | SaaS / 自托管服务,数据要出域或进它们的存储 | **纯库**,`pip install` 即用,数据不出你的进程 |
|
|
19
|
+
| 私有化与审计 | 自托管版要搭整套服务 | **JSONL 审计事件内建**,append-only,合规环境直接对接 |
|
|
20
|
+
| CI 门禁 | 依赖平台 CI 集成 | `prompt-gray check` 本地/CI 原生跑,**模板漂移直接挡合并** |
|
|
21
|
+
| 事故重演 | 数据集快照(要再调模型) | **确定性重演**:录下当时的响应,重放零调用、零成本、结果恒定 |
|
|
22
|
+
| 依赖 | 平台 SDK + 账号 + 网络 | **唯一依赖 PyYAML** |
|
|
23
|
+
|
|
24
|
+
一句话:**用平台看效果,用 prompt-gray-release 管上线。** 没有平台的小团队,它单独工作。
|
|
25
|
+
|
|
26
|
+
## 项目目标
|
|
27
|
+
|
|
28
|
+
**让任何团队——无需搭建平台——把 Prompt 变更纳入与代码同级的发布纪律。**
|
|
29
|
+
|
|
30
|
+
拆成三个可验收的子目标:
|
|
31
|
+
|
|
32
|
+
1. **安全上线**:Prompt 变更先过评测门禁,再按流量灰度,异常秒级回滚(对应 Registry / EvalGate / RolloutController);
|
|
33
|
+
2. **行为可验证**:Prompt 的每次行为变化都能被确定性重演并自动检出(对应 Recorder / Replayer / `prompt-gray check`);
|
|
34
|
+
3. **过程可审计**:谁、何时、把哪个版本、放了多少流量,全程 JSONL 可追溯。
|
|
35
|
+
|
|
36
|
+
**目标用户**:没有 MLOps 平台、也不打算搭平台的 LLM 应用团队;需要私有化与审计的强合规企业;个人开发者。
|
|
37
|
+
**非目标**:不做模型路由、不做评测平台、不做 Web 控制台——只做「发布链路」这一层。
|
|
38
|
+
|
|
39
|
+
## 为什么需要它
|
|
40
|
+
|
|
41
|
+
LLM 应用里,**Prompt 就是生产代码**——但它的变更方式却像在改配置:
|
|
42
|
+
|
|
43
|
+
- 改一句措辞就可能让格式崩坏、答非所问、成本飙升,而且是**全量瞬间生效**;
|
|
44
|
+
- 没有 diff 审查,没有回归测试,没有发布批次,出事了只能全网搜"刚才谁改的 Prompt";
|
|
45
|
+
- 观测平台能告诉你效果好不好,但**管不住上线那一刻**。
|
|
46
|
+
|
|
47
|
+
prompt-gray-release 补的就是"上线那一刻":**新 Prompt 先过评测门禁,再按 1% → 10% → 50% → 100% 灰度放量,出问题秒级回滚,历史事故随时确定性重演。**
|
|
48
|
+
|
|
49
|
+
## 安装
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install prompt-gray-release
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Python >= 3.10。唯一运行时依赖 PyYAML。
|
|
56
|
+
|
|
57
|
+
## 快速开始
|
|
58
|
+
|
|
59
|
+
完整可运行示例见 [examples/quickstart.py](examples/quickstart.py)(零 API key,
|
|
60
|
+
模拟模型 5 分钟跑通)与 [examples/openai_demo.py](examples/openai_demo.py)(真实模型全流程,
|
|
61
|
+
任意 OpenAI 兼容端点)。
|
|
62
|
+
|
|
63
|
+
### 1. 定义 Prompt 版本(prompts.yaml)
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
prompts:
|
|
67
|
+
support_reply:
|
|
68
|
+
v1:
|
|
69
|
+
template: |
|
|
70
|
+
你好 $name,这是 v1 的回复模板。关于 $topic 的问题我们会尽快处理。
|
|
71
|
+
metadata:
|
|
72
|
+
model: gpt-4o-mini
|
|
73
|
+
temperature: 0.2
|
|
74
|
+
v2:
|
|
75
|
+
template: |
|
|
76
|
+
你好 $name!这是 v2 的全新回复模板,专治 $topic 相关问题。
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. 灰度路由:同一用户永远同一版本
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from prompt_gray_release import GrayRouter, PromptRegistry, RolloutController
|
|
83
|
+
|
|
84
|
+
registry = PromptRegistry.from_yaml("prompts.yaml")
|
|
85
|
+
rollout = RolloutController()
|
|
86
|
+
rollout.set_stable("support_reply", "v1")
|
|
87
|
+
router = GrayRouter(registry, rollout)
|
|
88
|
+
|
|
89
|
+
rollout.start_rollout("support_reply", "v2", percent=10) # 先放 10% 流量
|
|
90
|
+
r = router.render("support_reply", {"name": "张三", "topic": "发票"}, user_key="user-42")
|
|
91
|
+
print(r.version, "->", r.text) # user-42 落在哪个版本,以后就一直在哪个版本
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
规则:`percent >= 100` 全量走 candidate;`0 < percent < 100` 按稳定 hash 分桶;
|
|
95
|
+
**没有 user_key 时不猜**——非全量一律走 stable(宁可不灰度,不可不确定)。
|
|
96
|
+
|
|
97
|
+
### 3. 评测门禁:不达标就拦截
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from prompt_gray_release import EvalCase, EvalGate
|
|
101
|
+
|
|
102
|
+
gate = EvalGate(registry)
|
|
103
|
+
cases = [EvalCase({"name": "张三", "topic": "发票"}, "期望的标准答案……")]
|
|
104
|
+
report = gate.evaluate("support_reply", candidate="v2", baseline="v1", cases=cases)
|
|
105
|
+
print(report.summary()) # [support_reply] v2=0.000 vs 基线 v1=1.000(要求 >= +0.000)→ 拦截
|
|
106
|
+
|
|
107
|
+
if report.passed:
|
|
108
|
+
rollout.set_percent("support_reply", 50) # 过门禁才放量
|
|
109
|
+
else:
|
|
110
|
+
rollout.rollback("support_reply") # 秒级回滚
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
评测函数可插拔:内置 `exact_match` / `contains`,也可以传自定义 `evaluator(output, expected) -> float`
|
|
114
|
+
(包括把真实模型当裁判——见 [examples/openai_demo.py](examples/openai_demo.py))。
|
|
115
|
+
`gate.enforce(...)` 在不达标时直接抛 `EvalGateBlocked`(fail-closed),适合放进 CI。
|
|
116
|
+
|
|
117
|
+
### 4. 放量、回滚与审计
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
rollout.set_percent("support_reply", 10) # 1% -> 10%
|
|
121
|
+
rollout.set_percent("support_reply", 50) # 观察指标没问题,继续放大
|
|
122
|
+
rollout.promote_full("support_reply") # 全量,candidate 升级为 stable
|
|
123
|
+
# 或发现异常:
|
|
124
|
+
rollout.rollback("support_reply") # 秒级回到 stable
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
传入 `state_path` 时,每次变更都会**原子落盘**并追加 **JSONL 审计事件**
|
|
128
|
+
(谁在什么时候把哪个版本放到了多少流量),满足企业合规的可追溯要求。
|
|
129
|
+
|
|
130
|
+
### 5. 录制与确定性重演
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from prompt_gray_release import Cassette, Recorder, Replayer
|
|
134
|
+
|
|
135
|
+
recorder = Recorder("traffic.jsonl")
|
|
136
|
+
model = recorder.wrap(real_model) # 调用即录制(输入 + 模型响应)
|
|
137
|
+
|
|
138
|
+
# 事后:确定性重演
|
|
139
|
+
cassette = Cassette.load("traffic.jsonl")
|
|
140
|
+
for item in Replayer(cassette).replay_registry(registry):
|
|
141
|
+
print(item.matches, item.rendered_now) # False = 当前模板与事故时行为已漂移
|
|
142
|
+
|
|
143
|
+
fake_model = Replayer(cassette).deterministic_model("support_reply", "v2")
|
|
144
|
+
fake_model("任意文本", {"name": "张三", "topic": "发票"}) # 永远返回当时录制的响应
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
确定性重演的意义:LLM 输出不可复现,但把"输入 + 当时的响应"录下来之后,
|
|
148
|
+
**同一批输入永远得到同一批输出**——下游解析逻辑、路由逻辑、降级逻辑的回归测试从此有了稳定靶子;
|
|
149
|
+
`matches=False` 则直接告诉你"事故之后有人改过模板"。
|
|
150
|
+
|
|
151
|
+
## CLI:不写 Python 也能用
|
|
152
|
+
|
|
153
|
+
安装后自带 `prompt-gray` 命令。运维子命令全部走唯一写入口,动作自动审计:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
prompt-gray set-stable support_reply v1 --state rollout.json
|
|
157
|
+
prompt-gray start-rollout support_reply v2 --percent 10 --state rollout.json
|
|
158
|
+
prompt-gray status support_reply --state rollout.json
|
|
159
|
+
prompt-gray set-percent support_reply 50 --state rollout.json
|
|
160
|
+
prompt-gray rollback support_reply --state rollout.json # 秒级回滚
|
|
161
|
+
prompt-gray promote-full support_reply --state rollout.json
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## CI 重演门禁:改一行模板,CI 变红
|
|
165
|
+
|
|
166
|
+
`prompt-gray check` 把"确定性重演"变成 CI 门禁——拿录制的真实流量对当前注册表逐条重演,
|
|
167
|
+
模板漂移 / 版本缺失 / 渲染失败任一出现即退出码非 0:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
prompt-gray check --cassette traffic.jsonl --registry prompts.yaml --strict
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`--strict` 下空 cassette 也算失败(防止"没录制所以全过"的假绿)。GitHub Actions 用法:
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
# .github/workflows/prompt-drift.yml
|
|
177
|
+
on: [pull_request]
|
|
178
|
+
jobs:
|
|
179
|
+
prompt-drift:
|
|
180
|
+
runs-on: ubuntu-latest
|
|
181
|
+
steps:
|
|
182
|
+
- uses: actions/checkout@v4
|
|
183
|
+
- uses: actions/setup-python@v5
|
|
184
|
+
with: { python-version: "3.12" }
|
|
185
|
+
- run: pip install prompt-gray-release
|
|
186
|
+
- run: prompt-gray check --cassette traffic.jsonl --registry prompts.yaml --strict
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
效果:**PR 里改一行模板 → CI 拦截合并;还原 → CI 恢复绿色。**
|
|
190
|
+
录制文件(traffic.jsonl)跟着仓库走,门禁在本地与 CI 行为完全一致,不依赖任何外部服务。
|
|
191
|
+
|
|
192
|
+
## 用真实模型 5 分钟跑通
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
pip install openai prompt-gray-release
|
|
196
|
+
export OPENAI_API_KEY=sk-...
|
|
197
|
+
export OPENAI_BASE_URL=https://... # 可选:任意 OpenAI 兼容端点
|
|
198
|
+
python examples/openai_demo.py
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
示例覆盖:灰度路由选中候选 → 真实模型调用并录制 → 真实模型当裁判的评测门禁
|
|
202
|
+
(不达标自动回滚)→ 零成本确定性重演。库本身不引入 openai 依赖。
|
|
203
|
+
|
|
204
|
+
## 集成
|
|
205
|
+
|
|
206
|
+
- FastAPI:[examples/fastapi_app.py](examples/fastapi_app.py)——`router.render` 进请求处理,
|
|
207
|
+
附带回滚端点;
|
|
208
|
+
- pytest:[examples/test_with_cassette.py](examples/test_with_cassette.py)——用 cassette
|
|
209
|
+
的确定性假模型做下游逻辑回归;
|
|
210
|
+
- OpenTelemetry(可选 `[otel]` extra):`traced(router)` / `traced_model(model)` 导出 span;
|
|
211
|
+
- 多环境:`RolloutController.for_env("rollout", "prod")` → `rollout.prod.json`,环境间状态天然隔离。
|
|
212
|
+
|
|
213
|
+
## 设计原则
|
|
214
|
+
|
|
215
|
+
1. **库,不是平台**:一个 `pip install` 就能嵌入现有业务,不要求你迁移到任何系统;
|
|
216
|
+
2. **fail-fast / fail-closed**:缺变量即报错、评测不达标即拦截、重演找不到即抛错——发布链路上不玩默认静默;
|
|
217
|
+
3. **确定性优先**:没有 user_key 不做随机灰度;重演靠录制响应而不是"再问一次模型";
|
|
218
|
+
4. **审计内建**:状态每次变更原子落盘 + JSONL 事件流,合规环境可直接对接审计。
|
|
219
|
+
|
|
220
|
+
## 路线图
|
|
221
|
+
|
|
222
|
+
详细计划与进度见 [TODO.md](TODO.md)。
|
|
223
|
+
|
|
224
|
+
- [x] CLI:`prompt-gray status / set-stable / start-rollout / set-percent / promote-full / rollback / check`
|
|
225
|
+
- [x] 评测函数注册表(正则 / 关键词集合 / JSON 合法性 / JSON Schema)
|
|
226
|
+
- [x] 与 OpenTelemetry 对接的调用追踪导出(`traced` / `traced_model`,可选 extra)
|
|
227
|
+
- [x] 多环境(dev / staging / prod)状态隔离(`for_env`)
|
|
228
|
+
|
|
229
|
+
## 开发
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
pip install -e ".[dev]"
|
|
233
|
+
pytest # 109 个测试
|
|
234
|
+
ruff check src tests examples && ruff format --check src tests examples
|
|
235
|
+
mypy src # strict
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
[MIT](LICENSE) © helloworldtang
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "prompt-gray-release"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "把 Prompt 当生产代码发布:版本化、灰度放量、评测门禁、一键回滚、事故重演。Prompt release engineering: versioning, canary rollout, eval gates, rollback and incident replay."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "helloworldtang" }]
|
|
13
|
+
keywords = ["prompt", "llm", "gray-release", "canary", "prompt-engineering", "llmops", "evaluation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: Libraries",
|
|
22
|
+
"Topic :: Text Processing",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["PyYAML>=6.0"]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
schema = ["jsonschema>=4.0"]
|
|
28
|
+
otel = ["opentelemetry-api>=1.20"]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=7.0",
|
|
31
|
+
"ruff>=0.6",
|
|
32
|
+
"mypy>=1.10",
|
|
33
|
+
"types-PyYAML",
|
|
34
|
+
"jsonschema>=4.0",
|
|
35
|
+
"build",
|
|
36
|
+
"twine",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
target-version = "py310"
|
|
41
|
+
line-length = 100
|
|
42
|
+
src = ["src", "tests"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = ["E", "W", "F", "I", "B", "C4", "UP", "SIM"]
|
|
46
|
+
|
|
47
|
+
[tool.mypy]
|
|
48
|
+
strict = true
|
|
49
|
+
python_version = "3.10"
|
|
50
|
+
|
|
51
|
+
[[tool.mypy.overrides]]
|
|
52
|
+
module = ["jsonschema", "jsonschema.exceptions", "opentelemetry", "opentelemetry.*"]
|
|
53
|
+
ignore_missing_imports = true
|
|
54
|
+
|
|
55
|
+
[project.scripts]
|
|
56
|
+
prompt-gray = "prompt_gray_release.cli:main"
|
|
57
|
+
|
|
58
|
+
[project.urls]
|
|
59
|
+
Homepage = "https://github.com/helloworldtang/prompt-gray-release"
|
|
60
|
+
Issues = "https://github.com/helloworldtang/prompt-gray-release/issues"
|
|
61
|
+
|
|
62
|
+
[tool.setuptools.packages.find]
|
|
63
|
+
where = ["src"]
|
|
64
|
+
|
|
65
|
+
[tool.setuptools.package-data]
|
|
66
|
+
prompt_gray_release = ["py.typed"]
|
|
67
|
+
|
|
68
|
+
[tool.pytest.ini_options]
|
|
69
|
+
testpaths = ["tests"]
|
|
70
|
+
addopts = "-q"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""prompt-gray-release: 把 Prompt 当生产代码发布。
|
|
2
|
+
|
|
3
|
+
版本化、灰度放量、评测门禁、一键回滚、事故确定性重演。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .envs import state_path_for_env
|
|
7
|
+
from .evaluators import json_schema, json_valid, keywords, regex
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
EvalGateBlocked,
|
|
10
|
+
PromptGrayReleaseError,
|
|
11
|
+
PromptNotFoundError,
|
|
12
|
+
ReplayMissError,
|
|
13
|
+
RolloutStateError,
|
|
14
|
+
TemplateRenderError,
|
|
15
|
+
VersionNotFoundError,
|
|
16
|
+
)
|
|
17
|
+
from .gate import EvalCase, EvalGate, EvalReport, contains, exact_match
|
|
18
|
+
from .recorder import CallRecord, Cassette, Recorder, Replayer
|
|
19
|
+
from .registry import PromptRegistry, PromptVersion
|
|
20
|
+
from .render import render_template
|
|
21
|
+
from .rollout import RolloutController, RolloutState
|
|
22
|
+
from .router import GrayRouter, RenderedPrompt
|
|
23
|
+
from .tracing import traced, traced_model
|
|
24
|
+
|
|
25
|
+
__version__ = "0.2.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"__version__",
|
|
29
|
+
# registry
|
|
30
|
+
"PromptRegistry",
|
|
31
|
+
"PromptVersion",
|
|
32
|
+
# render
|
|
33
|
+
"render_template",
|
|
34
|
+
# rollout / router
|
|
35
|
+
"RolloutController",
|
|
36
|
+
"RolloutState",
|
|
37
|
+
"GrayRouter",
|
|
38
|
+
"RenderedPrompt",
|
|
39
|
+
# gate
|
|
40
|
+
"EvalGate",
|
|
41
|
+
"EvalCase",
|
|
42
|
+
"EvalReport",
|
|
43
|
+
"exact_match",
|
|
44
|
+
"contains",
|
|
45
|
+
# evaluators
|
|
46
|
+
"regex",
|
|
47
|
+
"keywords",
|
|
48
|
+
"json_valid",
|
|
49
|
+
"json_schema",
|
|
50
|
+
# envs / tracing
|
|
51
|
+
"state_path_for_env",
|
|
52
|
+
"traced",
|
|
53
|
+
"traced_model",
|
|
54
|
+
# recorder / replay
|
|
55
|
+
"Recorder",
|
|
56
|
+
"Cassette",
|
|
57
|
+
"CallRecord",
|
|
58
|
+
"Replayer",
|
|
59
|
+
# exceptions
|
|
60
|
+
"PromptGrayReleaseError",
|
|
61
|
+
"PromptNotFoundError",
|
|
62
|
+
"VersionNotFoundError",
|
|
63
|
+
"TemplateRenderError",
|
|
64
|
+
"RolloutStateError",
|
|
65
|
+
"EvalGateBlocked",
|
|
66
|
+
"ReplayMissError",
|
|
67
|
+
]
|