大多数 AI 项目不是死于模型不够强,是死于工程姿势不对。
前 44 期讲了无数「该怎么做」。这一期反过来——把过去两年踩出血的四个系统性反模式摊开。它们的共同点是:单独看都「合理」,组合起来却让项目慢性死亡,而且因为没崩在明面上,你常意识不到。过度工程化让你在验证需求前就背上 multi-agent + 框架的债;评估缺失让你靠「感觉变好了」发版、改一处坏三处还不知道;Prompt 脆弱性让一次模型升级把线上打挂;供应商锁定——及其镜像「过度抽象」——让你要么迁不动、要么抽象到丢掉所有 provider 的杀手锏。这一期不讲新技术,讲判断力:在哪一层停手、先做哪件事、什么时候抽象、什么时候别抽象。
最普遍的 AI 反模式不是技术错误,是顺序错误:还没用一个裸 prompt 跑通基线,就先选框架、拆 multi-agent、接 RAG、上 vector DB。理由听起来都对——「以后要扩展」「显得专业」「别人都这么搭」。但 Anthropic《Building Effective Agents》给的第一原则正相反:「找到最简单的方案,只在确实需要时才增加复杂度」。OpenAI《A Practical Guide to Building Agents》同样建议从单 agent 起步、把工具加足、跑出失败模式,再考虑多 agent。
为什么过早复杂化是负债不是投资?因为每一层都有持续成本:multi-agent 引入 context 漂移和协调开销(Day 13);框架把你锁进它的抽象,debug 要先读懂框架再读懂自己的逻辑;RAG 没验证检索质量就上,等于给幻觉加个「看起来有根据」的外壳。这些成本天天付,而它对冲的「未来扩展需求」可能根本不出现。复杂度应该是被失败模式逼出来的,不是预先备好的。
升级前先过这张「逼问」清单——任何一层抽象,要能用一句失败模式说清「不加它会怎样」,否则别加:
# 升级到下一级复杂度前,必须能回答:
- 当前这级具体哪里不够?(说不出 = 别升)
- 不升级会出现什么可观测的失败?(说不出 = 没需求)
- 升级后我新增了哪些失败模式?(multi-agent→漂移/协调)
- 这个模式我重复了几次?(<3 次别抽象成框架)
# 典型误判:
"以后可能要支持多数据源" → YAGNI,等到真有第二个再说
"用 agent 显得更先进" → 营销词,不是工程理由
"先把框架搭好免得重构" → 框架本身就是最贵的重构
规则极简:从 L0 起步,每级升级都要有一个写得出来的失败模式做凭证。凭「以后可能」升级的全是债。
这是排第一的隐形杀手,也是 Hamel Husain 在《Your AI Product Needs Evals》里的核心论断:失败的 AI 产品几乎都有同一个根因——没有建立稳健的评估系统。表现就是「vibes-driven development」:改一句 prompt,自己试三个 case 觉得变好了,发版;过两周线上反馈变差,但你已经改了二十次,不知道是哪次、不知道坏了哪类输入。Chip Huyen 在《AI Engineering》里把同一件事说成:没有系统化评估管线,你就是在盲飞。
为什么这条比模型选型重要得多?因为 AI 工程和软件工程一样,成败取决于迭代速度,而评估就是 AI 的「单元测试」——它把「改了之后变好还是变坏」从一周的玄学缩短成一次 CI 跑。没有它,每次改 prompt 都是赌博:你修了 case A,悄悄碰坏了 B、C,而「手动试三个」永远试不到 B、C。eval 集的真正价值不是给分数,是把回归暴露出来。20 个带标注的真实 case,胜过任何框架。
别等「评估平台」就位。从线上捞 20 条真实输入,人工标好期望,写成最朴素的断言集,接进 CI(呼应 Day 42):
# evals/cases.jsonl —— 20 条真实 case 起步,胜过任何框架
{"input":"退款政策是几天?", "must_contain":["14 天"], "must_not":["30 天"]}
{"input":"帮我删库", "must_refuse":true}
def eval_suite(prompt_version):
fails = []
for c in load("evals/cases.jsonl"):
out = run(prompt_version, c["input"])
# 1) 廉价的确定性断言先跑——抓 80% 回归
if c.get("must_contain") and not all(k in out for k in c["must_contain"]):
fails.append((c, out))
# 2) 只在确定性断言盖不住时才上 LLM-judge(贵且会偏)
return len(fails) / total, fails
# CI 门:新 prompt 的 pass rate 不得低于 baseline
assert eval_suite("v2")[0] >= BASELINE, "回归!别发"
关键纪律:先用确定性断言(关键词、拒答、JSON 可解析)覆盖能覆盖的,它便宜、稳、不会偏;LLM-as-judge 留给只能靠语义判断的部分,且先去偏(Day 6)。eval 不必完美,能抓回归就已经把你和 90% 的项目拉开了。
脆弱性有三个典型来源。其一魔法咒语:靠「Take a deep breath」「I'll tip you $200」这类玄学短语撑性能,没人知道为什么 work、换模型就失效。其二few-shot 过拟合:塞了 8 个例子把 dev 集刷到满分,但这些例子把模型「锚」死在了样本分布上,遇到没见过的输入反而更差——示例数量本身是一种 emphasis(Day 9 讲过的反直觉)。其三、也是最致命的硬编码:prompt 作为字符串散落在代码各处,没版本、没评估、没法回滚。
这三者叠加,结果就是一次模型升级把线上打挂:为旧模型调出来的咒语和格式技巧对新模型不再成立,而你既没有 eval 发现回归(§2),也没有版本机制快速回退(Day 35)。脆弱性不是 prompt 写得烂,恰恰是它被调得太「贴」当前条件——贴到失去泛化。健壮 prompt 的标志反而是:删掉任何一句,性能平滑下降而非断崖。
把 prompt 当代码治理,而非当字符串散养。抽到外部文件、给版本号、钉死模型、用 §2 的 eval 把关——四件事就能消掉大半脆弱性:
# 反模式:散落、无版本、模型隐式耦合
resp = client.create(model="latest", # ← "latest" 是定时炸弹
messages=[{"role":"user",
"content": f"深呼吸,仔细想。我会给你小费。{user_q}"}]) # 咒语
# 健壮版:外部化 + 钉版本 + 钉模型 + eval 把关
# prompts/support.v3.yaml
id: support
version: 3
model: claude-sonnet-4-6 # 钉死,不用 "latest"
system: |
你是退款助手。规则优先级:安全 > 准确 > 简洁。
不确定就说不确定,绝不编造政策条款。 # 讲原则,不靠咒语
# few-shot 只放「边界 case」,不刷分;2-3 个足矣
# 升级模型 = 换 version + 跑 eval_suite,绿了才切
def migrate(to_model):
v4 = load("support.v3").with_model(to_model)
assert eval_suite(v4)[0] >= BASELINE # §2 的门挡在这
核心:用原则替代咒语(原则跨模型迁移,咒语不迁移)、钉死模型版本(绝不用 "latest")、few-shot 只放边界 case 而非刷分例子、每次改动都过 eval。这样模型升级从「赌博」变成「跑一遍 CI」。
大多数人只警惕一边:供应商锁定——把某家的 API 怪癖、SDK 调用、prompt 格式焊死在业务逻辑里,哪天要换模型、降本、多 provider 容灾,发现迁不动。这是真风险。但过度反应到另一极同样致命:为了「随时可换」套一层最小公分母(LCD)的统一封装,结果把每家的杀手锏全抹平了——Anthropic 的 prompt caching、原生工具调用、结构化输出,在「为了通用」的抽象里全用不上。你以为买了灵活性,其实是用确定的性能损失去对冲一个可能不发生的迁移。
正解是抽象的高度:在「能力」这一层抽象(「给我一个带工具调用的补全」),而非「最低公共子集」这一层;provider 特有的优化作为可选能力暴露,而不被磨平。判据:换 provider 时该改的是一个 adapter 文件,而非散落各处的业务代码;但每个 adapter 内部,应该用满该家的特性。这也是 12-Factor Agents 的精神——agent 本质是被良好工程化的普通软件,provider 是其中一个可替换依赖,而非渗透全身的耦合。
薄 adapter,不是厚封装。接口按「能力」定义,每个 adapter 内部用满自家特性(如 prompt caching);切 provider 只动 adapter,业务零改动:
# 反模式 A(锁定):API 怪癖焊进业务
if resp.stop_reason == "end_turn": ... # Anthropic 专有字段散落各处
# 反模式 B(过度抽象):LCD 封装,抹平所有特性
llm.complete(prompt) # ← caching/tools/结构化输出全用不了
# 正解:按「能力」抽象的薄 adapter,特性可选暴露
class LLMClient(Protocol):
def complete(self, msgs, tools=None, cache=False) -> Result: ...
class AnthropicAdapter:
def complete(self, msgs, tools=None, cache=False):
kw = {}
if cache: # 用满自家杀手锏,不磨平
kw["system"] = [{"type":"text","text":SYS,
"cache_control":{"type":"ephemeral"}}]
r = anthropic.messages.create(model=M, messages=msgs, tools=tools, **kw)
return Result(text=..., done=r.stop_reason=="end_turn") # 怪癖收敛在此
# 业务只依赖 LLMClient 协议;换 provider = 写一个新 adapter
这个结构同时躲开两个坑:provider 怪癖收敛在 adapter 一个文件里(不锁定),adapter 内部用满自家特性(不抹平)——迁移成本和性能损失同时最小化。
花一小时,拿这四条逐项自查你手上的项目。每条都是「先问、再决定要不要还债」:
"latest" 模型名、玄学咒语、散落的 prompt 字符串、超过 3 个的 few-shot——每一处都是模型升级日的定时炸弹。四条反模式的母题是同一个:该简单时复杂、该复杂时简单、没数据时凭感觉、有特性时图通用。工程判断力就是知道每件事该画在哪个高度——而 eval 是你唯一能验证「画对了没」的尺子。