Agent上下文管理从黑盒到白盒:可见性、压缩与持久化实践
Agent 上下文管理最近在 GitHub 上的热度明显涨起来了。和以往停留在“怎么把 prompt 写长一点”的讨论不同现在大家更关注执行上下文本身的问题Agent 到底记住了什么、忘记了什么、在哪一步被截断、压缩之后关键信息有没有丢。Claude Code 的上下文压缩命令、MCP 服务器的上下文进出、长会话自动总结失败后报错这些现象背后是同一个问题Agent 的上下文是一个不透明的黑盒。当一个 Agent 在黑盒里管理上下文你看到的只有最终输出看不到中间被压缩掉哪些信息也看不到上下文占用了多少 token、距离上限还剩多少。结果就是 Agent 跑着跑着丢失任务目标反复调用同一个工具或者直接抛出一句agent terminated due to error。这类问题不是个例而是长任务场景下的常态。这篇文章不绑定某一个具体仓库而是把 GitHub 上这类“上下文管理”项目的共通思路、核心能力、最小部署流程和排错方法整理出来。你可以把它当作一份选型清单也可以直接参考里面的服务代码搭一个自己的上下文管理中间层。1. 核心能力速览先给一张规格表方便快速判断这套方案适不适合你。注意下面的指标是这类上下文管理方案的通用目标具体到不同项目会有差异实际占用和参数需要以本机测试为准。能力项说明项目定位为 Agent / LLM 应用提供上下文可见性、压缩、持久化和批量任务管理要解决的核心问题上下文黑盒导致的记忆丢失、任务失效、token 浪费主要功能会话创建、消息追加、上下文快照、上下文统计、压缩触发、持久化存储推荐环境一台能运行 Python 的机器即可CPU 可跑不需要 GPU显存占用无 GPU 依赖显存占用为 0内存占用取决于会话数量和上下文长度支持平台Windows / Linux / macOS启动方式FastAPI 服务 配置文件命令启动是否支持 API是提供 HTTP 接口是否支持批量任务是可按会话维度批量处理也可通过脚本循环调用适合场景AI 编程助手、Agent 框架开发、长对话应用、上下文压缩测试不适合场景对延迟极敏感的生产环境直接裸用需要加鉴权、限流和持久化改造从这套能力来看这类项目最大的价值不是给你一个模型而是给你一个“能看到上下文内部结构”的中间层。有了中间层上下文从黑盒变成白盒Agent 的行为才可观测、可控制、可复现。2. 适用场景与使用边界2.1 适合谁第一类用户是做 AI 编程助手开发的。Claude Code、Cursor 这类工具虽然自带了上下文管理但如果你要接入自己的 Agent 框架很多内部状态是不可控的。自己维护一层上下文管理服务可以在每次调用模型之前明确知道当前上下文占用多少、是否触发压缩、压缩掉哪些内容。第二类用户是做 Agent 长任务编排的。比如让 Agent 连续访问多个工具、完成一个多步骤任务一旦会话上下文超过模型上限Agent 就可能丢失最早的目标。把这层管理独立出来之后你可以在任务边界做一次上下文快照出问题时翻历史记录就能定位。第三类用户是做 RAG 或文档解析的。这类应用经常需要把多份文档拆进上下文拆得太碎会丢语义整段塞进去又会超限。一个带统计和压缩策略的上下文服务可以帮你找到合适的拆分和压缩比例。2.2 使用边界与合规提醒这类工具的本质是“记录并处理上下文”所以它天然涉及数据安全和隐私问题。不要让上下文管理服务直接暴露在公网至少加一层 Token 鉴权或内网访问限制。如果上下文中包含源代码、客户数据、医疗信息或财务数据必须先脱敏再入库确认拥有合法处理权限。涉及人脸、声音、肖像、版权素材和商业机密时没有明确授权的情况下不要存入任何外部服务。多个用户共用一个服务时必须做会话隔离避免 A 用户读到 B 用户的上下文。上线前要做效果复核尤其是压缩策略对关键任务信息的保留程度务必人工验证。3. 环境准备与前置条件这类上下文管理服务的门槛很低不需要 GPU主要依赖 Python 环境。3.1 环境清单项目要求操作系统Windows 10/11、Linux、macOS 均可Python推荐 3.10 及以上依赖库fastapi、uvicorn、pydantic、sqlite3标准库可选依赖redis、faiss、sentence-transformers按需磁盘空间几百 MB 左右主要取决于依赖库和存储文件端口默认 8000可修改配置3.2 安装依赖先创建虚拟环境再安装依赖python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install fastapi uvicorn pydantic如果需要把上下文向量化再装pip install redis faiss-cpu sentence-transformers如果你的网络环境访问 GitHub 较慢可以用镜像源安装 Python 依赖或者直接把依赖版本锁定在一个镜像源文件中按需拉取。网络问题不影响本文的服务设计思路。4. 最小上下文管理服务部署下面给出一套最小可运行的上下文管理服务代码。它不依赖任何特定 Agent 框架你可以把它理解成一个通用的“上下文中间层”任何 Agent 通过 HTTP 接口把消息丢进来服务负责存储、统计、压缩和快照。4.1 目录结构context-server/ ├── main.py ├── config.json └── requirements.txtrequirements.txtfastapi0.115.6 uvicorn0.32.1 pydantic2.10.4config.json{ host: 127.0.0.1, port: 8000, max_context_tokens: 60000, compact_threshold_ratio: 0.8, storage: sqlite, sqlite_path: ./context_store.db }参数说明max_context_tokens单个会话上下文上限可按实际模型调整。如果你用的模型支持 128K这个值可以调到 100000 左右。compact_threshold_ratio触发压缩的比例。0.8 表示上下文使用量达到上限 80% 时触发压缩。storage存储后端这里用 SQLite方便本地测试。4.2 服务主代码import json import sqlite3 from datetime import datetime from pathlib import Path from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI(titleContext Manager) # 读取配置 with open(config.json, r, encodingutf-8) as f: CONFIG json.load(f) MAX_TOKENS CONFIG[max_context_tokens] COMPACT_THRESHOLD MAX_TOKENS * CONFIG[compact_threshold_ratio] DB_PATH CONFIG[sqlite_path] # 初始化数据库 def init_db(): conn sqlite3.connect(DB_PATH) conn.execute( CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, created_at TEXT, context_json TEXT ) ) conn.commit() conn.close() init_db() class MessageRequest(BaseModel): role: str user content: str metadata: dict {} class SessionCreateRequest(BaseModel): session_id: str initial_context: list [] app.post(/sessions) def create_session(req: SessionCreateRequest): 创建新会话 conn sqlite3.connect(DB_PATH) cur conn.cursor() cur.execute( INSERT OR REPLACE INTO sessions (session_id, created_at, context_json) VALUES (?, ?, ?), (req.session_id, datetime.now().isoformat(), json.dumps(req.initial_context, ensure_asciiFalse)) ) conn.commit() conn.close() return {session_id: req.session_id, status: created} app.post(/sessions/{session_id}/messages) def append_message(session_id: str, msg: MessageRequest): 追加消息到会话上下文 conn sqlite3.connect(DB_PATH) cur conn.cursor() cur.execute(SELECT context_json FROM sessions WHERE session_id ?, (session_id,)) row cur.fetchone() if not row: raise HTTPException(status_code404, detailsession not found) context json.loads(row[0]) context.append({ role: msg.role, content: msg.content, metadata: msg.metadata, timestamp: datetime.now().isoformat() }) # 简单 token 估算中英文混用按 1.3 字符/token 粗略估算 estimated_tokens estimate_tokens(json.dumps(context, ensure_asciiFalse)) payload { session_id: session_id, context_len: len(context), estimated_tokens: estimated_tokens, max_tokens: MAX_TOKENS, needs_compact: estimated_tokens COMPACT_THRESHOLD } cur.execute( UPDATE sessions SET context_json ? WHERE session_id ?, (json.dumps(context, ensure_asciiFalse), session_id) ) conn.commit() conn.close() return payload app.get(/sessions/{session_id}/context) def get_context(session_id: str): 获取当前上下文快照 conn sqlite3.connect(DB_PATH) cur conn.cursor() cur.execute(SELECT context_json FROM sessions WHERE session_id ?, (session_id,)) row cur.fetchone() conn.close() if not row: raise HTTPException(status_code404, detailsession not found) return json.loads(row[0]) app.get(/sessions/{session_id}/stats) def get_stats(session_id: str): 获取上下文统计信息 conn sqlite3.connect(DB_PATH) cur conn.cursor() cur.execute(SELECT context_json FROM sessions WHERE session_id ?, (session_id,)) row cur.fetchone() conn.close() if not row: raise HTTPException(status_code404, detailsession not found) context json.loads(row[0]) return { session_id: session_id, message_count: len(context), estimated_tokens: estimate_tokens(json.dumps(context, ensure_asciiFalse)), max_tokens: MAX_TOKENS, usage_ratio: estimate_tokens(json.dumps(context, ensure_asciiFalse)) / MAX_TOKENS } def estimate_tokens(text: str) - int: 粗略估算 token 数实际应替换为对应模型的分词器 return max(1, len(text) // 2)说明这个服务的核心思路是“所有上下文变更都有记录所有统计信息都可见”。estimate_tokens只是占位实现真实项目中应接入对应模型的 tokenizer否则统计会不准确。生产环境需要把 SQLite 换为 PostgreSQL 或 Redis并添加并发控制。4.3 启动服务uvicorn main:app --host 127.0.0.1 --port 8000启动后访问http://127.0.0.1:8000/docs可以打开 Swagger 文档直接测试接口。5. 从黑盒到白盒上下文可见性设计很多 Agent 的应用问题表面看是模型能力不够实际是上下文状态不可见。一个上下文管理服务最先解决的是“能看到”的问题。5.1 输出结构化日志如果你只是把上下文打印到控制台很难定位问题。更好的做法是每次追加消息时同时输出一条结构化日志{ event: context_update, session_id: task_001, message_count: 12, estimated_tokens: 45210, usage_ratio: 0.56, trend: up }有了这类日志你可以在任务结束后回放整个上下文的增长过程找出 Agent 在哪一步开始偏离任务目标。5.2 保留上下文快照每次执行关键任务前打一个快照可以存成单独的 JSON 文件mkdir -p snapshotsimport json from pathlib import Path def save_snapshot(session_id: str, context: list, snapshot_dir: str snapshots): Path(snapshot_dir).mkdir(exist_okTrue) path Path(snapshot_dir) / f{session_id}_{int(time.time())}.json with open(path, w, encodingutf-8) as f: json.dump(context, f, ensure_asciiFalse, indent2) return str(path)有了快照即使后续上下文被压缩得面目全非也可以随时回到某个时间点做对比分析。5.3 标记截断位置当上下文触发压缩时不要静默地丢掉消息。正确的做法是在上下文中插入一个标记{ role: system, content: [context compacted at 2025-06-01 10:30:00, removed 8 older messages], metadata: {event: compact} }这个标记会明确告诉后续的 Agent这里有一段历史被压缩了你需要依赖摘要信息继续任务。否则 Agent 不会知道自己少了先前的记忆任务断裂的几率会明显上升。6. 上下文压缩与持久化策略上下文不可能无限增长压缩和持久化是这类服务的关键能力。6.1 压缩触发条件常见的触发条件有三种按 token 数触发当前上下文达到上限的 80%例如 60K 上限的模型在 48K 时触发。按消息条数触发超过一定数量的消息后把早期消息聚合为摘要。按任务节点触发在 Agent 完成某个子任务后主动压缩保留任务结论丢弃中间过程。6.2 压缩方式对比方式优点缺点适用场景摘要压缩保留高层语义上下文体积小细节丢失长期对话、多轮任务关键信息提取保留结构化信息需要额外模型调用需要精确数据时丢弃中间过程简单直接成本低可能丢失推理链路过程不重要、只在意结果时实际项目中推荐先做一层“丢弃中间过程”把工具调用详情、中间输出收敛为一行状态记录再做一层“摘要压缩”对早期对话生成 summary。6.3 持久化要考虑的字段不建议只存消息内容最好把每次更新的元信息一起存下来session_id timestamp role content content_type: user/assistant/tool/system estimated_tokens trigger_compact: true/false metadata: {source, task_id, tool_name, ...}字段越完整事后定位问题越容易。不要为了省几 MB 磁盘空间丢掉元信息排错的时候你会感谢这些字段。7. 接口 API 与批量任务用法上下文管理服务的价值要通过接口体现。下面是标准调用流程。7.1 创建会话curl -X POST http://127.0.0.1:8000/sessions \ -H Content-Type: application/json \ -d {session_id: task_001, initial_context: []}7.2 追加消息curl -X POST http://127.0.0.1:8000/sessions/task_001/messages \ -H Content-Type: application/json \ -d {role: user, content: 读取 config.json 并输出 Nginx 配置 }返回内容会包含当前上下文的 token 估算和是否触发压缩的标记{ session_id: task_001, context_len: 3, estimated_tokens: 1820, max_tokens: 60000, needs_compact: false }7.3 Python 调用示例import requests BASE_URL http://127.0.0.1:8000 def run_task(session_id: str, messages: list): # 创建会话 requests.post(f{BASE_URL}/sessions, json{session_id: session_id}) # 追加消息并要求模型响应 for msg in messages: resp requests.post( f{BASE_URL}/sessions/{session_id}/messages, json{role: msg[role], content: msg[content]} ) stats resp.json() print(fmessage added, tokens{stats[estimated_tokens]}) if stats[needs_compact]: # 触发压缩实际应调用已实现的 compact 接口 print(f[{session_id}] context needs compact) # 获取最终上下文 context requests.get(f{BASE_URL}/sessions/{session_id}/context) return context.json() # 参数实际运行时按任务改动 messages [ {role: user, content: 分析项目目录结构}, {role: assistant, content: 已读取目录发现 12 个文件}, {role: user, content: 为每个文件生成迁移建议} ] print(run_task(task_001, messages))7.4 批量任务设计批量任务的要点是会话隔离和失败重试。每个独立任务使用独立的 session_idtask_list [task_001, task_002, task_003] for session_id in task_list: try: result run_task(session_id, task_messages[session_id]) except requests.exceptions.Timeout: print(f{session_id} timeout, retry later) # 添加重试逻辑 except Exception as e: print(f{session_id} failed: {e})批量任务一定要加日志、记录每个任务的状态并支持断点续跑。只 print 不落盘任务一多就没办法排查。8. 资源占用与性能观察上下文管理服务不跑模型资源占用主要来自两方面token 估算带来的计算开销以及存储层的读写压力。8.1 内存占用单进程内存占用一般不大主要看会话上下文在内存中的缓存策略。如果所有上下文都长驻内存会话数量多了之后内存会线性上涨。建议不活跃会话及时落盘并释放内存缓存。按 session_id 做 LRU 缓存限制同时活跃的会话数量。通过/stats接口定时采样观察 usage_ratio 变化曲线。8.2 Token 估算的代价estimate_tokens如果只是len(text)//2性能可以忽略。但如果你接入真实 tokenizer每次追加消息都跑一遍全量上下文会有一定 CPU 开销。优化方式是只对新增文本做 tokenize然后累加之前的结果。8.3 存储增长SQLite 文件会随着上下文增长变大。建议定期清理超过 N 天未访问的会话。只保留摘要删除完整上下文。快照文件单独存放使用压缩归档。9. 常见问题与排查方法下面按实际使用中的高频问题列出排查思路。问题现象可能原因排查方式解决方案Agent 中途丢失任务目标上下文超限后早期信息被丢弃但没有压缩标记查看/stats接口输出 usage_ratio在压缩时插入 system 标记保留任务目标字段上下文统计不准确estimate_tokens不是实际模型 tokenizer对比模型 API 返回的 usage 字段接入真实 tokenizer或按模型 API 返回值校准压缩后任务质量下降摘要丢失了关键数据对比压缩前后的快照先做“关键信息提取”再做“摘要压缩”大量会话后磁盘增长快未清理历史会话查看 sqlite 文件大小增加清理任务保留摘要删除正文接口超时存储层写入慢或 token 估算全量扫描看日志耗时改为增量统计存储层加索引端口冲突8000 端口被占用lsof -i:8000或netstat -ano修改config.json中的 port批量任务卡住某个会话上下文过大阻塞写入查看任务状态日志设置单任务超时失败隔离多用户串数据session_id 命名冲突检查是否有相同 id增加用户前缀或使用 UUID10. 最佳实践与使用建议这块内容不是可有可无而是要不要把这类项目落到生产环境的关键差异点。10.1 先小参数验证第一次跑通流程时不要直接拿长文档压测。先用 3~5 条消息确认上下文接口全部正常再逐步加长。10.2 设定上下文预算给每个任务设定明确的 token 预算。不要等模型自动报错再处理而是在使用率达到 70% 时主动压缩80% 时强制介入。10.3 日志和状态入盘不要只依赖控制台输出。每次上下文更新都写入结构化日志任务状态要持久化否则批量任务中断后无法恢复。10.4 接口加鉴权如果服务部署在公网环境至少加一层简单的 Token 鉴权from fastapi import Header, HTTPException def verify_token(authorization: str Header(default)): if authorization ! Bearer your_secret_token: raise HTTPException(status_code401, detailunauthorized)10.5 合规红线所有上下文数据默认视为敏感数据。涉及代码库、客户资料、人员信息时必须在确认授权、完成脱敏后再做存储和调用。人脸、声音、版权素材等场景同样需要先确认授权边界。11. 总结与下一步回到最开始的问题Agent 之所以会在黑盒里找上下文是因为缺少一层可见、可管理、可回放的中间状态。无论你用哪个 Agent 框架上下文管理都应该独立出来而不是任由模型内部静默处理。这篇文章给出的最小服务已经具备三件事可见性、统计告警、压缩触发。你不需要一上来就做复杂的向量检索和多级摘要先把“上下文在哪里、占了多少、什么时候被压缩”这三个问题答清楚黑盒问题就解决了一大半。下一步最容易验证的场景是取一个你过去失败过的长任务把同样的任务放到这个上下文管理服务中重新跑一遍对比关键信息保留率和任务完成率。看到明显提升再考虑接入 LangChain、LlamaIndex、MCP 或自己的 Agent 框架继续扩展向量检索、自动摘要、任务重放等功能。建议收藏备用。