构建一个真正高效、智能的 AI 代理,其挑战远超于编写一个好的初始提示(Prompt)。成功的关键在于对"上下文(Context)“的持续、精心的设计与管理。上下文是模型进行思考、推理和学习的唯一依据。

以下是 Manus 项目中总结出的六大上下文工程核心原则,它们共同构成了一套完整的设计哲学。

原文:https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus


一、围绕 KV 缓存进行设计 (Design Around the KV-Cache)

核心思想:为了最大化效率和降低成本,上下文的构建应严格遵循"仅追加(Append-Only)“原则。

1. 技术概念:

  • KV 缓存 (KV-Cache):在大语言模型(基于 Transformer 架构)中,当模型要生成下一个词(token)时,它需要回顾并理解之前已经处理过的所有词。这个过程通过一种叫做"自注意力(Self-Attention)“的机制完成。

    为了进行自注意力计算,模型会为每个输入的词生成三个关键的数学向量:Query (Q), Key (K), 和 Value (V)

    • Key (K)Value (V):可以理解为对过去每个词的"编码表示”。一个词的 K 和 V 向量一旦计算出来,只要它在上下文中的位置和内容不变,它就永远是固定的。
    • KV 缓存:就是一种存储机制,它把模型已经计算过的所有词的 K 和 V 向量保存下来。它的核心目的就是避免重复计算。当模型需要处理更长的文本时,可以直接从这个"缓存"中读取已经算好的 K 和 V 向量,而无需从头再来。
  • 上下文 (Context):指的是在某一个时间点,输入给大语言模型的完整文本序列。对于一个 AI 代理(Agent)来说,这个上下文通常由一系列指令、动作和观察结果组成。

  • 缓存命中 (Cache Hit) 和缓存未命中 (Cache Miss)

    • 缓存命中 (Cache Hit):当模型处理一个新的、更长的上下文时,如果其开头部分与上一步的上下文完全相同,那么这部分相同的文本对应的 K 和 V 向量就可以直接从 KV 缓存中读取,无需重新计算。这就叫"缓存命中”。
    • 缓存未命中 (Cache Miss):对于上下文中新的、之前没有处理过的部分,或者因为上下文结构改变而导致无法使用缓存的部分,模型必须对它们进行完整的计算来生成 K 和 V 向量。这就叫"缓存未命中”。

2. 问题背景:

大语言模型的推理成本和延迟是核心瓶颈。一个常见的错误是认为可以随意修改或重组上下文,比如在前面插入一个动态变化的时间戳,或为了"整洁"而删除某些历史步骤。这些操作会破坏模型用于加速计算的"草稿纸"——KV 缓存

3. 解决方案与工作原理:

将每一次与模型的交互都视为一个不可变的历史记录。任何新的信息(动作、观察)都必须被追加到现有上下文的末尾,而不是在中间插入或修改。这样可以确保每次调用都有一个尽可能长、完全不变的"前缀",模型可以直接从缓存中加载这部分内容的计算结果,只需处理真正新增的部分。

详细实践要点

1)保持提示前缀稳定 (Keep your prompt prefix stable)

你以为你在做:[固定前缀] + [历史记录] + [新内容]
实际发生的问题:[每次都变的前缀] + [历史记录] + [新内容]

这里的"前缀"通常指系统提示(System Prompt),也就是你给模型设定的初始指令。

典型错误示例:时间戳就是罪魁祸首。假设你的系统提示是:

1
"现在是 2023-10-27 10:30:05。你是一个乐于助人的助手。..."

下一次调用时,它变成了:

1
"现在是 2023-10-27 10:30:08。你是一个乐于助人的助手。..."

看!虽然你只是想追加新内容,但因为时间戳变了,导致整个上下文的第一个 token 就变了。这会让上一次调用的所有 KV 缓存全部失效,命中率直接归零。

所以,这一点的意思是:要实现真正的"追加",你必须保证被追加的那个"母体"(即前缀和历史记录)是一字不差、完全稳定的。

2)使你的上下文仅追加 (Make your context append-only)

这一点分了两部分,后半部分尤其关键:

  • 避免修改之前的操作或观察:这是对"追加"原则的重申。
  • 确保你的序列化是确定性的 (Ensure your serialization is deterministic):这是个非常隐蔽的陷阱!

序列化的意思是把你程序中的数据结构(比如一个 JSON 对象)转换成一串文本(string),以便发送给模型。

不确定性的意思是很多编程语言在把 JSON 对象转成字符串时,不保证 key 的顺序。

例子: 假设你的 Action 1 是一个 JSON 对象:{ "tool": "calculator", "input": "2+2" }

  • 第一次调用时,它被序列化成字符串:'{"tool": "calculator", "input": "2+2"}'
  • 第二次调用时,虽然数据没变,但可能因为语言库的内部实现,它被序列化成了:'{"input": "2+2", "tool": "calculator"}'

对于你和你的程序来说,这两个 JSON 代表的是完全相同的信息。但对于只认文本流(token stream)的 LLM 来说,这是一个完全不同的字符串!这个微小的顺序变化,就会导致从 Action 1 开始的所有缓存全部失效。

所以,这一点的意思是:即使你的逻辑是"追加",你也要保证你用来表示数据的文本格式是100%稳定不变的,否则追加就失去了意义。

3)在需要时明确标记缓存断点 (Mark cache breakpoints explicitly)

这一点是关于工具和框架的限制。

  • 你以为系统会自动帮你缓存:你发送了 [A, B, C],下次发送 [A, B, C, D],你期望系统自动缓存 [A, B, C]
  • 实际情况:有些推理框架(特别是自托管或某些特定 API)比较"笨",它不知道你的意图。你必须明确地告诉它:“嘿,[A, B, C] 这部分是可以被缓存的,请你记住它!”

这个"明确告知"的动作,就是插入一个缓存断点。

所以,这一点的意思是:你的"追加"策略需要你所使用的工具支持。如果工具不支持自动缓存,你就必须按照工具的规则手动标记,否则你的追加意图就无法转化为实际的缓存效果。

4. 这样做的好处:

  • 显著降低延迟:大大减少了"首个 token 生成时间(TTFT)",使代理响应更迅速。
  • 大幅削减成本:云服务商对缓存命中的 token 收费极低(例如便宜10倍),遵循此原则能最大化节省开支。
  • 提升系统吞吐量:更快的单次处理速度意味着更高的并发能力。

5. 关键引文:

“幸运的是,具有相同前缀的上下文可以利用 KV 缓存,这大大减少了首次生成标记时间(TTFT)和推理成本——无论你是使用自托管模型还是调用推理 API。(Fortunately, contexts with identical prefixes can take advantage of KV-cache, which drastically reduces time-to-first-token (TTFT) and inference cost…)”


二、遮蔽,而非移除 (Mask, Don’t Remove)

核心思想:当 AI 代理拥有大量工具时,不要通过动态地从上下文中添加或删除工具定义的方式来管理它们,而应该使用一种更智能的"遮蔽 (Masking)“技术。以及如何利用了模型提供商支持的"响应预填充 (Response Prefill)“功能,来高效地引导和约束模型的行为。

1. 问题背景:

当代理拥有的工具非常多时,一个自然的想法是动态地向上下文增删工具定义,以缩小模型的选择范围。但这种做法是"致命的”,因为它不仅会因为修改上下文前部内容而破坏 KV 缓存,还可能在模型看到历史中使用了某个已被移除的工具时,引发逻辑困惑和错误。

一个看似合理但错误的解决方案:动态加载工具

既然工具太多会干扰模型,那我能不能只在需要的时候才告诉模型有哪些工具可用?比如,当用户提到"计算"时,我才把"计算器"工具的定义加入到上下文中。当任务完成后,再把它移除。这听起来很像 RAG(检索增强生成)按需加载知识的思路。

为什么这是错的?

