https://manus.im/zh-cn/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus
随着代理能力的增强,其行动空间自然变得更加复杂——简单来说,工具数量爆炸式增长。最近流行的MCP只会火上浇油。如果你允许用户自定义工具,相信我:总会有人将数百个神秘工具插入到你精心策划的行动空间中。结果,模型更可能选择错误的行动或采取低效的路径。简而言之,你武装过度的代理变得更加愚蠢。
一个自然的反应是设计一个动态行动空间——可能是使用类似于RAG的方法按需加载工具。我们在mAnus中也尝试过这种方法。但我们的实验表明了一个明确的规则:除非绝对必要,避免在迭代过程中动态添加或移除工具。这主要有两个原因:
1.在大多数LLM中,工具定义在序列化后位于上下文的前部,通常在系统提示之前或之后。因此任何更改都会使后续所有动作和观察的KV缓存失效。
2.当先前的动作和观察仍然引用当前上下文中不再定义的工具时,模型会感到困惑。如果没有约束解码,这通常会导致模式违规或幻觉动作。
为了解决这个问题并仍然改进动作选择,Manus使用上下文感知的状态机来管理工具可用性。它不是移除工具,而是在解码过程中掩蔽token的logits,以基于当前上下文阻止(或强制)选择某些动作。
在理解 Manus 的 掩蔽 logits 实践之前,我们先要知道这么做的背景是什么?
一、缓存
上下文缓存
调用大模型时,不同推理请求可能出现输入内容的重叠(例如多轮对话或对同一本书的多次提问)。上下文缓存(Context Cache)技术可以缓存这些请求的公共前缀,减少推理时的重复计算。这能提升响应速度,并在不影响回复效果的前提下降低您的使用成本。
为满足不同场景的需求,上下文缓存提供两种工作模式,可以根据对便捷性、确定性及成本的需求进行选择,这里我们主要讨论「隐式缓存」。
隐式缓存:此为自动模式,无需额外配置,且无法关闭,适合追求便捷的通用场景。系统会自动识别请求内容的公共前缀并进行缓存,但缓存命中率不确定。对命中缓存的部分,按输入 Token 标准单价的 20% 计费。
提升命中缓存的概率
隐式缓存的命中逻辑是判断不同请求的前缀是否存在重复内容。为提高命中概率,请将重复内容置于提示词开头,差异内容置于末尾。
文本模型:
假设系统已缓存”ABCD”,则请求”ABE”可能命中”AB”部分,而请求”BCD”则无法命中。
视觉理解模型:
对同一图像或视频进行多次提问:将图像或视频放在文本信息前会提高命中概率。
对不同图像或视频提问同一问题:将文本信息放在图像或视频前面会提高命中概率。
工具Token的痛点
openai工具调用协议
# 定义工具列表
tools = [
{
"type": "function",
"function": {
"name": "get_current_weather",
"description": "当你想查询指定城市的天气时非常有用。",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市或县区,比如北京市、杭州市、余杭区等。",
}
},
"required": ["location"],
},
},
},
]
# 模拟天气查询工具
def get_current_weather(arguments):
weather_conditions = ["晴天", "多云", "雨天"]
random_weather = random.choice(weather_conditions)
location = arguments["location"]
return f"{location}今天是{random_weather}。"
# 封装模型响应函数
def get_response(messages):
completion = client.chat.completions.create(
model="qwen-plus",
messages=messages,
tools=tools,
)
return completion
tools是作为输入请求(会被序列化并插入到提示词(prompt)上下文中)中的一部分传入大模型
工具或MCP多不重要,有超长的上下文支持,但会导致准确性幻觉问题。如果我们A请求选择支持工具ABC,B请求支持DEF,则KV失效。
上图的这段话讨论的是AI代理(Agent)在复杂工具环境下的行动空间管理问题,核心是解决“工具过多导致代理变笨”的困境。
二、Manus的实践
问题背景
- 工具爆炸:随着AI代理能力增强,可用工具数量激增(如MCP框架、用户自定义工具)
- 代理变笨:工具越多,代理反而越容易选错工具或走低效路径
- 动态加载的陷阱:看似合理的“按需加载工具”(类似RAG)方案,实则带来两个严重问题:
- KV缓存失效:工具定义通常在上下文开头,修改工具会使后续所有计算的缓存失效,大幅降低效率
- 模型困惑:当历史记录中引用了已被移除的工具,模型会产生幻觉或违反格式规范
Manus的解决方案
不移除工具,而是用“掩码技术”动态约束可用工具:
- 上下文感知状态机:根据当前状态决定哪些工具可用
- 掩蔽token的logits:通过数学方法“屏蔽”不可用工具对应的token
- 响应预填充:巧妙利用预填充技术约束模型输出
- 命名规范设计:工具命名使用统一前缀(如
browser_、shell_),便于批量约束
三种约束模式(以Hermes格式为例)
- 自动模式:模型可自由选择是否调用工具 → 预填充回复前缀
- 必需模式:模型必须调用工具,但可自由选择 → 预填充到工具调用令牌
- 指定模式:模型必须从特定工具子集中选择 → 预填充到函数名称开头(如
{"name": "browser_)
关键概念详细解释
1. 什么是 Logits?
简单理解:Logits是大模型输出的“原始分数”,表示模型对每个可能token的“偏好程度”。
技术详解:
- 在LLM中,模型最后一层输出的是未归一化的数值,这些就是logits
- 例如:模型考虑下一个token时,对”apple”的logit=5.2,”banana”的logit=3.8,”cat”的logit=-2.1
- 这些logits会通过softmax函数转换为概率:
P(token) = exp(logit) / Σexp(all_logits) - 关键特性:logit值越大,该token被选中的概率越高;logit为负无穷时,概率为0
from openai import OpenAI
client = OpenAI(api_key="your-api-key")
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "请用一个词描述天空的颜色"}
],
logit_bias={
# token ID 15043 = "blue",设置为100表示强烈偏好
"15043": 100,
# token ID 1315 = "red",设置为-100表示几乎禁止
"1315": -100
},
max_tokens=10
)
print(response.choices[0].message.content)
# 几乎肯定会输出 "blue" 而不是 "red"
在文章中的意义:
当Manus说“掩蔽token的logits”,指的是将不可用工具对应token的logits设为负无穷,使模型完全不可能选择这些工具。
2. 什么是 “掩蔽token的logits”?
简单理解:这是一种“软性屏蔽”技术,在不修改模型结构的情况下,强制模型忽略某些选项。
技术详解:
# 伪代码示例:掩蔽logits original_logits = model.get_next_token_logits() # 原始logits mask = create_mask(available_tools) # 创建掩码:可用工具为1,不可用为0 masked_logits = original_logits + (mask - 1) * 1e9 # 不可用工具的logits变成极小值 next_token = sample_from_logits(masked_logits) # 采样时不可用工具概率≈0
在文章中的应用场景:
- 当用户新输入消息时,Manus需要立即回复而非执行工具 → 掩蔽所有工具调用token的logits
- 当处于“需要搜索”状态时,只允许浏览器工具 → 掩蔽所有非
browser_前缀工具的logits - 不修改工具定义:工具仍在上下文中,只是被“数学屏蔽”了
优势:
- ✅ 保持KV缓存有效(不修改上下文)
- ✅ 避免模型困惑(历史引用仍有效)
- ✅ 响应速度快(无需重新加载上下文)
3. 什么是 “响应预填充”?
简单理解:在让模型生成响应前,预先“写好”响应的开头部分,引导模型按特定格式继续生成。
技术详解:
- 预填充(Prefill):将部分响应内容直接插入到生成序列的开头
- 强制格式:通过预填充JSON结构、函数名前缀等,约束模型输出格式
- 与logits掩码协同:预填充 + logits掩码 = 精确控制生成内容
- 豆包火山大模型-续写
- 阿里云百炼大模型-续写
文章中的三种模式详解:
自动模式(Auto)
// 预填充内容: "" // 模型可自由选择: // - 直接回复用户 // - 或生成工具调用
- 实现:仅预填充空字符串或回复前缀
- 用途:让模型自主决定是否需要工具
必需模式(Required)
// 预填充内容:
"{"name": ""
// 模型必须:
// 1. 生成有效的JSON
// 2. 填写函数名称
// 3. 不能直接回复用户
- 实现:预填充到工具调用开始令牌
- 用途:强制模型调用工具,但不限制具体哪个工具
指定模式(Specified)
// 预填充内容:
"{"name": "browser_
// 模型必须:
// 1. 从browser_开头的工具中选择
// 2. 不能选择shell_或其他工具
// 3. 例如只能生成"browser_search"或"browser_scrape"
- 实现:预填充到函数名称开头
- 用途:精确限制工具选择范围
命名规范的重要性:
文章提到设计browser_、shell_等统一前缀,这样只需预填充"{"name": "browser_就能批量约束所有浏览器工具,无需逐个设置logits掩码。
三、核心价值总结
Manus的实践揭示了一个反直觉的AI工程原则:
“不是工具越少越好,而是要让模型‘看不见’不该用的工具”
传统思路:❌ 移除不用的工具 → 破坏上下文,降低性能
Manus方案:✅ 保留所有工具但“遮住眼睛” → 保持效率,避免混淆
这种技术的意义:
- 性能优化:避免KV缓存失效,推理速度提升2-5倍
- 认知稳定:模型不会因历史引用消失而困惑
- 工程友好:无需复杂的状态管理,用数学方法解决工程问题
- 成本节约:减少重复计算,降低API调用成本
实际应用场景:
- 客服机器人:用户新消息进来时,立即回复而非执行后台工具
- 代码助手:在编辑文件时,只暴露
file_相关工具,屏蔽browser_工具 - 数据分析Agent:在数据加载阶段,只允许
data_前缀工具
这种“掩码+预填充”的组合拳,代表了现代AI代理架构的先进实践——用软件工程思维解决AI认知问题,而非简单堆砌工具。




发表评论