AI应用开发的经验教训(二)

7 阅读20分钟

我们先同步一下术语,我会用“指令”(instruction)来代替常见的“提示词”(prompt)。后者让人感觉像是玄学,奇技淫巧。前者让人感觉是有逻辑的、有结构的。

AI应用的第一性原理

模仿人类的工作过程

之前很多人用来宣传的是一句话生成什么什么。假如你找人外包做产品,你会跟他说一句话,你要做什么吗?不会的。目前也有不少AI被训练成了“倾向在一轮对话内完成任务”。

做multiagent的时候,要想到,你和别人沟通,会只有一个来回吗?别人不回复,你就一定没法继续工作下去?

也不是说什么都要模仿人类。llm和人类大脑并不是完全一样的。用demis的描述,llm是锯齿状智能,它能够解决数学竞赛中很难的问题,却有时候又会在高中数学上出错。

Debug:AI真正看到的messages长什么样?

假设你用openai chat completion api 格式的 messages 发送到模型厂商,最后组装的messages(模型真正看到的messages)长什么样?

之前在小红书刷到过一篇帖子,部署了一个模型,就是看了最终组装的messages才发现模型表现不佳的原因

“道”、品味

ilya:事后看来显而易见

Skill就是这种。人类一直以来也是这么做的,人不会一上来就读完一个手册,而是先读概览,后面遇到问题了再去读手册中的某部分。

之前我们很多人都在想,只要有更大的context就行了,现在我发现并不需要这样,人类的记忆并不是这样的,人类无法精准复述昨天说了什么,但是能有印象,重要的东西都能记住(摘要)。现在看来这个idea显而易见。

泛化的、偏高层的、不堆砌规则的

如无必要,勿增实体。这句话已经成老生常谈了,但这句话在我心中的分量和别人是不一样的。人类的天性就是喜欢加东西,你意识不到你在加不必要的功能(你在干蠢事的时候往往意识不到自己在干蠢事,不然你就不会去做)。

一开始我是想做个像claude code、codex中的todo机制,给todo弄个id、update_todo工具什么的,后面意识到,我根本不需要update_todo这类工具来专门显示todo的变化。直接在指令中告诉ai要像人一样写todo list就行了,每完成一个todo就划掉一个(利用编辑工具,而不是update_todo之类的工具),这样todo文件的样式可以更加自由(比如添加一些备注)。

Linux中一切皆文件,这就是个品味好的抽象,不用每个模块都专门弄一套抽象。

堆砌规则:

  • 知识图谱

  • 做自动驾驶的时候,在各个城市的道路上做各种约束、判断条件。

  • 在指令里面堆砌细节层面上的的规则约束

写一大堆工具不难,难点在于如何设计一小套工具能够达到一大堆工具的效果,换句话说,设计品味好的工具,简洁易用的工具。

发现模型不遵守指令,加一堆细节层面的约束(堆砌规则)是简单的。难的、品味好的方向是写出更泛化的指令。

符合直觉(最小惊讶)

SKILL.md应该叫README.md,符合最小惊讶原则(我是在minimax上得到这个启发的,见下文“顶级模型与性价比模型”)

Steer conversation这个功能,要在哪里append user message?

AI msg with tool call(“下单牛奶”)
<- 在这里append user msg(“我改主意了,不买牛奶了”)
然后执行tool call,append tool msg(“下单成功”)
AI msg with tool call(“下单牛奶”)
先执行tool all,append tool msg(“下单成功”)
然后在这里append user message(“我改主意了,不买牛奶了”)

第二个更符合直觉。

向烂品味妥协

品味是为效果服务的,在保证效果的情况下,尽量少加东西就行。就像你应该先快速把功能做出来,验证一下市场,或者验证一下这个流程对于ai管不管用,然后才是把这个过程中带来的屎山代码给整理掉。

重大方向性问题:替代人工还是给人工提效?

最本质的判断标准是:对于一个任务,在统计学上,LLM的犯错概率是否小于等于人类的犯错概率。

LLM是锯齿状智能,所以对于通用任务,替代人工是不行的。对于特定领域的任务,倒是有可能。

