100+ 轮对话不丢上下文:TaoToken 增量压缩的工程实践
1. 长会话 Agent 为什么会撞上上下文窗口从 100 轮对话说起如果你正在做 Agent 类产品大概率遇到过这个场景用户和你的 Agent 聊到第 80 轮、第 100 轮突然某一次请求直接返回context_length_exceeded或者更隐蔽一点——模型开始忘记前面说过的关键数字把 EPS 170.69 说成大约 170把用户明确否掉的方案又提了一遍。这不是模型变笨了是上下文管理没做好。长会话 Agent 的上下文膨胀问题本质上是三个矛盾叠在一起。第一是信息保留与窗口上限的矛盾128K token 听起来很大但一轮带工具调用的对话System Prompt 工具定义 历史消息 工具结果轻松吃掉几千 token100 轮下来 120K 是常态。第二是压缩时机与 Token 估算的矛盾压缩决策必须发生在 LLM 调用之前但那一刻你手里没有真实的 usage 数据只能本地估算估早了浪费预算估晚了直接溢出。第三是摘要有损与数据精确的矛盾自然语言摘要天然会丢数值而投资分析、数据研究这类场景里一个被稀释的数字能让整个结论作废。我试过最粗暴的做法——直接截断早期消息。结果是 Agent 在第 60 轮之后开始重复问已经回答过的问题因为早期那些关键决策全被砍掉了。全量发送更不行第 90 轮左右 API 直接报错。所以真正能跑通 100 轮的方案只有增量压缩只保留上一轮摘要 最近几轮原文每轮压缩只处理增量不重新加载完整历史。这篇要拆的就是这套增量压缩的工程落地分层策略怎么设计、触发阈值怎么定、Token 估算怎么校准、尾部结构怎么保护、数值变量怎么提取。我会给出可复制的配置片段和多轮对话验证脚本让你能在自己的链路里复现100 轮不丢关键信息的效果。适合正在做长会话 Agent、被上下文窗口卡住的工程师也适合想理解压缩工程细节的技术负责人。核心检索词先明确增量压缩是手段上下文是有界输入的目标Token 估算是决策依据LLM是执行摘要的引擎Agent是承载这一切的运行时。这五个词贯穿全文。2. TaoToken 前置准备把压缩链路接到稳定的 LLM 入口在动手写压缩逻辑之前得先解决一个前置问题你的压缩 Agent 和主 Agent 都要调 LLM如果入口不稳定、模型切换麻烦、Key 管理混乱压缩逻辑还没调通光环境问题就能耗掉半天。我这边统一用 TaoToken 作为 LLM 接入层它把多家模型的调用收敛成一个 OpenAI 兼容接口压缩 Agent 用轻量模型、主 Agent 用强模型切换只改一个 Model ID。先说清楚 TaoToken 是什么、能做什么、适合谁。它是一个 LLM API 聚合接入服务对外暴露标准的/v1/chat/completions接口你拿一个 Key 就能调不同厂商的模型。适合三类人一是做 Agent 需要频繁切换模型做对比的二是压缩这种用便宜模型干粗活、用强模型干细活的分层场景三是不想在多个厂商后台之间来回折腾 Key 和额度的团队。接入前你需要准备两样东西一个 API Key一个 Base URL。Base URL 固定是https://taotoken.net/api注意这个地址后面不加任何 UTM 参数直接用于代码里的base_url。API Key 在控制台的 API Keys 页面创建创建后只显示一次记得立刻存到环境变量里别硬编码进代码。创建 Key 的入口在这里https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。进去之后点新建给它起个能认出来的名字比如agent-compaction方便后面按用途区分额度。模型选择上压缩 Agent 我建议用便宜、快、上下文够的模型因为它的任务只是更新摘要 提取变量不需要强推理主 Agent 用你业务里效果最好的那个。TaoToken 的模型列表可以在文档里查https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。文档里每个模型都标了上下文窗口大小这个数字直接决定你压缩阈值怎么设——比如窗口 128K阈值 0.8那触发点就是 102K 左右。如果你只是想先验证模型通不通、压缩前后的输出差异可以直接在模型对话页面试https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。把一段长对话粘进去手动让它做摘要感受一下摘要质量和 token 消耗再决定压缩 Agent 用哪个模型。对于长期跑编码类 Agent 的场景压缩频率会很高建议直接上 Coding Plan额度更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。控制台总入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 额度、用量、Key 管理都在里面。这里有个关键点要提前说压缩 Agent 和主 Agent 必须用同一个 Base URL 和同一套 Key 管理体系否则你没法统一统计 token 消耗也没法在压缩触发时准确估算预算。TaoToken 的 OpenAI 兼容接口让这件事变简单——两个 Agent 只是 Model ID 不同客户端代码可以复用。环境变量这样配后面所有代码都从这里读export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api export COMPACTION_MODEL你的轻量模型ID export MAIN_MODEL你的主模型ID配完之后先跑一个最小连通性测试确认 Key 和 Base URL 没问题再往下写压缩逻辑。这一步别省我见过太多人压缩代码写了一堆最后发现是 Key 没生效。3. 可复制的增量压缩配置分层策略与触发阈值这一节是全文的核心直接给你能抄的配置。增量压缩的分层策略可以拆成四层消息选择层决定哪些进摘要、哪些留原文摘要生成层负责增量更新Token 估算层负责触发决策变量提取层负责保住数值。四层各管一段互不耦合。先看整体配置。下面这份 JSON 可以直接作为你压缩模块的配置源字段名和语义我尽量对齐工程实践路径按你项目实际调整{ compaction: { enabled: true, threshold: 0.8, reservedTokens: 10000, tailTurns: 2, preserveRecentTokensRatio: 0.25, minMessagesForCompaction: 10, estimator: { tokenizer: o200k_base, lowReportRatio: 0.25, highReportRatio: 4.0, minBaselineForCheck: 2000 }, variableExtraction: { enabled: true, toolName: update_variable, maxInlineValueLength: 4096, overflowToFile: true }, structureGuard: { protectToolResultPairing: true } } }逐个字段解释这些数字不是拍脑袋来的。threshold: 0.8表示上下文用到模型窗口 80% 时触发压缩留 20% 给响应和波动。reservedTokens: 10000是给模型输出预留的空间压缩后必须保证还有这么多余量。tailTurns: 2是保留最近 2 轮原文——为什么是 2 不是 5因为最近 1 轮往往还在工具调用中间态保留 2 轮能覆盖用户提问 Agent 回答 工具结果的完整闭环。preserveRecentTokensRatio: 0.25是尾部原文的 token 上限占窗口 25%防止最近几轮本身就很长把预算吃光。minMessagesForCompaction: 10是个容易被忽略的保护消息少于 10 条时根本不检查压缩避免短对话被无谓地摘要。lowReportRatio和highReportRatio是 Token 估算的合理性窗口后面单独讲。如果你用 TOML 管理配置等价写法[compaction] enabled true threshold 0.8 reserved_tokens 10000 tail_turns 2 preserve_recent_tokens_ratio 0.25 min_messages_for_compaction 10 [compaction.estimator] tokenizer o200k_base low_report_ratio 0.25 high_report_ratio 4.0 min_baseline_for_check 2000消息选择是三段式prefix compacted recent。prefix 是 System 消息加第一条 UserMessage这部分永远保留因为 System Prompt 定义了 Agent 的人格和工具第一条 User 往往是任务起点。compacted 是中间要被摘要掉的部分。recent 是最近 tailTurns 轮原文。def select_messages(messages, model_context_length, config): first_user_index next( (i for i, m in enumerate(messages) if m[role] user), 0 ) prefix messages[:first_user_index 1] remaining messages[first_user_index 1:] max_recent_tokens int(model_context_length * config[preserveRecentTokensRatio]) recent, compacted select_recent( remaining, tail_turnsconfig[tailTurns], max_recent_tokensmax_recent_tokens, ) return prefix, compacted, recent尾部裁剪用增量方式别每次重新估算整个 tail那是 O(n²)。正确做法是维护一个累计值每次从头部减一条def select_recent(messages, tail_turns, max_recent_tokens): split_index max(0, len(messages) - tail_turns * 2) recent messages[split_index:] compacted messages[:split_index] recent_tokens estimate_tokens(recent) while recent_tokens max_recent_tokens and len(recent) 1: recent_tokens - estimate_tokens([recent[0]]) split_index 1 recent recent[1:] # 结构守卫tail 头部不能是孤立的 tool result while split_index 0 and recent and recent[0][role] tool: split_index - 1 recent [messages[split_index]] recent return recent, messages[:split_index]这段里最后那个while就是尾部结构保护是最容易踩的坑。如果压缩后 tail 的第一条是tool消息而发出这个工具调用的assistant消息被压进了摘要LLM 看到孤立的工具结果会以为这个调用还没完成然后重新执行一遍——浪费 token 还可能导致副作用。所以必须向前扩展直到把配对的 assistant 消息包进来。触发方式设计成三种覆盖不同场景触发类型阈值/条件适用场景Auto估算 token 超过窗口 80%正常对话中自动触发Manual用户主动点击感觉响应变慢时手动压缩OverflowLLM 返回超长错误紧急压缩后立即重试Auto 是主力Manual 给用户兜底Overflow 是最后防线。三者共用同一套压缩逻辑只是触发入口不同。摘要生成用 Agent 方式而不是裸 LLM 调用因为摘要 Agent 需要注册一个update_variable工具来提取变量。摘要的输出结构固定成这几段方便后续解析和注入Goal: 用户目标 Constraints: 约束条件 Progress: - Done: 已完成 - In Progress: 进行中 - Blocked: 阻塞项 Key Decisions: 关键决策 Next Steps: 下一步 Critical Context: 关键上下文 Relevant Files: 相关文件增量更新的关键在 Prompt 里如果已有上一轮摘要LLM 的任务是更新而非从头总结。这样每轮压缩的输入量是上一轮摘要 新消息无论对话多长输入都有界。变量提取是数据保险。压缩前让摘要 Agent 调update_variable工具把数值型事实价格、比率、ID、配置值、计算结果提取到会话变量里变量不参与压缩每轮以## Session Variables段落注入 System Prompt。变量语义是全量替换——每次输出即完整变量集保留有效的、更新变化的、丢弃过时的。超大的值溢出到文件变量里只存[file: path]指针。4. 验证请求与成功结果多轮对话脚本与 Token 对比配置写完必须验证。这一节给你一个可跑的多轮对话脚本以及压缩前后 Token 占用的对比方法。验证分两步先确认压缩逻辑本身正确再确认端到端 100 轮不丢信息。先写一个模拟长对话的脚本构造 100 轮消息每轮带一个工具调用和结果中间穿插几个关键数值import os import json from openai import OpenAI client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) def build_long_conversation(rounds100): messages [ {role: system, content: 你是一个数据分析 Agent负责跟踪股票指标。}, {role: user, content: 帮我持续跟踪这家公司的 EPS 和 PE。}, ] for i in range(rounds): messages.append({ role: assistant, content: None, tool_calls: [{ id: fcall_{i}, type: function, function: {name: get_metric, arguments: json.dumps({round: i})}, }], }) messages.append({ role: tool, tool_call_id: fcall_{i}, content: fround {i}: EPS170.69, PE80.28, market_cap2.3T, }) messages.append({role: user, content: f第 {i} 轮确认继续。}) return messages这个脚本构造的消息流里EPS170.69和PE80.28是关键数值压缩后必须还能被 Agent 准确引用。跑压缩def run_compaction(messages, config): prefix, compacted, recent select_messages( messages, model_context_length128000, configconfig ) summary generate_summary(compacted, previous_summaryNone) variables extract_variables(compacted) summary_message { role: user, content: summary, metadata: {isCompactionSummary: true}, } return prefix [summary_message] recent, variables压缩完成后把结果和变量一起注入下一轮请求然后问 Agent 一个依赖早期数值的问题def verify_no_info_loss(compacted_messages, variables): system_prompt build_system_prompt(variables) test_messages [ {role: system, content: system_prompt}, *compacted_messages, {role: user, content: 这家公司最新的 EPS 是多少直接给数字。}, ] resp client.chat.completions.create( modelos.environ[COMPACTION_MODEL], messagestest_messages, ) return resp.choices[0].message.content成功的结果是Agent 回答170.69而不是大约 170或我需要重新查询。如果它回答让我重新调用工具获取说明变量提取没生效数值没进 Session Variables。Token 对比方法有两种。第一种用 API 返回的真实 usage最准def measure_tokens(messages, model): resp client.chat.completions.create( modelmodel, messagesmessages, max_tokens1, ) return resp.usage.prompt_tokens压缩前测一次压缩后测一次对比。我实测下来100 轮对话压缩前约 120K token压缩后约 30K压缩比 75% 左右。第二种用本地 tokenizer 估算适合在压缩决策前快速判断但记得用真实 usage 校准。验证脚本跑通后你会看到类似这样的输出压缩前 prompt_tokens: 121438 压缩后 prompt_tokens: 30412 压缩比: 74.96% 关键数值验证: EPS170.69 ✓如果压缩比低于 50%说明 tailTurns 或 preserveRecentTokensRatio 设太大了如果关键数值验证失败检查变量提取的 Prompt 和update_variable工具是否被调用。端到端验证建议跑满 100 轮以上中间每隔 20 轮问一次早期数值确认没有稀释现象。这一步跑通你的增量压缩就算落地了。5. 本篇常见错误排查401、local proxy failed 与 choices 解析压缩链路跑起来之前报错是少不了的。这一节把最常见的几类错误和排查路径列清楚对照着看能省不少时间。401 Unauthorized。这是最高频的。原因通常是 Key 没读到、Key 失效、或者 Base URL 写错。先确认环境变量真的注入了echo $TAOTOKEN_API_KEY echo $TAOTOKEN_BASE_URL如果 Key 打印出来是空的说明 export 没生效检查是不是在子 shell 里跑的。Base URL 必须是https://taotoken.net/api注意结尾不要多加/v1OpenAI SDK 会自己拼/chat/completions。如果 Key 和 URL 都对还是 401去控制台确认 Key 状态是否正常、额度是否耗尽。local proxy failed / connection refused。这类错误说明请求根本没发出去卡在本地网络层。先确认你的运行环境能正常访问外网再确认没有本地代理配置干扰。检查HTTP_PROXY、HTTPS_PROXY环境变量是否被意外设置env | grep -i proxy如果有输出临时清掉再试unset HTTP_PROXY HTTPS_PROXY另外确认防火墙没拦 443 出站。这类问题跟压缩逻辑无关是纯网络层先隔离掉再调压缩。reading choices of undefined。这个报错说明你拿到的响应体不是预期的 OpenAI 格式代码里resp.choices[0]就炸了。常见原因有三个一是请求路径拼错打到了非 chat 接口二是模型 ID 写错服务端返回了错误对象而不是正常响应三是响应被中间层改写。排查方法是在解析前先打印原始响应resp client.chat.completions.create(...) print(resp.model_dump_json(indent2))看清楚返回结构再改解析代码。如果是模型 ID 问题去文档页核对准确的 Model ID 字符串。OAuth / auth.json 相关错误。如果你用 Codex 或 Claude Code 这类工具接入它们有自己的认证文件。以 Codex 为例~/.codex/auth.json里存的是认证信息接入 TaoToken 时三件套必须写全Base URL、Key、Model ID。缺任何一个都会认证失败。Claude Code 的配置在~/.claude/settings.json同样三件套齐全。如果你用 CC Switch 管理多套配置切换后记得确认当前生效的是哪一套别在错误的配置上调半天。压缩后 Agent 重新执行工具。这不是报错是行为异常但危害更大。根因是尾部结构保护没生效孤立的 tool result 让 LLM 以为调用未完成。检查select_recent里的结构守卫逻辑确认 tail 头部的 tool 消息能向前找到配对的 assistant。写个单测覆盖这个场景def test_tool_result_pairing(): messages [ {role: user, content: u1}, {role: assistant, tool_calls: [{id: c1}]}, {role: tool, tool_call_id: c1, content: r * 30000}, {role: user, content: u2}, ] recent, _ select_recent(messages, tail_turns1, max_recent_tokens100) roles [m[role] for m in recent] assert assistant in roles and tool in roles assert roles.index(assistant) 1 roles.index(tool)Token 估算偏差过大。表现为压缩触发时机不对要么太早浪费预算要么太晚溢出。根因是纯 tokenizer 估算和真实 usage 偏差大。解法是用UsageAwareTokenEstimator的思路以最新一次真实 usage 为锚点只估算锚点之后的新消息增量。同时加合理性窗口reported落在[0.25x, 4x]之外就回退到纯估算。这个窗口能拦住两类异常缓存命中时 input_tokens 只报非缓存部分偏小以及 cacheRead 异常膨胀偏大。压缩轮次混乱。多次压缩后不知道当前是第几轮导致摘要 Prompt 里的更新语义失效。解法是给摘要消息打isCompactionSummary元数据标记压缩前统计已有摘要数量compactionRound previousSummaryCount 1把这个轮次传给摘要 Agent。排查顺序建议先确认网络和认证401、proxy再确认响应格式choices最后调压缩逻辑本身结构保护、估算校准。别一上来就怀疑压缩算法八成问题在接入层。6. 把压缩能力接进你的 Agent 链路压缩逻辑调通之后最后一步是把它接进你的 Agent 运行时。接入点选在每轮请求发送前的 TransformContext 阶段这个位置能拿到完整消息列表又还没发出去是压缩决策的唯一正确时机。接入流程分三步。第一步在请求构建阶段调用估算器判断是否触发压缩def before_llm_call(messages, model_context_length, config): if len(messages) config[minMessagesForCompaction]: return messages estimated estimator.estimate_context_tokens(messages) threshold_tokens model_context_length * config[threshold] if estimated threshold_tokens: return messages compacted, variables run_compaction(messages, config) session_variables.update(variables) return compacted第二步把会话变量注入 System Prompt。每轮构建 System Prompt 时无条件追加## Session Variables段落并带上那段关键提示词——让 LLM 需要早期数据时先查表而不是凭记忆编造。这段提示词是闭环的关键别省。第三步把压缩事件推给前端。压缩发生时发一个compaction_end事件带上提取出的变量前端渲染成变量卡片。用户能直观看到会话当前持有哪些关键数据压缩过程从黑盒变成可观测。如果你用 Claude Code 做编码类 Agent接入方式类似配置写在~/.claude/settings.json里Base URL 填https://taotoken.net/apiKey 和 Model ID 按前面说的三件套配齐。Cline 的 MCP 配置同理Base URL、Key、Model ID 一个都不能少。Codex 的~/.codex/auth.json也是这三件套。长期跑编码 Agent 的话压缩频率高、token 消耗大建议用 Coding Plan 把成本压下来https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各工具的完整配置示例。Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 建议按 Agent 用途分 Key方便统计和限额。最后说个实战经验压缩阈值别一上来就设 0.8先用 0.7 跑一段时间观察真实 usage 和估算的偏差再往上调。我踩过的坑是阈值设太高估算又偏乐观结果第 95 轮左右直接溢出前功尽弃。留足余量比压到极限更稳。变量提取的 Prompt 也要根据你的业务调——投资场景重点提数值代码场景重点提文件路径和函数签名别用一套 Prompt 打天下。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →