TL;DR:一个LLM就像堂吉诃德——你无法修复它,它天生就是随机的。解决方案不是修复疯子,而是给他配一个确定性的桑乔潘萨。MDD有两个层次:首先研究其可能犯的错误类型,从而设计出能够吸收这些错误的工具;然后把它放入工具中验证是否还有漏洞。为疯狂设计,而不是对抗疯狂。


我已经在审查日志好几周了。165次AI代理与CLI交互的会话,总共产生了500多个错误和370次重试。一些模式一次次重复出现:代理使用了--status,而正确的参数名应该是--state。它写了Todo,而API期望的是unstarted。它传递urgent作为优先级,而系统只接受数字。

而有趣的是,每个错误都“看起来有道理”。这些并不是随机的错误。它们是合理的错误。就像是你对一个领域有些许了解,但从来没有细读文档时所可能犯下的错误。

某个时刻,当我盯着第十次出现的--status Done(实际上应该是--state completed)时,我意识到这些错误在文学上来说有一个对应的模式。一个有着400年历史的模式。

堂吉诃德就是一个LLM

想象一下。堂吉诃德看到风车,说“是巨人”。他不是笨——他很有文化、书读得多,对骑士小说了如指掌。问题在于,他的世界模型被带有虚假数据的训练集污染了。他读了太多的骑士小说,所以当他看到某些模糊的东西时,就根据他的训练数据来进行解读。风车→巨人。羊群→军队。客栈→城堡。

一个LLM(大语言模型)做的事情和堂吉诃德一模一样。它在训练过程中见了成千上万的API。当你要求它使用一个它不太熟悉的API时,它不会说“我不知道”。它会猜测。而且常常猜得很接近足以让你信任,但偶尔猜错时,它的错误也是合理的。

--status而不是--state。因为在它见过的60%的CLI中,参数名确实是--status。

Todo而不是unstarted。因为在工具的图形界面中,有一列名称就是“Todo”。LLM从文档中看了截图、读了博客,因此它推断如果UI写了“Todo”,那么API一定接受这个值。这听起来合情合理,但却是错的。

urgent而不是1。因为在多数优先级系统中,urgent确实是一个常见的有效值。谁会设计一个优先级必须用1到4的数字,而不能用标签表示的API呢?

每一次“幻觉”都是基于不完整数据的合理推断。堂吉诃德并不笨。他只是疯了。而一个疯子是无法被治愈的。

塞万提斯早就明白了

塞万提斯没有试图“治愈”堂吉诃德。他做的,是让桑乔潘萨陪着他。

桑乔并不聪明。他没读过书。他没有伟大的幻想。但他是确定性的。当堂吉诃德说道“瞧那些巨人”,桑乔会回答:“老爷,那是风车。”堂吉诃德不一定会听他的,但信息已经传递了。这个系统有两个层次:一个随机的层次生成假设(堂吉诃德),另一个确定的层次与现实进行对比(桑乔)。

当你和一个LLM一起工作时,这就是你需要的架构。你无法让它停止做梦,因为这就是它的天性。但你可以嵌入确定性的层次来捕捉这些幻觉,以便防止灾难发生。

这就是MDD方法大显身手的地方。

MDD: 疯狂驱动设计

MDD有两层架构,而顺序很重要。

第一层:先验考古学

在写第一行代码之前,需要研究“疯癫”。不要凭空猜测——要观察。你需要收集LLM与现有工具交互过程中的数据,并为这些错误分类整理。

以我的项目为例,我分析了165次会话,过程中AI代理使用了一个CLI来管理开发团队的任务。以下是数据统计:

错误类别出现次数触发的重试次数
虚构或无效的参数275~150
JSON/GraphQL转义失败2580+
名称混淆40+50+
CLI无法完成的操作60+90+
浪费token的冗余输出N/AN/A

基于这些数据,你设计一个能够“吸收”错误而不是拒绝它们的新工具。用通俗易懂的话来说:理性的人适应疯狂的人,而不是反过来。

下面是一些具体的吸收设计示例:

LLM错误                  → 工具设计
─────────────────────────────────────────
--status Done           → --status是--state的别名,
                           “Done”将被标准化为“completed”

--priority urgent       → “urgent”被标准化为1,
                           “high”→2,“medium”→3,“low”→4

--no-pager              → 安静地忽略此参数
                           (工具从不使用pager)

描述中引号转义失败      → 通过文件或stdin传递输入
                           永远不会内联,交给Serde处理

表中的每项设计决策都来源于真实观察到的错误,而不是关于“可能会出错什么”的猜测。

这种设计方法与传统方法有微妙但重要的区别。传统设计定义出一个正确的接口,然后拒绝一切不符合它的输入。而MDD则是定义出一个正确的接口,并_额外_定义所有用户可能尝试的错误用法,然后吸收这些错误。

就像设计一扇门,既能推也能拉着开。正确的门只会往一个方向开。好用的门却能够双向操作,因为你观察到40%的人在看到门时会向相反方向推/拉。

第二层:后验验证

在完成了第一层后,你需要将其投入使用,交给LLM试用,并观察它在有新工具时会产生哪些新错误。

假如第一层已经非常完备,那么新的错误应该极少。倘若出现了新的问题,这就说明你的设计还有漏洞,每个新问题就类似一个入侵测试。

在我用CLI做测试时,LLM产生了以下我没见过的新“发明”:

  • 一个不存在的排序枚举值。 API支持用createdAt和updatedAt排序,但LLM发明了一个priority值。听起来完全合理——为什么不能按优先级排序?然而在GraphQL schema中,这样的值并不存在。

  • 不存在的过滤操作符。 API支持通过state.type.in进行状态过滤,但LLM生成了state.id.or。看起来很符合逻辑模式,但完全是杜撰。

  • 来自另一种语言的文件锁定函数。 在一个Rust项目中,LLM建议使用fcntl.flock来锁文件。但这是个Python函数,而在Rust里应该用fs2库。

这些错误并不荒唐。它们全都是合理的推测。然而它们也揭示了设计缺陷:工具没有验证排序值,没能拒绝过滤器操作中的虚构字段,而文件锁定模块的文档也没有被正确提供给代理。

第二层完成了循环。不要假设你的设计是完美的——通过将工具交给最善于创造性想象(同时也最可能出错)的代理来验证它。

桑乔潘萨技术栈

堂吉诃德与桑乔潘萨的隐喻不仅仅是一个巧妙的比喻,更是一个架构模型。实际上,“桑乔潘萨”不只是一部分,而是一个确定性的技术栈,每一层都抓住一类疯狂:

┌──────────────────────────────────────┐
│         LLM(堂吉诃德)               │  生成看似合理的指令
│         天马行空,充满创意            │  但可能错误百出
└──────────────┬───────────────────────┘
               │ "--status Done --priority urgent"
┌──────────────▼───────────────────────┐
│  1. CLI解析器(clap)                 │  拒绝不存在的参数,
│     接受别名,比如--status→--state    │  即使是假设参数(alias)
└──────────────┬───────────────────────┘
               │ "--state Done --priority urgent"
┌──────────────▼───────────────────────┐
│  2. 标准化                            │  将"Done"标准化为"completed",
│     state别名,优先级别名             │  "urgent"→1
└──────────────┬───────────────────────┘
               │ "--state completed --priority 1"
┌──────────────▼───────────────────────┐
│  3. 验证                              │  验证“completed”
│     与已知的枚举值对比                │  是否是有效状态值。
└──────────────┬───────────────────────┘
               │ state=completed, priority=1
┌──────────────▼───────────────────────┐
│  4. 序列化(serde)                   │  通用的序列化逻辑,
│     GraphQL且变量正确转义             │  消除引号转义错误等问题
└──────────────┬───────────────────────┘
               │ {"state":"completed","priority":1}
┌──────────────▼───────────────────────┐
│  5. API和错误处理                     │  如果API拒绝某项内容,
│     备选及回退策略,清晰输出错误       │  提供可操作的错误信息
└──────────────────────────────────────┘