通过实验发现,这种"动态添加/移除工具"的方式会带来两个严重问题:

  1. 破坏 KV 缓存

    • 工具的定义(告诉模型这个工具叫什么、怎么用)通常被放在上下文的最前面,紧跟在系统提示(System Prompt)后面。
    • 想象一下上下文的结构:[系统提示] [工具A, 工具B的定义] [历史对话...]
    • 如果在中途你移除了工具A,上下文就变成了:[系统提示] [工具B的定义] [历史对话...]
    • 看到了吗?上下文的前缀发生了改变!这会导致从这个改动点之后的所有内容的 KV 缓存全部失效。每一次你增删工具,都相当于进行了一次高成本、高延迟的"冷启动”,完全抵消了动态加载带来的好处。
  2. 让模型感到困惑(逻辑上的矛盾)

    • 想象一下历史记录里有这样一步:Action: 使用了工具A。
    • 现在,你为了"优化",在新的上下文中把工具A的定义给删掉了。
    • 模型在处理新的上下文时,会读到历史记录里的"使用了工具A",但当它去查找工具A的定义时,却发现它不存在了!
    • 这会让模型彻底懵掉。它可能会开始"胡言乱语"(幻觉动作,hallucinated actions),或者生成不符合你预定格式的输出(模式违规,schema violations)。

2. 解决方案与工作原理:

既然不能从上下文中移除工具定义,那该怎么办呢?答案是:工具定义一直都在,但我们在模型生成答案的最后一步进行干预。

核心思想:让模型在完整的、包含所有工具定义的上下文下进行思考。但在它即将要从词汇表中选择下一个 token(也就是决定要用哪个工具)的那一瞬间,我们人为地进行干预。

通过"响应预填充"技术(如预先填好 {"name": "browser_)或使用 Logits 处理器来强行降低(或提升)特定工具对应 token 的选择概率。

1)什么是"遮蔽 Token Logits"?

  • Logits:在生成下一个 token 时,模型会为词汇表中的每一个词计算一个分数(logit),这个分数代表了选择这个词的可能性有多大。
  • 遮蔽 (Masking):就是我们强行修改这些分数。如果我们不希望模型选择"工具A",我们就把"工具A"对应 token 的 logit 分数设置成一个极小的值(比如负无穷)。这样一来,模型就绝对不会选择它了,即使它本来觉得"工具A"是最佳选项。反之,我们也可以强制它选择某个工具。

如何实现:用一个"上下文感知的状态机 (context-aware state machine)“来管理工具的可用性。这个状态机根据当前的对话状态,来决定哪些工具是"可用的”,哪些是"禁用的"。

例如,如果用户刚提供了航班信息,状态机就把"订票工具"设为可用,把"计算器工具"设为禁用。然后,在模型生成时,这个状态机的决策就会被用来"遮蔽"掉那些被禁用的工具对应的 token logits

2)响应预填充 (Response Prefilling)

这是一种非常有用的技术。通常情况下,我们给模型一个输入(Prompt),然后等待它从零开始一个字一个字地生成输出。而"响应预填充"允许我们替模型先把回复的开头写好,然后让模型接着这个开头继续写。

例如,我们给模型的输入是:“中国的首都是哪里?”

  • 普通调用:模型会自己生成:“中国的首都是北京。”
  • 使用预填充:我们可以给模型同样的输入,但同时告诉它:“你的回复必须以 中国的首都是 开头。” 然后模型只需要接着生成 “北京。” 就行了。

这个简单的功能,结合不同的预填充内容,就演变成了下面三种强大的"函数调用模式"。

比如: <|im_start|>assistant 是一个特殊的标记(token),告诉模型:“现在轮到你(助手)说话了。”

  • 自动 (Auto)

    • 预填充内容:只预填充 ...<|im_start|>assistant
    • 效果:我们只是告诉模型"到你说了",但没说你要说什么。所以模型可以自由选择是直接回答用户(生成普通文本),还是调用一个工具(生成 <tool_call> 格式的文本)。这是最自由的模式。
  • 必需 (Required)

    • 预填充内容:预填充到 ...<|im_start|>assistant<tool_call>
    • 效果:我们替模型把"我要调用工具"这句话 (<tool_call>) 都写好了。模型被强制必须调用一个工具,它唯一的选择是接下来该调用哪个工具。
  • 指定 (Specified)

    • 预填充内容:预填充到 ...<|im_start|>assistant<tool_call>{"name": "browser_
    • 效果:这是最强的约束!我们不仅强制模型必须调用工具,还把工具的名字都替它写了一半(比如 browser_)。现在,模型被强制必须从所有以 browser_ 开头的工具中选择一个。

3)如何利用这些模式实现智能"遮蔽"?

场景:当用户刚发来一条新消息时,代理(Manus)的规则是必须先回复用户(比如"好的,我正在处理"),而不是立刻去执行工具。

实现:他们可能会使用 Auto 模式,并结合 logits masking,“遮蔽"掉 <tool_call> 这个 token。这样,模型就无法选择调用工具,只能生成普通文本回复。

强制选择某一类工具

  • 设计:他们非常有远见地设计了工具的命名规则,比如所有浏览器工具都以 browser_ 开头,所有命令行工具都以 shell_ 开头。
  • 场景:假设代理的状态机判断当前需要操作浏览器。
  • 实现:他们直接使用 Specified 模式,将回复预填充到 ...<tool_call>{"name": "browser_。这样,模型甚至都不需要思考该用什么类型的工具,它的选择范围被直接缩小到了所有浏览器工具中。这是一种极其高效的引导。

4)“无需使用有状态的 logits 处理器"是什么意思?

  • 有状态的 logits 处理器 (Stateful logits processors):这是一种比较"重"的实现方式。它需要一个复杂的程序(处理器)在模型生成的每一步都去检查当前的状态,然后动态地计算哪些 token 应该被遮蔽。这需要维护很多状态信息,实现起来比较复杂。

  • Manus 的更优解:通过巧妙的命名规范(如 browser_)和预填充技术,他们在很多情况下避免了这种复杂的逐词(token-by-token)判断。他们可以直接在调用 API 的时候,通过设置预填充内容,一次性地、粗粒度地就把模型的选择范围给框定了。这比精细地去控制每一个 token 的 logits 要简单和高效得多。

3. 这样做的好处:

  • 保证了 KV 缓存的完整性,兼顾了性能与灵活性。
  • 避免了逻辑矛盾,模型不会因找不到历史工具的定义而困惑。
  • 实现了精确的动态控制,可以在恰当的时机引导模型做出正确的决策。

4. 关键引文:

“在大多数 LLMs 中,工具定义在序列化后位于上下文的前部… 因此,任何更改都会使所有后续动作和观察的 KV 缓存失效。(In most LLMs, tool definitions live near the front of the context after serialization… so any change will invalidate the KV-cache for all subsequent actions and observations.)”


三、将文件系统用作上下文 (Use the File System as Context)

核心思想:与其试图把所有信息都塞进模型有限的上下文窗口,不如把文件系统当作一个无限大、可持久化的"外部记忆"或"扩展上下文”,并教会 AI 代理自己去管理和使用这个外部记忆。

1. 问题背景:

前沿模型的上下文窗口虽大,但在处理网页、PDF、代码库等海量非结构化数据时仍捉襟见肘。强行塞入长上下文不仅成本高昂,还会因"迷失在中间"效应导致模型性能下降。而传统的截断或摘要方法则存在不可逆的信息丢失风险。

  • 观察内容过大 (Observations can be huge)

    • 场景:代理需要"阅读"一个网页、一份 PDF 报告或者一段代码库。这些内容的 token 数量可能轻易就达到几十万甚至上百万,一次就足以撑爆任何模型的上下文窗口。
    • 痛点:你无法把所有原始信息都提供给模型。
  • 长上下文性能下降 (Performance degradation)

    • 现象:著名的"迷失在中间 (Lost in the Middle)“问题。当上下文非常长时,模型对放在开头和结尾的信息记得最清楚,但很容易忘记或忽略中间部分的信息。
    • 痛点:即使你勉强把所有信息都塞进去了,模型的推理能力和准确性也会下降。它可能会"看不到"中间的关键信息。
  • 成本高昂 (Long inputs are expensive)

    • 原因:模型 API 的计费是按 token 数量来的。即使有 KV 缓存,每次调用时,新追加的那部分内容(Prefill)仍然需要付费。如果每次追加的内容都很长(比如一个网页的 HTML),成本会迅速飙升。
    • 痛点:维持一个巨大的上下文窗口在经济上是不可持续的。

