Skip to content

[P1][T34] 统一完整模型请求的上下文窗口预算与压缩策略 #137

Description

@Aias00

目标与优先级

不需要再造摘要器;需要组合既有压缩能力并在最终请求组装后判断是否真的满足模型窗口。

当前证据与判断边界

  • SummarizationHook/ContextEditingInterceptor 已有 message token threshold;LargeResultEvictionInterceptor 已有大工具结果外置。
  • 这些组件各自触发;SummarizationHook 在无 safe cutoff 时保留原消息,未对最终 system/tools/schema/预留输出组成的完整请求设置统一硬门。

以上是源码观察;能力补充结论是基于这些路径的推断,不等同生产故障或漏洞已经复现。框架已有ReAct/Hook、摘要、Todo、Schema声明、工具拦截及可选Sandbox,本任务是在其基础上补齐边界。

范围与落地方案

  • 明确模型 contextWindow/input/output reserve,统计最终system、messages、工具Schema、模板、RAG及多模态估算;区分准确计数与保守估算。
  • 复用既有摘要、编辑和结果外置,定义顺序、可保护内容和压缩后重新计数;请求仍过大则明示失败/缩小工具集合,不继续冒险调用。
  • 保护可信指令、当前计划、最近未完成 toolCall/result 配对、approval/input identity;摘要失败不得用错误字符串替代可靠上下文继续执行。
  • 结果外置只有写入确认才返回可读引用;失败不能伪称文件已保存。durable模式引用可恢复性复用 [P1A][T07] 增加版本固定的运行定义解析与恢复能力校验 #123/[P1C][T14] 持久化模型调用结果与稳定工具执行计划 #125 和存储任务能力检查。

前置依赖

  • 无前置Task,可独立交付基础契约和reference tests。

已有任务与去重边界

这些是关联/集成位置,不是额外硬阻塞。仅前置依赖要求先交付;持久化和跨实例能力继续由原任务承担。

验收条件

  • 最终请求预算包含toolSchema/system/RAG/output reserve,而不只消息列表。
  • 压缩后再次验证预算;所有内容不可压缩或摘要失败时无超预算 provider dispatch。
  • 跨多个消息的工具配对、审批参数和当前计划不被误删;重复压缩结果稳定可读。
  • 外置写失败不返回成功文件指针;计数器替换/模型窗口差异有模型无关测试,旧压缩默认行为不变。

非目标

兼容与验证

  • Java 17,新增路径opt-in;保留现有公开API、默认行为与历史state/checkpoint格式。不引入MQ或节点级分片。
  • Core不导入Redis/Extensions;优先复用已有API、reducer和组件,不直接添加新依赖。
  • 先模型无关单测,再按本任务范围运行模块测试/API与格式fixture;跨adapter路径用stub contract tests,真实Docker测试注明环境要求。
  • 当前main的兼容文档/CI曾调整,新任务不引用已删除文档或自行恢复CI;遵循实施时实际有效门禁,保留兼容回归目标。

源码依据(固定版本)

分析基线:Core f3bcbef24aa79e1e265a7d0d2011274982ac2978、Extensions 1ba3048f16d48c9f41dab171eed9a91148b45758。只读compare确认相对上次分析没有生产Java变更,主要是release/build/docs更新;本Issue不表示能力已实现。

