Vibe Code 之后 Vibe Clean:把“能跑”恢复为“可理解”

2 minute read

Published:

From Vibe Coder to Vibe Cleaner

Vibe Code 之后 Vibe Clean:把“能跑”恢复为“可理解”

Date: 2026-01-20

0. 背景:AI 编程把“写功能”变得太容易,也把“写屎山”变得太容易

Cursor、Claude Code 这类 AI 编程工具,让落地一个功能变得很快:你描述需求,它就能补齐大量实现细节,代码很快“能跑”。

但现实也很明显:它同样把“把系统写乱”这件事变得更容易。尤其在 Python 这类动态语言里,这种趋势会更明显——因为 Python 的表达方式太自由了:

  • 用 dict 传参几乎没有阻力
  • 字段可以随时出现、随时缺失
  • 同一个概念可以有很多别名
  • 默认值和兜底随手就能加

而 AI 写代码时,最擅长的策略之一就是:用更多兼容、更强兜底来让它“现在能跑”。这在短期非常高效,但在长期会让系统越来越难维护。

对比一下 Rust 这类强类型语言,情况往往相反:你很难“先写一坨 dict”糊过去,因为编译器会逼着你回答一些问题:

  • 这份数据到底长什么样(结构是什么)?
  • 字段是必选还是可选?
  • 数据会不会在流程中被修改?谁能修改?
  • 错误是怎么表达的?

所以我越来越觉得,除了 vibe coder(快速把功能写出来),还需要 vibe cleaner:在 Python 这种“太容易糊过去”的环境里,把数据形状、不变量、失败语义重新写回代码。

1. 一个通用例子:LLM 工作流为什么会越写越厚?

设想你在写一个小型的 LLM 工作流,把用户问题变成答案。它拆成多个 step:

  • Parse:解析输入(问题、用户信息、偏好)
  • Retrieve(可选):检索资料片段
  • Generate:生成草稿答案
  • Verify:校验/自检/格式检查
  • Finalize:输出最终结构(正文、引用、置信度、错误信息)

为了方便(也为了让 AI 更好参与编码),接口通常会写成:

async def step(request: dict) -> dict:
    ...

这在开始阶段很爽:灵活、迭代快、字段想加就加。但一旦功能叠加,你很快会遇到维护问题:数据在每一步到底长什么样?哪些字段一定存在?失败时返回什么结构?

如果这些问题没有明确答案,系统会开始变得“行为大致对,但难以解释;修改很危险,回归成本很高”。

2. 观察:Python + AI 很容易产出“到处解析 dict”的形态

在 Python 里,用 AI 加功能时,经常会出现下面这种模式:

input_data = request.get("input", {})
ctx = request.get("ctx", {})
params = request.get("params", {})

question = (
    input_data.get("q")
    or input_data.get("question")
    or ctx.get("last_question")
    or ""
)

lang = params.get("lang") or ctx.get("lang") or "zh"

这种写法本质上是在做两件事:

  • 让同一概念有多条来源路径(q / question / last_question …)
  • 把优先级和默认策略埋进局部实现里(每个 step 都可能不同)

在动态语言里,这很容易“先跑起来”;在 AI 辅助下,它更容易被不断复制和扩张。结果就是:每次加功能,都会多一些“兼容分支”和“兜底”,系统越来越厚。

3. 归因:问题不只是 vibe coding,而是“语言允许你把不变量变成隐式”

这里要抓住关键:复杂度真正来自隐式不变量。

不变量指的是:系统要正确运行,有些约束必须稳定成立,比如:

  • question 必须是非空字符串
  • citations 是列表且每项必须带 url
  • 失败必须带 error_code,否则上层无法做策略

在 Rust 里,你通常会被迫把这些写进类型里(Option<T>Result<T, E>、结构体字段是否可选、借用/可变性)。在 Python 里,你完全可以不写——然后用 or ““、get(…, {})、try/except: pass 把它糊过去。

这就是重点:Python 允许你不回答“数据契约是什么”,也能继续写下去。而 AI 工具在“局部正确”的驱动下,天然倾向把这种糊法扩散到全局。

当结构信息不在代码里,而在读者脑内时,可理解性会随着功能增长快速下降。

4. 推论:为什么系统会走向“只加不复用”

这条链非常稳定:

  • step 间用 dict → dict,字段可选、可变
  • 新需求来了,最快办法是再兼容一种输入、再加一个兜底
  • 兜底越多,字段语义越模糊(同义字段/别名字段/半成品字段并存)
  • 语义越模糊,你越不敢复用已有模块(因为你不确定它依赖的 shape)
  • 最后每次迭代都倾向于“再写一套能跑的分支”

