在开发复杂的 AI Agent 框架时,我们经常会遇到一个棘手的问题:如何优雅地管理模型有限的上下文窗口,同时让 Agent 拥有长期的“记忆”? 如果直接将所有对话历史丢给大模型,不仅会导致 Token 成本飙升,还会引发幻觉和“人设偏移”。本文将深入剖析一个工程化的高性能上下文(Context)模块设计,从窗口压缩、记忆提取、脏数据清洗到多层级提示词的构建,带你硬核拆解背后的机制。
1. Context 模块:如何在边界内跳舞?
在使用大语言模型时,我们面临的第一道墙就是“上下文窗口限制”。面对几千上万 tokens 的对话历史,如何取舍?
1.1 上下文窗口压缩机制
为了保证核心信息不丢失,我们设计了包含快速路径检查、结构化压缩和迭代降级的多级防御机制:
Plaintext
┌─────────────────────────────────────────┐
│ 输入: 完整消息历史 + 上下文窗口限制 │
└────────────────┬────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 1️⃣ 快速路径检查(是否跳过压缩) │
│ • 消息太少?→ 直接返回 │
│ • 估算 token < 窗口?→ 直接返回 │
└────────────────┬────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 2️⃣ 结构化压缩(ShapeHistory) │
│ │
│ 保留结构: │
│ ┌─────────────────┐ │
│ │ [System] │ ← 永远保留 │
│ ├─────────────────┤ │
│ │ [First User] │ ← 保留初始意图 │
│ ├─────────────────┤ │
│ │ [Summary] │ ← 中间历史摘要 │
│ ├─────────────────┤ │
│ │ [Last N Turns] │ ← 最近对话细节 │
│ └─────────────────┘ │
└────────────────┬────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 3️⃣ 迭代降级(预算不足时) │
│ keepLast: 20 → 19 → ... → 3 │
│ 直到: 估算 token < 窗口 或 达到下限 │
└────────────────┬────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 输出: 压缩后的消息列表 │
└─────────────────────────────────────────┘
❓ 问题:为什么必须采用这种特定的结构化保留方案?
💡 结果与原因分析:
大模型的注意力机制决定了文本首尾的信息更容易被记住(首尾效应)。我们对历史记录切片保留,是为了防止以下致命错误:
| 模块保留区域 | 核心作用 | 丢失可能导致的灾难后果 |
|---|---|---|
| System | 定义 Agent 角色、核心能力与安全约束 | 人设偏移,模型忘记自己是谁、能调用什么工具。 |
| First User | 记录用户的最原始、初始意图 | 当多轮纠偏后用户说“按照最开始的要求做”,模型将无据可依。 |
| Summary | 中间漫长历史的浓缩(高价值/低成本) | 上下文断层。用 100 tokens 概括 2000 tokens 的进展。 |
| Last N turns | 保留最近的对话细节与执行结果 | 模型变成“金鱼”,无法衔接用户针对上一条回复提出的具体修改。 |
2. 上下文总结系统:对抗信息流失
上文提到了 [Summary] 模块。那么,如何确保在压缩中间历史时,关键的排错经验和用户偏好不被大模型忽略?
❓ 问题:简单的一句话总结往往会丢失关键步骤,如何提高总结的保真度?
💡 结果:引入“两阶段思维链(CoT)”的设计。
我们强制要求总结模型先进行分析(Phase 1),再输出结果(Phase 2)。
- 第一阶段(
<analysis>): 防止模型“上来就写结论”。强制其按时间线梳理文件变更、用户纠正和遇到的 Error。 - 第二阶段(
<summary>): 基于分析提炼最终状态,输出真正对续写有用的信息。
Plaintext
// 总结系统的 Prompt 核心设计
Phase 1 — Write a chronological analysis inside <analysis> tags:
- 梳理对话的时间线
- 记录每一次用户的纠偏、决策和偏好变更
- 追踪文件的读取、修改或创建
- 记录遭遇的报错、阻塞及其解决方案
Phase 2 — Write the final summary inside <summary> tags:
- 将分析提炼为后续对话必须知道的内容
- 优先保留用户的决策和纠偏
- 包含当前任务状态和下一步计划
工程化策略保障:
为了确保该过程高效且稳定,该请求通常定向给推理速度快的小模型,设置低温度(Low Temperature)以保证事实性,并施加严格的长度限制。最重要的是多级防护机制:如果总结输出发生格式异常或质量极差,系统宁可返回空摘要,也绝不允许被污染的总结内容注入到主流程的上下文中。
3. 记忆持久化与冷压缩:从短期走向长期
Agent 的核心魅力在于“伴随成长”。除了单次对话的上下文,我们还需要跨会话的记忆持久化系统(如:踩坑经验、项目结构)。由于文件操作涉及并发,这里的工程实现必须足够严谨。
3.1 提取与追加链路
注意: 记忆提取时,我们会剔除 System Message,仅将用户的实际对话送入 LLM 进行“值得记住的事实”提取。
Plaintext
┌─────────────────────────────────────────────┐
│ 对话结束 / 上下文压缩前 │
└────────────────┬────────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 1️⃣ PersistLearnings: 提取知识 │
│ 处理: LLM 识别当前对话中"值得记住的事实" │
│ 输出: 追加到 MEMORY.md 或溢出到 auto-*.md │
└────────────────┬────────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 2️⃣ BoundedAppend: 控制文件大小(带 flock) │
│ • MEMORY.md ≤ 150 行 → 直接追加 │
│ • 超出 150 行 → 写入 auto-YYYY-MM-DD-xxx.md │
│ → MEMORY.md 只留一行指针 │
└─────────────────────────────────────────────┘
3.2 冷调用(Cold Consolidation)的设计
❓ 问题:随着时间的推移,auto-*.md 碎片文件越来越多,如何进行性能无损的垃圾回收(GC)和信息去重?
💡 结果:基于文件锁和冷却机制的“后台定时整理”。
我们设计了一套类似于日志轮转的触发合并机制:
Plaintext
┌─────────────────────────────────────┐
│ 触发点: 记忆追加后 / 定时任务 / 手动触发 │
└────────────────┬────────────────────┘
▼
┌─────────────────────────────────────┐
│ 1️⃣ 阈值检查 (文件数 ≥ 12?) │
│ 2️⃣ 冷却检查 (.memory_gc ModTime) │
│ • 距上次合并是否超过 7 天? │
│ 3️⃣ 获取文件锁 (flock) 避免并发冲突 │
│ 4️⃣ 执行 LLM 合并/去重/淘汰过时信息 │
│ 5️⃣ 原子写入新 MEMORY.md,删除旧碎片 │
│ 6️⃣ 更新 .memory_gc 时间戳,释放锁 │
└─────────────────────────────────────┘
这种设计完美地将耗时的 LLM 归纳任务与主交互流程解耦,通过 flock 保证了同一时间只有一个进程在修改记忆文件。
4. 上下文清洗漏斗:阻断脏数据污染
在使用大模型 API(尤其是 Tool Use 场景)时,非常容易遇到由于版本更迭或网络截断导致的孤立消息块。如果不进行清洗直接发给 OpenAI/Anthropic,通常会触发 400 错误。
我们将发送给 API 前的历史记录经过四阶段清洗漏斗:
Plaintext
原始消息列表
│
▼
┌─────────────────┐
│ 阶段 1: 丢弃无效消息 (shouldDrop)
│ 过滤旧版 Tool 角色、无用的报错回复
└────────┬────────┘
▼
┌─────────────────┐
│ 阶段 2: 合并同角色 (mergeConsecutiveRoles)
│ 严格保证 user/assistant 依次交替
└────────┬────────┘
▼
┌─────────────────┐
│ 阶段 3: 剥离孤立工具块(stripOrphanedToolPairs)
│ 确保 tool_use ↔ tool_result 完美 1:1 配对
└────────┬────────┘
▼
┌─────────────────┐
│ 阶段 4: 二次合并 (处理阶段 3 产生的新空隙)
└────────┬────────┘
▼
✅ 干净、符合 API 协议的对话历史
实战案例解析:
我们来看下面这段包含历史遗留物和未完成调用的 Go 代码对象:
Go
messages = []client.Message{
{Role: "user", Content: text("写个排序函数")},
{Role: "assistant", Content: text("[tool_call: quicksort]")}, // 🔴 旧版占位符,需剔除
{Role: "tool", Content: text("legacy tool message")}, // 🔴 废弃角色,需剔除
{Role: "assistant", Content: blocks([
{Type: "tool_use", ID: "call_1", Name: "search"}, // 🔹 有效调用
{Type: "tool_use", ID: "call_2", Name: "calc"}, // 🔴 孤儿(无后续结果)
])},
{Role: "user", Content: blocks([
{Type: "tool_result", ToolUseID: "call_1", Content: "结果..."}, // ✅ 成功配对
])},
{Role: "assistant", Content: text("The request was cancelled...")}, // 🔴 无效友善错误
}
经过漏斗清洗后,所有废弃角色、未能闭环的 call_2 以及友善错误都会被精准摘除。
最终输出的完美结构:
User (指令) -> Assistant (发起有效工具) -> User (返回有效结果)。
5. 提示词组装流水线:静动分离与网关缓存
优秀的提示词不仅仅是“写一段长文本”,而应该是模块化的数据结构。为了极致压榨 Prompt Caching(提示词缓存)的价值,我们将 Prompt 划分为三层:
Go
type PromptParts struct {
// 1️⃣ 静态部分(命中 Gateway 缓存)
// 包含角色设定、工具列表、技能表、记忆处理指南等
System string
// 2️⃣ 会话级稳定部分(写入缓存断点前)
// 包含当前 Session 事实、不可变的元数据(StickyContext)
StableContext string
// 3️⃣ 轮次级易变部分(写入缓存断点后)
// 包含动态 Memory、当前时间、目录信息、MCP (Model Context Protocol) 状态
VolatileContext string
}
为什么需要这样分层?
由于诸如 Current date 或 Memory 每次对话都在变,如果把它们和系统设定混在一起,会导致大模型 API 的上下文缓存完全失效。
通过将 System 和 StableContext 前置,并利用 `` 机制,我们能够让底层的 Agent 框架在处理复杂任务时,极大降低 Token 消费,并显著缩短首字响应时间(TTFB)。
组装后的最终形态示例:
Plaintext
[System Prompt]
(由网关深度缓存的几十 K Tokens)
─────────────────────────────────
[User Message]
## Session Facts
Session: sess_123
Task: fix_build
## Context
Current date: 2024-01-18 10:30 PST
Working directory: /Users/name/project
## Memory
- Project uses Go 1.21
## Instructions
- Run tests with: go test ./...
[用户实际请求:修复构建错误]
6. 总结与展望:构建拥有“真正心智”的 AI Agent
核心机制回顾
构建一个生产级别的 AI Agent,绝非简单地将大模型 API 封装进一个循环中。回顾本文探讨的四大核心模块,我们实际上是在为 Agent 搭建一个完整且高容错的“数字大脑中枢”:
- 上下文窗口机制(The Boundary): 通过结构化保留(System / First User / Summary / Last N Turns)与迭代降级策略,在模型 Token 限制的物理边界内,实现了关键意图与人设的“零丢失”。
- 高质量总结系统(The Compressor): 摒弃粗暴的文本截断,引入“分析-总结”的两阶段思维链(CoT),在压缩历史的同时,高保真地留存了用户的决策路径与排错经验。
- 记忆持久化与冷调用(The Long-term Memory): 巧妙结合了热数据追加(BoundedAppend)与冷数据后台回收(ConsolidateMemory)。利用文件锁(flock)与冷却阈值,将极其耗时的 LLM 归纳任务与主流程剥离,实现了跨会话经验的平滑累积。
- 数据清洗与缓存流水线(The Pipeline): 严苛的四阶段“清洗漏斗”彻底根治了工具调用(Tool Use)场景下极易触发的 API 400 错误;而“静动分离”的三层提示词构建(System / Stable / Volatile),则将 Prompt Caching 的经济价值压榨到了极致。
未来展望 (Outlook)
随着底层模型能力的跨越式发展,Agent 的上下文管理架构也将迎来新的演进。站在当前的设计之上,我们还有以下几个极具潜力的探索方向:
-
从文本记忆到向量检索(RAG)的无缝融合: 目前的
MEMORY.md机制非常适合记录项目级的高价值事实。未来,随着记忆体量的爆发,引入轻量级向量数据库配合语义检索,将让 Agent 拥有在海量历史踩坑记录中“精准捞针”的能力。 -
多智能体(Multi-Agent)的记忆隔离与共享:
当系统演进为“开发 Agent”、“测试 Agent”与“运维 Agent”协同作业时,如何设计一个基于事件总线(Event Bus)的分布式全局记忆池?如何利用读写锁保证多 Agent 并发更新记忆时的数据一致性?这将是下一代架构的重点。
-
自适应的注意力重定向(Attention Optimization):
随着支持百万级 Token 上下文窗口的基座模型日益普及,未来的挑战将从“如何压缩放不下的历史”转变为“如何引导模型关注超长文本中最核心的细节”。Prompt 架构的设计将更加侧重于动态插入注意力锚点。
-
人设与能力的自我进化(Self-Evolving Persona):
让 Agent 通过分析长期的
auto-*.md记忆文件,定期反思并“自动重写”自己的一部分 Volatile System Prompt,真正实现越用越顺手、与人类开发者心智同频的“伴生型智能”。
结语:
AI Agent 的架构之美,往往隐藏在这些处理“脏活累活”的细节之中。希望这套从记忆提取、清洗到组装的全链路设计方案,能为你在构建复杂 AI 基础设施的道路上提供坚实的参考。