AI智能体“上下文工程”实践:来自 Manus 项目的经验总结
转载:https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus
在启动 Manus (manus.im/app) 项目之初,我的团队面临一个关键抉择:究竟是基于开源基础模型训练一个端到端的智能体模型,还是在前沿大模型的“上下文学习”(In-Context Learning)能力基础之上构建智能体?
在我从事自然语言处理 (NLP) 的第一个十年里,我们没有这样“奢侈”的选择。在久远的 BERT (arxiv.org/abs/1810.04805) 时代(没错,已经七年了),模型必须经过微调和评估才能迁移到新的任务上。即使当时的模型比现在的大语言模型(LLM)小得多,这个过程每迭代一次也常常需要数周时间。对于快速发展的应用,尤其是在达到产品市场契合点 (PMF) 之前,如此缓慢的反馈周期是无法接受的。这是我上一家创业公司留下的痛苦教训,当时我从零开始训练模型以实现开放信息提取(Open Information Extraction)和语义搜索。后来,GPT-3 (arxiv.org/abs/2005.14165) 和 Flan-T5 (arxiv.org/abs/2210.11416) 横空出世,我内部开发的模型一夜之间便失去了竞争力。讽刺的是,正是这些模型开启了上下文学习(In-Context Learning)的新时代,也为我们指明了新的前进方向。
这次来之不易的教训使我们明确了选择:Manus 将侧重“上下文工程”(Context Engineering)。这让我们能够将改进的交付时间从数周缩短到数小时,并使我们的产品与底层模型保持独立:如果说模型进步是水涨船高,那么我们希望 Manus 成为那艘随波而动的船,而不是深陷海底的柱子。
尽管如此,实践证明上下文工程绝非易事。它是一门实验性科学——我们已经四次重构了我们的智能体框架,每一次都是在发现了更好的上下文构建方法之后进行的。我们亲切地将这种架构搜索、提示词调整和经验性猜测的手动过程称为“随机梯度下降法”(Stochastic Graduate Descent)。它虽然不够优雅,但确实有效。
本文将分享我们通过这种“随机梯度下降法”找到的局部最优解。如果你也在构建自己的 AI智能体,我希望这些原则能帮助你更快地收敛。
围绕 KV-缓存(KV-Cache)进行设计
如果只能选择一个指标,我会认为 KV-缓存命中率是生产阶段 AI 智能体最重要的单一指标。它直接影响延迟和成本。为了理解原因,我们来看看一个典型智能体 (arxiv.org/abs/2210.03629) 的运行方式:
智能体接收到用户输入后,会通过一系列工具使用来完成任务。在每次迭代中,模型会根据当前上下文从预定义的操作空间中选择一个动作。该动作随后在环境中执行(例如,Manus 的虚拟机沙箱)以生成一个观察结果。动作和观察结果会被附加到上下文中,形成下一次迭代的输入。这个循环持续进行,直到任务完成。
可以想象,上下文会随着每一步的执行而增长,而输出(通常是结构化的函数调用)则相对较短。这使得智能体中的预填充(prefilling)与解码(decoding)之间的比例,与聊天机器人相比,呈现出高度倾斜。例如,在 Manus 中,平均输入与输出 token 的比例大约是 100:1。
幸运的是,具有相同前缀的上下文可以利用 KV-缓存(medium.com/@joaolages/kv-caching-explained-276520203249),这大大减少了首个 token 的生成时间(TTFT)和推理成本——无论你是使用自托管模型还是调用推理 API。而且我们谈论的不是节省一小部分:以 Claude Sonnet 为例,缓存的输入 token 成本为 0.30 美元/百万 token,而未缓存的则需要 3 美元/百万 token——相差 10 倍。
从上下文工程的角度来看,提高 KV-缓存命中率涉及以下几个关键实践:
-
保持提示词前缀的稳定性。 由于大语言模型 (LLM) 的自回归(autoregressive)特性,即使是单个 token 的差异,也可能导致从该 token 开始的缓存失效。一个常见错误是在系统提示词的开头包含时间戳——特别是精确到秒的时间戳。当然,这可以让模型告诉你当前时间,但也会极大地降低你的缓存命中率。
-
确保上下文只追加不修改。 避免修改先前的操作或观察结果。确保你的序列化是确定性的。许多编程语言和库在序列化 JSON 对象时无法保证键的顺序稳定,这可能会悄无声息地破坏缓存。
-
在需要时明确标记缓存断点。 某些模型提供商或推理框架不支持自动增量前缀缓存,而是需要在上下文中手动插入缓存断点。在设置这些断点时,请考虑潜在的缓存过期,并且至少要确保断点包含系统提示词的末尾。
此外,如果你使用像 vLLM (github.com/vllm-project/vllm) 这样的框架来托管模型,请确保前缀/提示词缓存(docs.vllm.ai/en/stable/design/v1/prefix_caching.html)已启用,并且你正在使用会话 ID 等技术来确保请求在分布式工作节点之间保持一致地路由。
遮蔽,而非移除
随着智能体功能变得越来越强大,其操作空间自然也变得更加复杂——简单来说,就是工具的数量呈爆炸式增长。最近 MCP(modelcontextprotocol.io/introduction)的流行更是火上浇油。如果你允许用户配置工具,相信我:总会有人将数百个神秘工具插入到你精心策划的操作空间中。结果就是,模型更有可能选择错误的操作,或者采取低效的路径。简而言之,你的“全副武装”的智能体反而变得更笨了。
一个自然的反应是设计动态动作空间——也许使用类似 RAG (en.wikipedia.org/wiki/Retrieval-augmented_generation) 的方式按需加载工具。我们在 Manus 中也尝试过。但我们的实验表明了一个清晰的规则:除非绝对必要,否则避免在迭代过程中动态添加或移除工具。这主要有两个原因:
-
在大多数大语言模型 (LLM) 中,工具定义在序列化后位于上下文的前部,通常在系统提示词之前或之后。因此,任何更改都将使后续所有动作和观察结果的 KV-缓存失效。
-
当之前的动作和观察结果仍然引用当前上下文中未定义的工具时,模型会感到困惑。如果没有约束解码(constrained decoding),这通常会导致模式(schema)违规或幻想出的动作。
为了解决这个问题,同时仍然改进操作选择,Manus 使用了一种上下文感知的状态机(state machine)来管理工具的可用性。它不是移除工具,而是在解码时遮蔽(mask)token 逻辑值,以根据当前上下文阻止(或强制)选择某些操作。
在实践中,大多数模型提供商和推理框架都支持某种形式的响应预填充,这允许你在不修改工具定义的情况下约束操作空间。函数调用通常有三种模式(我们以 NousResearch 的 Hermes 格式 [github.com/NousResearch/Hermes-Function-Calling] 为例):
-
自动(Auto) – 模型可以选择是否调用函数。通过仅预填充回复前缀来实现:
<|im_start|>assistant
-
必需(Required) – 模型必须调用函数,但选择不受限制。通过预填充到工具调用 token 来实现:
<|im_start|>assistant<tool_call>
-
指定(Specified) –模型必须从特定子集中调用函数。通过预填充到函数名称的开头来实现:
<|im_start|>assistant<tool_call>{"name": “browser\_
通过这种方式,我们直接通过遮蔽 token逻辑值来约束操作选择。例如,当用户提供新输入时,Manus 必须立即回复而不是执行操作。我们还特意设计了具有一致前缀的操作名称——例如,所有与浏览器相关的工具都以 browser_
开头,命令行工具以 shell_
开头。这使我们能够轻松地在给定状态下强制智能体只能从特定工具组中选择,而无需使用有状态的逻辑值处理器。
这些设计有助于确保 Manus 智能体循环保持稳定——即使在模型驱动的架构下也是如此。
将文件系统用作上下文
如今前沿的大语言模型拥有 128K token 甚至更长的上下文窗口。但在实际的智能体应用场景中,这往往不够,有时甚至会成为一个负担。通常有三个痛点:
-
观察结果可能非常庞大,尤其是当智能体与网页或 PDF 等非结构化数据交互时,很容易超出上下文限制。
-
模型性能往往在超过一定上下文长度后下降,即使窗口技术上支持更长的上下文。
-
长输入成本高昂,即使有前缀缓存。你仍然需要为传输和预填充每个 token 付费。
为了解决这个问题,许多智能体系统会实施上下文截断或压缩策略。但是,过度激进的压缩不可避免地会导致信息丢失。这是一个根本性问题:智能体天生就需要根据所有先前的状态来预测下一个动作——而你无法可靠地预测十步之后哪一个观察结果可能变得至关重要。从逻辑角度来看,任何不可逆的压缩都带有风险。
这就是为什么在 Manus 中,我们将文件系统视为终极上下文:它大小无限、本质上是持久的,并且可以直接由智能体本身操作。模型学习按需写入和读取文件——将文件系统不仅用作存储,还用作结构化的外部化内存。
我们的压缩策略总是被设计为可恢复的。例如,只要保留 URL,网页内容就可以从上下文中删除;如果文档的路径在沙箱中仍然可用,其内容就可以省略。这使得 Manus 可以在不永久丢失信息的情况下缩短上下文长度。
在开发此功能时,我发现自己一直在思考,如何才能让状态空间模型(SSM)在智能体环境中有效工作。与 Transformer 不同,SSM 缺乏完整的注意力机制,并且在处理长距离向后依赖方面存在困难。但如果它们能够掌握基于文件的内存——将长期状态外部化而不是将其保存在上下文中——那么它们的速度和效率可能会开辟一类新的智能体。智能体 SSM 可能成为神经图灵机(Neural Turing Machines)的真正继承者。
通过复述(Recitation)操控注意力
如果你使用过 Manus,你可能会注意到一个有趣的现象:在处理复杂任务时,它倾向于创建一个 todo.md
文件——并随着任务的进展逐步更新,勾选已完成的项目。这不仅仅是“可爱”的行为,更是一种刻意为之的“注意力操控”机制。
在 Manus 中,一个典型的任务平均需要大约 50 次工具调用。这是一个漫长的循环——由于 Manus 依赖于大语言模型(LLM)进行决策,它很容易偏离主题或忘记早期的目标,尤其是在长上下文或复杂任务中。
通过不断重写待办事项列表,Manus 将其目标重复写入上下文的末尾。这会将全局计划推入模型的近期注意力范围,从而避免“迷失在中间”的问题,并减少目标错位。实际上,它正在使用自然语言来引导自己的注意力集中在任务目标上——而无需特殊的架构更改。
将“错误的数据”保留在上下文中
智能体(Agent)会犯错。这不是 Bug,而是现实。语言模型会产生幻觉,环境会返回错误,外部工具会表现异常,意想不到的极端情况也时常出现。在多步骤任务中,失败并非例外,而是循环的一部分。
然而,常见的本能是隐藏这些错误:清理轨迹、重试操作,或重置模型状态并将其留给神奇的“温度(temperature)”参数。这感觉更安全、更容易控制。但这会带来代价:抹去失败就等于消除了证据。而没有证据,模型就无法适应。
根据我们的经验,改进智能体行为最有效的方法之一出奇地简单:将错误的路径保留在上下文中。当模型看到失败的操作以及由此产生的观察结果或堆栈跟踪时,它会隐式地更新其内部信念。这使得它不再倾向于类似的操作,从而减少重复相同错误的机会。
事实上,我们相信错误恢复是真正智能体行为最清楚的指标之一。然而,这在大多数学术研究和公共基准测试中仍然关注不足,这些研究和测试通常侧重于理想条件下的任务成功率。
警惕少样本提示(Few-ShotPrompting)的陷阱
少样本提示(Few-Shot Prompting)是一种常见的提高大语言模型(LLM)输出质量的技术。但在智能体系统中,它可能以微妙的方式适得其反。
语言模型是非常优秀的模仿者;它们会模仿上下文中行为模式。如果你的上下文中充满了相似的过去“动作-观察”对,模型就会倾向于遵循这种模式,即使它不再是最优的。
这在涉及重复决策或动作的任务中可能很危险。例如,当使用 Manus 协助审核一批 20 份简历时,智能体常常会陷入一种固定的节奏——重复执行类似的操作,仅仅因为它在上下文中看到了这些模式。这会导致偏离、过度泛化,有时甚至产生幻觉。
解决方案是增加多样性。Manus 在动作和观察中引入少量结构化变量——不同的序列化模板、替换措辞、在顺序或格式上添加微小的噪声。这种受控的随机性有助于打破模式,并调整模型的注意力。
换句话说,不要让少样本提示将你引入困境。你的上下文越统一,你的智能体就越脆弱。
总结
上下文工程仍然是一门新兴科学,但对于智能体系统而言,它已经至关重要。模型可能变得越来越强大、越来越快、越来越便宜,但再多的原始能力也无法取代对记忆、环境和反馈的需求。你如何塑造上下文,最终决定了你的智能体如何表现:它运行的速度、恢复的能力以及扩展的程度。
在 Manus,我们通过反复重写、走入死胡同以及在数百万用户真实世界中的测试,汲取了这些经验教训。我们在此分享的并非普遍真理,但这些模式对我们来说是行之有效的。如果它们能帮助你避免哪怕一次痛苦的迭代,那么这篇文章就完成了它的使命。
智能体的未来将通过一次又一次地构建上下文来实现。请精心地进行这些工程。