智能代理体验:代理错误日志是你命令行界面的蓝图

我有一个代码代理——Claude Code,每月与我的问题跟踪工具Linear交互大约800次:列出任务、创建问题、更改状态、发表评论。我检查了其中165次会话,并统计出了超过500个错误和超过370次重试。 这些错误没有一个是Linear的API故障引起的,全部都是接口错误:代理与命令行通信,但命令行无法理解。 保守估计,每月大约浪费了70万个token仅仅是与工具“斗争”:重试、读取错误信息、纠正再尝试。这种无形的成本不会出现在任何账单中,但却存在于每次会话中。 背景:代理现在是主要用户 命令行工具(CLI)——从历史上看,是为人类设计的。而人类用户是非常灵活的。如果一个命令失败了,他们会通过 --help 查看帮助。如果错误信息很难理解,他们会去查阅文档。如果该工具有某些特性,他们会记住并避免再犯同样的错误。 而AI代理基本不会这么做。它在不同会话之间不会积累经验,也不会投入太多精力去阅读文档。当某些操作失败时,它也不会深入分析问题所在,而是根据认为最有可能的方式尝试再次执行。 这改变了你的工具客户是谁的定义。如果代理每月调用该工具800次,而你手动使用它只有三次,那么这个接口的主要使用者就是代理。为了人类用户设计工具并期待代理能适应,这是在为次要用户优化。 为这个主要用户专门设计有一个名字:智能代理体验(agentic experience)。它之于代理,正如用户体验(UX)之于人类用户,开发者体验(DX)之于API的开发者。更重要的是,一种最有效的测量工具早已存在——代理的错误日志。 错误有其规律 我将所有以非零退出代码结束的CLI调用定义为错误。根据这一标准,这500多个错误并非随机发生:几乎全部集中在三个模式中。 错误模式 代理行为 设计暴露的问题 虚构的标志 输入 --status 而非 --state;--priority urgent 而非 --priority 1 真正的标志不是人们会优先猜到的名称 缺少的操作过程 尝试搜索文本、按项目筛选或在创建时分配项目:CLI不具备这些功能 工具未涉及实际的工作流程 忘记必要的标志 忽略 --sort,--no-pager,--no-interactive 要求人工决策,而工具本可以自动处理 当代理输入 --status 时,它并非胡乱猜测,而是在推测出最可能的接口名称。--status 和 --state 都非常合理。而我设计的CLI并未与这种直觉一致。 还有一个问题没有反映在统计数据中,因为它不会导致命令失败:冗长的输出。CLI以JSON格式返回列表,每个问题大约50个token。如果命令成功返回(退出代码为零),这类问题就不会被统计为错误;但长列表和每月800次调用叠加起来,这就是那70万个token成本的另一半。这种成本往往被忽视,因为不会妨碍任务完成。 重新审视错误 容易的解释是直接了当的:代理在错误使用工具。这是错误的解释,应逐一反驳。 虚构的标志表明真正的标志名称不够直观。忘记的必要标志表明这些标志完全不应该是必要项:如果工具可以推测出合理的默认值,那么强制要求填写这些标志只会额外增加使用者的工作量。一段没有用的输出或者产生过多token意味着选错了应对的输出格式。 代理的错误日志不是错误列表,而是一个规范: 每个错误都“否定性”地描述了你原本该如何设计接口。而这个规范是你最真实的反馈:无需代价,无数的真实数据,且没有人为善意隐藏工具缺陷。一个人类用户遇到糟糕的CLI时,可能会保持沉默并自动适应它。但代理不会适应:它会一遍又一遍地犯同样的错误,并记录下每一次。 这与我在另一篇文章中提出的一个原则有关:不应允许错误路径存在,而非仅仅禁止其使用。禁止只是一种文档说明——“不要使用--status“——而文档取决于是否有人认真阅读。让错误变得不可能,是设计的职责。 因此,解决方案并非更好的文档,而是更好的工具。 重设计 我围绕这个原则重写了CLI,工具叫做lql。以下四个设计决策贯穿整个改进过程。 宽容优于拒绝。 如果代理输入了 --status,工具会将其接受为 --state 的别名并继续运行。如果输入了 --priority urgent,将其解析为 --priority 1 并说明假定的结果。最常见的“错误”路径直接被转变为正确路径。工具不会因为合理的猜测而惩罚用户,而是直接适应它。 通过错误信息指导正确方向。 当某个功能确实不存在时,错误信息不会简单地说 unknown flag,而是给出具体指导:--filter 不存在。要按状态筛选,请使用:--state <状态>。要搜索,请使用:lql search "文本"。 错误消息本身变成了文档,并且恰好呈现在用户最专注的时间点:他们刚刚失败时。 取消所有强制标志。 lql list 无需提供任何参数即可运行:默认按优先级排序,筛选出活跃状态,并根据工作目录自动检测团队。因为不存在 --sort 参数,自然也就没有遗漏它的可能性。一个代理无法忘记不存在的标志。 ...

2026年5月22日 · Fernando

将 Async Swift 桥接到 Sync C:使用四个函数从任何语言调用 Apple 的 LLM

Apple 的 Foundation Models 框架(macOS 26)提供了一个约 30 亿参数的 LLM,可以在设备端运行,免费,无需 API key。但有个问题:它只能使用 Swift。而且不是普通的 Swift——是 Swift 的 async 版本。 你的工具链是 Python 吗?没有绑定。用 Rust?也没支持。脚本语言 Shell?完全没法用。这个世界上最便宜的 LLM(确实免费)被困在两堵墙后:语言和并发模型。 显而易见的解决方案是搭建一个本地 HTTP 服务器,将模型暴露为 REST API,像 Ollama 那样。但这就像是用大炮打蚊子:一个进程在后台运行,占用一个端口,JSON 来回传递,每次问一个简单的问题都要用到 curl。如果只是给一个 commit 分类为 fix 或 feat,你不需要 HTTP,你需要的只是一个简单的 C 函数。 仅有 4 个函数的 dylib 所以我做了 libfoundationmodels:一个动态库,把 Apple 的框架编译为 .dylib,它仅导出 4 个 C 函数: int32_t fm_init(void); int32_t fm_is_available(void); int32_t fm_generate(system, prompt, output, output_len); int32_t fm_classify(system, prompt, choices, output, output_len); --- 嗯,算上 `fm_generate_json` 是 5 个函数,但核心思想未变:四个概念——初始化、检查可用性、生成自由文本,以及在选项中强制分类。全部同步。全部阻塞。输入缓冲区、输出缓冲区、返回代码。经典的 C 风格。 从 Python 调用它是这样的: ```python import ctypes fm = ctypes.CDLL("libfoundationmodels.dylib") fm.fm_init() buf = ctypes.create_string_buffer(256) fm.fm_classify( None, # 没有系统提示 b"fix: handle nil response in OAuth", # 要分类的文本 b"fix\nfeat\nrefactor\ntest\ndocs", # 选项 buf, 256 ) print(buf.value.decode()) # "fix" 就是这样。没有 requests。没有 urllib。没有 JSON。没有运行的服务器。调用一个 C 函数,它会内部唤醒你 Mac 的 Neural Engine,传入文本,并将结果写回缓冲区。典型延迟:200-800ms。 ...

2026年4月5日 · Fernando

我花了 $15 百万 token 写了 'fix: typo'

昨天我用 Claude Code 写了一条 commit message。diff 是一行修改:一个注释里的拼写错误。Claude Opus 读取了 diff,思考了两秒钟,生成了 fix: correct typo in auth comment。为此,它用了大约 800 个输入 token 和 30 个输出 token,分别花费 $15 和 $75 每百万 token。总成本:不到一分钱。但是,把这个过程放大到一天 40 次 commits,250 个工作日,一家公司有 200 名开发者使用 coding agents,那么这些看似微不足道的花费会积累成数千美元的开销,付出的只是等同于贴创可贴的智力劳动。 问题并不是 Opus 太贵。问题在于 coding agents 不会区分 $0.001 的任务和 $0.10 的任务。一切都通过同样的大模型执行。生成 commit 信息、分类一个 issue、验证格式 —— 全部使用高成本的模型,就像雇用一位外科医生来贴创可贴。 数据与成本 我们用 Claude Opus 4(上一代,目前大部分生产环境还在用的版本)的价格来算一个账: 任务 输入 Token 数 输出 Token 数 成本 生成 commit 信息(小型 diff) ~800 ~30 $0.014 分类某个 issue ~500 ~50 $0.011 验证 commit 格式 ~300 ~20 $0.006 生成 standup 报告 ~2000 ~200 $0.045 这些任务都不需要一个拥有 2 万亿参数和多步推理能力的大模型。这些仅仅是带有强约束的分类和生成任务。本质上就像是根据颜色对卡片进行分类。 ...

2026年4月5日 · Fernando

你的 Mac 自带一款免费的 LLM,你可能还没用上

你每个月可能花费 20 到 200 美元来访问 LLM(大型语言模型)。Claude、GPT、Gemini,无论哪一个都在烧你的钱。而这些模型从你的脚本和开发工具调用的大多数场景都可归结如下几类: “把这个错误分类到这五个类别之一。” “给这个变量起个名字。” “告诉我这个 commit 是 fix、feat 还是 refactor。” “用两句话总结这段文字。” 与此同时,你的 Apple Silicon Mac 上还坐着一个拥有 30 亿参数的语言模型:全系统集成、无需支付费用、无需网络连接、无需 API 密钥、甚至不需要网络延迟。而你大概根本没用过。 基础模型框架(Foundation Models Framework) 自 macOS 26 (Tahoe) 起,Apple 推出了 Foundation Models Framework,允许访问 Apple Intelligence 支持的语言模型。这是一个原生的 Swift 框架,适用于 macOS 26、iOS 26 和 iPadOS 26,兼容所有支持 Apple Intelligence 的 Apple Silicon 设备。 值得注意的不仅仅是它免费——这的确很棒,更特别的是它生成 Swift 类型化输出。它不会返回需要你用正则表达式和上帝保佑才能解析的 String,而是直接输出一个结构体(struct)。 import FoundationModels @Generable struct CommitClassification { @Guide(description: "The type of change") @Guide(.anyOf(["fix", "feat", "refactor", "test", "docs", "chore"])) let type: String @Guide(description: "One-line summary of the change, max 72 chars") let summary: String } --- `@Generable` 宏告诉框架在编译时生成架构(schema)。语言模型利用这个架构生成结构化输出。`@Guide` 则限制了可能的值——通俗地说,你给模型铺好了轨道,它就不会出轨。 以下是用法示例: ```swift let session = LanguageModelSession(instructions: """ You are a commit message classifier. Given a git diff, classify the change and write a summary. """) let diff = "..." // 你的 git diff 数据 let result = try await session.respond( to: "Classify this diff:\n\(diff)", generating: CommitClassification.self ) print("\(result.type): \(result.summary)") // "fix: handle nil response in auth flow" 就是这样。没有 URLSession(网络请求),也没有 API 密钥,也不需要解析 JSON,不需要 try? JSONDecoder().decode(SomeType.self, from: data)。整个模型在设备内部运行,依托于你 Mac 的 Neural Engine(神经引擎),并返回 Swift 类型数据,编译器将会为它进行类型检查。 ...

2026年4月4日 · Fernando

疯狂驱动设计:堂吉诃德、桑乔潘萨和你的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

对抗式编程:当你的AI助手凭空编造出API

TL;DR:你的AI可能会凭空臆造出合理但不存在的API字段。解决办法不是祈祷它正确,而是:在编写代码之前下载真实的_schema_,使用API的实际返回值作为_fixtures_,并将_fetch_与数据处理分离,以便无需网络测试代码。这就是对抗式编程:假设你的AI助手会撒谎。 你是否有过这样的经历:写了针对某个API的代码,一切都编译通过,测试也没问题,逻辑看起来合理……但连接真实API时,完全行不通? 如果你独自开发,这往往是因为你读错了文档。而如果你和AI一起编程,会是因为AI编造了那份文档。 不像“幻觉”的幻觉 我当时正在用Rust构建一个命令行工具(CLI),用于与某个GraphQL API交互。我让我的AI助手为实现一个按优先级排序的过滤器编写代码,它返回了这样一段: query { issues(orderBy: { priority: ASC }) { nodes { id title priority } } } --- 整洁、合理、完全符合预期。唯一的问题是:这个API的`orderBy`字段并不接受`priority`作为值。实际的枚举类型叫做`PaginatedOrder`,合法值包括`createdAt`和`updatedAt`,而不是`priority`。 我是如何发现的?当API返回一个毫无意义的400错误时,我花了20分钟才弄明白问题不在我的代码,而是因为我使用了一个**根本不存在的字段**。 ## 总是相似的模式 这并不是个例。在几周的开发过程中,AI一再出现类似的“幻觉”: - **不存在的过滤字段**——比如建议`state.id.or`代替实际的`state.type.in`。听起来合乎逻辑,但API使用了完全不同的模式。 - **凭空捏造的枚举**——提供的值名称看起来合理,但实际API中从未定义过。 - **完全错误的生态系统方法**——在Rust项目中,AI建议使用`fcntl.flock`来处理文件锁定。这是Python的做法,而在Rust中应该用`fs2::FileExt`。 这些错误具有一个共同点——**它们都看上去非常可信**。没有一个错误显而易见。一个新手程序员在粗读文档后可能会犯下完全相同的错误。这正是危险所在:它们不像“幻觉”,更像是对领域知识“一知半解”的代码。 ## 为什么LLM会编造API? 简单来说:LLM不知道你的API具体有哪些字段。在训练中它可能见过成千上万的GraphQL APIs,而当你要求它使用其中一个时,它就像一个有着良好直觉但没有文档的开发者:**猜测**。 而且它猜得挺好,足以让你信服。然而偏偏这个“差不多”就足以让你的项目进度完全崩盘。 这就像和一个从不看文档但总是自信满满的聪明同事一起工作。对方会无比肯定地告诉你:“是的,这个端点接受名为`priority`的字段。”你完全信了,结果运行时发现它是编造的。 ## 解决方案:对抗式编程 在经历了一周内三次类似的“幻觉”后,我采取了一种不同的策略。从“先信任,后验证”,改为**“先怀疑,先验证”**。我称之为**对抗式编程**:假设你的AI助手总是会编造一些东西。 这不是敌意,而是一种开发安全手段。 ### 1. 编码前进行Schema自省 如果你在使用GraphQL API,请在向AI提出任何需求之前,下载真实的Schema: ```bash # 下载API的完整Schema curl -s https://api.example.com/graphql \ -H "Authorization: token-here" \ -H "Content-Type: application/json" \ -d '{"query":"{ __schema { types { name fields { name type { name kind ofType { name } } } } } }"}' \ > schema.json 现在你有了真实的内容。当AI告诉你“使用orderBy: { priority: ASC }”时,你可以在Schema里查找priority字段并验证。然后将相关部分的Schema片段交给AI,明确告诉它“只使用这些字段”。猜测就此终结。 ...

