疯狂驱动设计:堂吉诃德、桑乔潘萨和你的AI副驾驶

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转义失败 25 80+ 名称混淆 40+ 50+ CLI无法完成的操作 60+ 90+ 浪费token的冗余输出 N/A N/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试用,并观察它在有新工具时会产生哪些新错误。 ...

2026年3月26日 · Fernando

为什么我的CLI输出不是XML(以及我如何无意中重塑了TOON)

TL;DR: 当你的主要消费者是LLM时,XML和JSON因在每个元素中重复结构而浪费token。一个紧凑的定位格式能够将使用量减少50%。结果发现这种想法早已有了名字:TOON(面向Token的对象标记法)。同样的选择压力——有限的token和重复的键值——导向了同样的解决方案。 Anthropic几乎所有东西都用XML。他们的_system prompts_被包裹在<instructions>标签中,示例在<example>标签中,工具列在<function>标签中。如果你在用Claude,就会发现自己到处都是标签。 于是当我让Claude为一个专为LLM设计的CLI输出格式时,显而易见的诱惑是:XML。既然模型以XML形式接收信息,CLI不给它返回XML难道不是更顺理成章吗? 但实际上,这并非最佳选择。 问题:强迫症般的重复 想象一下,你的CLI正在列出一个项目跟踪器中的任务。假设你有50个任务。在XML中,每个任务会是这样的: <issue> <id>PROD-587</id> <state>Backlog</state> <labels>backend</labels> <title>从NAS备份中导入会话</title> <age_days>14</age_days> </issue> --- 看起来很美观,也很自描述。每个字段都有名字,一个XML解析器可以准确识别出每个字段的含义。 现在,把它乘以50个任务。`<issue>`、`<id>`、`<state>`、`<labels>`、`<title>`、`<age_days>`这些标签将被**重复50次**。它们没有提供任何新的信息,仅仅在浪费空间。大约每个任务需要70个token,列出50个任务需要约3,500个token。 JSON稍微好一点(去掉了闭合标签),但仍重复键值: ```json { "id": "PROD-587", "state": "Backlog", "labels": ["backend"], "title": "从NAS备份中导入会话", "age_days": 14 } 每行都重复"id":、"state":等键值。每个任务约需50个token,总计2,500个token。 如果我们去掉这些重复会怎样? PROD-587 [Backlog] backend — 从NAS备份中导入会话 (14d) 25个token。没有键值、没有标签、没有大括号或引号。50个任务只需1,250个token。 请注意这一点:相比JSON减少了一半,比XML减少了三分之二。目的却完全相同。 “但LLM需要结构” 这时很多人会怀疑:“那LLM怎么知道每个字段代表什么呢?” 这是一个合理的问题,而我的回答是:就像你知道的一样。 看看这行内容: PROD-587 [Backlog] backend — 从NAS备份中导入会话 (14d) 你需要别人告诉你PROD-587是ID吗?需要解释[Backlog]是状态吗?需要说明长横杠后的内容是标题吗?不需要。你通过位置和视觉格式就可以推断出来。 LLM做的事情完全一样。它们是一种识别文本模式的机器。一个一致的、定位明确的格式——ID放在第一、状态放在括号里、标签列在标题之前——对它们而言是直观的。不需要<state>Backlog</state>这种标签说明“Backlog”是个状态。 关键在于区分两种看似相似但完全不同的操作: 阅读是LLM的工作。它有上下文,理解语义,可以推断结构。就像一个人在浏览报告时不需要每个词都用标签标注——位置和惯例已经足够。 解析是程序的工作。它没有上下文,不理解语义,需要显式的分隔符来提取字段。jq '.state'需要"state"这样的键值因为它不知道什么是状态。 XML和JSON是为“解析”而设计的。它们是机器之间交流的格式,它们不“理解”内容。而LLM是“阅读”的。显式结构对它们来说是冗余的,这种冗余却消耗了额外的token。 格式选择:什么时候用什么 我并不是说XML和JSON不好。但它们并不适合这一情境。简单来说,用表格展示: 格式 每个任务的token数 自描述性 最适合 XML ~70 完全 SOAP API、配置文件、有schema的文档 JSON ~50 完全 REST API、服务间数据交换 JSONL ~50 完全 脚本、jq工具、数据流水线 定位式格式 ~25 否 LLM、人类、紧凑信息面板 规则简单:如果消费者能够理解上下文(人类、LLM),就可以省去显式结构。如果消费者无法理解上下文(脚本、解析器),才需要明确的键值。 ...

2026年3月26日 · Fernando

召唤智者:如何使用LLM与任何专家进行导师对话

我妻子召唤Charlie Munger来规划家庭预算。在ChatGPT里。不是开玩笑。 她会说"像Charlie Munger审查我们家庭财务一样行动",然后把本月的开支告诉它。这家伙会回复类似"你在教育支出项目上把投资和花费搞混了"或"那个基金有你没有计算的隐性成本"这样的话。这些是Munger会说的话。用Munger会用的语调。 我也做了同样的事。但我没有召唤投资者,而是召唤了一个不同的专家:Edward Tufte。 谁是Edward Tufte(以及为什么你应该关心) Edward Tufte可能是对我们如何可视化数据影响最大的人。他的书《定量信息的视觉显示》(1983年)是那种永远改变你看图表方式的罕见文本之一。40多年来,它一直是全世界大学、新闻编辑部和设计工作室的绝对参考。 他的原则非常简单: 最大化数据-墨水比。 你图表的每个像素都必须传达数据。如果不传达数据,就删掉它。 不要装饰。 网格、3D渐变、阴影、装饰性边框……所有这些都是Tufte称之为图表垃圾的东西。分散数据注意力的视觉垃圾。 让数据自己说话。 如果你需要用200字的图例来解释你的图表,那图表就设计错了。 Tufte还发明了迷你图——那些能放在一行文字内的微型图表。图表不需要标题、坐标轴或图例来交流的想法。只需要数据。 我的图表很糟糕 我正在构建一个macOS菜单栏应用,用来监控我的Claude Max配额。应用的一部分显示一个迷你图——一个小小的线图——显示我消费配额的速度。 我的第一个版本有这些问题: Y轴固定在0到100% — 如果你的使用量在8%,图表就是一条贴在底部的平线。85%的空间什么都不显示。 在80%处有一条阈值线 — 任意的,没人要求,不传达任何有用信息。 显示当前%的浮动标签 — 冗余的:下面的卡片已经显示了确切数字。 “pp/min"作为单位 — 每分钟百分点?连我都不知道这意味着什么。 线下方的渐变填充 — 纯装饰。图表垃圾。 简单说:我在一个56像素高的图表中违反了Tufte的每一个原则。 技术:循环召唤专家 我没有试图自己修复它(显然我对此没有判断力),而是做了不同的事。我让Claude成为Tufte。 提示词: 像Edward Tufte审查这个图表一样行动。 分析VelocityCardView的SwiftUI代码。 识别你数据可视化原则的所有违规之处。 对于每个违规: 1. 违反了什么原则 2. 为什么这是个问题 3. 具体的处方(代码,不是哲学) 要无情。如果某些东西是图表垃圾,就说出来。 用PASS或FAIL评估。 关键是最后部分:PASS或FAIL。因为没有这个,LLM会给你温和的建议,告诉你"总体上还可以”。有了二元判决,它必须承诺。不能躲在"有改进空间"后面。 然后是循环: 应用Tufte的处方。 实施每个改变。 不要停止迭代,直到他给出认可。 四轮直到PASS 我第一轮没通过。第二轮也没有。第三轮也没有。 第1轮 — FAIL(6个处方): 自适应Y轴而不是固定的0-100 消除80%的阈值线(图表垃圾) 消除浮动标签(与下面的卡片冗余) 用简单词语替换"pp/min"(上升、下降、稳定) 将摘要行折叠为仅时间窗口+趋势 将迷你图高度从44提高到56点 我实施了6个。第二轮。 第2轮 — FAIL(1个处方): ...

2026年2月18日 · Fernando