.agents/issue/agent-context-tool-compression-analysis.md
日期:2026-06-23
当前 Agent 上下文链路已经从旧的 compressed_messages: ChatCompletionMessageParam[] 转为 checkpoint 压缩模式:历史消息在超过阈值后被压成一条隐藏的 user message,并通过 contextCheckpoint 写入 AI history。工具结果压缩仍是单次 tool response 级别的压缩,执行后作为 tool message 回灌到同一条 agent loop 消息链。
整体设计方向是正确的:避免历史 assistant.tool_calls / tool message 被 LLM 改坏配对关系,同时保留 ask resume、plan、tool result 的连续上下文。但当前实现里有一个需要优先确认的风险:compressRequestMessages 的结构化工具 checkpoint 分支不产生 usage,而 onCompressContext 只有存在 result.usage 才返回压缩结果,导致这条无 LLM 压缩路径在 agent loop 中可能被忽略。
| 模块 | 职责 |
|---|---|
packages/service/core/workflow/dispatch/ai/agent/index.ts | Workflow Agent 节点入口,准备历史、用户上下文、工具、sandbox,并调用 unified loop。 |
packages/service/core/ai/llm/agentLoop/loop/unified.ts | 单主 Agent Loop 适配层,注入 ask_agent、update_plan 和 runtime tools,处理 stop gate。 |
packages/service/core/ai/llm/agentLoop/loop/base.ts | 底层循环:每轮请求前压缩上下文,请求 LLM,执行工具,压缩工具结果,回灌 tool message。 |
packages/service/core/ai/llm/compress/index.ts | 压缩实现:历史 checkpoint、通用长文本压缩、JSON 工具结果结构摘要、tool response 压缩。 |
packages/service/core/workflow/dispatch/ai/agent/adapter/eventMapper.ts | 将 loop 事件写入 assistantResponses 和 SSE;after_message_compress 在这里写 checkpoint。 |
packages/global/core/chat/adapt.ts | history -> GPT messages 适配;识别最新 checkpoint,丢弃 checkpoint 前普通历史。 |
packages/service/core/workflow/dispatch/utils/index.ts | 按节点 history 配置裁剪历史;存在 checkpoint 时优先从 checkpoint 开始保留。 |
dispatchRunAgent 通过 useUserContext 拿到 chatHistories、改写后的历史和当前用户消息。chats2GPTMessages({ reserveTool: true }) 将 FastGPT history 转为 LLM messages,并保留 agent/tool 结构。runUnifiedAgentLoop 注入 Main Agent system prompt,过滤历史里的 system message,组成初始 messages。runAgentLoop 每轮请求前调用 onCompressContext,由 compressRequestMessages 判断是否压缩。tool message 追加回 requestMessages,下一轮继续沿同一条消息链请求。ask_agent,pendingMainContext.messages 会保存当时 messages;用户回答后作为对应 ask tool response 接回原链路。关键代码:
runAgentLoop 每轮请求前压缩 request messages:packages/service/core/ai/llm/agentLoop/loop/base.ts:286requestMessages:packages/service/core/ai/llm/agentLoop/loop/base.ts:331pendingMainContext.messages 接回 tool response:packages/service/core/ai/llm/agentLoop/loop/unified.ts触发逻辑在 compressRequestMessages:
system/developer 与其它消息。系统类消息不参与摘要,但最终保留在最前面。model.maxContext * 0.8 才触发历史压缩。<context_checkpoint>...</context_checkpoint>。{ role: user, hideInUI: true } message 返回。关键代码:
packages/service/core/ai/llm/compress/index.ts:720packages/service/core/ai/llm/compress/index.ts:742packages/service/core/ai/llm/compress/index.ts:755packages/service/core/ai/llm/compress/index.ts:791packages/service/core/ai/llm/compress/index.ts:914checkpoint 不在 dispatchRunAgent 末尾显式追加,而是通过 loop 事件写入:
runAgentLoop 压缩成功后触发 onAfterCompressContext。runUnifiedAgentLoop 转发为 after_message_compress 事件。eventMapper 收到事件后向 assistantResponses push { contextCheckpoint, hideInUI: true }。getHistories 发现 AI history 中有 checkpoint 时,从最新 checkpoint 所在 history 开始保留,避免先按最近 N 轮裁掉 checkpoint。chats2GPTMessages 再次从最新 checkpoint value 精确切片,把 checkpoint 转为隐藏 user message,并跳过同一 value 的其它字段。关键代码:
packages/service/core/workflow/dispatch/ai/agent/adapter/eventMapper.ts:374packages/service/core/workflow/dispatch/utils/index.ts:387packages/global/core/chat/adapt.ts:73packages/global/core/chat/adapt.ts:479工具执行和压缩在 runAgentLoop 内完成:
requestMessages。onRunTool,runtime 内部工具如 ask_agent / update_plan 会设置 skipResponseCompress。compressToolResponse 压缩结果。tool message,追加到 requestMessages 和 assistantMessages。onAfterToolCall 把压缩后的 response 和压缩详情传给 workflow adapter,用于工具卡和运行详情。关键代码:
packages/service/core/ai/llm/agentLoop/loop/base.ts:425packages/service/core/ai/llm/agentLoop/loop/base.ts:461compressToolResponse:packages/service/core/ai/llm/agentLoop/loop/base.ts:469packages/service/core/ai/llm/agentLoop/loop/base.ts:506compressToolResponse 的预算策略:
model.maxContext * 0.5。(model.maxContext - currentMessagesTokens) / toolLength,避免并行工具结果整体打爆上下文。compressLargeContent。关键代码:
packages/service/core/ai/llm/compress/index.ts:1299packages/service/core/ai/llm/compress/index.ts:1316packages/service/core/ai/llm/compress/index.ts:1326compressRequestMessages 的结构化工具 checkpoint 分支返回:
return {
messages: finalStructuredMessages,
contextCheckpoint: structuredToolCheckpoint
};
该返回没有 usage。但 onCompressContext 只有 if (result.usage) 才返回压缩结果。结果是:结构化 checkpoint 虽然在 compressRequestMessages 内生成了,但 runAgentLoop 不会替换 requestMessages,也不会向外传播 contextCheckpoint。
影响:
建议:
onCompressContext 应在 result.messages !== requestMessages 或 result.contextCheckpoint 存在时也返回压缩结果。usagePush 和 onAfterCompressContext 需要允许无 usage 的压缩事件,或为本地压缩生成 0 usage 记录。compressRequestMessages 返回无 usage 但有 contextCheckpoint 的情况。availableCompressedTokenLimit = max(0, floor((maxContext - currentMessagesTokens) / toolLength))。当当前 messages 已接近或超过 maxContext 时,工具结果压缩目标可能为 0。后续 compressLargeContent 是否能稳定处理 0 token 预算,需要专项测试。
建议:
结构化 checkpoint 是本地压缩,不会产生 requestId 和 usage。即使修复 R1,也需要决定是否在运行详情里显示“本地上下文压缩”。否则用户只能看到上下文突然变短,缺少可观测性。
建议:
after_message_compress 事件,允许 compressionMode: 'structured_tool_checkpoint' 和 0 usage。当前 checkpoint 通过 after_message_compress 事件即时 push 到 assistantResponses。这能保证压缩发生在本轮中间时,checkpoint 排在后续 plan/tool/text value 之前。但如果未来某些压缩路径不触发事件,只在 result.contextCheckpoint 返回,正常完成路径不会兜底保存。
建议:
after_message_compress 写入。dispatchRunAgent done/ask 分支增加去重兜底,避免事件丢失导致 checkpoint 不落库。onCompressContext 对无 usage checkpoint 的忽略问题。contextCheckpoint。checkpoint -> 后续 plan/tool/text。