2026年3月26日 · Fernando

你的AI写了可以编译但毫无意义的代码(一个linter可以抓住它)

想象一下,你请一个人工来为你制作一个书架。最终交给你的作品看上去很漂亮:架子、螺丝,一切都井井有条。但是你把书架靠在墙上,结果它倒下了。那些螺丝只是道具,看着像螺丝,但其实是塑料做的。 这就是LLM(大语言模型)滥用类型系统时的表现。它交给你的代码可以编译、能通过测试、格式上看起来挑不出毛病。但内部本该是有意义的类型,却被填上了字符串(string)。本该有明确状态的地方,用了nil,而它的意义因读代码的人而异。本该是一个有两个案例的枚举,却被换成了一个== "claude",某天有人可能会写成错拼的== "cladue",但你要等到线上了才会发现。 模型的最爱捷径:万能的字符串 过去几个月,我一直和一个AI助手合作,开发一个中型的Swift应用。它生成的代码干净、结构清晰、命名合理。但有一个模式一再出现:模型尽量避免创建新类型。 这不是因为它懒惰,而是因为创建新类型需要做设计决策,比如:这是一个枚举(enum)吗?有多少个可能值?应该放在哪儿?由谁来导入?而String则无需任何决策。它可以放在任何地方,总是可以编译。 结果就是会生成这样的代码: func sessions(harness: String? = nil) -> [Session] { if let harness { return all.filter { $0.harness == harness } } return all } // 用法: let claudeSessions = sessions(harness: "claude") let codexSessions = sessions(harness: "codex") --- 看起来没问题,确实能运行。但这个代码有三个隐藏的问题: 1. **如果你写错了`"cladue"`,没人会提醒你。**字符串可以是任何内容,但如果是枚举类型,这种错误是可以避免的。 2. **`nil`被用来表示“全部”。** 但这不是定义在某个类型中,而是一种存在于代码注释中、甚至仅仅是你的大脑中的约定。 3. **每个按`harness`过滤的函数都重复了同样的`String? = nil`模式。** 如果你以后想添加一个第三种`harness`,就必须手动搜遍代码中所有的字符串比较。 换句话说,编译器无法帮助你,因为你剥夺了它所有的语义信息。你用一个`String`代替了领域概念。 ## 模型应该写成这样 ```swift enum HarnessID: String, Codable { case claude case codex } enum HarnessFilter { case all case specific(HarnessID) } func sessions(harness: HarnessFilter = .all) -> [Session] { switch harness { case .all: return all case .specific(let id): return all.filter { $0.harnessID == id } } } // 用法: let claudeSessions = sessions(harness: .specific(.claude)) let allSessions = sessions() // 默认是.all — 很明确 现在,"cladue"不会编译。“全部”不再是一个含糊不清的nil,而是一个显式的枚举情况。如果你添加一个第三个harness,编译器会强制你在每个switch语句中处理新情况。它将运行时错误转化为了编译时错误。这才是类型系统本该发挥的作用。 ...

