核心观点很直接:Manus 从一开始就选择了基于前沿模型的上下文学习能力来构建 Agent,而不是自己训练端到端模型。原因是——在产品还没找到 PMF 之前,微调模型的反馈循环太慢了,动辄几周一次迭代。而上下文工程的方式,几小时就能交付改进,而且产品和底层模型是解耦的。用他们的话说,"如果模型进步是上涨的潮水,我们希望 Manus 是那条船,而不是固定在海床上的柱子。"

这个思路对独立开发者来说其实更适用——我们大概率没有资源去训练自己的模型,所以怎么把上下文工程做好,就是核心竞争力。

围绕 KV 缓存来设计一切

如果只能关注一个指标,他们选的是 KV-cache 命中率。这个指标直接决定了延迟和成本。

Agent 的工作方式是每次迭代都把之前的动作和观察结果追加到上下文里,然后让模型决定下一步。这意味着输入会越来越长,而输出(通常是一个函数调用)很短。Manus 的平均输入输出 token 比是 100:1。这种场景下,KV 缓存的价值就非常大了——以 Claude Sonnet 为例,缓存命中的 token 成本是 0.30 美元/百万,未命中是 3 美元/百万,差了 10 倍。

几个实操要点:

  • 保持提示前缀稳定。一个常见的坑是在系统提示开头放时间戳(精确到秒那种),看起来很贴心,但会导致每次请求的前缀都不一样,缓存全部失效。
  • 上下文只追加,不修改。不要回头改之前的动作或观察结果。还有一个隐蔽的问题:JSON 序列化时键的顺序不稳定,不同语言和库的行为不一样,会悄悄破坏缓存。
  • 手动标记缓存断点。有些推理框架不支持自动增量缓存,需要你手动插断点,至少要确保系统提示的结尾是一个断点。
  • 如果用 vLLM 自托管,记得开启前缀缓存,并用 session ID 做一致性路由。

工具太多怎么办:遮蔽而不是移除

Agent 的工具数量一多,模型就容易选错动作。特别是现在 MCP 协议火了之后,用户可以随意插入各种工具,你精心设计的动作空间分分钟被搞乱。

直觉上你会想动态加载工具——类似 RAG 的思路,按需检索。Manus 试过,结论是:除非绝对必要,不要在迭代过程中动态增删工具。原因有两个:

  1. 工具定义在序列化后通常在上下文最前面,改了就意味着后面所有内容的 KV 缓存全部失效。
  2. 之前的动作和观察还在引用已经不存在的工具定义,模型会困惑,导致幻觉或格式错误。

他们的解法是用上下文感知的状态机来管理工具可用性——工具定义一直在,但通过在解码阶段遮蔽 token 的 logits 来控制哪些工具可以被选择。

具体实现上,利用了响应预填充的三种模式:

  • 自动模式:模型自己决定调不调用函数,只预填充 <|im_start|>assistant
  • 必选模式:必须调用函数,预填充到 <|im_start|>assistant<tool_call>
  • 指定模式:只能从特定子集中选,预填充到函数名开头,比如 <tool_call>{"name": "browser_

这里有个很巧妙的设计:他们故意让所有工具名共享一致的前缀,比如浏览器相关的都叫 browser_xxx,命令行的都叫 shell_xxx。这样只需要预填充到前缀就能限制工具组,不需要有状态的 logits 处理器。我觉得这个命名约定的思路很值得借鉴。

把文件系统当作上下文来用

128K 的上下文窗口听起来很大,但在真实 Agent 场景里经常不够用。网页、PDF 这些非结构化数据一塞进来就爆了。而且超过一定长度后模型性能会下降,长输入的成本也高。

很多人的做法是截断或压缩上下文,但压缩就意味着信息丢失。问题是你没法预测哪个观察结果在十步之后会变得关键——任何不可逆的压缩都有风险。

Manus 的解法是把文件系统当成终极上下文。大小不受限,天然持久化,Agent 可以直接读写。模型学会了按需把信息写入文件、需要时再读取,文件系统既是存储也是结构化的外部记忆。他们的压缩策略也是可恢复的——比如网页内容被压缩掉了,但只要 URL 还在,就能重新获取。

这个思路其实和 Claude Code 的工作方式很像——它也会把中间状态写到文件里,然后在需要的时候再读回来。


说实话这篇文章让我重新审视了自己搭 Agent 时的一些做法。之前总觉得上下文管理就是拼 prompt,但实际上从缓存命中率、工具管理到外部记忆,每一层都有讲究。如果你也在搭建自己的 Agent,建议先把 KV 缓存命中率这个指标监控起来,这可能是性价比最高的优化起点。