Coding Agent 上下文模块拆解:压缩上下文后为什么还能继续工作?
发布时间:2026/9/26 14:18:52来源:尧图网络
1. 长会话跑到一半就“失忆”问题出在上下文模块如果你正在搭一个 Coding Agent大概率遇到过这种场景让它改一个中等规模的项目前几轮读文件、搜符号、跑测试都挺顺到了第十几轮突然开始胡言乱语——明明刚读过的文件说没读过用户最早说的“不要动数据库迁移脚本”也忘了甚至把工具调用和工具结果拆开模型看到一个没有来源的tool_result直接报错。很多人第一反应是“模型不行”但换更大的模型往往只是把崩溃点往后推几轮。真正的原因在于Coding Agent 的上下文不是聊天记录而是它的工作内存。系统提示词、工具定义、文件内容、命令输出、工具调用结果全都占窗口。用户可能只说了一句“帮我修这个 bug”但 Agent 为了修它读了八个文件、跑了三次测试真正吃掉上下文的是这些工作材料不是那句需求。所以上下文模块要解决的不是“Token 太多”这一个问题而是一整套工程问题什么时候压缩、压缩时保留什么、压缩后怎么恢复任务状态、压缩失败怎么兜底。这篇就按一个可运行的上下文模块骨架把窗口管理、压缩触发、Prompt Cache 复用这几层拆开讲配置和验证步骤都能直接抄。2. 动手前先把 TaoToken 的接入准备好这套上下文模块最终要调用模型接口所以先把接入层配好。我用的是 TaoToken 的 API它的接口格式和主流模型服务兼容改base_url就能接上省得在上下文模块里再写一层适配。先去官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册然后在控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 里创建一个 API Key。Key 只在创建时完整显示一次复制下来存到环境变量里别写进代码提交。export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面写了请求格式和参数说明。如果你只是想先验证模型能不能正常对话可以直接用模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 试几句确认 Key 有效再往下写代码。这里有个容易踩的坑上下文模块里会频繁读取模型窗口大小如果你用的模型不在内置映射表里一定要在配置里显式指定窗口否则会走保守默认值压缩触发得太早白白浪费可用空间。3. 上下文模块配置骨架窗口、预算、压缩阈值下面这份配置骨架是我实测下来比较稳的结构分成四块窗口解析、Token 估算、工具结果预算、压缩阈值。你可以直接建一个context_config.py。# context_config.py from dataclasses import dataclass, field dataclass class ContextConfig: # 1. 窗口解析显式配置优先其次运行时发现最后内置映射 model_name: str claude-sonnet-4-20250514 explicit_window: int | None None # 用户显式指定最高优先级 fallback_window: int 100_000 # 无法识别时的保守默认值 # 2. Token 估算 chars_per_token: float 3.5 # 中英混合的粗略系数 real_usage_anchor: int | None None # 上次 API 返回的真实用量 # 3. 工具结果预算 single_result_limit: int 50_000 # 单条结果字符上限 single_result_preview: int 2_000 # 外移后保留的预览字符数 aggregate_budget: int 80_000 # 单轮工具结果聚合预算 stale_keep_rounds: int 3 # 保留最近几轮完整结果 # 4. 压缩阈值 summary_output_reserve: int 8_000 # 给摘要输出预留 auto_compact_margin: int 20_000 # 自动压缩安全余量 manual_compact_margin: int 5_000 # 手动压缩安全余量 tail_keep_tokens: int 30_000 # 尾部保留原文的 Token 预算 min_prefix_to_compact: int 10_000 # 前缀太短就不压缩 # 5. 熔断 max_compact_failures: int 3 def resolve_window(self) - int: if self.explicit_window: return self.explicit_window # 实际项目里这里查运行时缓存和内置映射表 builtin {claude-sonnet-4-20250514: 200_000} return builtin.get(self.model_name, self.fallback_window) def auto_compact_threshold(self) - int: return (self.resolve_window() - self.summary_output_reserve - self.auto_compact_margin)几个参数的含义值得说清楚。single_result_limit控制单条工具结果超过就原文落盘、上下文只留预览。aggregate_budget管的是单轮多个结果的总量超了就按大小排序优先外移最大的那个这样修改次数少、回收空间多。auto_compact_threshold不是窗口本身而是窗口减去摘要输出预留和安全余量——生成摘要本身也要一次模型调用和输出空间阈值等于窗口的话摘要请求还没发出去就已经撞墙了。Token 估算这块别用字符数硬估整段历史误差会累积。正确做法是“真实锚点 增量估算”每次模型响应后把 API 返回的真实用量记下来当锚点下一轮只对锚点之后新增的消息做字符估算。公式就是当前用量 ≈ 上次真实用量 新增消息估算值。刚启动或刚压缩完锚点失效时才退化成估算整段历史等下一次 API 响应重新建锚。4. 压缩触发与 Prompt Cache 复用决策必须冻结压缩触发逻辑嵌在每轮模型请求之前不是独立定时任务。核心判断就一句当前估算用量是否超过auto_compact_threshold()。超了就执行 Auto-Compact把对话切成两段——较早的前缀交给模型生成结构化摘要最近一段消息保持原样拼回。def maybe_compact(conv, cfg: ContextConfig, failures: int): if failures cfg.max_compact_failures: return conv, failures, circuit_open estimated estimate_tokens(conv, cfg) if estimated cfg.auto_compact_threshold(): return conv, failures, no_need prefix, tail split_by_tail_budget(conv, cfg.tail_keep_tokens) # 边界对齐不能让 tail 从 tool_result 开始而 tool_use 在 prefix 里 prefix, tail align_tool_boundary(prefix, tail) if estimate_tokens(prefix, cfg) cfg.min_prefix_to_compact: return conv, failures, prefix_too_short try: summary call_summary_model(prefix) # 摘要模型禁止调用工具 new_conv rebuild(summary, tail, restore_attachments(conv)) return new_conv, 0, compacted except Exception: return conv, failures 1, compact_failed这里有两个细节决定成败。第一是边界对齐如果保留区从一条tool_result开始却把前面的tool_use放进了摘要区模型会看到一个找不到来源的工具结果直接报错。所以切分点要向前对齐保证工具调用和结果成对保留。第二是前缀太短就不压缩如果可摘要的前缀只有几千 Token生成摘要消耗的可能和回收的差不多纯属浪费一次调用。然后是 Prompt Cache 复用。Prompt Cache 按提示词前缀匹配覆盖 tools → system → messages 的有序前缀缓存点之前的内容一变前缀哈希就变原缓存失效。这意味着同一段旧历史在每轮请求前被重新处理时替换决策必须冻结——某个工具结果一旦被判定为“外移”后续每轮都用完全相同的那段预览文本不能重新生成。class ReplacementLedger: def __init__(self): self.decisions {} # tool_call_id - {replaced: bool, preview: str} def decide(self, tool_call_id: str, content: str, cfg: ContextConfig): if tool_call_id in self.decisions: return self.decisions[tool_call_id] # 冻结不重新评估 if len(content) cfg.single_result_limit: d {replaced: True, preview: content[:cfg.single_result_preview]} else: d {replaced: False, preview: content} self.decisions[tool_call_id] d return d决策冻结表面上是省重复计算本质是稳定请求前缀和跨轮行为。恢复会话时根据当前消息里的工具调用 ID 重建状态只恢复仍然存在的决策避免旧记录污染新会话。5. 验证请求压缩后任务连续性怎么测配好之后要验证三件事压缩确实触发了、缓存命中了、压缩后任务还能继续。先验证压缩触发。构造一段超长对话把auto_compact_margin临时调小让它必然触发cfg ContextConfig(explicit_window20_000, auto_compact_margin2_000) conv build_long_conversation(rounds30) # 每轮带工具结果 new_conv, failures, status maybe_compact(conv, cfg, failures0) print(status) # 期望输出 compacted print(len(new_conv)) # 期望明显小于 len(conv)再验证缓存命中。连续发两次前缀相同的请求第二次的响应里应该能看到缓存读取的用量字段resp1 client.messages.create(modelcfg.model_name, messagesmsgs, ...) resp2 client.messages.create(modelcfg.model_name, messagesmsgs, ...) print(resp1.usage) # 第一次cache_creation_input_tokens 有值 print(resp2.usage) # 第二次cache_read_input_tokens 有值如果第二次cache_read_input_tokens是 0说明前缀被改动了回去检查替换决策有没有冻结、预览文本是不是每轮重新生成的。最后验证任务连续性。这是最关键的一步让 Agent 执行一个多轮任务中途强制压缩看它还能不能接着干。task 读取 utils.py找到 parse_config 函数把它改成支持环境变量覆盖然后跑测试 # 在第 5 轮工具调用后手动触发压缩 result run_agent(task, force_compact_at_round5) assert parse_config in result.final_diff # 压缩后仍记得目标函数 assert result.tests_passed # 压缩后仍能继续执行实测下来只要恢复附件里带了文件快照、Skill 状态和关键用户约束原文压缩后 Agent 基本能无缝接上。恢复附件里要明确提示“文件快照不是当前真相需要精确内容请重新读取”否则模型会拿旧快照猜代码。6. 本篇常见错排查报错一tool_result找不到对应的tool_use。压缩切分点落在了工具调用和结果之间。检查align_tool_boundary确保保留区不会从孤立的tool_result开始。报错二压缩后 Agent 反复重试同一个失败操作。摘要里丢了“这个方案已经试过且失败”的状态。摘要 Prompt 里要强制包含“已尝试方案与失败原因”字段恢复附件里也要带上最近的错误原文。报错三缓存命中率一直是 0。三个常见原因替换决策没冻结预览文本每轮变系统提示词里带了时间戳之类的动态内容工具定义顺序不稳定。逐个排查把动态内容挪到 messages 里别放 system。报错四压缩请求本身超限。摘要请求也占窗口。如果前缀太长摘要请求自己就超了。这时要按对话轮次裁掉一部分最早内容再重试连续三次失败就打开熔断提示用户手动处理别让 Agent 陷入“超限→压缩失败→再压缩”的死循环。报错五恢复会话后行为不一致。替换记录没做会话隔离。恢复时要用当前消息里的工具调用 ID 重建状态只恢复仍然存在的决策旧会话的记录不能带进来。7. 把接入和排障串起来上下文模块调通之后日常调试主要看两个地方一是压缩日志确认触发时机和回收空间是否符合预期二是缓存用量确认前缀稳定。如果你还在选模型或者对比不同模型的长上下文表现可以用模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 快速试几轮长对话观察压缩行为。接入层如果遇到 Key 或请求格式问题去 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 重新生成一个再对照接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 检查base_url和请求头。长期跑编码任务或者多 Agent 协作的话Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 在配额和并发上更适合持续会话场景省得跑到一半被限流打断。最后留一个我踩过的坑别把压缩阈值设得太激进。我一开始为了省 Token 把auto_compact_margin调到很小结果每几轮就压缩一次摘要调用本身的开销反而比省下来的还多而且频繁压缩让 Agent 的任务状态反复重建行为变得不稳定。后来把余量调回两万 Token 左右压缩频率降下来整体反而更省。阈值这东西得结合你的模型窗口和任务类型实测没有万能数字。
网站建设高端定制企业官网