2. 解决方案与工作原理:

改变范式,不再将上下文作为唯一的"信息容器”。当遇到海量数据时,让代理调用 writeFile 工具将其存为本地文件。在上下文中,只保留一个轻量级的"指针”(如文件路径或URL)。当后续需要时,代理可以再调用 readFile 等工具来访问这些信息。

传统解决方案的局限性:压缩与截断

面对上述问题,常见的解决方案是"想办法把上下文变短":

  • 截断 (Truncation):简单粗暴地砍掉最前面或中间的内容。
  • 压缩 (Compression):用一个总结(summary)来代替冗长的原文。

为什么这些方法有根本性的风险?

  • 信息丢失是不可逆的。你永远无法预知,被你砍掉或总结掉的某一个细节,会不会在十步之后成为解决问题的关键。
  • 代理的本质:AI 代理需要基于所有历史状态来做决策。任何信息的丢失都可能导致它做出错误的判断。比如,你删掉了一个早期的错误尝试,模型可能在后面又犯一遍同样的错误。

Manus 的解决方案:文件系统即终极上下文

既然把所有东西都记在模型的"短期记忆"(上下文窗口)里既不可靠又昂贵,Manus 团队提出了一种更优雅的范式。

将文件系统(File System)提升到"上下文"的地位。

好处

  • 大小无限 (Unlimited in size):硬盘空间几乎是无限的,可以存下任何大小的网页、PDF 或日志。
  • 持久化 (Persistent by nature):即使代理程序重启,信息也不会丢失。它就像一个真正的大脑,记忆是永久的。
  • 代理可直接操作 (Directly operable):最关键的一点!教会模型使用 writeFile, readFile, ls 等工具。

如何工作

  1. 当代理获得一个巨大的观察结果(比如爬取了一个网页),它不会把整个网页内容塞进下一次的上下文中。
  2. 相反,它会调用 writeFile 工具,把这个网页内容保存成一个文件,例如 webpage_content.html
  3. 在它的"短期记忆"(即实际的上下文窗口)里,它只需要记录一个简短的信息:“我已将网页内容保存在 webpage_content.html 文件中。”
  4. 当后续步骤需要用到网页中的某个具体信息时,代理会自己决定调用 readFile 工具去读取这个文件(或者文件的一部分),找到所需信息,然后再进行下一步操作。

从"存储"到"外部化记忆"的升华

  • 存储 (Storage):是被动地存放东西。
  • 外部化记忆 (Externalized Memory):主动地、有结构地去组织、存取和利用信息。

模型学会了把不重要的、冗长的细节"卸载"到文件系统,并在需要时再"加载"回来。这极大地解放了它宝贵的、有限的上下文窗口,让其可以专注于当前最重要的任务逻辑和短期思考。

3. 这样做的好处:

  • 突破了上下文长度限制,赋予代理近乎无限的记忆容量。
  • 降低了成本和延迟,因为上下文本身可以保持非常简短。
  • 避免了信息丢失,因为所有原始数据都被"可恢复地"存储着。

4. 关键引文:

“这就是为什么我们将文件系统视为 Manus 中的终极上下文:大小无限,具有持久性,并且可以由代理自身直接操作。(That’s why we treat the file system as the ultimate context in Manus: unlimited in size, persistent by nature, and directly operable by the agent itself.)”


四、通过复述操控注意力 (Manipulate Attention Through Recitation)

核心思想:通过在上下文的末尾周期性地"复述"或"重申"核心任务目标,可以强制模型重新关注全局计划,防止其在复杂的长任务中"分心"或"跑偏"。

1. 问题背景:

一个复杂的任务可能需要几十甚至上百步(文中提到平均50次工具调用)。在如此漫长的执行链条中,模型很容易犯和人类一样的错误,长任务中的"目标漂移"。

  • 忘记初衷:执行到后面,忘记了最初的、最高级别的目标是什么。
  • 偏离主题 (Drifting off-topic):被某个中间步骤的细节带跑偏,钻进牛角尖,而忽略了全局任务。
  • 技术原因:这与大模型的"迷失在中间 (Lost-in-the-middle)“问题直接相关。最重要的任务目标(Objectives)通常是在上下文的最开头定义的。随着上下文越来越长,这个最初的目标就被推到了遥远的"中间地带”,模型的注意力很容易从它身上移开。

2. 解决方案与工作原理:

让代理维护一个"待办事项列表"(如 todo.md 文件)。在执行关键步骤之间,让代理将更新后的完整待办事项列表作为一次新的"观察结果"追加到上下文的末尾。这个"复述"行为,将最重要的全局计划重新推到了模型注意力最集中的区域。

Manus 的具体实现:todo.md 文件

文本解释了 Manus 是如何巧妙地实现这个"复述"机制的:

  1. 创建 todo.md:对于复杂任务,代理首先会创建一个待办事项文件,把任务分解成几个步骤写进去。

  2. 逐步更新和重写:每完成一步,代理不是简单地追加日志,而是会读取 -> 修改 -> 重写整个 todo.md 文件。

    • 比如,它会把已完成的任务用 [x] 标记出来。
    • 然后,它把这个更新后的、完整的待办事项列表作为一次观察结果(Observation)添加到上下文的末尾。

这种做法的精妙之处

  1. 将全局计划推入近期注意力范围:通过在每次或每几次迭代时都把更新后的 todo.md 完整地展示一遍,最重要的"任务目标"和"当前进度"就永远出现在了上下文的最末尾。这是模型注意力最集中的地方,从而有效对抗了"迷失在中间"的问题。

  2. “复述"而非"修改历史”:注意,这个操作没有破坏 KV 缓存的原则。Objectives 在最开始的定义依然保留,保证了前缀的一致性。后面插入的 Objectives 是作为一次新的观察(Observation)出现的,它是一个全新的、被追加的内容。这在图示中看得非常清楚:右侧的 CONTEXT @ step n+1 相比 CONTEXT @ step n,只是在末尾追加了 [Objectives, Action 3, Observation 3],完美符合了"仅追加"原则。

  3. 利用自然语言进行自我引导:这是一种非常"原生"的引导方式。它不需要任何特殊的模型架构改动或复杂的 logits 处理。它仅仅是通过构造特定的 prompt(让模型不断地看自己的待办清单),就实现了类似人类"反思"和"回顾目标"的行为,从而"偏置 (bias)“了模型的注意力,使其始终聚焦于任务本身。

3. 这样做的好处:

  • 有效对抗"迷失在中间”,确保代理始终聚焦于最高级别的目标。
  • 提升了任务的连贯性和成功率,减少了因"分心"导致的错误。
  • 实现方式优雅,无需修改模型架构,仅通过 prompt engineering 即可实现。

4. 关键引文:

“通过不断重写待办事项列表,Manus 将其目标背诵到上下文的末尾。这将全局计划推入模型的近期注意力范围,避免了’中途丢失’问题… (By constantly rewriting the todo list, Manus is reciting its objectives into the end of the context. This pushes the global plan into the model’s recent attention span, avoiding ’lost-in-the-middle’ issues…)”


五、保留错误内容 (Keep the Wrong Stuff In)

核心思想:不要向 AI 代理隐藏它的失败记录。相反,应该把每一次失败的尝试、错误信息和堆栈跟踪(stack trace)都完整地保留在上下文中,因为这些"错误内容"是模型学习和改进的宝贵养料。

1. 问题背景:

一个常见的错误冲动是向模型"隐藏"失败,只给它看成功的路径,认为这样更"干净"。但这剥夺了模型从错误中学习的关键机会。

2. 解决方案与工作原理:

当一次动作失败时,将这次失败的动作连同环境返回的完整错误信息(如堆栈跟踪)一起追加到上下文中。这个"失败记录"为模型提供了强有力的反面教材。

3. 这样做的好处:

  • 实现了情境中学习(In-Context Learning),模型看到"此路不通"的证据后,会自然地避免重蹈覆辙。
  • 显著提升了代理的鲁棒性,使其具备了从失败中恢复并寻找新路径的真正智能。
  • 构建了完整的学习闭环,让代理更像一个学习者,而非单纯的执行者。

4. 关键引文:

“根据我们的经验,提升智能体行为最有效的方法之一简单得令人迷惑:将走错的弯路留在上下文中。(In our experience, one of the most effective ways to improve agent behavior is deceptively simple: leave the wrong turns in the context.)”


六、避免被少样本击倒 (Don’t Get Few-Shotted)

核心思想:在执行重复性任务时,要警惕大模型的"模仿"天性。如果上下文中充满了高度一致的成功范例,模型会陷入思维定式,机械地重复该模式,而不再进行批判性思考。因此,需要人为地在上下文中引入一些"可控的变数"来打破这种催眠般的节奏,保持模型的灵活性。

1. 问题背景:

大模型是出色的模仿者。如果一个任务包含大量重复步骤,上下文中充满的、高度一致的成功范例会像"少样本提示"一样"催眠"模型,使其陷入机械重复的"车辙",丧失灵活性。

少样本提示:从好事到坏事的转变

  • 它通常是好事:少样本提示是一种强大的技术,我们通过给模型几个"例子(shots)“来教会它如何执行任务。模型通过模仿这些例子,可以很好地完成类似任务。

在代理系统中如何变成坏事?

  • 模型是出色的模仿者:AI 代理在执行多步骤任务时,其上下文(Context)会自然地被过去的"动作-观察"对填满。

  • 陷入节奏 (Falls into a rhythm):想象一个代理要处理20份简历。它的上下文中会充满这样的记录:[打开简历1 -> 分析简历1], [打开简历2 -> 分析简历2], …, [打开简历20 -> 分析简历20]

  • 思维僵化:当处理完第20份后,下一个合理的步骤应该是"生成总结报告”。但由于上下文中充满了20个"打开-分析"的范例,模型被这种强大的模式"催眠"了。它可能会忽略任务已经完成的事实,而去尝试执行第21次"打开-分析",从而导致错误或幻觉。

这就是"被少样本击倒":你无意中用大量的重复样本,把自己(的代理)“训练"成了一个只会单调重复的"笨蛋”,陷入了思维的"车辙 (rut)"。

2. 解决方案与工作原理:

通过在观察结果的措辞、格式或元数据中引入一些微小的、结构化的变化,来打破上下文的完美一致性。例如,工具的成功返回信息可以有几个不同的模板随机选用,或者在返回信息中加入"这是第k次尝试"之类的元数据。

如何打破这种模式?

答案是引入**“受控的随机性 (controlled randomness)“或"结构化的变体 (structured variation)”**。

具体方法

  • 交替的措辞 (Alternate phrasing):工具的成功返回信息可以有几种不同的模板,随机选用。例如,有时返回 Content saved to ...,有时返回 Successfully wrote content to file ...

  • 不同的序列化模板 (Different serialization templates):如果你的动作是 JSON 格式,可以偶尔改变一下键的顺序(前提是这不会破坏你的 KV 缓存策略,或者你有意为之)。

  • 在观察中加入元数据:就像图中的例子一样,让工具的返回值更"智能”,包含一些关于当前状态的元信息(“这是第k次尝试”、“这个文件是只读的"等)。

目的:这些微小的、不影响任务逻辑的变化,足以"搅乱"上下文的完美一致性,防止模型进入"自动驾驶"模式。它让上下文变得不那么"整齐”,从而迫使模型在每一步都必须更仔细地"阅读和思考",而不是简单地"复制粘贴"上一步的行为。

3. 这样做的好处:

  • 防止模型思维僵化,使其在需要改变策略时能够及时反应。
  • 提升了代理的鲁棒性,使其在看似单调的任务中也能保持"警觉"。
  • 迫使模型进行真正的思考,而非简单的模式匹配和模仿。

4. 关键引文:

“语言模型是出色的模仿者;它们会模仿上下文中的行为模式。如果你的上下文充满了类似的过去动作-观察对,模型往往会遵循这种模式,即使这已不再是最优选择。(Language models are excellent mimics; they imitate the pattern of behavior in the context. If your context is full of similar past action-observation pairs, the model will tend to follow that pattern, even when it’s no longer optimal.)”