人与 Agent 的关系其实很有意思。我们要想指导好、用好 Agent,首先要理解:人在其中究竟扮演什么角色。
其实可以用“负责人和执行者”的关系来类比:负责人,就是我们,也就是人;执行者,就是 Agent。
执行者完成的是负责人想做的事情,标准也是负责人制定的。所以,本质上虽然是 Agent 在处理问题,但“问题是否已经处理完成”并不由 Agent 决定,而是由人决定。
为什么不是由 Agent 自己决定?因为目标、价值取舍和最终责任仍然在人的一侧。一个好的负责人会明确说明自己要什么、什么算完成;一个不好的负责人只有模糊要求,比如“我要这个”“我要那个”。更糟的是,他一边不给标准,一边把责任全部推给执行者,说“我只要结果”“你把事情做好,其他我不管”。这就是“五彩斑斓的黑”式的关系。
理解了什么是一个好的上下级关系,也就理解了如何使用 Agent:不是只把任务交出去,而是要给它一个可以判断的标准。
一、Goal:我们究竟想要什么
我们首先要给 Agent 一个目标。这个目标至少要包含两件事:
- 我们希望事情最终变成什么样;
- 我们明确不希望它变成什么样。
后者可以叫作 anti vision。它不是可有可无的补充,而是目标的边界。
怎么理解这个边界?在《How to Fix Your Life in One Day》一文中,作者不但提到了愿景,也提到了反愿景。所谓反愿景,就是你不希望这件事情最后变成什么样子。愿景与反愿景同样重要。想象一下,你为了得到某个东西付出了巨大的代价,最后虽然得到了它,得到的却远远不如失去的;即使目标达成了,但这同样是一种失败。
而“目标与非目标共同勾勒边界”这件事,也可以用雕塑来理解:
“Every block of stone has a statue inside it and it is the task of the sculptor to discover it.”
“每一块石头里都有一座雕像,雕塑家的任务是发现它。”
在这个比喻中,目标就在“石头中”。我们要做的是确定目标与非目标的边界:看到一部分不属于目标的石头,就用凿子把它敲掉;看到一部分属于目标的石头,就把它保留。本质上,我们在分类所有“目标石头”与“非目标石头”。
这样也就知道怎样描述目标的边界了:告诉 Agent,我们要做什么,以及我们不要做什么。
也许一开始,我们并不知道自己真正想要什么。最初的目标往往只是一个方向。也就是说,我们需要让目标在不断推进的过程中逐渐清晰,让边界逐渐明确。
如何做到这一点?需要把目标放进现实中检验。
现实世界会不断用失败告诉我们,目标大致是什么形状。虽然这话很老套,但失败确实是成功之母。
我们想做一款受欢迎的应用,起初以为“受欢迎”意味着功能多、界面好看;投放市场后,用户可能告诉我们,他们真正需要的是更快、更便宜,或者更值得信任。我们想造一枚能飞向银河的火箭,起初以为“能飞”就够了;一次次试飞会告诉我们,什么叫飞得更高、飞得更稳、飞得足够远。
每一次失败,都是现实让我们看见“我们以为的目标”和“真正的目标”之间的差距。但现实留下的是谜语:它通常只告诉你失败了、不对,却不会直接告诉你为什么不对。所以我们还需要从失败中分析原因,确定原因后再进行改进。
从某种意义上说,我们能不能成功,取决于三种能力:能够承受多少次失败、能以多快的速度完成一次试错,以及能否从每一次失败中归纳、总结出下一次更好的做法。只会承受失败而不会总结,失败只是重复;只会总结却无法继续承受失败,也等不到验证结论的机会;试错太慢,则有限的时间和资源会先被耗尽。
检验、修正、得到更多检验、继续修正。这样下来,目标的边界会越来越清晰。最开始目标可以是模糊的,但在不断推进的过程中,它应该越来越清晰,而不是越来越模糊。
二、Facts:当前世界究竟是什么样
在准确地告诉模型我们想要什么之后,简单问题往往已经可以被解决。但复杂问题,尤其是需要特定专业知识、项目上下文或特殊场景信息的问题,模型本身未必掌握这些知识,甚至可能超出当前上下文。
这时,模型接下来的核心需求就是 Facts:事实,或者说事实文档。
但为什么需要 Facts?让 Agent 直接去看代码、直接去现场寻找事实,不就可以了吗?当然可以,代码、数据库和运行环境本身就是事实的来源。但“直接看到所有事实”和“理解当前问题所需要的事实”并不是同一件事。
直接阅读代码时,Agent 面对的是未经整理的原始材料:信息分散在不同目录、模块和文件中,重要性也处在同一个平面上。它需要先自己判断该从哪里开始看、哪些内容相关、哪些只是噪音;即使最终找到了,也可能把大量无关细节带进上下文。项目越大,这个问题越明显。
Facts 文档并不是要复制一遍代码,而是要对事实做一次组织:分类,告诉 Agent 哪些事实属于同一个问题;分层,让它先看到全局架构,再进入相关模块;压缩,把大量细节概括成足以判断的结构;渐进式暴露,在需要时才继续展开到接口、实现和原始代码。这样,Agent 不必一开始把整个项目塞进上下文,也不会因为只看到了局部而误判整体。
那么,什么样的 Facts 才是好的?对于代码项目来说,直觉上的答案是清晰的架构文档。但如果我们把代码问题类比到其他复杂问题,并抽去具体场景,会得到什么?我觉得得到的不是“要有一份文档”这么简单,而是:对于事实的描述,需要有清晰的结构。
这句话是什么意思?可以先想一想,一份不好的事实文档是什么样。它可能每一行都在说一个点:这里有一个服务,那里有一张表,这个接口会调用那个接口,某个用户有某种权限。每句话单独看都是真实的,但点和点之间没有关系;它们是散乱的,没有章法的。
顺着这个反例再往前走,我们会发现,在 Facts 之上还有更重要的东西,也就是事实的结构:事物和事物的关系。一个事情 A 为什么会影响事情 B?一个模块依赖什么,又被什么依赖?一个约束究竟作用在哪个边界?这些关系决定了 Agent 能否把事实用于判断,而不只是把它们当作一堆需要背诵的资料。
所以 Facts 不只是架构文档、模块边界、数据模型、接口约束和运行方式的集合。事实本身很重要,但在复杂问题里,事实如何被组织、事实之间如何相互连接,往往甚至比某一条孤立的事实更重要。散乱的事实会让 Agent 很难判断什么与当前问题相关;有结构的事实才能帮助它在正确的位置做正确的判断。
接着还能推出另一个原则:单一事实原则。想想我们写一个大型项目,显然,对于实现同一个功能的函数,我们应该尽量复用,而不是复制粘贴出两份。否则修改时就必须同时修改多个位置,复杂度会不断增加。
Facts 文档也是一样。如果我们需要记录某个事实,就应当有一个权威的来源,而不是在多个文档里分别写一份。否则事实变化时,就必须同时修改多个位置,复杂度会不断增加,最后还会得到彼此矛盾的描述。如果这样的 Facts 文档还不存在,那么就需要创建它。事实应该只有一个可靠的源头,这就是单一事实原则。
好了,现在有了 Goal,有了 Facts。对于平常的问题,模型似乎已经足以解决:只要设计一个循环,让模型不断向目标推进,同时更新 Facts,就能不断向目标推进。也许。
三、错误与 Feedback
前面说过,目标不是一次就能想清楚的:当现实结果与我们的预期不一致时,我们发现偏差、分析原因,再修正目标。
当 Agent 开始向目标推进时,同样会出现“结果与预期不一致”的情况,只是它表现得更直接:Agent 做出的结果没有达到我们想要的样子,我们把这种不一致称为错误。推进目标的模型本身并不会察觉到错误;等到我们检查模型的完成情况时,才发现它没有做成我们想让它做的事情。也许是因为描述不清楚,也许是模型不知道该怎么做;不管如何,错误发生了。
如果是好的上下级关系,或者好的人机关系,那么我们应该意识到:这并非只是因为模型太烂、能力不够。问题甚至不在于简单地追问“问题出在哪里”。问题在于,我们接下来怎么处理问题。
正确的做法是指出模型具体错在哪里,告诉它应该怎么做。于是这一次问题解决了。直到你开了一个新的 session,直到模型再次遇到同样的问题。
一个具有主观能动性的人,在犯错后会归纳错误原因、总结方法,避免下次再犯。对于 Agent,至少现在模型通常不会主动做这件事。所以我们要解决的是:不能让本次错误的处理只停留在当前对话里,而要让之后遇到同类问题的 Agent 也能使用这次经验。
再往前一步说,这不是简单地“记住一条错误”,而是让系统发现偏差、分析原因、沉淀经验,并在下一次行动中验证修正是否有效。现在我们才真正接触到了控制论中的核心概念:Feedback。
Feedback 的价值不在于责备执行者,而在于分析偏差的来源。这个偏差可能是目标没有说清,也可能是事实缺失或过期,也可能是 Agent 还不知道该如何完成某一种具体工作,还可能是它需要工具、权限、专业知识或更合适的模型。
但这里又会出现一个问题:沉淀下来的经验,究竟应该放在哪里?
四、Guide:经验应该沉淀在哪里
重新思考一下我们真正想解决的事情:我们想要的,是 Agent 不要在同一个问题上每次都犯同样的错误。于是一个很自然的想法出现了——也许需要一个类似 AGENTS.md 的文件,时刻放在 Agent 的上下文中,记录它每次犯过的错误,让它下次注意。
这个想法看起来很不错,但再往下推演一步:如果面对的是复杂问题,Agent 犯过的错误会持续累积,最终形成一份很长的文档。这份文档会不断挤占 Agent 的注意力空间。
我们不妨真的去分析一下 Agent 的上下文:此刻它正在服务器上操作数据库,但上下文中却塞着许多“代码里不要用临时变量命名”的经验;反过来,当它正在写代码时,又未必需要同时注意数据库删除时的操作约束。这些信息并不是错误,只是和当前的事情无关。既然无关,就理应在当前场景里被剔除。
所以,我们不应该维护一份包罗万象的错误清单,而应该让经验按问题和场景分开存放:做数据库操作时,加载数据库操作的经验;写代码时,加载代码工作的经验。现在最后一个核心概念终于出来了:Guide。
Guide 文档就是说明如何做好某一件事情的文档。这个事情也许不是我们最终要解决的复杂问题,但它是解决其中一个子问题的方法。因此,Guide 的本质是递归地解决复杂问题:先把一个大问题拆成子问题,再为每个子问题准备解决它所需要的上下文。
我们可以这样想:对于一个复杂事情,我们有 Goal、Facts、Feedback、Guide 四个要素;而对于这个复杂事情的子问题,同样也需要 Goal、Facts、Feedback、Guide 四个要素。大问题和小问题本质上没有区别,它们是近似分形的,可以无限向下递归。区别只在于,随着问题越来越简单,这些要素承载的内容越来越少,直到简单到不需要再单独维护它们。
Guide 把某一类场景所需的目标、事实、经验和操作方法放在一起,并只在这个场景真正出现时加载。
其中,Goal 对 Guide 同样必不可少:先说明我们希望这件事情变成什么样,也说明不希望它变成什么样。前者是目标,后者是边界。
然后是 Facts。这里单一事实原则又有用了:如果我们要告诉 Agent 当前情况是什么,应当引用权威的 Facts 文档,而不是在 Guide 中重写“当前情况”。如果 Facts 不存在,就创建它。从职责上说,Facts 负责描述事实,Guide 只负责说明如何行动。
接下来是 Feedback。Agent 在做这件事情时犯过的错误,以及从错误中总结出来的经验,应该沉淀在对应的 Guide 中。不过这不只是“避免哪些错误”:还应说明在这个场景里应该怎么做,又不应该怎么做。
最后一点才是具体的操作方法:这件事情一步一步要怎么做。这个顺序不是偶然的,而是按重要性排序的。
因此,一个好的 Guide 可以按这个结构组织:
- Goal 与 Anti-vision:这件子任务要达到什么结果,又要避免什么结果;
- Facts 的引用:应以哪些权威事实为准;
- 已沉淀的 Feedback:在这个场景中,应该怎么做,又不应该怎么做;
- 操作方法:具体如何执行。
到这里,你可能会想:这不就是 Skill 吗?把某类任务中总结出的反馈放进对应的 Skill,让 Agent 在做特定事情时加载对应的 Skill。这样它既不会重复犯同类错误,又不会把所有历史经验都塞进上下文里。
我不得不承认,这就是 Skill。可是它其实并不是什么全新的发明。在遥远的几年前,我们就会编写代码规范,告诉新人怎样改进项目;会编写数据库操作指南;会编写各种应用程序的使用文档。如果两年前有人跟我说“他给我写了个 Skill,让我看一下”,我大概只会笑出来。
所以,Skill 并非一个全新的东西,而是一个老事物的新名字:它就是关于某个东西的使用文档。不同的是,现在这份 Guide 不只是静态地躺在某个目录里,而是可以在 Agent 面对相应场景时被主动加载。我们也因此找到了经验应该沉淀的位置:不应把所有经验堆进一份全局文档,而应按问题和场景分类,沉淀进相应的 Guide / Skill,在需要时再取出来。
五、回到整体:一个持续校准的循环
现在回头看,我们并不是用一份巨大的提示词解决所有问题,而是在每一层为当前问题提供恰当上下文,也就是 Goal、Facts 与 Guide,再用 Feedback 持续淘汰错误、补全边界,让系统能够持续推进复杂问题的解决。
可以把它理解为一个循环:
Goal 定义方向与边界
↓
Facts 描述当前世界
↓
Guide 指导特定场景下的行动
↓
执行与验收
↓
Feedback 发现偏差,并更新 Goal / Facts / Guide
└──────────────────────────────────→ 下一轮行动
这就是一个好的开始,不是不断催促agent“把事情做好”,而是建立一个结构清晰的系统。我不想说一些很有大厂味的黑话,什么闭环啦之类的,虽然事情是这么回事。
正如我在前一篇文章所讲,我们要做的是建立一套解决复杂问题的系统。那么最重要的也就是确定这套系统里有哪些元素,goal facts guide和feedback正是构建整个系统的基本粒子。而这些元素是基于逻辑的推演产生的,也许在更高的维度它们只是某一个更为基本的东西的不同视角的投影,但它们是真是存在的而不是什么新造出来的名词。在本人写出这篇文章的当下,又有了诸如loop engineer/graph engineer这种奇异名字。我觉得harness engineer本身还算是一个比较好的概念,尽管没有那么清晰,但是往后这些被硬是制造出的名字在我看来就是一坨大便了。
这是一种什么现象?以前搞安全的时候,也经历过这种技术泡沫,或者说虚假的技术狂热。 那时候有很多人对于 syscall 这种技术,不断地用新的词汇去命名成各种 gate。但其实本质上都是一样的,并没有真正的新内容。我不觉得用一个已经有的思路在没有跨领域的情况下去结合一些已经有的元素产生的东西是新内容。
把老的东西改改,就当成新的拿过来大声炫耀,声称自己发明了全新的技术。这种虚假的狂热是不会带来真正的产出,也不会真正让个人能力得到提升的。
也许刚开始刚用的时候,会感觉“哇,好牛逼,这个 Agent 干得好快”,但最后留下来的一定只是一坨没有办法维护、也掌控不了的垃圾,最后全都腐烂了。这是因为概念的模糊会直接导致能力无法积累,由于没有理解结构,也就不能复用、迁移、维护、组合或修复最终产物。
如果能意识到 harness 这个词的模糊性,那么这种感觉是正确的。因为这个东西本身就没有被定义清楚,他的边界是不清晰的,或者说这个东西几乎囊括了所有可以囊括的东西,但是那些被囊括的东西明明本来就存在。难道没有ai之前就没有代码文档,就没有linter或者type checker等工具吗,那必然不是的。因此这只是对一些元素组合后的命名。一个术语如果只是命名了复杂现象,却没有揭示其结构、边界与组成要素,那么它不能替代对基本元素的理解。
难道我们在学的是linter/type checker 技术文档的意义吗?
只能说一半是,我们是在重新理解已有的东西在系统里的意义,并把它们分类。更进一步说,理解一个工具为什么有效,比单纯会用它更重要。理解了一类工具的作用,就可以在新的场景里创造同样有效的工具,而不只是等待别人再给它起一个新名字。这样,Goal、Facts、Guide 和 Feedback 就不只是几个概念,而是解决问题时可以反复使用的基石。之后无论面对什么新问题,都可以从这些基石出发,分析、组合,再构造出新的解决方案。
透过现象看到本质,理解事物与事物之间的关系是比追逐新的名词更重要的事情。