Shopify CEO 提出的新范式——AI 编程失败多因上下文缺失而非模型能力不足,提倡用规则文件、项目记忆、组件指针替代盲目 prompt。
上下文工程:让 AI 避免写出垃圾内容的那门学问
"提供一切能让任务对 LLM 而言尚可解决的上下文。"
Tobi Lütke,Shopify CEO,于 X,2025 年 6 月 19 日
这是 Tobi Lütke 给出"上下文工程"这个术语的定义,当时他觉得自己更喜欢这个叫法,而不是"提示词工程"。我认为这个定义是对的,而且我认为大多数抱怨 AI 垃圾内容的人离意识到原因只差一次推理。
一个 vibecoder 不带任何项目上下文就向 agent 发送提示词,他用的模型并不会比我用的差。他用的模型只是在没有任何依据的情况下尽力给出最好的结果:没有规则文件、没有对昨天决策的记忆、没有指向已经解决这个问题的组件的指针。所以它只能发明看起来合理的东西。命名不一致、一个三个文件之外已经存在的辅助函数被重新发明、某个两个重构周期之前就已经错误的架构假设。这些不是模型的问题。这是模型在信息不足时的表现,而且自信满满。
Anthropic、LangChain 以及 Manus 背后的团队在生产级 agents 上都得出了一致的发现:大多数 agent 失败都是上下文失败,而不是模型失败。输入的上下文糟糕,输出的软件就糟糕。
关于上下文如何失败,最清晰的分解来自 Drew Breunig 的《How Long Contexts Fail》:中毒、分心、困惑和冲突。以下是我对这四种方式的重新标签化,用代码库中实际呈现的样子来翻译,而不是用 Breunig 原始术语复述:
这些都不是模型问题。它们都是人类决定(或未能决定)agent 看到什么以及何时看到的种种情况。这意味着它们可以用解决任何其他工程问题的方式来修复:有意识地,而不是靠运气。
我更愿意展示而不是画图解,所以这里是我维护的 Laravel 包 Truss 的真实 CLAUDE.md。它 52 行,说明了技术栈、命令,以及一份必须永远不被破坏的不变量简短列表,比如"不暴露任何数据,永远只暴露结构",然后就结束了:
## Pointers
- Architecture and domain model: `docs/DESIGN.md`
- Phased build plan: `docs/INSTRUCTIONS.md`
- Decision log: `docs/DECISIONS.md`
- Path-scoped rules (auto-load when matching files are touched): `.claude/rules/`
This file should stay short enough to read in under a minute. If you're
about to add detail, it probably belongs in `docs/` instead, with a
pointer added here.
所有不需要在每个回合都用到的东西都放在一跳之隔的地方,而不是默认驻留在上下文中。这不是一种文档选择,而是一种上下文预算:文件声明了自己的约束并坚守它。那些 .claude/rules/ 文件是真实存在的,不是占位符:introspection.md 限定在 src/Introspection/**,frontend.md 限定在 resources/js、resources/css 和 resources/views,release.md 限定在 CHANGELOG.md,每一个都只在 Claude 实际触达匹配路径时才会加载,而不是像 CLAUDE.md 本身那样默认常驻。我已经写了一份更完整的关于这种分割如何运作的机制——RAM 与按需加载规则、skills 与磁盘——在《CLAUDE.md Is RAM, Skills Are Not Disk》中,如果你想了解这种形态背后的原因而不仅仅是结果的话。
这里的重点不是"复制这个文件"。而是这个文件足够小到可以审查。对比一下普通情况:一份 300 行的 CLAUDE.md,几个月没人重新阅读过,那不是上下文工程,那是上下文考古,而且做挖掘工作的是 agent,每次会话都很糟糕。
检查清单一览而过然后被遗忘。真正让团队坚持下来的是足够机械化到可以强制执行的东西,所以以下是三项检查,按投入精力递增排列,都不需要依赖任何人的自律:
行数预算。Truss 的 CLAUDE.md 用文字声明了自己的限制,"短到一分钟内可以读完",但没有强制执行,而阅读时间不是 CI 能直接检查的。行数是最接近的廉价替代指标,所以作为 CI 步骤看起来是这样的(120 是我建议的阈值,不是 Truss 现行强制执行的数字):
- name: Keep CLAUDE.md readable in under a minute
run: |
LINES=$(wc -l < CLAUDE.md)
if [ "$LINES" -gt 120 ]; then
echo "CLAUDE.md is $LINES lines, over the 120-line budget. Move detail to docs/."
exit 1
fi
简单、机械,而且执行的是文件自己声称的规则。
漂移检查。这是我最信任的一个,因为它就是 Truss 已经应用到数据库架构上的相同思路。一份已提交的导出文件,与实时架构对照,如果它们不一致则构建失败:
php artisan truss:export --output=schema.dbml --check
它将新生成的导出与该路径下已提交的内容进行比较,如果字节不完全匹配则退出非零值,与 git diff --exit-code 对生成文件的处理机制相同,只是对架构敏感。上下文文件同样会腐烂,只是通常被提交的是空无一物:没有一个文件被用来对照它所描述的东西进行检查。CLAUDE.md 指向的每个路径还存在吗?docs/DESIGN.md 最后一次修改是在它所描述的架构变更之前吗?一个 pre-commit hook 或 CI 步骤,解析每个被引用的路径并标记任何缺失或过时的东西,能够捕捉到真正会咬人的失败模式:agent 自信满满地基于悄然失效的文档工作。
黄金任务回归检查。一小套固定的代表性提示词,"添加一个新的 artisan 命令"、"解释授权门",定期针对当前上下文 bundle 产生的结果运行,并将答案的预期形态写在某处。这一项不能完全自动化。但即使是在发布前进行一次手动检查,也能捕捉到其他两项检查无法覆盖的情况:一条规则文件的变更本身是合理的,但悄悄地破坏了两个文件之外的东西,因为没有人在发布变更之前让 agent 实际执行那个任务。
这些都不稀奇。它们就是 linting、drift 检测和回归测试,与如果 artifact 是代码而不是散文时你会采用的三项完全相同。这就是真正的要点:它就是代码,在所有真正重要的方面除了语法高亮之外。
Vibecoder 把提示词发射到上下文真空中,然后出货任何返回的东西,因为"提示词"是他们唯一知道的杠杆。工程师把上下文、规则文件、记忆、它指向的文档当作一等公民:版本控制、审查、预算、检查漂移,与任何其他出货的东西相同。相同的模型、相同的工具,输出却截然不同,而差异从来不在提示词。
如果你在看一个 vibe-coded 的应用,感觉不一致、半成品、对你自己代码库的工作方式自信地错误,不要问是什么模型构建了它。问它被允许看到什么。