结论很直接:系统变乱不是因为写得快,而是因为每次迭代都在增加不确定性。

5. Vibe cleaner:在 Python 里“补上 Rust 会逼你做的那部分”

如果把 Rust 的强类型理解成一种“强制提前决策”,那 vibe cleaner 在 Python 里做的,就是把这部分决策补回来:

  • 数据结构是什么?
  • 哪些字段必选、哪些可选?
  • 哪些位置允许修改数据、哪些不允许?
  • 错误如何表达,调用方如何处理?

下面是三条最通用的落地方式。

5.1 入口收敛:把 dict 解析集中到一个地方

目标:主流程不再到处 .get() 猜字段。你只在一个入口解析处处理别名/默认值/优先级。

from dataclasses import dataclass
from typing import Optional, Any


@dataclass(frozen=True)
class StepRequest:
    question: str
    lang: str = "zh"
    user_id: Optional[str] = None
    raw: dict[str, Any] | None = None


def parse_request(request: dict) -> StepRequest:
    input_data = request.get("input", {})
    ctx = request.get("ctx", {})
    params = request.get("params", {})

    question = (
        input_data.get("q")
        or input_data.get("question")
        or ctx.get("last_question")
        or ""
    ).strip()

    lang = (params.get("lang") or ctx.get("lang") or "zh").strip()

    return StepRequest(
        question=question,
        lang=lang,
        user_id=ctx.get("user_id"),
        raw=request,
    )

这一步的价值很像 Rust:你至少在入口把“数据契约”集中表达了。

5.2 语义收敛:把“同一字段多种形态”变成唯一形态

比如 citations 有多种表示方式,那就做一次性规范化,让主流程只接触一种结构。

from dataclasses import dataclass
from typing import Any


@dataclass(frozen=True)
class Citation:
    url: str
    title: str = ""
    snippet: str = ""


def normalize_citations(x: Any) -> list[Citation]:
    if not x:
        return []
    if isinstance(x, list) and x and isinstance(x[0], str):
        return [Citation(url=u) for u in x]
    if isinstance(x, list) and x and isinstance(x[0], dict):
        return [
            Citation(
                url=i.get("url", ""),
                title=i.get("title", ""),
                snippet=i.get("snippet", ""),
            )
            for i in x
        ]
    return []

这相当于在 Python 里人为制造一个“类型边界”。

5.3 错误语义固定:失败也要有结构

Rust 里的 Result<T, E> 会逼你显式处理失败;Python 里你很容易用空值/异常/字典标记混着来。清理的目标是:让失败语义固定下来。

from dataclasses import dataclass


@dataclass(frozen=True)
class StepResult:
    ok: bool
    data: dict
    error_code: str = ""
    error_message: str = ""


def ok(data: dict) -> StepResult:
    return StepResult(ok=True, data=data)


def fail(code: str, msg: str, data: dict | None = None) -> StepResult:
    return StepResult(
        ok=False,
        data=data or {},
        error_code=code,
        error_message=msg,
    )

这样调用方不需要通过“空不空”来猜,也不需要到处 try/except。

6. 验证:你真的变得更可维护了吗?

vibe cleaner 是否有效,可以用三个指标自检:

  • 入口收敛:request.get(...) 这类解析逻辑是否基本只剩一个入口?
  • 主流程分支减少:主流程里因为“形状不确定”产生的 if/elif 是否明显减少?
  • 失败路径一致:失败是否总能用同一种结构表达?调用方是否不再靠空值推断?

这些指标衡量的是:读者需要脑补多少信息。脑补越少,维护就越便宜。

7. 结语:AI 让编码更快,但不会自动让系统更清晰

强类型语言靠编译器强迫你提前做决策,Python 的自由度 + AI 的“局部正确”倾向,会自然把系统推向“能跑但难懂”。

vibe cleaner 的意义就是:在你享受 vibe coding 带来的速度之后,补上那些“本来应该明确的数据契约”,把系统从不可维护的方向拉回来。

当然,如果要问,如果你一直说 Rust 这种语言好,你怎么写代码不用 Rust 啊?😭唉,Rust 也有 Rust 的烦恼,它写起来太复杂,我对 Rust 驾驭能力弱,而且最近这些 Agent 工具主要还是 Python 库多。