像 Rust Arena Allocator 一样管理上下文
真正决定系统生死的,是你如何管理那块有限的、昂贵的、有物理约束的内存空间。而 Rust 社区的 Arena Allocator,恰好是这个问题最精确的心智模型。
以下文章来自微信公众号“觉学社”,作者张汉东,发表于 2026 年 2 月。
昨天有位犀利哥(评论很犀利),他给了我很大启发:
“Stop "writing prompts", start managing like a Rust Arena Allocator.
The Context Window is a contiguous memory block. Append-only AMAP. Dynamic loading skills. Spatial locality of relevant content. Reasoning in Goldilocks Zone.
BTW: Pruning and RAG are tricks, not principles.
结合我昨天发的《Agent 设计的核心约束和原则》,再展开说说。
一、Prompt Engineering 的隐喻危机
"Prompt Engineering" 这个词害了整个行业。
它让人觉得,和 LLM 打交道的核心能力是"写出好的文字"。措辞精准、格式漂亮、Few-shot 示例到位。于是所有人都在研究怎么写更好的 system prompt,怎么措辞让模型"听话",怎么在一段文字里塞入更多指令。
这就像一个操作系统工程师说:"我的核心工作是写出漂亮的注释。"
不。你的核心工作是 内存管理。
当你运行一个 LLM Agent,一个需要执行几十轮工具调用、消耗 100K+ token 上下文、持续工作数小时的系统,"写提示词"只是最表层的事。真正决定系统生死的,是你如何管理那块有限的、昂贵的、有物理约束的内存空间。
而 Rust 社区的 Arena Allocator,恰好是这个问题最精确的心智模型。
二、Arena Allocator:30 秒速览
Rust 的 Arena Allocator(也叫 bump allocator)是内存管理的极简范式
预留一大块连续内存
↓
每次分配:指针往前推(bump),永不回退
↓
所有分配都在这块内存中连续排列
↓
需要释放时:整块一起丢(reset),不单独回收
它的核心特性:
-
Append-only:新分配的内存总在前一次的后面,指针只会向前。
-
连续内存块:所有数据在一块物理连续的区域内,没有碎片。
-
空间局部性(Spatial Locality):时间上相近的分配,在内存中物理相邻,对 CPU cache 极其友好。
-
按需分配(Lazy/Dynamic):不预分配所有可能用到的结构,用到时再 bump。
-
批量释放而非逐个回收:你不能 free 单个对象——要么全留着,要么整块 reset。
这些不是实现细节,是设计原则。每一条都精确映射到 LLM 上下文管理的核心约束。
三、Context Window 就是一块 Arena
把 LLM 的上下文窗口想象成一块预先分配好的连续内存:
┌──────────────────────────────────────────────────────────┐
│ 128K Token Arena │
│ │
│ [System Prompt] [Turn 1] [Turn 2] ... [Turn N] → │
│ ← 指针只会向前 bump bump_ptr │
│ │
│ 前缀相同 → KV Cache 命中($0.30/M) │
│ 前缀变了 → 全部重算($3.00/M) │
└──────────────────────────────────────────────────────────┘
这块"内存"有三条铁律:
铁律一:它有固定上限。128K token 就是 128K,不可能变成 256K。和物理内存一样,它是你最稀缺的资源。
铁律二:前缀改变的代价是灾难性的。KV Cache 的匹配规则是从第一个 token 逐一比对——第一个不同的 token 之后,后面全部 cache miss。这不只是"多花钱",DualPath 论文证明了在推理系统中,KV Cache 命中率 ≥ 95%,瓶颈在于把缓存搬回来的 I/O 带宽。一旦 prefix 变化,整条带宽被打满。
铁律三:注意力不均匀。LLM 对开头和末尾的 token 注意力最强,中间会衰减(Lost in the Middle)。你的上下文不只是"一块内存",它更像一块两端热、中间冷的 cache line。
这三条铁律,逼出了和 Arena Allocator 完全一致的设计原则。
四、五条 Arena 原则在上下文工程中的映射
原则 1:Append-Only AMAP
Arena Allocator 最核心的约束:指针只往前推,永不回退。
对上下文来说,这意味着:
✓ 在末尾追加 user / assistant / tool_result 消息
✓ 打 tag、读 status(只读元数据,不修改消息)
✗ 在中间插入新消息
✗ 重排消息顺序
✗ 修改 system prompt(任何原因)
✗ 删除消息后让后续消息"前移"
AMAP (As Much As Possible)。不是绝对不能回退(pruning 有时必须发生),而是把 append-only 作为默认模式,任何违反都需要付出代价并显式标记。
我曾尝试设计 "滑动窗口 + 动态组装上下文"的架构(记作 V1 版),每轮根据需要从记忆库中抽取片段插入上下文中间。这条路走进了死胡同。
根本矛盾:
滑动窗口要求:每轮上下文内容不同(按需组装)
KV Cache 要求:连续调用的前缀相同(追加增长)
这两个要求在数学上不可调和。
v1 的每一次"优化",都在把方案往 append-only 方向拉。当我们把动态部分缩到足够小来保住 cache,方案就退化成了带有额外复杂度的 append-only。演化路径是这样的:
"不做压缩" → "动态组装" → "破坏 cache" → "优化 cache"
→ "把动态部分推到末尾" → "把静态部分做成追加式"
→ "等等,这不就是 append-only 吗" → "是的"
这和 Rust 社区的经验一模一样。每次有人尝试用更"灵活"的 allocator 替代 arena,最后都会发现 arena 的约束就是它的力量。
你以为你在优化,其实你在重新发明 bump pointer。
我们现在的架构把这条原则刻进了“骨头”:System Prompt 构建一次不变,所有信息回流上下文的方式只有一条,作为 tool_result 追加到末尾。序列化用 BTreeMap 保证 key 顺序。
技能指南不改 system prompt 而是附着在 tool_result 上。连 Extension API 都在编译期强制 append-only。hook 的类型定义里根本没有 set_system() 或 insert_at() 方法。
原则 2:Dynamic Loading Skills(Demand Paging)
Arena Allocator 不预分配所有可能用到的对象。你不会在 arena 初始化时就把未来可能需要的每一种数据结构都实例化。那会把 arena 塞满,给真正需要的数据留不下空间。
对上下文来说,这是按需加载:
全预装(反模式):
[System Prompt: 身份 + 核心规则 + 浏览器指南(2k) + Shell指南(1k)
+ 代码重构指南(3k) + 数据库指南(2k) + 测试指南(2k) + 部署指南(1.5k)]
→ System Prompt 总计 15-30k tokens
→ Agent 写代码时,"浏览器操作指南"占了 2k 却完全用不上
→ 15k 指南中当前只需要 2k,其余 13k 全是噪声
按需加载(Demand Paging):
System Prompt 放一行摘要(~50 tokens/skill)
首次使用工具时 → 完整指南(~2K tokens)附着在 tool_result 上
后续使用(5-20 轮后)→ 关键提醒(~100 tokens)
这是操作系统 Demand Paging 的精确类比:
操作系统:
程序有 100MB 代码,但任何时刻只有 ~10MB 在物理内存
用到哪个页面才加载(page fault → load from disk)
长时间不用的页面被 swap out
Agent:
30k tokens 的技能知识,但任何一步只需要 ~3k
当前步骤需要什么技能才加载什么
agent 看到的是"在需要的时候,指南就出现在旁边"
判断标准:如果一段信息放进 system prompt 后,有 >30% 的 session 不会用到它,就不该预装。
我们把技能加载设计成三层:摘要始终在 system prompt(~50 tokens/技能,永不变,prefix 完全稳定)、完整指南在首次使用时注入、关键提醒在后续使用时注入。还加了错误触发重载:连续 2 次同类工具出错 → 自动重新注入完整指南。顺利时省 token,犯错时加强引导。
原则 3:Spatial Locality(空间局部性)
Arena Allocator 保证了一个天然属性:时间上相近的分配,在内存中物理相邻。这对 CPU cache 极其友好。当你访问一个对象时,它旁边的对象大概率也在同一条 cache line 里。
对上下文来说,空间局部性意味着:相关信息应该在 token 序列中物理相邻,而不是散落在上下文各处。
这解释了为什么把技能指南塞进 system prompt 是错的。agent 在第 42 轮执行浏览器操作时,需要看到浏览器指南,但指南在 system prompt 里(上下文最开头),和当前操作隔了几万个 token。LLM 的注意力在中间衰减(Lost in the Middle),这段距离就是死亡地带。
正确的做法:
... | tool_call(browser_navigate, url) | tool_result(浏览器操作指南 + 实际结果)
^^^^^^^^^^^^^^^^^
指南和它指导的操作紧密相邻 → 空间局部性
技能指南作为 tool_result 的一部分追加到末尾,和它指导的那次工具调用物理相邻。Agent 在做决策时,相关指南就在它刚刚"看到"的位置,这就是 cache hit。
同样的逻辑也适用于工作状态的复述。Agent 30 轮前设定的任务目标在哪?在上下文开头附近,已经沉入注意力衰减区。解法不是把它固定在开头(那会破坏 prefix),而是让 agent 主动把关键信息"搬到"末尾:
Agent 读取 todo.md → 工作状态作为 tool_result 出现在末尾
→ LLM 在做决策时直接"看到"当前进度
→ 不需要额外的"工作记忆"抽象
→ 复用已有的文件读写工具
这就是 Manus(百万用户级 Agent 产品)在生产中验证的"复述模式":通过复述操控注意力,比在 system prompt 中塞工作记忆更有效。 本质上就是 CPU prefetch 的上下文版本,主动把即将需要的数据搬到 cache 最热的位置。
原则 4:Reasoning in Goldilocks Zone
Arena Allocator 有一个最佳大小。太小会频繁 reset(丢弃所有数据重来),太大会浪费物理内存且降低 cache 效率(数据分散在更大的地址空间中,局部性变差)。
上下文同理。并非越大越好。
太空(20% usage):
→ 可能预装了太多无关信息
→ Agent 在噪声中迷失方向
太满(90%+ usage):
→ Pruning 频繁触发,丢失关键历史
→ Agent 变成金鱼——记不住 10 轮前做了什么
→ 系统在"保信息"和"留空间"之间挣扎
Goldilocks Zone(40-70% usage):
→ 上下文只包含当前决策需要的信息
→ 有足够空间给接下来的工具调用
→ Pruning 不触发或只做最轻量的 Soft Trim
→ 信噪比最优
维持 Goldilocks Zone 的手段是两层压缩:
Layer A: Agent 主动压缩
Agent 判断"这段研究/调试已完成" → tag 起点 → squash 为 summary
→ 压缩质量高:agent 知道什么是 signal vs noise
→ 如果 agent 自觉好,Layer B 可能永远不触发
Layer B: 系统自动 Pruning(安全网)
三阶段:Soft Trim → Hard Clear → 分级压缩
→ Agent 不自觉时的兜底机制
Agent 主动管理自己的"内存使用",这不是比喻。我们给 Agent 提供了 context_status 工具,让它看到自己的上下文仪表盘:
[Context Dashboard]
• Usage: 78.2% (100k/128k)
• Steps since tag: 35 (last: 'auth-refactor')
• Pruning status: Stage 1 approaching
• Est. turns left: ~12
Agent 看到 78% 使用率、距上次 checkpoint 35 步 → 决定主动压缩。Agent 看到 12% 使用率 → 继续工作。这不是 prompt engineering,这是 memory pressure monitoring。
原则 5:批量释放而非逐个回收
Arena Allocator 不支持逐个 free——你不能释放 arena 中间的某个对象然后让后面的对象"紧凑"过来。要释放,就整块 reset。
上下文的约束更严格:你甚至不能真正 reset,因为 reset 意味着丢失所有对话历史。但 arena 的 reset 思维模型仍然适用——当你必须"释放内存"时,单位不是单条消息,而是连续区间:
Agent squash:
tag("auth-start") → 工作 35 步 → squash("auth-start"到现在, summary)
→ 这 35 步被替换为一段 summary → 释放了大块连续空间
→ backup tag 保留原始历史(arena 的 snapshot 语义)
系统 Pruning:
不是"删掉第 17 条消息",而是"从 Turn 5 到 Turn 20 的 tool_result 全部缩减"
→ 连续区间操作,不产生碎片
这和 arena 的 reset 语义一致,批量操作连续区间,而非逐个回收离散对象。
五、Pruning 和 RAG 是补救手段,不是设计原则
原文中最犀利的一句话:**"Pruning and RAG are tricks, not principles."**
这不是在否定 pruning 和 RAG 的价值——它们在生产系统中不可或缺。但它们在概念层级上被高估了。
Pruning 是什么? 是你的"内存管理"失败后的止损操作。如果你的上下文布局足够好——按需加载、不预装垃圾、Agent 主动保持 Goldilocks Zone,pruning 触发的次数应该极少。就像一个 well-tuned 的 arena allocator 几乎不需要 reset:
如果 arena 频繁 reset → 说明你的 arena 太小,或者你分配了太多短命对象
如果 pruning 频繁触发 → 说明你预装了太多无关信息,或者 agent 不会管理上下文
我们的两层压缩模型把这个逻辑体现得很清楚:
Layer A(agent 主动压缩) → 根本不是 pruning,是 agent 自觉的上下文整理
Layer B(系统 pruning) → 安全网,理想情况下 永不触发
Layer B 的三个 Stage:
Stage 1 (Soft Trim) → 止血(缩减大 tool result)
Stage 2 (Hard Clear) → 截肢(旧 tool result 替换为占位符)
Stage 3 (分级压缩) → 器官移植(按事件类型差异化处理)
越到后面,信息损失越大。每一个 Stage 都是一次承认:"我们的布局没能阻止上下文膨胀。"如果 pruning 是你的核心策略,你已经输了。
RAG 是什么? 是在上下文中"page fault"后的磁盘读取。Agent 需要一条 20 轮前的决策记录,上下文里已经被 prune 掉了,于是调用 memory_search 从旁路存储中找回来。这是有价值的——它让 prune 掉的信息仍然可召回,而不是永久丢失。但它不应该是你的主要信息传递机制。
操作系统类比:
在内存中直接访问 → 1 纳秒(L1 cache hit)
从磁盘 page in → 10 毫秒(disk I/O)
如果你的系统频繁 page fault → 说明你的内存管理有问题
如果你的系统频繁 memory_search → 说明你的上下文布局有问题
我写的 v1 架构把 RAG 当成核心设计:每轮从记忆库中检索片段插入上下文。这意味着每一步都是 page fault。结果就是 KV Cache 完全失效。因为插入的片段每次都不同,prefix 永远不稳定。
v2 (我的新架构)的修正:"检索是 fallback,不是核心路径。" memory_search 是一个 Agent 按需调用的工具,返回结果作为 tool_result 追加到末尾。大部分信息应该已经在上下文里(因为 append-only + 审慎的 pruning),检索只在信息确实被 prune 掉之后才需要。
所以:
原则(布局):
Append-only → 前缀稳定 → Cache 命中
Demand Paging → 只加载需要的 → 不浪费空间
Spatial Locality → 相关信息相邻 → 注意力集中
Goldilocks Zone → 信噪比最优 → Agent 不迷失
补救手段(trick):
Pruning → 布局失效后的止损
RAG → 信息被 prune 后的恢复
先把布局做对,补救手段自然用得少。
六、一个完整的心智模型
把所有 Arena 原则组合起来,我们得到一个完整的上下文工程心智模型:
Context Window = Arena(128K token 连续内存块)
┌─ System Prompt ─────────────────────────────────┐
│ 身份 + 核心规则 + 工具 Schema + 技能摘要(索引) │ ← 不可变前缀
│ ~4K tokens, 构建一次永不改变 │ (arena 基地址)
└─────────────────────────────────────────────────┘
┌─ 对话历史 ──────────────────────────────────────┐
│ [Turn 1] [Turn 2] ... [Turn N] │ ← Append-only bump
│ 每轮追加 user + assistant + tool_result │ (指针只前进)
│ │
│ 当 agent 使用浏览器 → │
│ tool_result = 浏览器指南 + 实际结果 │ ← Spatial Locality
│ 指南和操作物理相邻 │ (相关数据同 cache line)
│ │
│ 当 agent 读 todo.md → │
│ 工作状态被"复述"到末尾 │ ← Recency Anchoring
│ │ (热数据 prefetch)
│ │
│ context_status 显示 78% usage → │ ← Memory Pressure
│ agent 主动 squash 释放空间 │ Monitoring
│ │
│ [...被 squash 的区间替换为 summary...] │ ← Batch Reset
│ │ (arena 区间释放)
└─────────────────────────────────────────────────┘
│
┌─ 旁路存储 ─────────▼──────────────────────────┐
│ Event Log(JSONL) 完整的磁盘备份 │ ← 磁盘 / Swap
│ memory_search 被 prune 的信息可召回 │ (page fault 恢复)
│ 文件系统 todo.md / decisions.md │ (外部记忆)
└─────────────────────────────────────────────────┘
你的工作不是"写好 system prompt"。你的工作是管理这块 arena。
布局决定了 cache 命中率。加载策略决定了空间利用率。局部性决定了注意力效率。压缩策略决定了信息保持率。这些不是语言学问题,不是修辞学问题,它们是内存管理问题。
同样的信息,放在上下文的不同位置、不同时机、以不同密度呈现,效果可以差 10 倍。不是因为模型"没理解"你的文字,而是因为你的布局违反了物理约束。
从今天开始,别说"我在写 prompt"了, 应该说"我在管理一块 128K token 的 arena" 。
附:Arena 原则 ↔ 本文的设计映射表
Arena Allocator 原则 Context Engineering 映射 我的架构中的具体实现
────────────────────────────────────────────────────────────────────────────
连续内存块 上下文窗口是固定大小的 C1: 128K tokens 是硬上限
token 序列
Append-only bump 消息只追加,不插入/重排 P1: KV Cache 前缀一致性
P2: Append-only 上下文
System prompt 构建一次不变
所有回流通过 tool_result
按需分配 不预装可推迟的信息 P3: 按需加载一切
(Lazy Allocation) 三层技能表示(摘要/完整/提醒)
Demand Paging 模式
空间局部性 相关信息在 token 序列 技能指南附着在 tool_result 上
(Spatial Locality) 中物理相邻 todo.md 复述模式(P5 末尾复述)
最优大小 信噪比最优的上下文占用 Goldilocks Zone(40-70%)
(Goldilocks Zone) context_status 仪表盘
两层压缩模型(A 主动 + B 兜底)
批量释放 区间操作,不逐条回收 Agent squash(tag → summary)
(Batch Reset) 系统 Pruning 三阶段
不产生碎片
不可回退性 Pruning 代价 = 全价 Cache TTL 感知:
(No Individual Free) cache miss 只在 cache 已过期时执行
让 pruning 成本 = 0
──────────────────────────────────────────────────────────────────────
Pruning = 止损 布局失效后的补救 Layer B: 系统 Pruning(安全网)
RAG = Page Fault 信息被 prune 后的恢复 memory_search 按需检索
不是核心路径 lex/vec/hyde 类型化查询
开放原子旋武开源社区(简称“旋武社区”)是由开放原子开源基金会孵化及运营的技术社区,致力于在中国推广和发展Rust编程语言生态,推动Rust在操作系统、终端设备、安全技术、基础软件等关键领域的产业落地,构建安全、可靠、高效的软件基础设施。
更多推荐



所有评论(0)