Activity

  1. added theissue type on Oct 9, 2026
  2. Carl-Zhou-CN commented on Oct 23, 2026

    @Carl-Zhou-CN
    Contributor

    assigne me @Aias00

  3. konlue commented on Nov 2, 2026

    @konlue
    Contributor

    您好,我想认领 #137。

    基于 main 738354633 确认:

    1. 预算口径只数消息,不数最终请求的其他组成:TokenCounter.countTokens(List<ChatMessage>) 只累计消息正文与 assistant 的 tool call arguments(chars/4 近似),不计 system 消息、工具 schema 与输出预留;而最终派发的是 ReActAgent.invokeModel 里的 new ChatRequest(composeMessages(request), effective.definitions(), request.getOptions(), request.getAttributes())——composeMessages 把 system 拼进消息,effective.definitions() 是 ToolDefinition(name, description, inputSchema)。GenerationOptions 只有 maxTokens(输出上限),全仓没有 contextWindow / input budget / reserve 的概念。
    2. 压缩后没有统一复算,也没有超预算不派发的硬门:SummarizationHook.beforeModel 只按 previousMessages 计数并与阈值比较;findSafeCutoff 找不到安全切点时 log.warn("Cannot find safe cutoff point for summarization") 后原样返回原消息继续,摘要抛异常时也「保留原历史继续」;ContextEditingInterceptor.interceptModel 找不到可清候选时原样放行。两处都没有「最终请求仍然超预算 → 不得调用 provider」的收口。
    3. 外置写失败仍返回“已落盘”指针:LargeResultEvictionInterceptor.processLargeResult 在 backend.write(...) 返回错误或 backend == null 时只 log.warn,随后仍返回 TOO_LARGE_TOOL_MSG(「was saved in the filesystem at this path: …」)并置 status=evicted_to_filesystem——伪称文件已保存。
    4. 不可压缩内容的保护没有统一契约:可信指令、当前计划、跨消息的 toolCall/tool-result 配对、approval/input identity 的保护分散在各组件,没有统一判定,也没有对应测试。

    方案(单 PR,只做预算契约与硬门,不动既有默认行为)

    • 中立预算契约:在 argi-agent-api 新增 io.github.agentic.ai.agent.budget 包,定义 ContextWindowBudget(输入窗口 + 输出预留)、RequestTokenEstimate(system / 消息 / 工具 schema / 输出预留的分解估算,区分准确计数与保守估算)、RequestTokenEstimator(可替换的计数口径)与默认近似实现、BudgetComponent 与 BudgetViolation(稳定越界分类)、ContextWindowBudgetExceededException(明确失败,不静默截断)。
    • 预算硬门:同包新增 ContextBudgetInterceptor(中立层 ModelInterceptor,opt-in 挂进既有 interceptors(...) 链):估算 → 超限则调 RequestCompactor → 重新估算 → 仍超限则抛异常且不调用 provider;未超限才放行。中立 ReAct 与框架 ReactAgent 两条路径共用同一个闸门,不需要改任何引擎。
    • 复用既有压缩,不重建:提供 RequestCompactor 接缝,框架侧用适配器接既有 SummarizationHook(不改其默认阈值与默认行为);配对保护用 ToolCallPairingValidator 校验「assistant toolCall ↔ tool 结果」一一配对,压缩破坏配对即拒绝派发。
    • 修外置写失败语义:LargeResultEvictionInterceptor 写失败或未配置 backend 时不再返回声称已落盘的指针,改为显式失败状态并保留原 metadata;不动装配形态(迁移到生命周期插件属于 [Refactor][P1] 将摘要、上下文编辑与大结果处理接入生命周期插件 #302 的范围)。

    测试(离线,模型无关)

    1. 预算口径:system、工具 schema、输出预留都进入估算,分解可断言;未超限 → handler 调用一次。
    2. 超限不派发:超预算 → 抛 ContextWindowBudgetExceededException,handler 调用次数为 0。
    3. 压缩后复算:超限 → 压缩 → 重新估算通过 → handler 调用一次,且收到的是压缩后的请求。
    4. 压缩无效仍失败:压缩后仍超限 → 抛异常,handler 调用次数为 0。
    5. 配对保护:压缩破坏 toolCall/tool-result 配对 → 拒绝派发,handler 调用次数为 0。
    6. 计数口径可替换:换成精确计数器后,同一请求的判定不同。
    7. 边界:预算等于总量时放行、超出 1 时拒绝。
    8. 框架侧:大结果写失败或无 backend → 结果不含文件指针、不含 “saved in the filesystem” 文案、状态不是 evicted_to_filesystem;压缩适配器在摘要成功时返回压缩后的请求、摘要失败时原样返回。
  4. konlue commented on Nov 2, 2026

    @konlue
    Contributor

    补充一处测试口径:LargeResultEvictionInterceptor.Builder.build() 在启用驱逐时会自动补一个默认 LocalFilesystemBackend,所以「未配置 backend」的分支只有防御意义、无法经 builder 构造;测试覆盖的是写失败这一条真实路径——backend 返回错误时,结果不含文件指针、不含 “saved in the filesystem” 文案,状态为 error,原 response 的 metadata 保留。其余测试清单不变。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority/important-soon高优先级:近期应安排处理的重要问题

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions