Agent 上下文工程与记忆管理实战:Token 爆炸、上下文丢失、长期记忆

做 Agent 的人都踩过这几个坑:跑着跑着上下文爆了、模型忘了前面说过的话、多轮对话越答越傻、长任务到后面完全跑偏。根因不在模型,而在**上下文工程(Context Engineering)**没做好。这是 2025 年以来 CSDN/AI 技术圈最热的 Agent 落地话题。本文不讲空泛概念,全部围绕真实问题:现象 → 根因 → 可运行方案(代码 + 配置),覆盖 Token 预算、上下文裁剪、压缩摘要、结构化记忆、长期记忆(向量/文件)、多 Agent 共享上下文、常见坑,全程带可运行 Python 代码。


一、先看三个真实问题

1.1 问题一:跑到第 15 轮,上下文爆了

现象:
- 单次对话 20 轮后,请求报错:
  400 Bad Request: context_length_exceeded
  (maximum context length is 128000 tokens, your messages resulted in 131072)
- 或者费用暴涨(每轮都把历史全带上)

根因:
- 每轮把完整历史 + 所有工具返回结果无脑塞进 prompt
- 工具结果(网页抓取/日志/文件内容)动辄几万 token

1.2 问题二:模型"失忆"

现象:
- 用户第 3 轮说"用 Redis",第 10 轮 Agent 又选了 MySQL
- 前面已经确认过的约束(不能删数据)后面被无视
- 多 Agent 之间互相不知道对方干了什么

根因:
- 关键约束没被"固化",只是混在历史消息里
- 历史被截断时,重要的早期信息被裁掉

1.3 问题三:越答越傻(Lost in the Middle)

现象:
- 上下文越长,模型对中间内容的注意力越差
- 塞了 50 篇文档,模型只记得开头和结尾
- 回答开始"编"(幻觉),因为有效信息被稀释

根因:
- 长上下文 ≠ 好上下文
- 无关信息越多,信噪比越低
一句话总结:
上下文不是"越多越好",而是"越准越好、越省越好"。
Context Engineering = 在有限 token 预算里,放最该放的信息。

二、上下文工程的核心框架

2.1 四层结构

Agent 上下文 = 系统层 + 任务层 + 记忆层 + 工具层

1. 系统层(System Prompt)
   - 角色、规则、输出格式、红线
   - 稳定不变,放最前(模型最关注开头)

2. 任务层(Task State)
   - 当前目标、已完成步骤、待办、约束
   - 结构化!不要用自然语言散着放

3. 记忆层(Memory)
   - 短期:最近 N 轮对话
   - 长期:摘要/事实/用户偏好(存外部)
   - 按需检索,不是全塞

4. 工具层(Tool Results)
   - 工具返回结果(最占 token)
   - 必须裁剪/摘要后再入上下文

2.2 Token 预算分配(关键)

总预算 = 模型上下文窗口 × 安全系数(如 0.7)

示例(128k 窗口):
- 系统提示:≤ 2k
- 任务状态:≤ 4k
- 短期对话:≤ 8k(保留最近 N 轮)
- 检索/记忆:≤ 6k
- 工具结果:≤ 10k
- 输出预留:≤ 8k
→ 合计 ~38k,远低于 128k,留足余量

原则:
- 永远不要贴满窗口(留 30% 余量给输出和突发)
- 每层有硬上限,超了就裁剪

三、方案一:Token 预算与裁剪(解决"爆上下文")

3.1 计数与监控

# 依赖:pip install tiktoken
import tiktoken

def count_tokens(text: str, model: str = "gpt-4o") -> int:
    enc = tiktoken.encoding_for_model(model)
    return len(enc.encode(text))

class TokenBudget:
    def __init__(self, limit: int, reserve_output: int = 8000):
        self.limit = limit
        self.reserve = reserve_output
        self.used = 0

    @property
    def available(self) -> int:
        return self.limit - self.reserve - self.used

    def add(self, text: str, model: str = "gpt-4o") -> bool:
        t = count_tokens(text, model)
        if t > self.available:
            return False        # 超预算,拒绝加入
        self.used += t
        return True
# 使用:每次拼上下文前先算预算
budget = TokenBudget(limit=96000, reserve_output=12000)
budget.add(SYSTEM_PROMPT)
budget.add(task_state_text)
for msg in recent_messages:
    if not budget.add(msg["content"]):
        break        # 超了就不再加更早的

3.2 滑动窗口(保留最近 N 轮)

def sliding_window(messages: list, max_turns: int = 12) -> list:
    """保留 system + 最近 N 轮"""
    system = [m for m in messages if m["role"] == "system"]
    rest = [m for m in messages if m["role"] != "system"]
    return system + rest[-max_turns:]
要点:
- 系统提示永远保留(不参与滑动)
- 按"轮"而不是"条"滑(一轮含 user+assistant+tool)
- 关键约束不放在会被滑掉的位置 → 放系统层或任务状态

3.3 工具结果裁剪(最大 token 杀手)

def trim_tool_result(content: str, max_chars: int = 3000) -> str:
    """工具结果太长 → 头尾保留 + 中间摘要"""
    if len(content) <= max_chars:
        return content
    head = content[:max_chars // 3]
    tail = content[-(max_chars // 3):]
    mid = f"\n...[已截断 {len(content) - len(head) - len(tail)} 字符]...\n"
    return head + mid + tail
# 更优:让模型先摘要工具结果,再入上下文
def summarize_tool_result(content: str, llm, max_tokens: int = 500) -> str:
    prompt = f"""把下面工具结果压缩成不超过 {max_tokens} token 的要点,
只保留与任务相关的事实,丢弃样板内容:
{content}
"""
    return llm.invoke(prompt).content
适用场景:
- 网页抓取的 HTML → 只留正文
- 日志文件 → 只留关键错误行(grep error)
- 大 JSON → 只留需要的字段
- SQL 结果 → 只留前 N 行

四、方案二:压缩与摘要(解决"越聊越忘")

4.1 滚动摘要

def rolling_summary(messages: list, llm, keep_recent: int = 6) -> list:
    """
    历史太长时,把较早的对话压缩成一段摘要,
    保留最近几轮原文。
    """
    if len(messages) <= keep_recent:
        return messages

    old = messages[:-keep_recent]
    recent = messages[-keep_recent:]

    summary_prompt = f"""把下面的对话历史压缩成摘要,要求:
1. 保留:已确认的需求/约束/决策/关键事实
2. 丢弃:寒暄、重复、无结论的讨论
3. 输出格式:条列式,每条一句话
历史:
{format_messages(old)}
"""
    summary = llm.invoke(summary_prompt).content

    return [
        {"role": "system", "content": f"【历史摘要】\n{summary}"}
    ] + recent
效果:
- 20 轮 → 1 段摘要 + 最近 6 轮
- token 从 30k 降到 5k
- 关键决策不丢(在摘要里)

4.2 关键信息固化(防遗忘)

把"必须记住的事"从对话历史里提出来,单独存:
- 约束清单:不删数据、不改表结构、只允许 Redis
- 决策清单:选了方案 B、接口路径 /api/v2
- 事实清单:用户是 VIP、订单号 10086

这些放进"任务状态",每轮都注入(不参与滑动/裁剪)
class TaskState:
    """结构化任务状态:每轮都完整注入"""

    def __init__(self):
        self.goal = ""
        self.constraints = []      # 约束
        self.decisions = []        # 已做决策
        self.facts = []            # 关键事实
        self.done = []             # 已完成步骤
        self.todo = []             # 待办

    def to_prompt(self) -> str:
        return f"""【任务状态】
目标:{self.goal}
约束:{'; '.join(self.constraints) or '无'}
已决策:{'; '.join(self.decisions) or '无'}
关键事实:{'; '.join(self.facts) or '无'}
已完成:{'; '.join(self.done) or '无'}
待办:{'; '.join(self.todo) or '无'}
"""
# 每轮都放在 system 尾部(最稳定位置)
messages = [
    {"role": "system", "content": SYSTEM_PROMPT},
    {"role": "system", "content": task_state.to_prompt()},
    *recent_messages,
]

4.3 结构化 > 自然语言

对比:
❌ 散在对话里:
"上次说了要用 Redis,还有那个不能删数据的要求,
  对了接口是 /api/orders..."

✅ 结构化清单:
constraints: ["不删数据", "只读写接口"]
decisions: ["缓存用 Redis"]
facts: ["订单接口 /api/orders"]
原因:
- 结构化的信息密度高(token 少、歧义少)
- 每轮固定注入,不会被裁/被稀释
- 模型照着清单执行,可靠性高

五、方案三:长期记忆(跨会话不丢)

5.1 记忆分类

会话内记忆(短期):
- 最近对话 + 任务状态
- 存在内存里

跨会话记忆(长期):
1. 事实记忆:用户偏好、业务规则(键值/JSON)
2. 情景记忆:历史任务、做过什么(向量检索)
3. 程序记忆:成功的执行方式(模板/SOP)

5.2 事实记忆(结构化存储)

import json, os

class FactMemory:
    """简单键值记忆:存用户偏好/业务事实"""

    def __init__(self, path="memory.json"):
        self.path = path
        self.data = {}
        if os.path.exists(path):
            with open(path, encoding="utf-8") as f:
                self.data = json.load(f)

    def save(self):
        with open(self.path, "w", encoding="utf-8") as f:
            json.dump(self.data, f, ensure_ascii=False, indent=2)

    def remember(self, key: str, value):
        self.data[key] = value
        self.save()

    def recall(self, key: str, default=None):
        return self.data.get(key, default)
memory = FactMemory()
memory.remember("user.preferred_db", "PostgreSQL")
memory.remember("user.timezone", "Asia/Shanghai")

# 每轮注入
prefs = f"用户偏好:DB={memory.recall('user.preferred_db')}"

5.3 情景记忆(向量检索)

# 依赖:pip install chromadb
import chromadb

class EpisodicMemory:
    """向量库:存历史任务,按相似度检索"""

    def __init__(self, path="./chroma"):
        self.client = chromadb.PersistentClient(path=path)
        self.col = self.client.get_or_create_collection("episodes")

    def add(self, episode_id: str, content: str, metadata: dict = None):
        self.col.add(
            ids=[episode_id],
            documents=[content],
            metadatas=[metadata or {}],
        )

    def search(self, query: str, k: int = 3) -> list:
        results = self.col.query(query_texts=[query], n_results=k)
        return [doc for doc in results["documents"][0]]
# 使用:任务开始前检索相似历史
episodes = mem.search("怎么处理 MySQL 死锁")
context = "\n".join(episodes[:3])
# 把相关历史注入上下文(只注入相关的,不是全部)
关键:
- 只注入"检索命中的相关记忆",不是全量塞
- 检索分数低的不注入(信噪比)
- 记忆要定期清理/更新(过期事实别用)

5.4 记忆写入的时机

该写:
- 用户明确说"记住..."、"以后都用..."
- 确认了重要决策(方案、约束)
- 任务完成后的复盘(成功经验/踩坑)

不该写:
- 一次性临时信息
- 未经确认的猜测
- 敏感数据(密钥/密码,不落盘)

六、方案四:多 Agent 共享上下文

6.1 问题

多 Agent 协作(Planner + Executor + Reviewer):
- Executor 不知道 Planner 的约束
- Reviewer 不知道 Executor 改了哪些文件
- 每个 Agent 各自维护一份上下文 → 不一致

6.2 共享黑板的模式

import json

class SharedBoard:
    """多 Agent 共享状态:单一真相源"""

    def __init__(self):
        self.goal = ""
        self.constraints = []
        self.artifacts = {}      # 产物(文件路径/结果)
        self.log = []            # 事件流
        self.errors = []

    def snapshot_for(self, agent_role: str) -> str:
        """按角色裁剪:只给它需要的"""
        base = f"""目标:{self.goal}
约束:{'; '.join(self.constraints)}
产物:{json.dumps(self.artifacts, ensure_ascii=False)}
"""
        if agent_role == "reviewer":
            base += f"\n近期改动:{self.log[-5:]}"
            base += f"\n错误:{self.errors[-3:]}"
        return base
要点:
- 全局状态只存一份(避免各自漂移)
- 每个 Agent 拿到"裁剪过的快照"(不是全量)
- 事件流按时间追加,便于追溯

6.3 消息传递而非共享内存

class AgentMessage:
    def __init__(self, sender, receiver, intent, payload):
        self.sender = sender
        self.receiver = receiver
        self.intent = intent      # 如 "handoff" / "review_request"
        self.payload = payload    # 结构化数据
# Planner → Executor
msg = AgentMessage(
    sender="planner",
    receiver="executor",
    intent="执行任务",
    payload={
        "task": "给登录接口加限流",
        "constraints": ["不改表结构", "用 Redis"],
        "acceptance": ["单测通过", "接口返回 429"],
        "relevant_files": ["AuthService.java"],
    },
)
好处:
- 上下文明确、可审计
- 不依赖隐式共享状态
- 出问题能定位"谁传错了什么"

七、方案五:检索增强(只放该放的)

7.1 RAG 上下文注入的正确姿势

def build_context(query: str, retriever, max_docs: int = 3,
                  max_chars: int = 6000) -> str:
    """检索 → 过滤 → 裁剪 → 注入"""
    docs = retriever.search(query, k=10)      # 先多召回

    # 1. 分数过滤(低相关丢弃)
    docs = [d for d in docs if d.score > 0.75]

    # 2. 去重(相似内容只留一条)
    seen, unique = set(), []
    for d in docs:
        sig = d.text[:80]
        if sig not in seen:
            seen.add(sig)
            unique.append(d)

    # 3. 按分数取 top-N
    top = unique[:max_docs]

    # 4. 总长裁剪
    out, total = [], 0
    for d in top:
        if total + len(d.text) > max_chars:
            break
        out.append(f"[来源:{d.source}]\n{d.text}")
        total += len(d.text)

    return "\n\n".join(out)

7.2 常见错误

❌ 检索 20 篇全塞进去 → 噪音大、token 爆、模型混乱
❌ 不做分数过滤 → 不相关内容污染
❌ 不去重 → 同一内容多版本互相矛盾
❌ 不标来源 → 模型编造引用

✅ 少而准(top 3-5)+ 分数阈值 + 标明来源 + 总长上限

八、完整实战:一个会"记事儿"的 Agent

8.1 组装

class MemoryAgent:
    def __init__(self, llm, fact_mem, episodic_mem):
        self.llm = llm
        self.facts = fact_mem
        self.episodes = episodic_mem
        self.state = TaskState()
        self.history = []

    def build_messages(self, user_input: str) -> list:
        # 1. 检索相关历史
        related = self.episodes.search(user_input, k=3)
        related_text = "\n".join(related) if related else "无"

        # 2. 事实记忆
        facts_text = json.dumps(self.facts.data, ensure_ascii=False)

        # 3. 组装(分层,带预算)
        system = f"""你是执行型 Agent。
【长期事实】{facts_text}
【相关历史】{related_text}
"""
        messages = [
            {"role": "system", "content": system},
            {"role": "system", "content": self.state.to_prompt()},
        ]
        messages += rolling_summary(self.history, self.llm, keep_recent=8)
        messages.append({"role": "user", "content": user_input})
        return messages

    def run(self, user_input: str) -> str:
        messages = self.build_messages(user_input)
        resp = self.llm.invoke(messages)
        self.history.append({"role": "user", "content": user_input})
        self.history.append({"role": "assistant", "content": resp.content})
        return resp.content

8.2 效果对比

优化前:
- 20 轮后 token 3.5 万、费用高、偶发超限
- 关键约束常被忽略

优化后:
- token 稳定在 6000-9000
- 约束在任务状态里,每轮都在
- 历史记忆按需检索,不丢关键事实

九、常见坑速查

现象根因解决
上下文超限历史/工具结果全塞token 预算 + 滑动窗口 + 裁剪
越聊越忘长上下文注意力衰减结构化任务状态 + 滚动摘要
越答越编噪音多、信噪比低检索过滤 + top-N + 去重
工具结果撑爆HTML/日志原样塞摘要/截断/只留关键行
关键约束丢失混在历史里被裁提成清单,每轮固定注入
多 Agent 不一致各自维护上下文共享黑板 + 消息传递
成本失控全量历史每轮重发预算封顶 + 轻量模型分流

本章小结

Agent 上下文工程的主线:分层(系统/任务/记忆/工具)→ 预算(每层硬上限 + 留 30% 余量)→ 裁剪(滑动窗口 + 工具结果摘要)→ 压缩(滚动摘要 + 关键信息固化)→ 长期记忆(事实键值 + 情景向量)→ 多 Agent 共享(黑板 + 消息)→ 检索(少而准)。核心心法:上下文不是越多越好——系统层放规则、任务层放约束清单(每轮必现)、记忆层按需检索、工具层先摘要再入;token 永远留余量,关键信息永远不放进会被裁掉的位置。做好上下文工程,Agent 才能从"聊两句就崩"变成"长任务稳定跑"。

第二篇讲 Agent 工具调用不稳定问题与解决方案。

(全文完)

Logo

更多推荐