2026年3月23日 · Fernando

你的LLM缓存让你付出双倍成本省钱(并且有道理)

几周前我写了一篇文章,解释了为什么你发给Claude的99%数据已经在缓存里。KV张量、VRAM、本地SSD——所有的内部机制。但我遗漏了最痛的部分:账单。 因为prompt缓存是那种看起来像超值优惠的东西,但一旦仔细看看数字,你就会发现为了节省成本,你竟然被要求付更多的钱。 成本悖论 让我们用一些数字来说明。在Claude Sonnet的定价中: 项目 每百万tokens价格 常规输入 $3.00 缓存写入 $3.75 (1.25倍) 缓存读取 $0.30 (0.10倍) 注意一件事:写入缓存的成本比直接处理输入高25%。你为了让下一次使用更便宜而需要额外付费。 这就像加入Costco那样。年费有点心疼。但如果买得足够多,就会值回票价。 问题是,“足够多”取决于你能在缓存过期前读取它的次数。 什么时候你会亏钱? 假设你用cache_control发了一个100K tokens的prompt。第一次请求: 100,000 tokens × $3.75/M = $0.375 (缓存写入) 如果你没有用缓存发送它: 100,000 tokens × $3.00/M = $0.300 (常规输入) 你额外多付了$0.075,比常规价格高出25%。你亏钱了。 现在第二次请求,同样有100K tokens的前缀: 100,000 tokens × $0.30/M = $0.030 (缓存读取) 对比来看: 100,000 tokens × $3.00/M = $0.300 (常规未缓存输入) 你节省了$0.27。两次请求的总成本不仅回本了之前多支付的$0.075,现在还实现了盈亏平衡。 盈亏平衡点是1.4次读取。 用更直白的话说:如果你计划在接下来的5分钟内至少重复使用该前缀两次,缓存就是值得的。 为什么对Claude Code来说使用缓存是明智选择 在Claude Code的一次会话中,每条消息都会包含system prompt、工具定义以及整个对话历史。每条消息都在重复发送相同的上下文。如果没有缓存,每次发送“把这个按钮换个颜色”,你都需要为相同的150K tokens上下文支付$3.00/M。 没人能负担得起这个。 而有了prompt缓存,你只需为写入缓存支付一次费用,之后每次读取只需要$0.30/M。在包含150K上下文的50条消息会话中,差距很明显: 无缓存: 50 × 150,000 × $3.00/M = $22.50 有缓存: 1 × 150,000 × $3.75/M + 49 × 150,000 × $0.30/M = $2.77 从$22.50降到$2.77。节省了88%。这就是为什么Anthropic默认在Claude Code中启用缓存。如果不这样做,从经济角度看是不可行的。 ...

2026年3月10日 · Fernando

RustyClaw:我要用 Rust 重写一个AI代理(因为梗在召唤我)

“你知道 Rust 最棒的一点是什么吗?它不会允许你编译粗制滥造的代码。你知道最糟糕的一点是什么吗?起初你写的所有代码都是粗制滥造的。” —— 蟹老板,大概是这样说的 比一个 AI 代理更好的是什么?是一个用 Rust 重写 的 AI 代理。 如果你上网超过五分钟,就会知道这个梗。不管是什么项目:文本编辑器、DNS 服务器、BMI 计算器,总会有人跳出来评论“你应该用 Rust 重写它”。这就是 Rewrite It In Rust —— 简称 RIIR,和地心引力一样不可避免的存在。 好吧,那我就来做一次真的。我将把一个有 8,300 行代码的 Python AI 代理移植到 Rust。但不是因为这个梗在召唤我(嗯…有一点是因为它)。我这么做,是因为我需要一个实验对象。 论点 最近几周,我一直在写关于静默失败、五种防止幻觉的方法、以及"一个 LLM 如何生成看似正确但实际上错误的代码"的文章。我甚至还给它起了个名字:对抗性开发。永远不要相信,总要验证。 很多理论,是时候实践了。 于是,我需要一个项目,满足三个特点:范围适中(而不是一个需求会不断变化的新应用)、明确的真相来源(现有可用的 Python 代码)、以及足够的复杂度,让 LLM 的幻觉能“藏起来”。一个纯粹的移植可以完全满足这三点。输入和期望输出已然存在。如果 Rust 版本的行为和 Python 的不完全一样,那肯定有问题。就是这么简单。 既然要做移植,那为什么不顺便真正学学 Rust 呢?借用检查器 (borrow checker)、所有权 (ownership)、生命周期 (lifetimes)… 我读了好几年资料,却几乎没有亲自实践过。如果是写一个真实项目而不是第 N 次看教程,一切或许会大不相同。 目标对象 它的名字叫 nanobot。这是一个基于 OpenClaw 开发的个人 AI 代理。它能将各种 LLM(如 Claude、GPT、DeepSeek)接入聊天渠道——Telegram、Discord、Slack、电子邮件——并赋予它们更多功能。比如读取和编辑文件、执行命令、网络搜索、通过 cron 编排任务,甚至在对话之间保存记忆。 它可以正常工作。而且已经运行了几个月。在 Python 上。 问题呢?它是单线程的。一次只能处理一条消息。如果你连续发送三条信息,它会像周六中午的超市购物队伍一样排队等待。它的内存消耗约为 50MB,而它实际上只是在不同的 API 之间传递 JSON。此外,它的错误处理方式令人羞愧:到处都是return f"Error: {str(e)}"。 ...

2026年2月24日 · Fernando