从构建Manus中学到的经验

Manus项目之初,我和我的团队面临一个关键决策:我们应该使用开源基础训练一个端到端的代理模型,还是在前沿模型的上下文学习能力上构建一个代理?

在我最初十年的NLP工作中,我们没有这种选择的奢侈。在BERT的时代(是的,已经七年了),模型必须经过微调——并在微调后才能转移到新任务。这个过程每次迭代可能需要数周时间,尽管与现在的LLM相比,这些模型非常小。对于快速发展的应用来说,特别是PMF之前,这种缓慢的反馈循环是一个致命的问题。这是我上一家初创公司的一个痛苦教训,我曾从头开始训练模型用于开放信息抽取和语义搜索。然后出现了GPT-3Flan-T5,我的内部模型一夜之间变得无关紧要。讽刺的是,这些相同的模型标志着上下文学习的开始——以及一条全新的前进道路。

这个用金钱换来的教训让选择变得清晰:Manus将押注于上下文工程。这使我们能够在几小时内而不是数周内发布改进,并保持我们的产品与底层模型正交:如果模型进步是上升的潮水,我们希望Manus是那艘船,而不是被海床固定的柱子

然而,上下文工程实际上并不简单。它是一门实验科学——我们重建了我们的代理框架四次,每次都在发现更好的上下文塑造方法之后。我们亲切地称这个手动的过程为“随机研究生下降”。它并不优雅,但有效。

这篇帖子分享了我们通过自己的“SGD”所达到的局部最优解。如果你正在构建自己的AI代理,我希望这些原则能帮助你更快地收敛。

1、围绕KV缓存设计

如果我只能选择一个指标,我会认为KV缓存命中率是生产阶段AI代理最重要的指标。它直接影响延迟和成本。为了理解为什么,让我们看看一个典型的代理是如何运作的:

在接收到用户输入后,代理会通过一系列工具使用来完成任务。在每次迭代中,模型根据当前上下文从预定义的动作空间中选择一个动作。然后该动作在环境(例如Manus的虚拟机沙箱)中执行以产生一个观察结果。动作和观察结果被附加到上下文中,形成下一次迭代的输入。这个循环持续进行,直到任务完成。

可以想象,随着每一步的进行,上下文会增长,而输出——通常是结构化的函数调用——则相对较短。这使得代理中的预填充解码之间的比率高度倾斜。例如,在Manus中,平均输入到输出的token比大约是100:1

幸运的是,具有相同前缀的上下文可以利用KV缓存,这大大减少了首次token的时间(TTFT)和推理成本——无论你是使用自托管模型还是调用推理API。而且我们谈论的不是小幅度的节省:例如,使用Claude Sonnet时,缓存的输入token成本为$0.30/MTok,而未缓存的为 $3/MTok——相差10倍。

从上下文工程的角度来看,提高KV缓存命中率涉及几个关键实践:

  1. 保持你的提示前缀稳定。由于LLM的自回归特性,即使是单个token的差异也会从该token开始使缓存失效。一个常见的错误是在系统提示的开头包含时间戳——尤其是精确到秒的时间戳。当然,这可以让模型告诉你当前时间,但它也会破坏你的缓存命中率。
  2. 让上下文只追加。避免修改之前的动作或观察结果。确保你的序列化是确定性的。许多编程语言和库在序列化JSON对象时并不保证稳定的键顺序,这可能会无声地破坏缓存。
  3. 在需要时显式标记缓存断点。一些模型提供者或推理框架不支持自动增量前缀缓存,而是需要在上下文中手动插入缓存断点。当分配这些断点时,应考虑潜在的缓存过期,并至少确保断点包括系统提示的结尾。

此外,如果你使用vLLM等框架自托管模型,请确保启用了前缀/提示缓存,并使用会话ID等技术确保分布式工作者之间的请求一致路由。

2、使用掩码,不要移除

随着代理获得更多功能,其动作空间自然变得更加复杂——通俗地说,工具的数量爆炸性增长。最近MCP的流行更是火上浇油。如果你允许用户配置的工具,相信我:有人最终会不可避免地将数百个神秘的工具插入到你精心策划的动作空间中。因此,模型更有可能选择错误的动作或采取低效的路径。简而言之,你的武装齐全的代理反而变得更“笨”。

一种自然的反应是设计一个动态的动作空间——或许使用类似RAG的方法按需加载工具。我们在Manus也尝试过。但我们的实验表明有一个明确的规则:除非绝对必要,避免在迭代过程中动态添加或删除工具。有两个主要原因:

  1. 在大多数LLM中,工具定义位于序列化后的上下文前面,通常在系统提示之前或之后。因此任何更改都会使所有后续动作和观察的KV缓存失效。
  2. 当之前的动作和观察仍然引用当前上下文中不再定义的工具时,模型会感到困惑。如果没有受限解码,这通常会导致模式违规或幻觉动作

为了在仍改善动作选择的同时解决这个问题,Manus使用了一个上下文感知的状态机来管理工具可用性。与其移除工具,不如在解码期间掩码token logit,根据当前上下文防止(或强制)选择某些动作。

在实践中,大多数模型提供者和推理框架都支持某种形式的响应预填充,这允许你在不修改工具定义的情况下约束动作空间。一般来说,有三种函数调用模式(我们将以NousResearch的Hermes格式为例):

  • Auto — 模型可以选择调用函数或不调用。通过预填充回复前缀实现:<|im_start|>assistant
  • Required — 模型必须调用一个函数,但选择不受限制。通过预填充到工具调用token实现:<|im_start|>assistant<tool_call>
  • Specified — 模型必须从特定子集调用一个函数。通过预填充到函数名的开始实现:<|im_start|>assistant<tool_call>{“name”: “browser_

通过这种方式,我们直接通过掩码token logit来约束动作选择。例如,当用户提供新的输入时,Manus必须立即回复而不是采取动作。我们还特意设计了动作名称,使其具有一致的前缀——例如,所有浏览器相关工具都以browser_开头,命令行工具以shell_开头。这使我们能够轻松地在给定状态下仅从特定组的工具中选择,无需使用有状态的logit处理器

这些设计有助于确保Manus代理循环保持稳定——即使在模型驱动的架构中也是如此。

3、将文件系统作为上下文

现代前沿LLM现在提供了128K token或更多的上下文窗口。但在实际的代理场景中,这往往不够,有时甚至是一种负担。有三个常见的痛点:

  1. 观察结果可能很大,尤其是在代理与非结构化数据(如网页或PDF)交互时。很容易超过上下文限制。
  2. 模型性能在某个上下文长度后往往会下降,即使窗口技术上支持它。
  3. 长输入很昂贵,即使有前缀缓存。你仍然要支付传输和预填充每个token的费用。

为了处理这些问题,许多代理系统实现了上下文截断或压缩策略。但过于激进的压缩不可避免地导致信息丢失。问题的根本在于:代理本质上必须基于所有先前状态预测下一步动作——你无法可靠地预测哪条观察结果会在十步后变得至关重要。从逻辑上讲,任何不可逆的压缩都带有风险。

这就是为什么我们在Manus中将文件系统视为终极上下文:大小无限,天生持久,并且可以直接由代理操作。模型学会了按需写入和读取文件——不仅将其作为存储,还作为结构化的外部化记忆。

我们的压缩策略始终设计为可恢复的。例如,只要保留URL,就可以从上下文中删除网页内容,如果文档路径在沙箱中仍然可用,则可以省略文档内容。这使Manus能够在不永久丢失信息的情况下缩小上下文长度。

在开发此功能时,我发现自己想象着 状态空间模型(SSM)在代理环境中有效工作需要什么。与Transformer不同,SSM缺乏完整的注意力机制,并且在处理长距离的反向依赖关系时存在困难。但如果它们能够掌握基于文件的记忆——将长期状态外部化而不是保留在上下文中——那么它们的速度和效率可能会解锁一类新的代理。代理的SSM可能是神经图灵机的真实继承者。

4、通过复述操纵注意力

如果你曾经使用过Manus,你可能注意到一些有趣的现象:在处理复杂任务时,它倾向于创建一个todo.md文件,并随着任务的进展逐步更新它,勾选已完成的事项。

这不是仅仅可爱的举动——这是有意设计的机制来操纵注意力

Manus中的典型任务平均需要约50次工具调用。这是一个很长的循环——并且由于Manus依赖LLM进行决策,它容易偏离主题或忘记早期目标,特别是在长上下文或复杂任务中。

通过不断重写待办列表,Manus正在将其目标反复复述到上下文的末尾。这将全局计划推入模型的近期注意力范围,避免“中间迷失”问题并减少目标错位。实际上,它使用自然语言来偏置自己的注意力朝向任务目标——而不需要特殊的架构变化。

5、保留错误的内容

代理会犯错误。这不是一个bug——这是现实。语言模型会产生幻觉,环境返回错误,外部工具行为异常,而且意想不到的边缘情况经常出现。在多步骤任务中,失败并不是例外;它是循环的一部分。

然而,一种常见的冲动是隐藏这些错误:清理痕迹、重试动作或重置模型的状态并让它依靠神奇的“温度”。这感觉更安全、更受控。但这付出的代价是:抹去失败会消除证据。没有证据,模型就无法适应。

根据我们的经验,改善代理行为最有效的方法之一是出人意料的简单:在上下文中保留错误的步骤。当模型看到一个失败的动作——以及由此产生的观察结果或堆栈跟踪——它会隐式地更新其内部信念。这会使其先验远离类似的动作,从而降低重复相同错误的可能性。

事实上,我们认为错误恢复是真正代理行为的最清晰指标之一。然而,它在大多数学术研究和公开基准中仍然代表性不足,因为这些基准通常专注于理想条件下的任务成功。

6、不要少样本提示

少样本提示是一种常用的提升LLM输出的技术。但在代理系统中,它可能会以微妙的方式产生反效果。

语言模型是非常出色的模仿者;它们模仿上下文中的行为模式。如果你的上下文充满了类似的过去动作-观察对,模型会倾向于遵循该模式,即使它不再是最佳选择。

这在涉及重复决策或动作的任务中可能很危险。例如,当使用Manus帮助审查一批20份简历时,代理常常陷入节奏——只是因为上下文中能看到相似的动作。这会导致偏差、过度泛化,有时甚至会出现幻觉。

解决办法是增加多样性。Manus在动作和观察中引入少量结构化的变化——不同的序列化模板、替代措辞、顺序或格式中的轻微噪声。这种受控的随机性有助于打破模式并调整模型的注意力。

换句话说,不要让自己陷入少样本提示的困境。你的上下文越统一,你的代理就越脆弱。

7、结束语

上下文工程仍是一门新兴的科学——但对于代理系统来说,它已经变得必不可少。模型可能变得更强大、更快、更便宜,但没有任何数量的原始能力可以取代对记忆、环境和反馈的需求。你如何塑造上下文最终决定了你的代理的行为方式:它运行得多快,恢复得多好,以及它能扩展得多远。

在Manus,我们通过反复重写、死胡同和数百万用户的实际测试学到了这些经验。我们在这里分享的都不是普遍真理——但这些都是对我们有效的模式。如果它们能帮助你避免哪怕一次痛苦的迭代,那么这篇文章就完成了它的任务。

代理的未来将一次上下文构建而成。好好地构建它们吧。


原文链接:Context Engineering for AI Agents: Lessons from Building Manus

汇智网翻译整理,转载请标明出处