目录
一场事故复盘会,有人说了句「这是个概率性问题」,会议就散了。
这句话的信息量是零,但它的效率是满分。它听起来像一个技术判断,实际是一个社交动作——意思是「我还没查清楚,但如果继续追下去,今天下不了班」。
技术黑话不是骗子的话术,它是长期在压力下形成的语言压缩。压缩的代价很直接:信息丢了,责任也模糊了。下面这份词典,每一条都试着把这层压缩解开,看看底下藏着什么。
一、甩锅类:把问题推给一个不可证伪的对象
「在我机器上是好的」
翻译:代码没问题,是你的环境有问题——而且我不打算查是哪里不一样。
这句话最气人的地方在于,它经常是对的。环境差异是真实存在的技术问题,只是没人愿意为它负责。常见的根因就那几个:
- lockfile 没提交,或者别人跑了
npm install而不是npm ci,传递依赖每次解析出不同版本; - PATH 里躺着两份 Node,终端里是 v22,IDE 里是 v18;
- 环境变量只在
.env里有,CI 上压根没配,于是连的是另一个数据库; - 文件系统大小写:macOS 的 APFS 默认大小写不敏感,
import './Button'在本地能过,到了 Linux 上因为真实文件叫button.tsx而报错; - 本地时区、本地 glibc 版本、本地装了某个全局工具。
止血方式也早就有了,只是需要有人愿意做:lockfile 进仓库、CI 用 npm ci、运行时版本写进 .nvmrc 或 mise.toml、配置从代码里赶出去(12-Factor 的第三条)。这套东西的收益不是「少吵几次架」,而是让「在我机器上是好的」这句话变得没有意义。
「你重启一下试试」
这是整份词典里最诚实的一句——它承认自己不知道原因,但它知道一个几乎总有效的操作。
重启不是玄学,重启是状态重置。它有效,说明问题出在进程的累积状态里。候选者:内存泄漏、连接池或文件描述符泄漏(未 close 的 socket 最终撞上 EMFILE)、全局单例里越攒越多的对象、缓存里的脏数据、某个未捕获的 Promise rejection 让一处状态机永久卡死。
所以「重启一下好了」应该被当成线索而不是结论。一个服务如果每周都需要重启,它已经在用泄漏速度告诉你它病在哪了。
「这是缓存问题」
缓存是最容易被栽赃的组件:它确实经常是原因,而且清它的成本看起来很低。
但「缓存问题」不是诊断,是猜测。想验证,成本也不高:
curl -sI https://example.com/api/posts \ -H 'Cache-Control: no-cache' \ | grep -iE 'age|x-cache|cache-control|etag'Age 很大说明命中的是旧副本,X-Cache: HIT 说明前面真有一层缓存在拦。
而真正的缓存问题,形态基本只有三种,清缓存一个都治不了:
- 缓存键漏了维度——多租户场景下 key 里只有资源 id 没有租户 id,于是 A 的数据被返回给了 B。这是事故,不是性能问题。
- 失效策略错——写入时忘了让缓存失效,或者 TTL 长得离谱,用户改完资料刷新十次还是旧的。
- 缓存击穿——大批 key 同时过期,流量瞬间打穿到数据库,把数据库也带走。
二、状态描述类:把 bug 说成设计
「这是个特性,不是 bug」
这句话有一半时候是真的,而它成真的机制有个名字:Hyrum 定律——当你的接口有了足够多的用户,你对其所有可观察行为的依赖都不再是偶然,而是契约的一部分。
一个真实形态的例子:某接口在列表为空时返回 null 而不是 []。下游五百个服务老老实实写了 if (res == null) { ... }。三年后你想修这个「显然的 bug」,代价是发大版本、写迁移指南、挨个通知调用方。兼容性成本已经超过了正确性收益,于是 bug 就地转正,领取了「特性」的编制。
这句黑话在多数场合是自我解嘲,但它的技术内核是硬的:正确性不是免费的,它要和兼容性竞价。
「预期行为」
上一句的正式版本,通常出现在 issue tracker 的关闭理由里,和 WONTFIX 的区别只在于礼貌程度。
三、偶现类:把不确定性当成不可知
「偶现」「概率性问题」
翻译:我复现不出来,而且我怀疑复现需要条件。
这类问题的根因几乎总在这几类里:竞态(多线程/多协程读写共享状态没有同步)、时序依赖(隐含假设某个网络请求会先返回)、未初始化或已释放的资源、GC 时机、时钟。
它们的共同点值得单独拎出来说:代码不是「有时候错」,而是代码对执行顺序有隐含假设,而这个假设不总是成立。
所以正确的做法不是反复尝试「复现」,而是把不确定性变成可观测性:
# Go:竞态检测器,专治「本地跑一万次都没事」go test -race -count=10000 ./...
# Node:把未捕获的 Promise 拒绝变成崩溃,而不是静默node --unhandled-rejections=strict app.js加日志时带上 request id 和协程/线程 id,把顺序记下来。概率性 bug 只要样本足够,一定会复现——它缺的只是样本量,不是运气。
「线上没复现出来」
翻译:生产和我们能碰的环境不一样。
真正的差异通常不在代码,而在数据:生产有几亿行、有真实并发、有三年前那次迁移留下的空字段。本地拿一张十行的 users 表测出来的「没问题」,不能说明任何事。
缩短差距的办法是具体的:生产数据脱敏后灌进预发、做流量回放、用 feature flag 灰度而不是一把梭全量。
四、稳定性类:把放大器当成修复
「加个重试就好了」
这条是整篇词典里最危险的一句。
重试真正能治的只有一类问题:瞬时故障——网络抖动、单个副本重启、被限流。超出这三类,重试都在帮倒忙:
- 下游如果是因为过载而超时,重试等于继续加压。一次故障就是这样被放大成全站故障的,它有专门的名字:retry storm。
- 操作如果不幂等,重试就变成了重复扣款、重复发券、重复建单。
一条能上线的重试,至少要凑齐五个条件:有上限、有指数退避、有抖动(否则所有客户端会在同一毫秒一起重试,形成同步尖峰)、只对可重试的错误码重试(400/404 重试毫无意义)、配熔断。写操作还必须带幂等键。
// 退避 + 抖动 + 上限 + 熔断,一个都不能少const base = 100, cap = 5000, maxAttempts = 5;
async function retryable(fn, attempt = 1) { try { return await fn(); } catch (err) { if (attempt >= maxAttempts || !isTransient(err)) throw err; const exp = Math.min(cap, base * 2 ** (attempt - 1)); // full jitter:在 [0, exp] 里随机,避免重试风暴 await sleep(Math.random() * exp); return retryable(fn, attempt + 1); }}少任何一个条件,这段代码都是一颗定时炸弹。
「先这样,后面再优化」
翻译:没有 ticket 的重构,等于永不重构。
这句不只是笑话。临时方案有个致命特性:它会变成事实标准,然后被依赖,然后按 Hyrum 定律变得不可移除。于是「后面再优化」这句话的诚意是可以被验证的——有没有开 issue、有没有写进本季度的待办、有没有设一个真的会响的提醒。三者全无,那就是「永不」。
五、AI 时代的新黑话
旧黑话还没退休,新的一批已经上岗了。
「模型幻觉」
「幻觉」是个正儿八经的学术词,现在被用成了万能背锅词。但真实的失败模式至少有四种,对症下药完全不同:
| 你看到的现象 | 真实原因 | 该做的事 |
|---|---|---|
| 答了但完全是编的 | 知识不在模型里 | 上 RAG,把资料检索进上下文 |
| 资料给它了还是答错 | 检索到的片段不对 | 调分块策略、加重排 |
| 格式/字段总是错 | 指令遵循失败 | 结构化输出、约束解码 |
| 同一问题答案飘忽 | 采样随机性 | 降温度、自洽性投票 |
把四类都叫「幻觉」,约等于把 404、500、超时统称为「网络不好」——听着像结论,实际是放弃了。
「是提示词的问题」
先确认你要的信息到底在不在上下文里。 提示词调得再漂亮,也变不出没给它的事实。绝大多数被归为「提示词问题」的故障,翻到最后都是上下文里根本没有那一条。
「这个 case 模型没学过」
多数时候,它是学过,但你的调用方式让它没机会答对。
黑话的自我修养
黑话不是罪。它是团队的速记:说「在我机器上是好的」比说「我怀疑是 lockfile 未提交导致的传递依赖漂移」快十倍。
问题出在信息量的差距。前者让人停下,后者让人继续。
所以给一条能落地的建议:黑话可以说,但后面跟一句提问方向。
- 「这是缓存问题。」——讨论结束了。
- 「这是缓存问题吗?我看下
X-Cache是不是 HIT。」——讨论开始了。
复盘会上最贵的成本从来不是故障本身,而是「偶现」「概率性」这类词把复盘提前按停。无责复盘(blameless postmortem)的核心原则是追机制不追人,但这一切有个前提:这个机制得先被说出来。