任务跑一半,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...

相关推荐
念何架构之路6 分钟前
go-grpc设计思想
开发语言·后端·golang
system_error_x37 分钟前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn2 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈3 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780514 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123454 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒4 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记4 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598784 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端
雨中飘荡的记忆4 小时前
Claude Code 入门必会:@、!、Slash Commands,一篇讲清楚
人工智能·后端