我故意构建了一个糟糕的决策系统(让你免于如此)
Source: Dev.to
任务:相同问题,两种实现
两个系统解决完全相同的问题:
输入文本 → 提取关键词 → 计算分数 → 推荐动作
动作空间故意保持小:
WAIT_AND_SEEBUY_MORE_STOCKPANIC_REORDER
保持任务简单,使我们能够完全专注于 系统行为,而不是模型质量。
基准理念
该基准故意保持简约:
- 采用 单一、固定的输入文本。
- 在系统中 多次运行 它。
- 观察输出是否保持稳定。
为什么这很重要:仅能运行一次的系统并不是系统——那只是巧合。如果相同的输入产生不同的输出,则系统层面存在根本性问题。
基准结果:BAD 与 GOOD
以下结果是通过对 相同输入 在两个系统中各运行五次得到的。
BAD 系统输出(摘录)
BAD 系统会逐步升级其决策:
| Run | Score | Action |
|---|---|---|
| 1 | 14 | WAIT_AND_SEE |
| 3 | 42 | BUY_MORE_STOCK |
| 5 | 74 | PANIC_REORDER |
相同的输入。相同的关键字。却产生了完全不同的决策。
汇总基准摘要
BAD 系统
- 运行次数:5
- 唯一分数:5 →
[14, 28, 42, 58, 74] - 唯一动作:3
GOOD 系统
- 运行次数:5
- 唯一分数:1 →
[14, 14, 14, 14, 14] - 唯一动作:1
GOOD 系统的行为像一个纯函数。BAD 系统的行为则像内存泄漏。
故障分类:BAD 系统的崩溃方式
坏系统并不会以单一、显而易见的方式失效。相反,它表现出 多种相互作用的故障模式,这些模式在真实的 AI 与数据系统中很常见。给这些故障模式命名可以让它们更容易被检测到——也更难被意外上线。
1️⃣ Drift
- 定义:即使输入保持完全不变,系统的输出也会随时间改变。
- 根本原因:跨运行的全局分数累计;状态单调增长且未被重置。
- 为何危险:
- 业务逻辑在没有任何显式更改的情况下发生变动。
- 历史执行顺序会影响当前决策。
- 监控面板常常因为数值仍在“合理”范围而忽视问题。
漂移尤其危险,因为它看起来像是学习——但实际上并不是。
2️⃣ Non‑determinism
- 定义:相同的输入会产生不同的输出。
- 根本原因:在评分过程中注入随机噪声;对执行历史的隐式依赖。
- 为何危险:
- 错误无法可靠复现。
- 测试失败变得不稳定且不可信。
- A/B 实验失去统计意义。
如果你无法复现一次决策,就无法调试它。
3️⃣ Hidden State
- 定义:函数依赖于在其接口或输入中不可见的数据。
- 根本原因:全局变量,如
CURRENT_SCORE、LAST_TEXT和RUN_COUNT。 - 为何危险:
- 代码无法在局部范围内被理解。
- 重构会以非显而易见的方式改变行为。
- 新加入的贡献者在不知情的情况下引入回归。
隐藏状态把每一次函数调用都变成了猜谜游戏。
4️⃣ Silent Corruption
- 定义:系统在没有错误的情况下继续运行,但其决策错误率逐渐上升。
- 根本原因:缺乏显式的失败信号;没有不变式或健全性检查。
- 为何危险:
- 错误输出向下游传播。
- 问题仅通过业务影响才会显现。
- 回滚变得困难甚至不可能。
响亮的故障会被修复,沉默的故障会被部署。
为什么这个分类法重要
这些失败模式很少单独出现。在 BAD 系统中,它们相互强化:
- 隐藏状态导致漂移。
- 漂移放大非确定性。
- 非确定性掩盖静默腐败。
理解这些模式比修复单个 bug 更有价值——因为相同的分类法适用于更大、更复杂的 AI 系统。
单一指标:稳定性得分
为了概括系统行为,我使用了一个单一指标:
stability_score = 1 - (unique_scores / runs)
- 1.0 → 完全稳定
- 0.0 → 完全不稳定
稳定性结果
| 系统 | 稳定性得分 |
|---|---|
| BAD | 0.0 |
| GOOD | 0.8 |
这个数字已经告诉你哪个系统值得信赖。
最小修复:四个小补丁改变一切
这不是一次重写。这些是 外科手术式的更改。每个补丁都消除了一整类失败模式,而不引入新的抽象或框架。
补丁 1 — 移除全局状态
之前(错误):
# global mutation + history dependence
GS.CURRENT_SCORE += base
return GS.CURRENT_SCORE
之后(正确):
def score_keywords(keywords, text):
return sum(len(w) % 7 for w in keywords) + len(text) % 13
此更改修复的内容
- 消除分数漂移。
- 移除隐藏的历史依赖。
- 使函数确定且可测试。
依赖全局状态的函数不是函数——它是内存泄漏。
补丁 2 — 将副作用推到边界
之前(错误):
def extract_keywords(text):
print("Extracting keywords...")
open("log.txt", "a").write(text)
return tokens[:k]
之后(正确):
def extract_keywords(text):
# Pure computation – no I/O, no printing
return tokenize(text)[:k]
此更改修复的内容
- 移除隐藏的 I/O 副作用,使运行确定性更强。
- 将日志记录与核心逻辑分离(例如通过装饰器或包装器)。
补丁 3 — 强制不变量
之前(错误):
def compute_score(keywords):
# No sanity checks
return sum(len(k) for k in keywords) * random.random()
之后(正确):
def compute_score(keywords):
assert all(isinstance(k, str) for k in keywords), "Keywords must be strings"
base = sum(len(k) for k in keywords)
return base # deterministic, no random factor
此更改修复的内容
- 及早检测损坏的输入。
- 保证分数保持在预期范围内。
补丁 4 — 重置每次运行的状态
之前(错误):
RUN_COUNT += 1 # global counter never reset
CURRENT_SCORE += 5 # accumulates across runs
之后(正确):
def run_pipeline(text):
# Local state only
keywords = extract_keywords(text)
score = compute_score(keywords)
action = decide_action(score)
return {"score": score, "action": action}
此更改修复的内容
- 保证每次调用相互独立。
- 消除跨运行的漂移和隐藏状态。
补丁详细信息
补丁 3 — 明确依赖关系
Before (BAD):
if GS.LAST_TEXT is not None:
base += len(GS.LAST_TEXT) % 13
After (GOOD):
def score_keywords(keywords, text):
base = sum(len(w) % 7 for w in keywords)
return base + (len(text) % 13)
What this fixes
- 没有隐藏输入。
- 数据流清晰。
- 安全的重构。
补丁 4 — 为魔法数字命名
Before (BAD):
if score > 42:
action = "PANIC_REORDER"
After (GOOD):
@dataclass(frozen=True)
class Config:
panic_threshold: int = 42
if score > cfg.panic_threshold:
action = "PANIC_REORDER"
What this fixes
- 决策变得可解释。
- 参数可供审查。
- 行为变化变得有意图。
摘要
这四个补丁:
- 移除隐藏状态
- 消除非确定性
- 使行为可解释
- 恢复系统的信任
没有代理。没有框架。只有工程纪律。
最终要点
The BAD system works. 那就是问题所在。
It fails in the most dangerous way possible: plausibly and quietly.
The GOOD system is boring, predictable, and easy to reason about — which is exactly what you want in production.
Working code is not the same as a working system.
代码与可复现性
本文中使用的所有代码——包括有意破坏的系统、干净的实现以及基准测试——均已在 GitHub 上公开:
👉 https://github.com/Ertugrulmutlu/I-Intentionally-Built-a-Bad-Decision-System-So-You-Don-t-Have-To
如果你想复现结果,请运行:
python compare.py
该基准测试会多次将相同的输入分别通过两个系统,并在几行输出中展示为何可预测性比华丽的抽象更重要。