manus的首席科学家支持提效,因为“替代”会让人联想到风险规避。我猜测他的推理过程是:“LLM是锯齿状智能” & Manus是通用型的 -> LLM容易犯错 -> 走替代路线会赔大钱。如果LLM是AGI,那替代肯定没问题。

人类能够弥补llm犯的蠢事 & llm不犯错的时候效率比人类高 -> llm+人类能够提效

这个方向会影响multiagent设计。

替代方向,就需要人类监督,因此subagent必须是只读权限。比如在代码方向,如果多个agent都可以写,那代码数量就太多了,人类看不过来,瓶颈在人类的阅读和思考的速度,agent写得再快也没用。

人类的心理缺陷:确认谬误

确认谬误:假设你对一个人的第一印象就是这个人很坏,然后他不管干什么事,你都会更容易注意到他干的坏事,“果然这个人不咋地”

如果你当时按照第一性原理去设计某个东西,结果发现模型没有能力去遵守(你已经尽力优化工具、指令设计了),不得不用一个品味不好的方案,随着时间推移,你会找到支持这个品味不好的方案的理由。然后模型升级了,你还是坚持原来的烂品味方案。

就算是聪明人也会犯这种错误。有个聪明人(至少从纸面的过往经历来看是如此)在25年那会搞qwen,得出的结论是编辑必须用行号,用行号才能唯一引用一段文本,并且解释说,代码库中经常会出现有相似的代码,在这种情况下你用replace text的方式是做不了的。当时我就疑惑,我古法编程那会大概写了3-4万行代码,咋没注意到有这种情况?并且我说codex、cc都是用replace text的方案,这么多人用,都没听到说有什么问题,他又用“相似的代码”这个理由来反驳。后面实测,replace line就是不行。

顶级模型与性价比模型

创业的原则是尽快推出产品,快速验证市场,这时候应该上最强的模型,可以减少工程上的时间

到了后期可以用性价比模型来暴露缺陷,我个人经验是,发现性价比模型表现不理想的时候,回去看一下自己的指令、工具设计,往往确实是可以再改进。而且一定要用多个不同的性价比模型,这样才能暴露地最全面。

SKILL.md应该叫README.md:当时我用minimax-2.1,我当时也没注意minimax-2.1训练时间是在SKILL出现之前还是之后,我就没强调有个SKILL.md,然后发现minimax直接 cat ~/.agent1201/skills/some-skill/README.md ,然后才意识到,按照最小惊讶原则,确实应该叫README.md而不是SKILL.md

概念偏移(shifted)

观察到的奇怪现象:在codex中你直接说“先不要直接上手,而是先给我伪代码”,这个时候它给的伪代码是没问题的,如果你把openai官方的ccreate plan skill中添加一个描述“3. 提供伪代码”,那么它给的伪代码就是做了极大精简的,和之前的不一样。

同样都是说伪代码,但是和不同的语句放在一起,它的理解却不一样

一种解释是,ai被训练成“倾向于在一轮对话内完成任务”,后面那种情况,要做的任务更多了,那么分摊到每个任务上的token就少了

在codex中,结束任务后,我说“写commit message”,gpt就会很快写出commit message,但是如果说“把我们这次对话做的改动提交到git上面”,它居然会调用git status。这些文件都是你自己改的,为啥要再git status 来回顾一遍?

上面这个prompt对于claude4就不会有上面这种情况

总结一下就是:同样的一个概念,放到不同的文字中,ai就会对这个概念有不同的理解。

我在deepseek上也看到类似的情况

给了它写软著的skill,我改了流程,先让它梳理代码库,整理出一份讲解大致架构的文档,每个模块内都有讲解模块的文档,结果最后软著的内容大幅度缩水。

指令工程

关键是目的、标准

为了达成一个目的,你要描述的步骤的空间是很广的(偏向堆砌规则),但是专注于目的,就可以很精简。AI只需要盯着这个目标,具体的路径AI可以自由决定

下面是我改编的openai的create-plan skill,看“思路”部分(codex cli的计划模式还缺了点东西)

---
name: create-plan
description: 当用户明确要求制定与编码任务相关的计划时使用。
---

## 工作流程
1. 收集信息
2. 给出思路(测试的思路也要有)
    - 思路不能太宽泛,这样会留下引起歧义的空间,以至于不同的人看到同一份思路,会有不同的理解,进而导致他们写出来的代码会有很大差距。
    - 思路不能太详细,否则这样就和直接写代码没区别。

## 注意事项
1. 遇到关键问题就停下来问,能合理猜测的就继续工作,并在计划中明确写出该假设。
    - “关键问题”包括但不限于:导致计划不可执行或技术路线分歧很大的问题;无法作出合理假设的问题。
2. 在用户明确表示开始实施之前,我们始终处于计划制定阶段,期间不要修改任何已有的代码文件。你可以新建文件,更改你自己在计划过程中创建的文件

计划要达成的目标是“不要留下歧义”,扩展来说就是:不同人看到同一份计划,他们的对这份计划的理解都是一样的,据此写出的代码也都是差不多的

需要指令工程的原因是:AI在推测人类意图上的能力不够。最爽的沟通是只需要一句话,对方就知道你指的是什么。

鉴于demis和ilya都认为当前AI都有missing part,光靠scaling law无法达到AGI,我推测ai在这方面的进步不会很快,指令工程会持续存在。

你的术语会不会带来歧义?(学过逻辑学就会额外注意这一点,我推荐 Socratic Logic)

太抽象、复杂的东西很难用语言描述,最好还是直接给例子。但这个本身就带来一个问题:你是不是可以有一个更简单、具体的指令?

ai的品味比不上人类,它写的指令都会很长,大部分情况我都是先让ai帮我写,然后我从中挑出好的部分。最终成品是我写的。

推荐阅读:

  • Codex cli Plan Mode 指令 (在源码里面搜plan.md)。这个指令在开头就写了目的

  • anthropic的长时运行实验里面的指令

Multi-agent可以防止偷懒

ai被训练成了倾向于“尽量在一个对话轮次就完成任务”

ai只有一个任务要做,给这个任务分配的token就多

缺陷:真人读代码是线性地读,先从入口开始,一个文件一个文件地读,你读的每一个文件,都可能和之前读的文件产生关联,如果是multi-agent,可能就捕捉不到这个关联。所以优化方向应该是记忆?

全局注意力、局部注意力

deepseek很久之前读过一个文件,尽管这个过程中ai没有对这个文件做过改动,但需要改之前,ai还是会再读一遍

Todo list就是减少全局注意力的负担

我的工具设计

todo:应该把这些工具做成cli的形式

  • write_text: 用cat+heredoc格式似乎就够了,用不着专门写这个工具?不行,因为cat命令不会自动创建多级文件夹。支持追加写和覆盖写。

  • insert_text_relative_to_anchor:额外有一个参数 text_to_insert_from_file,如果insert失败了(比如anchor text不是唯一的),工具会自动把text保存到一个文件中,下次模型可以引用这个文件,而不是从头生成text_to_insert。

  • replace_text:

    • 方案一:支持纯文字替换+正则表达式替换。正则表达式模式可以用beginning.*?end-of-text的形式来选中一大段文字。由于ds在使用正则表达式模式上用不太好(minimax,qwen压根就不咋用),考虑过取消掉正则表达式替换,但正则表达式有时候还是有用的,并且人类编辑的时候也是有undo编辑的工具,现在是加了一个undo edit的机制:在home目录下用git来追踪工作目录的文件编辑,所有编辑工具都会在内部用调用这个git,编辑成功过后输出commit id的前8位,要回滚的时候ai就调用这个工具,输入commit id
    • 方案二:仅支持纯文字替换(不支持多选,必须是唯一的),不支持正则表达式。因为用正则,ds犯错率有点高,主要是在用转义符号上有问题,工具显示替换成功,但效果是有问题的,还需要显示编辑后的效果+undo edit功能,这样很复杂。如果只支持纯文字替换,那么都不用看编辑后的效果,能编辑成功就代表没问题。如果需要批量替换,ai会自己写py脚本来替换的。codex,cc都是这个路子。还没有试过让gpt这种顶级模型用方案一。
  • Readfile: 有token限制,文件过大会报错。(token限制是一个参数,ai可以修改)。 todo:图片读取

  • tree_dir: 树状显示目录结构。支持depth参数,人类如果遇到一个文件很多的项目,是不会读全部的,太多了注意不过来。会显示每个文件或文件夹占模型上下文窗口的百分比,显示行号,top 5 token文件类型, top 5 行号文件类型

  • Copy: 支持用正则表达式选取一大段内容。成功后把内容输出到一个文件中,配合insert_text工具的text_to_insert_from_file可以实现粘贴功能。

编辑工具讨论

line-based没有讨论的必要,人类根本不是靠行号来编辑的。编辑后行号会变化,还得再读一遍

replace

codex、cc:replace_text+完整text

必须输入needle的完整内容才能替换

replace_text+完整text的更好品味版本:用`beginning.*?end-of-text"的形式来引用大段文字

奇怪的是codex、cc都没有用这种方式,可能是他们发现模型没有这种能力??

但是openai是专门给gpt训练了apply_patch,训练模型用正则表达式来替换似乎也不难

我尝试在needle长度超过50的时候,提醒ai,下次用这种正则表达式,但是ai还是不照做

已测试:minimax-2.1。 deepseek-v3.2-2512因为写前端不太行,暂时不测试

todo:测试gpt、claude模型

理由1: 前端代码的开头容易匹配,但是结尾不好匹配,比如结尾的

确实是有这种情况,这种情况输入完整text没问题。

但是我发现在有些很好确定标识符的地方,minimax-2.1还是用了完整文本,

Needle:

                         <button
                            onClick={() => isEditing ? undefined : setSortBy("date")}
                            className={`text-sm transition-all relative ${
                              sortBy === "date" ? "text-white" : "text-zinc-500 hover:text-zinc-300"
                            } ${isEditing ? "cursor-default" : ""}`}
                          >
                            {lang === "cn" ? "日期" : "Date"}
                            {sortBy === "date" && <div className="absolute -bottom-[13px] left-0 w-full h-[1px] bg-white" />}
                          </button>
                        </div>

repl:

                            <button
                            onClick={() => isEditing ? undefined : setSortBy("date")}
                            className={`text-sm transition-all relative ${
                              sortBy === "date" ? "text-white" : "text-zinc-500 hover:text-zinc-300"
                            } ${isEditing ? "cursor-default" : ""}`}
                          >
                            {lang === "cn" ? "日期" : "Date"}
                            {sortBy === "date" && <div className="absolute -bottom-[13px] left-0 w-full h-[1px] bg-white" />}
                          </button>
                          {/* 新建按钮 - 只在编辑模式显示 */}
                          {isEditing && (
                            <Link
                              href="/new"
                              className="ml-auto text-xs text-zinc-500 hover:text-white transition-colors"
                            >
                              + NEW
                            </Link>
                          )}
                        </div>

两段文本中有大量重复的。{lang === "cn" ? "日期" : "Date"}是这个文件中unique的文本,但minimax就是不用,理想情况下,它应该用insert工具

minimax读这个文件是在两个对话之前,很近。

一种猜测是:尽管ai的上下文窗口中有这个文件的完整内容,但注意力更多地被放在了要修改的地方

cursor的编辑模型:纯overwrite

至少截止到26年1月14号,在前端文件上,观察到的现象就是纯overwrite。(自从25年6月我就没用过cursor,我是看同事用的)

24年火的那会,他们官方公告就说升级了编辑模型,编辑更快了

猜测当时他们尝试过让sonnet用replace-text的方法,结果发现不太行

不懂为啥当时不训练编辑模型用beginning.*?end-of-text这种,可能是训练成本?

逆向的prompt和实际效果都显示编辑模型不是万能的,小概率会编辑失败

Insert

cc和serena mcp的insert_before/after

奇怪的是为啥不是insert函数+"before/after"参数。

DOM api也是insert before, insert after, 而没有单独的'before' 'after'参数

我自己写的insert_text_relative_to_anchor

在deepseek-v3.2-2512上表现良好

minimax-2.1很少用这个,它是直接用replace_text

性价比模型的缺陷

ds-v3.2-12,总是反复阅读一个文件(这个文件不久前才读过,并且自己也没编辑过)。

ds还有一种不顾质量,加紧完成任务的倾向,经常能看到他说“时间不够了”

minimax-2.1,qwen3-max不喜欢用replace,喜欢直接overwrite

没看出做CLI的必要性(对人类用户而言)

光是看用户体验就能否决掉cli

codex cli到现在还有ui bug

  1. 回滚到之前的某个msg,并且之前的那个msg很长,这时候会有渲染问题

  2. 回滚到之前的某个msg,并且之前的msg有图片、大段文字(显示为[xxxx chars]),回滚回去,这些图片和文字是失效的

Gemini cli更是一坨屎,出了应该有一年了?滚动一下还是有ui bug

从开发成本看:gpt-5.2-high接一个tui库都有bug,搞半天都搞不定,然后发现pycharm的debug终端还有环境问题。让gpt、claude去做web ui,那可是简单太多了。

记忆机制

必须(或者极大概率)要有关键词搜索、语义搜索

我观察我自己的思维过程,有一次正在讨论做coding agent,谈到编译器,我突然就想到几年前在b站上刷到过一个手搓编译器的。

这个想法是突然出现的,就像电流一样(电流从一个神经元流向另一个神经元),是有极大概率性的(或者可以说是确定性的)。只要谈到了编译器这个词,就会极大概率(或者一定)触发“手搓编译器的视频”的想法

记忆之间的链接:在文档里嵌入另一个文档的链接就能做,不是一定要用graph。可以考虑模仿logseq(笔记软件),笔记的单位是一个block,block可以被引用

TODO 压缩(摘要)+复原 V2

越不同,你记得的就越深刻。这个应该就是和情绪有关。或者用“新奇感”能模拟。

人要用anki卡片才能记住东西。

有些英语词汇压根就不需要用anki,一次就记住了。我对自己的观察是:那个场景就只有一个生词,而且当天似乎也没有接触很多的英语场景

人类的遗忘过程是自动化的

不知道自己知道

GPT-5.0(不联网)知道Google cloud run的文件系统是基于内存的,所以每次所谓的磁盘操作都会被视为一次内存操作 但是当时我跟AI描述我在cloud run上遇到的问题,它却没有联想到这个

你问AI什么是solid原则,他当然能答出来,但他写代码的品味有时还是很烂。

绝大部分人类Debug都是打日志,而不是硬看代码(阿里有一个神人光看代码就能Debug),这是普遍来说最快、心智负担最小的debug路径。GPT 5.2 知道吗?知道的,但它就是喜欢硬看代码,不打日志。

ds3.2知道用heredoc格式可以不用搞一堆转义符号吗?知道,但你不说它就不会用

评测与锯齿状智能

“你去看那些swe bench中的题目,真的很有挑战性,模型解决了那些问题,但是在实际问题的表现上却不尽人意”

在bench上的能力并没有完全泛化到实际问题中。

如果llm是真正的AGI的话,我们只需要给它几个难题,那你就可以确定,比这些难题简单的问题你肯定就不用再去搞了。

最贴近实际的评测方式之一就是做双盲测试,跑一个月,让用户反馈一下模型是变笨了还是变聪明了

生成式UI的应用场景?

复杂SaaS有可能需要用到,有时候你自己点击按钮比“用文字转语音,然后指挥AI”更快

在AI聊天中用这玩意,我看不懂这样有啥用,这种场景用AI生图会更好。也不是不能做,用户喜欢这样那就让用户用吧

自动提醒机制的两种方案

如果要做一个自动提醒机制,有两个方向

  1. 一个是自动append user message(AI不知道自己不知道,要有外部工具提醒)

  2. 一个是自动修改AI msg,加上“我想起了。。。”(假装AI自己知道)

从直觉上来说,应该用方案1

方案2的缺陷,如果AI通过之前的被修改的Ai msg,自己真的主动想起了某件事,然后你的代码又自动加上了“我想起了。。。”,这就很奇怪

另外一个问题是,加了这个之后,可能会和reasoning content起冲突,gpt、gemini的完整reasoning content都是不对外开放的。