"Vibe Code"——氛围编程——最近在开发者圈子里火得不像话。每次新模型发布,社交媒体上就涌出一波宣言:LLM 将消除软件开发的一切摩擦,人类只需"思考",代码自然流出。听起来很美。但我没法认同,因为我本身就不进行氛围编程。
这不是对 AI 的否定。我在日常开发中大量使用 LLM,但用法和"氛围编程"截然不同。区别在哪?让我拆开来说。
氛围编程到底在"氛围"什么
氛围编程的核心假设是:你只需要描述意图,模型就会产出可运行的软件。开发者从"写代码的人"变成"描述氛围的人"——模糊的意图、大方向的感觉,就够了。
这个假设在特定场景下确实成立。如果你要的是一个一次性脚本、一个原型 demo、或者一段你永远不会再读的配置,氛围编程可以快速出活。问题在于,当这些代码进入需要维护、调试、演进的系统时,"氛围"就变成了债务。
原因很简单:你无法维护你不理解的代码。 模型生成的代码看起来流畅、完整,甚至有注释,但它的内部逻辑——为什么选择这个数据结构、为什么在这层做缓存、为什么用这个边界条件——你并不真正掌握。当 bug 出现,或者需求变化时,你面对的不是自己写的代码,而是别人写的代码,而那个"别人"不会留下来帮你调试。
我怎么用 AI:有边界的协作
我的做法是反氛围的:给 AI 明确的、可验证的小任务,而不是开放式的"帮我做个项目"。具体来说,遵循三个约束:
- 任务可验证——AI 输出必须有客观的正确标准(测试通过、类型检查、输出匹配预期)。
- 上下文可控——我提供精确的上下文(相关函数签名、数据结构定义、约束条件),而不是让模型自己猜。
- 结果可审查——每一行生成代码我都逐行读过,确认理解后才合入。
这不是慢,这是不同节奏的快。因为后续的调试和维护成本大幅降低。
下面是一个具体的工作流示例——用 AI 生成一个有明确边界的小模块,并立即验证。
实践:用 AI 生成可验证的单个模块
假设我需要一个函数:从嵌套的 JSON 结构中提取所有指定 key 的值,支持深层嵌套和列表。我会这样组织与 AI 的协作:
# extract_keys.py — 明确的输入输出契约
from typing import Any, List
def extract_values(data: Any, target_key: str) -> List[Any]:
"""
从任意嵌套的 dict/list 结构中提取所有 target_key 对应的值。
Args:
data: 嵌套的 dict 或 list,可能多层嵌套
target_key: 要提取的键名
Returns:
所有匹配值的列表,保持出现顺序
Examples:
>>> extract_values({"a": 1, "b": {"a": 2}}, "a")
[1, 2]
>>> extract_values([{"a": 1}, {"c": {"a": 3}}], "a")
[1, 3]
"""
results: List[Any] = []
if isinstance(data, dict):
for key, value in data.items():
if key == target_key:
results.append(value)
# 递归处理子结构
results.extend(extract_values(value, target_key))
elif isinstance(data, list):
for item in data:
results.extend(extract_values(item, target_key))
return results
关键步骤不是让 AI 直接写完整个文件,而是:
- 我先写好函数签名、类型标注和 docstring 中的 Examples——这是契约。
- 把契约发给 AI,让它填充实现。
- 用 doctest 立即验证:
# 运行 doctest,秒级反馈
python -m doctest extract_keys.py -v
输出应该是:
Trying:
extract_values({"a": 1, "b": {"a": 2}}, "a")
Expecting:
[1, 2]
ok
Trying:
extract_values([{"a": 1}, {"c": {"a": 3}}], "a")
Expecting:
[1, 3]
ok
1 items passed all tests:
2 tests in extract_keys.extract_values
2 passed in 0.00s.
如果测试失败,我立刻知道 AI 的实现有问题,不需要人工阅读每一行去找 bug。这就是"可验证"的力量——用测试代替信任。
更进一步,我还会加一个边界测试,确保 AI 的实现没有处理意外的输入类型:
# test_extract_keys.py — 补充边界测试
from extract_keys import extract_values
def test_non_dict_list_input():
"""非 dict/list 的叶子值应被安全跳过"""
assert extract_values(42, "a") == []
assert extract_values("hello", "a") == []
assert extract_values(None, "a") == []
def test_empty_structures():
"""空结构应返回空列表"""
assert extract_values({}, "a") == []
assert extract_values([], "a") == []
def test_key_not_present():
"""目标 key 不存在时返回空列表"""
assert extract_values({"b": 1, "c": 2}, "a") == []
# 用 pytest 运行完整测试
pytest test_extract_keys.py -v
这个流程里,AI 写的是实现,我写的是契约和验证。我对这段代码的理解,不是靠读 AI 的实现获得的,而是靠我自己定义的契约保证的。 即使 AI 的实现有细节我不完全熟悉,契约告诉我它应该做什么,测试告诉我它确实做到了。
氛围编程的真实代价
有人会说:这样用 AI 太保守了,为什么不直接让模型生成整个项目?
因为代价不在生成阶段,而在维护阶段。几个具体场景:
调试黑箱。 模型生成的 200 行函数出了 bug,你没有逐步构建的心理模型,只能从头读。而如果是你自己设计的结构、让 AI 填充细节,你至少知道大方向,定位问题快得多。
需求漂移。 产品需求变了,你需要改一个模型生成的模块。你不理解原来的设计意图,改动要么过于保守(不敢动),要么过于激进(改了依赖方没预料到的行为)。
安全盲区。 模型可能引入你没注意到的依赖、不安全的默认配置、或者看似合理但存在边界漏洞的逻辑。如果你只是"氛围"地扫了一眼就合入,这些风险就潜伏在系统里。
一个更安全的 AI 编程工作流 checklist
不是拒绝 AI,而是拒绝无结构的依赖。以下是我实际遵循的 checklist,每次用 AI 生成代码前过一遍:
| 步骤 | 做法 | 目的 |
|---|---|---|
| 定义契约 | 先写函数签名、类型、docstring、测试用例 | 锁定意图,不让模型自由发挥 |
| 精确上下文 | 提供相关的数据结构定义、已有函数签名、约束条件 | 减少模型的猜测空间 |
| 生成实现 | 让 AI 在契约约束下填充代码 | AI 做它擅长的事:模式匹配和细节填充 |
| 立即验证 | 运行测试、类型检查、lint | 用客观标准代替主观信任 |
| 逐行审查 | 读每一行生成代码,确认理解 | 建立心理模型,为后续维护做准备 |
| Git 分支提交 | 在独立分支上提交,不直接合入主线 | 保留回滚能力,隔离风险 |
用 git 分支隔离是一个简单但关键的习惯:
# 为每个 AI 生成任务创建独立分支
git checkout -b ai/extract-keys-module
# 生成、验证、审查后提交
git add extract_keys.py test_extract_keys.py
git commit -m "feat: add extract_values with AI-assisted implementation, verified by doctest+pytest"
# 审查无误后再合入
git checkout main
git merge ai/extract-keys-module
这样即使 AI 生成的代码后来发现问题,你也有清晰的边界可以回退。
写代码这件事,核心不是打字速度
氛围编程的叙事隐含了一个前提:写代码的瓶颈是打字速度和语法记忆,LLM 消除了这个瓶颈,所以生产力飞跃了。
但真正消耗开发者时间的,从来不是打字。是设计决策、是调试、是理解他人代码、是处理边界情况、是权衡性能和可维护性。这些事情,LLM 能辅助,但不能替代你的判断。
AI 是极好的细节填充器和模式匹配器。让它做这些,你做设计和验证。这不是保守,这是让每个参与者做它最擅长的事——包括你自己。
氛围编程承诺消除摩擦,但软件开发的摩擦从来不是多余的。摩擦是你和问题域之间的对话,是理解深度的来源。去掉摩擦,你也去掉理解。而没有理解的代码,不管生成得多快,最终都会慢下来。