这是五个层次。每个都是确定性的。每个层次都会捕捉LLM可能犯的某一种特定错误。LLM不需要完全正确——它只需要大致正确,而这个技术栈会处理剩下的一切。

这就像一个净化漏斗。顶部输入的是污水(LLM的随机输入),底部输出的是干净的水(有效的GraphQL查询)。每一层都过滤一种杂质。单靠某一层是行不通的,但所有层次结合起来就足够了。

MDD与模糊测试(fuzz testing):核心区别

如果你了解模糊测试(fuzz testing),可能会觉得“MDD和模糊测试差不多”。其实并不。

对比点模糊测试MDD
输入随机、畸形数据看似合理但实际上错误的数据
目标查找崩溃或断错误找出语义上的逻辑错误
输入是否合法否是——正因为如此问题才棘手
示例\x00\xff\xfe作为名称--priority urgent作为参数

模糊测试生成垃圾输入,检测系统是否崩溃。MDD生成看似正确但与事实不符的输入。--priority urgent并不是垃圾,它是一个有领域知识却不了解API的用户可能会写的东西。而模糊测试永远不会生成这样的数据,因为它太具体、太合理了。

同样的原则也适用于变异测试(mutation testing)和混沌工程(chaos engineering)。这些方法要么变异代码,要么破坏基础设施以测试系统的健壮性。MDD并不破坏任何东西——它生成了根据另一套世界模型变得“有道理”的输入。

值得立即采取的行动

你不需要用Rust写一个CLI程序来应用MDD。这个模式适用于任何需要被LLM使用的工具:

第一步:观察疯狂。 在设计(或重新设计)工具之前,让LLM尝试当前版本的工具,并记录所有错误。不只是5次会话——至少50次。足够的样本量才能暴露出模式。

第二步:分类错误。 是命名问题?格式问题?语义问题?每一类错误都需要不同的防御措施。

第三步:为吸收错误而设计。 不要用晦涩的错误信息拒绝--status。接受--status作为--state的别名。不要拒绝urgent作为优先级。将其标准化为1。大多数用户会是那些对领域了解80%但又对API不完全熟悉的人群,为这些用户设计你的工具。

第四步:释放与验证。 把新工具交给LLM试用,不提供特殊指示。每一个新错误都是Capa1的空白点,修补它们然后重试。

如果你的工具既要供人类使用,又要给LLM使用,那么MDD带来的防御机制也会提升两者的用户体验。因为人类犯的错误和LLM是一样的,只不过次数更少,且犯错后会羞愧。

设计桑乔的人是架构师

最后我想澄清一个常见的误解。不是LLM设计了“桑乔潘萨”技术栈。LLM是堂吉诃德,而你才是塞万提斯。

是你观察到疯狂的模式。是你决定哪些应该被标准化、哪些需要拒绝。是你去构建这些确定性的防御层。LLM只能帮你写代码——这它很擅长——但设计决策永远掌握在你手里。

这就是“我指望我的AI纠正自己的错误”(行不通——它还是会犯同样的错误)与“我观察了AI的错误,并构建了一个能吸收这些错误的系统”(行得通——因为系统是确定的)之间的差别。

别指望LLM自我纠错。其随机性注定了它会以丰富多样的方式重复犯错。你需要的不是更好的LLM,而是一个更好的桑乔。

真正重要的是什么

MDD不是一种测试方法,这是一个工具设计方法。问题不是“如何发现LLM犯错”,而是“如何设计,让犯错不会有后果”。

这就像在山路边装护栏。你不能阻止人们转错方向——但可以装个护栏,确保错误不会带来生命危险。你不修正驾驶员,而是让道路更安全。

塞万提斯在四百年前已经明白了。他没试图治好堂吉诃德的疯病,而是派了一个桑乔来辅助他,让故事得以顺利推进。

你的CLI、你的API、你的SDK——无论是任何可能被LLM接触的工具,都需要自己的“桑乔潘萨”。一个确定性、固执、绝对正确的系统。不要聪明,不需要天马行空——只要正确。

为疯狂设计吧。理智的人应该去适应疯狂。