Agent Harness 这个词被造出来之后,就意味着它开始进入被人们命名的领域。 一旦一种做法开始被人们命名,一旦人们发现语言无法精确地描述,而赋予他一个借用的词汇,他就开始从本质上是“小众”的实践变为“最佳实践”。
我依旧记得在过去,人们通过各种方法,把源码打包进一个长文本,急头白脸地粘贴到 ChatGPT 聊天框中的日子。 哪怕现在,带着对 Claude Code 启动十轮 Explore Agent 的恐惧,对一些简单代码问题,我也会复制、粘贴到 Google AI Studio ↗ 得到天真无邪的回复。
无论如何,人们结束了推理模型是否需要 Prompt Engineering 的争论,一套 well-crafted 提示词和工具 schema 能让模型获得卓尔不凡的能力,以悄无声息的方式,开始成为人们的共识。
等等。所以什么是 Stateless Agent#
我的这种观点起源于一种奇怪的感受,它是关于 Memory 的。最开始我们在 ChatGPT 和 Google Gemini 里面见到记忆功能,Claude Code 也加入了所谓的 MEMORY.md。 后来,一些 LLM 客户端比如 alma ↗ 提供了令人印象深刻的记忆功能。4 月 16 日,OpenAI Codex 的新版本 ↗也加入了记忆功能。
其实对记忆的定义比较广泛。绝大多数做法比较“原始”,比如他们基本上是基于一个或多个文本文件,让 LLM 在后台总结、更新这些文件。 也有一些“学院派”的努力,如 Nowledge Mem ↗ 围绕记忆的聚合与提炼构造了大量 primitives。
但是我开始思考一个问题:记忆在 LLM 工作流程中的角色是什么?
当我们让 LLM 在代码库中工作时,基本上是这样的流程:
- 人类提供指令或目的,也许还有约束和过程。此时模型拥有了一部分上下文。
- Agent 通过一些方式,收集其它的上下文,直到上下文足够大,达到某一界限
- 此时 Agent 开始思考怎么做,上下文进一步扩大到下一界限
- 最终,Agent build up 解决方案。
如果这么看,Memory 是一种上下文。上下文还包括代码库中的其他代码,Agent 中途向用户发起意图询问,等等。
在沟通这件事上,人们主要被两点所困扰。一是对方理解能力差,二是对方和自己没有默契(补全缺失的语境)。 LLM 还不够强大,有时候更冗余的上下文可以弥补这点,于是出现了 spec-driven vibe coding,这主要是通过大量(无论是手写还是 AIGC)类似 Specification 描述,确保 vibe coding 产物符合意图的编程方式。 同理,有时候人们希望得到默契,或者希望保持一致性,或是认为历时性的东西可以带来进步。因此,Memory 也加入了。
可以说,这两个做法都是相当正确的,实践已经证明了这样做确实可以带来成效。 但是问题是:这些做法意在让原本可能不正确的结果(vibe coding 产物)变得正确,因此,使得结果变得正确的做法也就不止这一种; 更甚,在这个问题上,应该做的是寻找最优的、确保可能不正确结果变得正确的这样一个做法。 这很好理解:如果我们只关心 LLM 能否输出正确的代码的话,我们只需要提供越来越细致的 Plan 和 Specification,最后干脆告诉他应该怎么写代码,他只负责调用工具,真正将这些代码应用到代码库中。
我知道这个(数学意义上的)“极限”推演可能 makes no sense,但是这能够证明:我们应该寻找有效的方式,而单纯地做到“让 LLM 输出结果变得正确”在这里不完全意味着有效。
Robert Englander 撰写了这篇文章:Engineering Alignment in Probabilistic Generation ↗。 他认为,软件工程的目标始终是正确性。LLM 改变了错误发生的方式。在概率性的生成过程中,正确性在不同层级之间的转换边界会发生降级(从意图到 Specification,从 Specification 到 CoT,从 CoT 到生成)。 这种降级表现为细微的语义偏移、隐性假设的填充和歧义的压缩。与传统系统的显性错误不同,LLM 生成的系统可能编译成功、通过所有测试、运行平稳,但在语义上已经偏离了最初的意图。
因此,在那篇文章中,作者进一步提出了五大平面,并强调治理他们之间的边界的重要性。但我想从另一个视角看待这个问题。 LLM 的本体论在于预测生成,这决定了 n-shot 是对他最有力的影响方式。换句话说,LLM 生成的内容很大程度上不能用“正确性”去衡量; 正确性并不存在于 LLM 中,而是存在于上下文中。某个上下文意味着一些输出的概率分布,单纯地依靠上下文的极限施压不能让 LLM 100% 生成某种输出, 甚至不一定意味着在概率分布中,那些小概率的“幻觉”结果会更加“正确”。 正确性也不来源于“工程纪律”,Specification 事实上没有制造正确性,只是从一个错误转向另一个错误,在大量的螺旋式发展中趋于所谓的正确,每个正确实际上相对于每前一个 harness 都是“错误”,而这样的级数是不收敛的。
如果进一步分析下去,人的意图本身就带有不确定性,并且总存在无法用语言描述的实在界之剩余。这种观点在软件工程领域不一定受待见,甚至在自然科学领域都不一定。 但是,假如真的如此——我相信真的如此——那么,哪怕没有 LLM 辅助,人写的代码也势必不完全正确。
因此,我得出的结论是:正确性不位于 LLM,也不位于 Specification。它位于所有上下文的共同作用,并依赖于 Reviewer 的心理。
如果这是真的,这意味着什么?首先我们不能再寄正确性的希望于提供完整、不偏移的意图上。这是我提出 Stateful / Stateless context 的原因。 当我们写 Specification,或者将记忆引入上下文的时候,我们实际上在做一件事:将我们的意图进行了一步派生。 从 Intend 到 Specification 是那篇文章所说的一个转换边界,这里产生了语义的漂移,很好理解。 使用 Memory 时,同样放弃了“我”的意图的“马尔科夫链”-like 的性质,希望模型通过 Memory 获得对意图周延之后的理解。
这才是我觉得奇怪的地方。
我认为,上下文应该严格地只包括我的意图描述,而 LLM 被训练到应该具备这样的能力:他能够识别目前的上下文是否允许自己写出足够正确(而非完全正确)的代码; 如果不够,他应该使用 Explore, Ask User, context7 ↗ 之类的工具进一步获取上下文,直到自己有把握。
为什么这样做是对的?很显然,人们把过多的注意力放到了状态上。LLM 是黑箱,但人们在无意识中总认为 LLM 是函数式的黑箱,只要输入足够精确,输出也就能足够精确。 然而,LLM 本质上是薛定谔那样的黑箱。所以当这种性质浮现时,人们就会开始调整输入,进一步增加输入的精确性,或者引入历时性,引入十二个步骤,每一步都精确一下; 或者维护一个记忆文件,不断滚动、更新。这其实是一种神经症行为——我没有恶意,也没有在攻击任何人,因为根据精神分析的理论,任何人不是精神病就是神经症结构(也有可能是变态或孤独症,这些分类尚有争议)。 指出这一点的目的在于,指出这种行为只是在 satisfy 人自身的心理能量,符合人无意识中的欲望——再精确一点、再舒适一点,或者质疑、推翻、重建、再质疑。 这不意味着我所主张的做法就不包含这种因素,但是当 satisfication 成为实质上的驱力时,我不得不抽出身来思考。
Stateless 方法论#
经过上面的讨论,我的观点大概明了了:基于 Specification 或 Memory 补全上下文,在 vibe coding 领域,属于 stateful 的派生形做法。 而 stateless 的做法则指望 LLM 拥有决策能力,让“更加精确一点”或“质疑-重来”的循环留在 Agent Loop 内部。
一个典型的 Stateless Agent Loop 是这样的:
- 用户描述意图,可能还包括约束和过程
- 进行 Explore,获得代码方面的事实来源
- 就关键问题询问用户获得 clarification
- 重复 2-3 步
- Plan and build up
这个想法与 Claude Code 或 Claude Agent SDK 的想法不谋而合。如果您体验过一次标准的 Claude Code 作业,会知道这个流程相当丝滑: 您只需要具备基本语言能力,清晰(并且务必简洁)地描述任务,然后 Claude 就会搜集他所需要的信息。有时候,他会停下来问出一些问题, 这些问题相当牛逼,因为刚好覆盖了你最开始的描述忽略掉的一些点,甚至有时候会在问题中 suggest 更好的设计。 然后,他生成一份 Plan,并在几分钟内构建完毕。
当然,stateless 的做法也存在限制:
- 必须提供足够的获取上下文的方式。假如没有 Agent Skills 或者 context7 之类的东西,知识不会凭空产生,这将限制 LLM 的能力高度。
- LLM 必须配合我们。在尝试一些模型时,比如 OpenAI 的 Codex 系列,我明显感知到它并没有被训练地像这样工作,Spec-driven 的方案更适合它。我想这也是为什么 Codex App 最近加入了记忆功能。而 Claude 和 (GLM)[https://z.ai/ ↗] 之类的模型则很擅长 stateless loop。
现在,我们只剩下一个问题了:在我的观点中,我明确指出了“vibe coding”这一场景。别的场景呢?
- 广义 vibe coding:譬如项目管理、笔记整理这样的场景。用户总需要提供 Notion MCP 来读写 Notion 数据库,提供 Obsidian CLI 来访问 Obsidian 笔记内容,等等。在这种情况下,事实来源位于外部,模型的任务是对这些外部状态进行操作,它本质上与 vibe coding 没有区别,适用于我们上面的推理。
- 非广义 vibe coding:譬如个人相关的话题。在这件事上,除非您将您生活中所有琐碎的事务事无巨细地罗列在备忘录里,并且当您作出决定或成长时,会先更改备忘录然后告诉自己:bingo,经验+1,否则这种场景显然就是 Memory 会大显身手的场景。
最终#
所以,我们可以得出温和派的结论了。“Stateful” 是一种做法,“Stateless” 是缺失的另一种。在很长一段时间内,人们很少命名或赞美它,这是我写这篇文章的原因之一。 我并不认为两种方法之间有高下之分,Stateless 不适用的场景和限制条件都很清晰。我带着个人喜好推崇 Stateless 模式,就像人们推崇 Stateful 的实践一样,但我从不认为 Stateful 应该被摒弃、Stateless 才是完全正确的。 提出 Stateless 的目的仍在于:在试图确保 LLM 可能不正确的输出变得正确的实践中,找出更多的道路,因为这样才能向真理更进一步。