本文是我开源项目 codeAgent(终端 AI 编程智能体)的系列第 2 篇,每天讲一个里面真实用到的技术点,全部附源码。
仓库:https://github.com/Harvil1/codeAgent
场景:最猝不及防的一种死法
长任务跑到第 30 轮,一个大工具结果塞进来,下一轮请求直接被 API 拒了:
Error: prompt_too_long / maximum context length exceeded …
此时最不能做的就是把异常抛给用户——前面 30 轮的工作全卡在半空。我的做法是修一条"紧急逃生通道":识别它 → 立刻瘦身 → 马上重发,任务自己缓过来。
第一关:四家服务商,四种报错措辞
DeepSeek 说prompt_too_long,OpenAI 说maximum context length,还有context_length、too long各种变体。如果识别口径不统一,OpenAI 风格的错误会走普通失败重试——白烧一次熔断次数。
所以我全项目只认一个判定函数,源码在agent/context_compressor.py:
# PTL 错误识别的统一口径(四家服务商措辞全认)。此前三处各写各的# 子集(摘要重试认 2 种、主循环认 3 种、丢条数计算认 4 种)——OpenAI# 风格的 "maximum context length ..." 在摘要重试路径会被当普通失败,# 白计一次熔断还降级规则总结。全项目判定一律走 is_prompt_too_long_error_PTL_MARKERS=("prompt_too_long","context_length","too long","maximum context",)defis_prompt_too_long_error(err_str:str)->bool:"""判断一段错误文本是不是「输入超长」(PTL)。四家措辞口径统一。"""s=(err_stror"").lower()returnany(kwinsforkwin_PTL_MARKERS)教训藏在注释里:错误分类这类"知识"必须收敛到一处,三个地方各写一个子集版本,迟早有人只改一处。
第二关:紧急压缩函数(核心源码)
识别出来之后立刻瘦身重发。做法简单粗暴:只留 system + 一条边界占位 + 最近 5 条消息,其余的靠落盘的 transcript 快照找回。源码在agent/context_pipeline.py:
# 紧急压缩的两道保险默认值(config 可覆盖):冷却窗口秒数 / 单会话次数上限REACTIVE_COOLDOWN_SECONDS=60REACTIVE_MAX_PER_SESSION=5defreactive_compact(messages:list,*,session_state:CompressionSessionState,keep_recent:int=5,cooldown_seconds:float=REACTIVE_COOLDOWN_SECONDS,max_per_session:int=REACTIVE_MAX_PER_SESSION,now_fn=time.time,)->Tuple[list,bool]:"""紧急通道:API 报「对话超长」(prompt_too_long)时立刻调用。 做法简单粗暴:只留 system + 一条说明占位 + 最后 keep_recent 条消息, 其余全靠 transcript 文件找回。 **可多次触发**:每次报错都可以再压,但有两道保险: 1. 冷却窗口:距上次触发不足 cooldown_seconds 秒(默认 60s)→ 跳过 2. 单会话上限:本场已触发 max_per_session 次(默认 5)→ 跳过 """# 保护 1:冷却窗口now=now_fn()elapsed=now-session_state.reactive_last_atifsession_state.reactive_count>0andelapsed<cooldown_seconds:logger.info("reactive_compact 冷却中(距上次 %ds < %ds),跳过",int(elapsed),int(cooldown_seconds),)returnmessages,False# 保护 2:单会话上限ifsession_state.reactive_count>=max_per_session:logger.warning("reactive_compact 达到单会话上限 %d 次,跳过",session_state.reactive_count,)returnmessages,Falsesystem,conv=_split_system(messages)keep=conv[-keep_recent:]iflen(conv)>keep_recentelseconv[:]placeholder={"role":"user","content":(# 边界前缀和大写统一:恢复侧 _truncate_at_last_compact_boundary# 只认 [COMPACT_BOUNDARY] 开头,reactive 的占位也得能被裁剪定位"[COMPACT_BOUNDARY]\n""[紧急上下文压缩:API 返回 prompt_too_long,"f"已只保留最近{len(keep)}条消息。""如 .transcripts/ 下有快照(latest.txt 指向最新一份)可 ""read_file 分段找回;无快照则被裁原文已不可恢复]"),}new_conv=[placeholder]+keep new_conv=_fix_tool_call_pairs(new_conv)new_messages=_reassemble(system,new_conv)session_state.reactive_last_at=now session_state.reactive_count+=1returnnew_messages,True这 60 行里藏了四个细节
1. 为什么允许触发 5 次而不是 1 次?砍到只剩 5 条后,下一轮又来一个巨型工具结果,还会再超——所以留了多次触发的余地。但必须有冷却窗口(60 秒)+ 次数上限(5 次)两道闸,否则会陷入"压了又超、超了又压"的死亡循环,日志和账单一起爆炸。
2. 占位消息以[COMPACT_BOUNDARY]开头。会话恢复时只认这个前缀来定位"历史上次压到哪",紧急压缩和常规压缩共用同一套边界标记——逃生通道也得上户口,不然恢复逻辑找不到断点。
3._fix_tool_call_pairs不能省。从中间砍消息,很容易把"模型要调工具"和"工具结果"切成两半——孤儿 tool_call 会让下一次请求直接被 API 拒掉(400)。砍完必须对账补配。
4. 被砍的内容给了一条活路。压缩前完整对话已落盘.transcripts/,占位消息里明确告诉模型:可以去读快照找回上下文。丢的是上下文窗口里的位置,不是信息本身。
小结
三句话带走:
- 错误识别统一口径:四家服务商措辞全收进一个函数,全项目只认它
- 紧急压缩 = 保 system + 边界占位 + 最近 5 条,立刻重发不断线
- 两道保险(冷却 60s / 上限 5 次)防死亡循环,砍完修 tool_call 配对
下一篇讲它的上游:四级上下文压缩怎么分层——目标是让任务压根走不到"报超长"这一步(照样附源码)。
仓库在这,注释全中文,欢迎 Star ⭐:https://github.com/Harvil1/codeAgent