任务跑一半,API 突然报“对话超长“断流:我给 Agent 修的紧急逃生通道

本文是我开源项目 codeAgent(终端 AI 编程智能体)的系列第 2 篇,每天讲一个里面真实用到的技术点,全部附源码。 仓库:github.com/Harvil1/cod...

场景:最猝不及防的一种死法

长任务跑到第 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:

python 复制代码
# PTL 错误识别的统一口径(四家服务商措辞全认)。此前三处各写各的
# 子集(摘要重试认 2 种、主循环认 3 种、丢条数计算认 4 种)------OpenAI
# 风格的 "maximum context length ..." 在摘要重试路径会被当普通失败,
# 白计一次熔断还降级规则总结。全项目判定一律走 is_prompt_too_long_error
_PTL_MARKERS = (
    "prompt_too_long", "context_length", "too long", "maximum context",
)


def is_prompt_too_long_error(err_str: str) -> bool:
    """判断一段错误文本是不是「输入超长」(PTL)。四家措辞口径统一。"""
    s = (err_str or "").lower()
    return any(kw in s for kw in _PTL_MARKERS)

教训藏在注释里:错误分类这类"知识"必须收敛到一处,三个地方各写一个子集版本,迟早有人只改一处。

第二关:紧急压缩函数(核心源码)

识别出来之后立刻瘦身重发。做法简单粗暴:只留 system + 一条边界占位 + 最近 5 条消息 ,其余的靠落盘的 transcript 快照找回。源码在 agent/context_pipeline.py:

python 复制代码
# 紧急压缩的两道保险默认值(config 可覆盖):冷却窗口秒数 / 单会话次数上限
REACTIVE_COOLDOWN_SECONDS = 60
REACTIVE_MAX_PER_SESSION = 5


def reactive_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_at
    if session_state.reactive_count > 0 and elapsed < cooldown_seconds:
        logger.info(
            "reactive_compact 冷却中(距上次 %ds < %ds),跳过",
            int(elapsed), int(cooldown_seconds),
        )
        return messages, False

    # 保护 2:单会话上限
    if session_state.reactive_count >= max_per_session:
        logger.warning(
            "reactive_compact 达到单会话上限 %d 次,跳过",
            session_state.reactive_count,
        )
        return messages, False

    system, conv = _split_system(messages)
    keep = conv[-keep_recent:] if len(conv) > keep_recent else conv[:]
    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 += 1
    return new_messages, True

这 60 行里藏了四个细节

1. 为什么允许触发 5 次而不是 1 次? 砍到只剩 5 条后,下一轮又来一个巨型工具结果,还会再超------所以留了多次触发的余地。但必须有冷却窗口(60 秒)+ 次数上限(5 次)两道闸,否则会陷入"压了又超、超了又压"的死亡循环,日志和账单一起爆炸。

2. 占位消息以 [COMPACT_BOUNDARY] 开头。 会话恢复时只认这个前缀来定位"历史上次压到哪",紧急压缩和常规压缩共用同一套边界标记------逃生通道也得上户口,不然恢复逻辑找不到断点。

3. _fix_tool_call_pairs 不能省。 从中间砍消息,很容易把"模型要调工具"和"工具结果"切成两半------孤儿 tool_call 会让下一次请求直接被 API 拒掉(400)。砍完必须对账补配。

4. 被砍的内容给了一条活路。 压缩前完整对话已落盘 .transcripts/,占位消息里明确告诉模型:可以去读快照找回上下文。丢的是上下文窗口里的位置,不是信息本身。

小结

三句话带走:

  1. 错误识别统一口径:四家服务商措辞全收进一个函数,全项目只认它
  2. 紧急压缩 = 保 system + 边界占位 + 最近 5 条,立刻重发不断线
  3. 两道保险(冷却 60s / 上限 5 次)防死亡循环,砍完修 tool_call 配对

下一篇讲它的上游:四级上下文压缩怎么分层------目标是让任务压根走不到"报超长"这一步(照样附源码)。


仓库在这,注释全中文,欢迎 Star ⭐:github.com/Harvil1/cod...

相关推荐
用户EasyAdminBlazor1 小时前
EasyAdminBlazor SignalR 实时消息源码解析:从 NotificationHub 到站内消息
后端
程序猿乐锅1 小时前
【黑马点评 | 第十一篇】关注 Feed 流实现
java·数据库·redis·分布式·后端·缓存·maven
用户EasyAdminBlazor1 小时前
EasyAdminBlazor 定时任务:FreeScheduler 可视化调度与任务管理
后端
Cosolar1 小时前
云端部署阿里 Qwen-Image-2.1 保姆级教程
人工智能·后端·github
yunwei372 小时前
eBPF 开发实践:使用 sockops 加速网络请求转发
linux·后端·性能优化
看浪的路人2 小时前
第7讲:实时告警与自动化响应
开发语言·后端·golang
Gopher_HBo2 小时前
zap WriteSyncer与Sink体系
后端
合尘猫2 小时前
Nginx stream 做 GitHub 443 SNI 透传:完整配置、多上游故障转移与三个坑
后端·程序员·开源
知守观2 小时前
百万级数据导出OOM:POI的坑与EasyExcel的流式写入实战(附内存对比)
java·后端