AI编程工具在模型无文本输出时,回退逻辑只统计写文件和命令执行次数,搜索和读取不计入,导致用户看到"0写入0命令"的空洞总结后认为工具失效而流失。
我们在云端构建了一个 AI 编程 Agent。六个人在 21 天内注册并使用了它。
六个。不是一次发布,不是 traction,不是增长故事——只是六个人,少到我们可以坐下来读完他们发给它的每一条消息。于是我们真的读了。
以下是所有出错的地方,包括那一条我们以意想不到的方式搞砸了的。
这个字符串在六个用户中出现了三次。其中一个人点击了 Continue,等待,然后收到了这条回复。那是他们那个会话的最后一条消息。他们没有再回来。
我们最初以为是 Agent 卡住了——一个挂起的工具调用、模型挂了、或者什么明显的问题。其实并没有。
这条消息来自一个兜底逻辑。当模型没有返回任何文字时,我们会描述它做了什么:
const summary = fullResponse || this.describeTurnActions(turnActions);
而 describeTurnActions 统计了这些:
if (type === "write_file" || type === "edit_file") {
files.push(action.path);
} else if (type === "run_command") {
commands++;
}
写入和命令。这就是整个统计口径。
再来看结束那个用户会话的那一次:117 秒。254,000 个 token。8 次文件读取。10 次搜索。Agent 打开了代码库、搜索了它、读了一遍,然后没有任何编辑就停下来了。
这些操作都没有被计数器捕捉到。所以产品把两分钟的真正工作,在一个正认真关注的人类面前,报告成了什么都没发生。
一个句子里面藏着两个独立的 bug:
我们在第一个小时里一直在调试 bug (2),却以为问题出在 bug (1)。
另一个用户的第一次操作产生了 48 个文件和 41 条命令——一个真正的应用骨架。然后他们输入了 continue.,得到了 2 个文件和 73 条命令。再来一次:2 个文件,59 条命令。
几十条 shell 命令几乎什么都没产生。从表面看这像是系统空转,也可能确实是。
但想想我刚才告诉你的那一轮的信息:文件数和命令数。它和 #1 用的是同一个工具,而我还不知道那 73 条命令是瞎折腾还是一次漫长的合法诊断。我们目前没有办法区分这两者。这才是真正的发现。
一个用户写了一个 518 字符的随意提示——那种你打字时不会刻意思考的东西。Agent 开始了工作,中途撞上了 token 预算,然后写了一段诚实的情况交接,解释了它进展到哪里了。
这是正确的行为,我宁可要它也不要一个自信的谎言。但这仍然是一个没有完成可运行应用的构建,用户感受到的是产品放弃了。
同一个用户,紧接着的下一条消息。他们原话,不是我们的转述。
原因:Scaffold 出来的 Vite 应用发出时没有带 base 配置,所以经过我们的预览代理,所有资源都解析到了一个不存在的路径。一片空白的白页,底下却承诺着一个实时预览。
这个已经修复并部署了。它之前也在一个分支里修复好了待了一段时间,才有人注意到它其实没在生产环境里,这是另一个小教训。
一个用户写了一份真正具备首席架构师水准的 PRD,运行后得到了 36 个文件——Prisma schema、认证、API 路由、中间件。然后他们关掉了标签页。
12.8 小时后他们回来了,继续构建。
没有 onboarding 流程。没有邮件。没有提醒。他们第二天回来了,因为开始的那件事值得继续做下去。六个用户的时候这不是一个统计数字,但它是数据里最令人鼓舞的事情。
而在同一个会话里,Agent 遭遇了一个 Next.js 16 的 async-params 问题,涉及三个路由处理器,它诊断出来了,然后主动修了全部三处。没有人要求它这么做。
一周内有两次,我们看到一个糟糕的数字,结论是模型表现不佳,然后发现我们错了——因为那个在计数的工具坏了。
第一次是上面那个:读取和搜索没有纳入统计,所以工作看起来像是闲置。
第二次是我们的评估工具链。Fixture 在轨迹质量上得分很高,但几乎所有验收检查都失败了。同一次运行的两种度量,差异巨大。我们在 Agent 身上花了真金白银来迭代,才有人问出了那个显而易见的问题——而那个出问题的验收检查本身才是错误的。Agent 一直都没问题。
两次都是尺子的问题,我们却去被测的东西里面找原因。
在结论模型不行之前,先检查一下你的工具在计数什么。这大概只需要二十分钟。我们跳过了两次。
现在空轮消息能区分"调查后停下了"和"真的什么都没做":
if (files.length === 0 && commands === 0) {
if (reads > 0 || searches > 0) {
return `**I looked, but I did not change anything this turn.** I ${looked} ` +
`and stopped without editing — usually that means I could not work out ` +
`the next step on my own.\n\n` +
`Tell me what to do next and I will act on it.`;
}
return `**That turn produced nothing — that is a fault on our side, not ` +
`something you did wrong.**\n\nSend your last message again.`;
}
关于这段代码有两件事值得说。
一:它没有解决 Agent 过早停止的问题。它解决的是我们在这件事上撒谎的问题。这是两个不同的工作,把它们混为一谈会让你发一条更友好的错误消息然后宣布 bug 已关闭。
二:再读一下第二个分支。当真的是我们的错时,产品会直说,用的是那几个字。一个被告知"未产生任何文件变更"的用户会得出是自己的提示有问题的结论。这是最糟糕的结果——他们怪自己,然后就离开了。
这六个用户的提示中有两个是 15,000 字符级别的产品规格说明书。那些是他们业务的命脉,写进了一个他们合理假设是私密的文本框里。如果发出来,那会是一篇远比这篇有趣得多的文章。
上面没有任何名字、没有任何产品概念、没有任何提示文本,以后也不会——除非本人说可以。
如果你宁愿看 Agent 干活而不是读它失败的故事,这里有一段未经剪辑的运行:https://youtu.be/2sR4H1q9QCQ —— 精彩的部分在 1:12,它自己的构建检查失败后自己去修了配置。