
TL;DR30 秒速览上下文压缩 是事后补救——上下文已经大了再去压缩这篇讲事前防御从一开始就让上下文小、让缓存命中阿里云 Token Plan 账单几个小时烧掉一周的量排查发现Prompt 缓存使用率只有 20% 出头根因一System Prompt 里注入了秒级当前时间和 Memory 内容——前缀每次都变前缀缓存全量失效根因二工具输出不加节制——单次工具结果实测527K 字符约 15 万 token一条就能吃掉大半个上下文窗口解法一System Prompt 完全静态化——时间改为 calc 工具按需获取Memory 改为按需检索缓存使用率提升到92% 以上解法二大文本落盘 指针通知——超大工具输出写临时文件只回头尾样本和文件位置单行大 JSON 用字符预算采样避免一行就是全文核心代码EnvironmentInfoSegmentPromptTemplateServiceToolResultGuard212 行前瞻当 Skill/Tool/MCP 多到只装名称和描述都撑爆上下文时答案是 RAG 按需装载——见文末预告刚开源的 EasyRAG前情提要前面我们讲了增量上下文压缩——对话超过 128K 窗口时用上一轮摘要 最近几轮原文做事后压缩。但压缩本身也要花钱一次 LLM 摘要调用而且总有信息损耗。更好的思路是事前就不让上下文膨胀。这篇讲我们踩过的两个大坑以及怎么把缓存使用率从 20% 出头拉到 92% 以上。事故几个小时烧完一周的量某天早上打开阿里云控制台我愣住了——Token Plan 的用量曲线像一根垂直的火箭几个小时内消耗了过去一周的总量。第一反应是谁在死循环调 LLM查了一圈没有异常循环Agent 都在正常干活。那就只剩一个可能同样的钱买到的有效计算变少了。现在的 LLM API 普遍提供 Prompt 缓存请求前缀与最近请求相同时命中部分的输入 token 按 1~2 折计费。对多轮 Agent 来说这至关重要——每一轮对话都要把 System Prompt 全部历史重新发给 LLM如果前缀能命中缓存从第二轮开始输入成本直接降一个数量级。拉出账单明细答案浮出水面缓存使用率只有 20% 出头。也就是说大部分本该命中缓存的输入 token都在按全价计费。接下来的问题是前缀到底被谁破坏了根因一System Prompt 里的秒级时间戳核心原理前缀缓存按从头开始的逐 token 精确匹配工作。前缀中任何位置出现一个变化的字符从那个字符往后的缓存全部作废。我们逐段 diff 了两次相邻请求的完整 Prompt立刻抓到了元凶——System Prompt 中部的环境信息段## Environment - OS: Mac OS X - Current working directory: /Users/xxx/project - Current date and time: 2026-08-19 10:30:45 CST ← 每秒都在变Agent 循环每一轮都会重建 System Prompt这个秒级时间戳每次都不同。它位于 Prompt 中部意味着从它开始往后的所有内容——System Prompt 剩余部分、全部对话历史——一次都命中不了缓存。更隐蔽的是 Memory当时我们把用户 Memory 内容直接注入 System Prompt。Memory 内容随会话变化同样在破坏前缀。一句话总结当时的 System Prompt静态内容中间掺了几处动态沙子整条金链子都断了。修复三条纪律System Prompt 完全静态化纪律一动态内容能删就删。时间戳从环境信息段中彻底移除这一段只剩 OS 和工作目录——一次会话内恒定不变objectEnvironmentInfoSegment{// 只保留 OS cwd故意做成静态的保障缓存前缀稳定fungenerate(cwd:String?,os:String?):String{...}}纪律二必须保留的动态信息改成工具按需获取。Agent 确实经常需要知道现在几点但没必要每轮都塞给它。我们在 System Prompt 末尾追加一段静态指引告诉模型需要时间时自己调 calc 工具## Current Time The current date and time is NOT included in this prompt to keep it stable for caching. When you need the current date, time, or timezone, use the calc tool with a script such as: ZonedDateTime.now().toString() This returns the current timestamp with the systems local timezone.注意措辞里那句 “is NOT included … to keep it stable for caching”——这不是写给模型看的废话而是让模型理解为什么拿不到时间、该去哪拿。实测模型适应得很好需要报日期时会主动调一次 calc。纪律三Memory 不进 Prompt按需检索。System Prompt 里只留静态的 Memory 使用指引真正的记忆内容通过memory_search/memory_read工具在需要时检索。这除了保缓存还有个额外好处Memory 多了也不怕撑爆 Prompt。// AgentLoopRunner明确注释了这条设计决策// Memory is NOT injected into the system prompt (keeps the prompt stable for LLM caching).// The LLM retrieves relevant memories on demand via memory_search / memory_read tools.三条纪律落到一条可执行的规范上保持 LLM 请求前缀稳定——禁止在 Prompt 开头或中部注入时间戳、用户 ID、随机数等每次调用都变化的内容。动态内容要么删除要么降级为按需获取实在要放也只能放在整个请求的最末尾。修复上线后缓存使用率从 20% 出头一路爬到92% 以上账单曲线应声回落到正常水平。根因二一条工具输出吃掉半个上下文缓存问题解决后我们又撞上了第二堵墙上下文本身被工具输出撑爆。典型场景投研 Agent 调用行情工具拉取某只股票 10 年的日 K 线。LLM 的真实工作流是——拿到数据后写一段 Python 代码做分析它需要的只是数据在哪、有哪些字段完全不需要知道 2500 根 K 线每一根的具体数值。但工具的原始输出会被原样塞进上下文实测最大的一次工具输出527K 字符约 15 万 token——一条结果就吃掉 200K 窗口的四分之三。后果是连锁反应窗口被挤爆 → 提前触发上下文压缩压缩把这条工具结果摘要掉 → 模型觉得数据丢了 →重新执行同一个工具→ 又拉回 527K死循环式烧钱解法大文本落盘只给 LLM 一个指针思路很直接这种大输出对 LLM 来说是中间产物不该进上下文。超过阈值默认 50K 字符约 1.9 万 token占 200K 窗口的 9~10%的工具结果全文写入系统临时目录上下文里只留一份结构化的指针通知// ToolResultGuard工具结果生成点统一拦截suspendfunguardEntry(entry:ToolResultEntry,maxChars:IntmaxToolResultChars):ToolResultEntry{if(entry.result.lengthmaxChars)returnentry// 小结果原样通过returntry{// 大结果全文落盘替换为指针通知entry.copy(resultspillText(entry.result,entry.toolCallId),truncatedtrue)}catch(e:Exception){// 落盘失败兜底头 60% 尾 40% 截断保证请求构造永不失败valguardedguard(entry.result,maxChars)entry.copy(resultguarded.text,truncatedtrue)}}LLM 实际收到的内容长这样[TRUNCATED] Original output was 527381 chars (2501 lines). It has been saved to: /tmp/easyai-tool-output/call_abc123_f3d9a2b1.txt What you see below is ONLY a small sample — NOT the complete output. Do NOT draw conclusions based solely on this sample. Access patterns: - read tool: offset0limit500 for LINE-based slicing - grep pattern /tmp/.../call_abc123_f3d9a2b1.txt for targeted lookup - bash python for full computation Sample (first ~1000 chars / last ~1000 chars of 527381 total): --- head --- date,open,high,low,close,volume 2016-08-19,10.52,10.68,... --- tail --- 2026-08-18,45.31,45.88,...几个容易被忽略的设计细节① 头尾样本给模型数据结构感。看到 CSV 表头和最后几行模型就能写出正确的 pandas 分析代码——它要的从来不是数据本身而是数据的形状。② 文件名幂等。文件名 toolCallId 内容 SHA-256 前 8 位。同一份内容重复落盘历史回放、重试不会产生重复文件。③ 明确告知样本不是全部。不加这句模型真的会拿 1000 字符的样本直接下投资结论。同时告知临时文件可能被系统清理读不到就重跑工具重新生成不依赖文件的持久性。一个隐蔽的坑大文本可能只有 1 行落盘之后出现了新问题。通知里引导模型用 read 工具分段读取但 read 的 offset/limit 是按行切片的——如果落盘的文件是一整行的压缩 JSON / 单行 CSVoffset0limit500等于把 527K 全文再塞回上下文防护瞬间破防。两层应对采样本身按字符预算不按行数。头尾样本各取约 1000 字符并在行边界处截断。哪怕全文只有 1 行样本也只是这 1 行的前 1000 字符不会退化成样本 全文// 字符预算采样而非固定行数单行超大载荷压缩 JSON/CSV/base64// 若按行采样样本就等于全文valheadRaworiginalText.take(SAMPLE_CHARS)valheadSampleheadRaw.substringBeforeLast(\n).ifEmpty{headRaw}valtailRaworiginalText.takeLast(SAMPLE_CHARS)valtailSampletailRaw.substringAfter(\n,missingDelimiterValuetailRaw)通知里明确警告单行文件不要按行读。直接在 Access patterns 里写清楚单行 JSON/CSV 场景下行切片无效请改用grep -o定向提取或 Python 整体计算。把怎么正确使用这份数据教给模型比指望它自己悟出来可靠得多。效果与成本账指标优化前优化后Prompt 缓存使用率20% 出头92%多轮会话输入 token 计费近全价命中部分 1~2 折单条工具结果上限无限制实测 15 万 token50K 字符约 1.9 万 token窗口的 ~10%上下文压缩触发频率频繁被大输出挤爆显著下降压缩后重跑工具死循环偶发消除大结果本就不进上下文两件事的共同本质是同一句话上下文是 Agent 最贵的资源。缓存命中率决定了单价上下文大小决定了用量——账单 单价 × 用量两头都要管。踩坑记录坑原因解法缓存命中率莫名走低System Prompt 中部有秒级时间戳动态内容删除 / 按需获取 / 移到最末尾模板变量{{ current_date_time }}到处在用历史自定义 Prompt 依赖该变量移除后渲染为空 warning不抛异常平滑过渡Memory 注入 Prompt 导致前缀漂移Memory 内容随会话变化改为静态指引 memory_search 按需检索大工具输出挤爆窗口无上限ToolResultGuard 50K 字符阈值 落盘单行大 JSON 让按行采样失效1 行 全文按字符预算采样 通知中警告改用 grep/python落盘文件被 OS 清理后模型懵了临时目录非持久通知写明读不到就重跑工具截断通知本身消耗 token通知太长通知控制在原文 1/100 以内有测试断言更进一步当能力清单本身开始爆炸——RAG 按需装载本篇的两道防线挡住了动态内容污染前缀和中间产物淹没上下文但还有一个正在逼近的问题Agent 的能力清单本身。我们的 Agent 挂着三类可装载的能力内置 Tools、SkillSKILL.md 定义的技能、MCP Server 提供的工具。当前做法是把它们的名称和描述全量注入上下文让模型知道有哪些能力可用。工具少的时候没问题但当 MCP 接入多个 Server、Skill 积累到几十个上百个时每个工具/技能的名称 描述 参数 schema 至少几百 token几十个累加起来就是数万 token 的常驻开销更糟糕的是这部分内容每一轮对话都全量重发——大部分能力在当前任务里根本用不上纯属白花钱而且能力清单随配置变化同样会破坏前缀缓存。换句话说“全量加载能力清单和全量注入 Memory是同一个病把可能用到的东西全部预支进上下文。Memory 的解法是按需检索能力清单的解法也是同一个思路——RAG把全部工具/技能定义建成索引每轮根据用户意图做语义检索只把最相关的几个装载进上下文。上下文从全量能力目录退化为本次任务恰好需要的能力”规模与能力总数解耦。这正是我们刚刚开源的 EasyRAG 项目要做的事一个可独立部署的 RAG 服务文档摄入 → 切块 → 索引 → 检索流水线EasyAI 的 Agent Memory 已经在对接它做语义检索工具与技能定义的 RAG 化装载是下一步的目标场景。RAG 这条线内容很多——索引策略、切块粒度、检索召回与缓存前缀的兼容性等我们后续会出RAG 专题系列展开这里先留个钩子。总结[上下文压缩](https://blog.csdn.net/Jinkwin2025/article/details/163989821解决的是上下文已经大了怎么办而本篇的两件事解决的是别让它变大、别让它变贵稳定前缀System Prompt 全静态化动态信息降级为工具按需获取 → 缓存使用率 20% → 92%管住入口超大工具输出落盘换指针头尾样本 访问指引单行载荷字符预算采样 → 上下文不再被中间产物淹没事前防御永远比事后补救便宜。这句话在软件工程里不新鲜在 LLM 账单上格外真实。下一篇让 LLM 随便跑代码也不怕Groovy 计算沙箱的三重防御开源地址https://github.com/haibingzhao/easyaiRAG 服务https://github.com/haibingzhao/easyrag欢迎 Star、Issue 和 PR。