Yuxi 上下文压缩机制详解:单阈值压力控制、工具结果落盘与线程级手动压缩 📅 发布时间:2026/9/17 12:09:21 👁 浏览次数: Yuxi 上下文压缩机制详解单阈值压力控制、工具结果落盘与线程级手动压缩【免费下载链接】Yuxi可私有部署的多租户知识智能体平台统一 RAG、知识图谱、多智能体、MCP/Skills、沙盒与权限管理。Self-hosted knowledge agent platform for RAG, knowledge graphs and multi-agent workflows.项目地址: https://gitcode.com/GitHub_Trending/yu/Yuxi导读本文围绕 Yuxi 智能体平台可私有部署的多租户知识智能体平台统一 RAG、知识图谱、多智能体与 MCP/Skills的上下文压缩设计展开系统讲解其收敛为单一压力阈值的自动压缩算法、完整工具结果写入 Workdir 后可恢复的确定性裁剪策略以及在线程空闲时同步执行、由 Conversation 行锁保证互斥的手动压缩机制。读完本文你将掌握summary_threshold等全部相关配置项的含义与调优方法、自动/手动压缩的完整调用链含源码佐证与对应测试文件路径并理解 85% 建议提示线与自动压缩之间的边界可直接用于日常部署调优与二次开发。本文依据项目内实现决策文档 2026-09-02-context-compression-pressure-and-manual-action.md 编写并对照 summary.py、context_compression_service.py、agent_state_repository.py 等源码及对应测试逐项核实。一、背景为什么要把两个压缩门槛收敛成一个在本次决策落地之前自动压缩链路存在两个相互叠加的门槛由summary_threshold触发确定性压缩再由summary_threshold * summary_l2_trigger_ratio这个第二比例决定是否调用摘要模型。从配置者的视角看这种设计存在明显问题你无法只凭一个预算数值判断摘要模型到底会不会被调用——同样的summary_threshold行为还取决于另一个隐性比例参数。与此同时当时采用的通用头部裁剪策略会直接丢弃检索结果中的文档标识、来源和 URL而这些恰恰是知识库问答和联网搜索场景下模型继续推理所必需的证据信息。另一方面用户存在任务结束后主动压缩上下文的真实诉求但若把手动压缩建模为一个完整的 AgentRun就需要新增 Run 类型、队列分支、worker 分支和数据库 shape成本与当前仅需在空闲时点一下的诉求完全不匹配。围绕这两个痛点本次决策最终确立了三件事单一压力阈值summary_threshold成为唯一的上下文压力预算删除了summary_l2_trigger_ratio有损投影、无损恢复完整工具结果先写入当前 Workdir再以保留关键字段的预览替换模型可见的 ToolMessage轻量手动压缩以同步的线程维护请求形态提供主动压缩不引入新的 Run 生命周期。二、整体架构与职责分工决策文档明确划分了各模块的事实 Owner源码归属关注点事实 Owner源码位置自动与手动压缩算法、确定性工具结果预览summary middlewarebackend/package/yuxi/agents/middlewares/summary.py线程互斥与用例编排context compression servicebackend/package/yuxi/services/context_compression_service.pycheckpoint 写入边界Agent state repositorybackend/package/yuxi/repositories/agent_state_repository.py压力指标token usage statetoken usage middlewarebackend/package/yuxi/agents/middlewares/token_usage.py界面投影85% 提示、按钮Agent chat UIweb/src/components/AgentChatComponent.vue、web/src/utils/contextUsage.js一个值得注意的架构原则Agent graph 只负责 graph 装配并通过 capability 声明按钮是否可用capabilities中包含context_compression时前端才展示手动压缩入口压缩算法与编排逻辑完全由 middleware 和 service 承担graph 保持纯净。三、自动压缩单一阈值下的三级流程自动压缩的核心入口是YuxiSummarizationMiddleware继承自 DeepAgents 的SummarizationMiddleware核心方法为同步的_wrap_model_call_with_compaction与异步的_awrap_model_call_with_compaction两者逻辑一致。流程可以拆成三级3.1 第一级到达阈值前的直通effective_messages self._get_effective_messages(request) total_tokens self._count_tokens(effective_messages, request.system_message, request.tools) truncated_messages, _ self._truncate_args(effective_messages, total_tokens) should_compact self._should_summarize(truncated_messages, total_tokens) overflow_triggered False if not should_compact: try: return handler(request.override(messagestruncated_messages)) except ContextOverflowError: overflow_triggered True上下文未达到summary_threshold时直接调用主模型只做一次轻量的参数截断见 3.4 节完全不触发任何压缩动作。唯一的例外是 Provider 抛出ContextOverflowError上下文溢出此时即便未达阈值也会进入压缩流程——Provider 报上下文溢出时仍生成摘要。3.2 第二级确定性压缩 重新计量一旦达到阈值先执行确定性压缩_compact_messages这一步不调用任何模型compacted_messages self._compact_messages(truncated_messages) compacted_tokens self._count_tokens(compacted_messages, request.system_message, request.tools) pressure_threshold self._entry_trigger_tokens() should_summarize overflow_triggered or pressure_threshold is None or compacted_tokens pressure_threshold if not should_summarize: try: response handler(request.override(messagescompacted_messages)) except ContextOverflowError: overflow_triggered True else: _emit_compression(completed) return response关键设计在于重新计量压缩完大工具结果后重新计算 token 数如果降到同一阈值以下就直接用压缩后的上下文调用主模型不再消耗一次摘要模型调用。这是单阈值决策最直接的经济性收益——确定性压缩本身是零模型成本的。_entry_trigger_tokens()从触发子句中解析出最小的 token 门槛支持tokens绝对值与fraction相对值两种写法供压缩后是否仍超阈值这一判断复用。3.3 第三级摘要生成与状态更新若压缩后仍不低于阈值或发生了溢出则进入摘要阶段cutoff_index self._determine_cutoff_index(compacted_messages) messages_to_summarize, preserved_messages self._partition_messages(compacted_messages, cutoff_index) # 溢出时额外裁剪保留窗口的尾部 if overflow_triggered: preserved_messages, new_state_tail _clip_overflow_tail(...) offloaded_messages, failed_media self._offload_inline_media(self._backend, messages_to_summarize) session_id self._get_session_id(request.state) file_path self._offload_to_backend(self._backend, offloaded_messages, session_id) summary self._create_summary(offloaded_messages) new_messages self._build_new_messages_with_path(summary, file_path) new_event self._build_summary_event(request.state, cutoff_index, new_messages[0], file_path) response handler(request.override(messages[*new_messages, *preserved_messages]))流程要点被摘要的历史消息先落盘到 backendWorkdir得到file_path摘要模型基于落盘后的消息生成 summary异步路径用asyncio.gather并行落盘与摘要摘要消息 保留的近期消息组成新的模型可见上下文通过Command(update...)把_summarization_event、_summarization_session_id等状态写回 checkpoint。文档明确摘要 event 只替换模型可见历史后续 graph 仍注入当前 Agent 的原 system prompt 和 tools。同时决策文档谨慎指出由于没有可验证的 Provider KV cache 命中证据本次不改变摘要调用形状即未对摘要调用做 cache 相关的特殊处理。3.4 附加的确定性裁剪AI 工具调用参数截断_compact_messages中还有一类处理对write_file、edit_file的AIMessage.tool_calls参数做截断tool_arg_max_length默认 2000 字符超长部分替换为...(argument truncated for context view)避免大文件写入参数占据过多上下文。四、确定性压缩细节完整结果落盘与有损投影这是本次决策中最具技术含量的一部分目标是解决通用头部裁剪丢掉检索证据的问题。4.1 触发条件与落盘_should_offload_tool_message判断 ToolMessage 是否超过tool_result_offload_token_limit默认 300 token约 1200 字符_APPROX_CHARS_PER_TOKEN 4def _should_offload_tool_message(message: ToolMessage, token_limit: int | None) - bool: if token_limit is None or token_limit 0: return True content _extract_text_content(message.content) estimated_tokens max((len(content) _APPROX_CHARS_PER_TOKEN - 1) // _APPROX_CHARS_PER_TOKEN, 1) return estimated_tokens token_limit超过限制的完整原文通过_write_tool_result写入 backend即当前 Agent 的 Workdir路径由_tool_result_path生成{prefix}/{tool_name}-{sha256前16位}.txtSHA-256 摘要同时写入预览头用于校验恢复的完整性。写入失败backend 缺失或报错时不替换消息内容——即无法写入完整结果时不裁剪宁可保留原文占用上下文也不做不可恢复的丢弃。4.2 通用工具结果HEAD/MIDDLE/TAIL 三段式_generic_tool_result_preview按 2:1:2 的比例把预算拆成三段开头HEAD、中间MIDDLE取自文本中部、结尾TAIL替换后的 ToolMessage 形如[Tool result saved] Tool: run_command Approx tokens: 15230 SHA-256: 3f2a1b...c9e0 Full output path: outputs/tool-result/run_command-3f2a1bc9e0.txt Output preview: [HEAD] ... [MIDDLE] ... [TAIL] ... [Truncated 58000 chars. Read the full output from the saved file.]模型既能看到首、中、尾三个片段把握整体结构又能通过路径与 SHA-256 精确恢复原文例如在后续轮次用读文件类工具读取完整输出。4.3 检索类工具保留证据关键字段的结构化预览对query_kb和web_search源码中的_STRUCTURED_SEARCH_TOOL_NAMES不再做简单裁剪而是解析 JSON 后按原顺序保留结构化记录。核心实现是_structured_search_preview与_search_result_recordquery_kb保留字段id、kb_id、file_id、title、source、score、distance以及metadata中的source、filename、title、chunk_index、score、rerank_score、hybrid_score、graph_score、distanceweb_search保留字段title、url、site_name、publish_time、score正文预览从content/text/snippet/summary中按序选取超出预算时用头 … 尾的_clip_search_content裁剪结果按原顺序选取上限 8 条被略过的结果以omitted_results计数上报预览整体以紧凑 JSON 编码separators(,, :)头部还保留kindknowledge_base/web_search、result_count、query、response_time等检索元信息。这样模型看到的预览麻雀虽小五脏俱全文档/网页标识、来源、标题、URL、分数全部在场检索证据不会因压缩而丢失。坏 JSON 或非预期结构时安全回退到通用 HEAD/MIDDLE/TAIL 预览JSON 结构完全不可用时_parse_structured_tool_result返回None也不会抛错。对应测试test_summary_middleware.py 中的test_query_kb_preview_preserves_document_identity_and_metadata、test_web_search_preview_preserves_citations_and_reports_omitted_results、test_compaction_does_not_replace_tool_result_when_recoverable_write_fails、test_compaction_rejects_large_tool_result_without_recoverable_backend等用例分别覆盖字段保留、坏 JSON 回退、写入失败不替换等负向场景。五、手动压缩线程级同步维护请求5.1 使用前提与互斥语义手动压缩通过 HTTP 端点触发POST /api/chat/thread/{thread_id}/compress见 chat_router.py前端封装在 agent_api.js。service 层入口是compress_thread_contextcontext_compression_service.py其互斥语义由两层保证第一层Conversation 行锁。service 从检查空闲到 checkpoint 更新完成始终持有 Conversation 行锁lock_conversation_by_thread_id。由于普通 intake 使用同一把锁手动压缩与普通消息 intake 无法并发写同一线程。第二层线程空闲检查。_ensure_thread_idle依次检查是否存在活跃 Runget_active_run_by_thread_for_user是否有等待交互的 Run最新 chat/resume Run 状态为interrupted是否有排队中的 Requestlist_queued。三者任一存在即拒绝raise HTTPException( status_code409, detail{code: thread_busy, message: 线程仍有运行、交互或排队请求暂时不能压缩}, )线程忙时返回409 thread_busy且不进入 FIFO 队列——这是有意为之的取舍宁可让用户稍后重试也不给同步维护请求引入排队语义。对应测试见 test_context_compression_service.py 的test_rejects_non_idle_thread断言 409 thread_busy以及 test_agent_request_queue_concurrency.py 中手动压缩与 intake 的并发场景。此外手动压缩还要求线程归属当前用户且未删除否则 404Agent 后端存在404Agent 的capabilities包含context_compression否则 422当前智能体不支持主动上下文压缩。5.2 压缩执行一次性 Sandbox 生命周期_compress_agent_checkpoint_in_runtime展示了手动压缩的运行时管理prepare_agent_runtime_context(context)准备 Agent 运行时上下文解析模型、资源、Skills_ensure_runtime_available通过ProvisionerSandboxBackend.ensure_available确保 Sandbox 可用主动压缩必须能写入可恢复历史执行_compress_agent_checkpoint无论成败finally/else分支都通过get_sandbox_provider().release释放 Sandbox失败释放时仅记日志不掩盖原始错误。5.3 checkpoint 写入边界必须经由 canonical graph_compress_agent_checkpoint的写入链路刻意绕开了直接操作 checkpoint 表graph await agent.get_graph(contextcontext) compressor create_summary_middleware_from_context(context, backendcreate_agent_composite_backend(context)) state_repository AgentStateRepository(graph, uidstr(context.uid), thread_idstr(context.thread_id)) values await state_repository.get_values() update, result await compressor.aforce_summarize(values) if update: await state_repository.update(_with_compression_usage(update, ...))agent_state_repository.py 全文只有两个方法async def get_values(self) - dict[str, Any]: state await self._graph.aget_state(self._config) return dict(getattr(state, values, {}) or {}) async def update(self, values: dict[str, Any]) - None: if not values: return await self._graph.aupdate_state(self._config, values)AgentStateRepository只调用 canonical compiled graph 的aget_state/aupdate_state不直接读写 checkpoint 表。决策文档解释得很清楚直接改 checkpoint 表会绕过 reducer、channel version 和 metadata 语义因此写入必须经由 compiled graph。构造时若 graph 没有 checkpointer 会直接抛ValueError。5.4 摘要生成与压缩指标合并aforce_summarize是手动压缩的算法核心它读取当前 state 的 messages应用已有的摘要 event先做确定性压缩_compact_messages历史不足时返回no_op/insufficient_history且不写 checkpoint可压缩时同样把历史落盘、调用摘要模型_acreate_summary_or_raise空摘要或空历史会抛错返回待持久化更新与结果指标before_tokens、after_tokens、compressed_messages、file_path。压缩结果通过_with_compression_usage合并进token_usagestate写入compression结果指标、summary_activeTrue、summary_trigger_tokens并剔除旧的TOKEN_USAGE_CONTEXT_FIELDS字段保证下一轮的压力指标基于压缩后的真实状态计算。5.5 与 AgentRun 生命周期的边界决策文档特别强调手动压缩不是一个 AgentRun它不产生持久消息、不进入可见对话、不可排队、不可取消、崩溃后不可恢复。占用的是 API 请求、数据库连接和 Conversation 行锁直到摘要结束。响应丢失后重试可能再次摘要不提供 request-id 级幂等或后台恢复因此前端在点击后只跟踪一次 HTTP 请求成功后重新读取 Agent state。六、配置参数详解所有相关参数集中在 context.py 的BaseContext中均标注auth: admin仅管理员可配置参数默认值单位/说明summary_threshold100K唯一的上下文压力阈值。上下文超过100Ktoken 时启用摘要功能create_summary_middleware_from_context中trigger_tokens summary_threshold * 1024换算为绝对 token 数。单位 Ksummary_keep_messages10条摘要触发后除摘要消息外保留的最近消息数量summary_prompt内置 Yuxi 摘要提示词摘要提示词必须能接收{messages}占位符summary_tool_result_token_limit300token确定性压缩历史工具结果时超过该 token 数的 ToolMessage 会写入 outputs 并保留不超过该数的预览未超过则保持原样tool_arg_max_lengthmiddleware 构造参数2000字符write_file/edit_file工具调用参数的截断长度注意summary_l2_trigger_ratio已被删除不存在于当前BaseContext中任何依赖它的旧配置都需迁移。默认摘要提示词DEFAULT_YUXI_SUMMARY_PROMPT要求摘要模型按五个固定章节输出## SESSION INTENT —— 用户当前主要目标、任务范围和最终交付物 ## USER REQUIREMENTS AND PREFERENCES —— 用户明确要求、偏好、禁忌、输出格式、验收标准 ## PROGRESS AND DECISIONS —— 已完成步骤、关键结论、已确认/被否定的方案及原因 ## ARTIFACTS AND REFERENCES —— 已创建/修改/读取的文件、路径、工具输出路径、线程或运行标识 ## NEXT STEPS —— 后续最应该继续做的具体步骤无待办写 None并明确约束不逐字复述冗长工具输出、不编造对话中不存在的事实、未解决问题与风险需记录、使用与用户对话一致的语言。这套提示词同时服务于自动与手动压缩两者共用create_summary_middleware_from_context创建的摘要器保证了摘要风格的统一。七、前端压力指标与 85% 建议提示线7.1 压力指标如何计算token_usage.py 在每次模型调用后生成用量快照其中与压缩相关的关键指标summary_trigger_tokens从运行时 context 读取summary_thresholdK 值summary_pressure_ratio round(next_llm_input_tokens / summary_trigger_tokens, 4)下一轮模型输入对压缩阈值的压力占比——这是 85% 提示线的直接数据来源context_usage_ratiollm_input_tokens / context_window对模型上下文窗口的占用比与压缩阈值无关summary_active当前模型可见上下文首位是否为摘要消息。前端 contextUsage.js 中明确注释/** * 85% 是只用于提示的派生线不参与自动压缩判断。 */ export function shouldSuggestContextCompression(ratio) { return Number.isFinite(Number(ratio)) Number(ratio) 0.85 }7.2 85% 只是提示线85% 是固定提示线不参与自动压缩84.9%不提示85%提示对应验证表中的边界用例。它在状态面板中表现为一段建议文案与一个按钮AgentChatComponent.vue当shouldSuggestContextCompression为真时显示当前上下文已达到压缩阈值的 XX%建议先压缩再开始下一次运行按钮在isContextCompressionPending || isProcessing || hasQueuedRequests || isWaitingForUserAction任一条件成立时禁用按钮可见性由supportsContextCompressioncapabilities.includes(context_compression)决定点击后调用agentApi.compressThreadContext(threadId)跟踪单次 HTTP 请求成功后重新读取 Agent state 刷新指标。为何不让前端单独判断空闲决策文档的理由很直接多标签页和直接 API 调用仍会产生竞态后端必须共用 Conversation 行锁——前端禁用按钮只是体验优化真正的互斥保证在后端。7.3 流式事件压缩过程中的状态变化通过yuxi.context_compressionSSE 事件流式下发started/completed/faileduseAgentStreamHandler.js 据此把threadState.contextCompressing置为对应状态驱动 UI 的压缩中反馈。注意_emit_compression_started_once通过 ContextVar 保证同一轮只发一次started。八、验证与测试证据决策文档给出的验证矩阵全部通过并与源码/测试相互印证主张证据位置负向案例确定性压缩后只按原阈值决定是否摘要test_summary_middleware.py如test_awrap_model_call_emits_completed_for_compaction_without_summary压缩后低于入口阈值时摘要模型调用数必须为 0工具原文可恢复检索关键字段稳定保留同文件test_compaction_does_not_replace_tool_result_when_recoverable_write_fails、test_query_kb_preview_preserves_document_identity_and_metadata、test_web_search_preview_preserves_citations_and_reports_omitted_results等backend 缺失或写失败时不替换坏 JSON 回退 head/middle/tail空闲线程通过 canonical graph 更新 checkpointtest_context_compression_service.pytest_compresses_checkpoint_through_canonical_graph、test_context_compression_router.pytest_compress_thread_persists_canonical_checkpoint_through_http无 checkpointer graph 被拒绝历史不足时不写 checkpoint手动压缩与普通 intake 不并发test_agent_request_queue_concurrency.py、HTTP busy 测试活跃、interrupted 或 queued 状态返回 40985% 只形成可操作提醒backend/web unit、lint、build84.9% 不提示85% 提示执行结果backend 非慢速 unit 共 1682 个通过真实 PostgreSQL 并发、HTTP busy 与 HTTP 成功回读测试各 1 个通过web unit 167 个通过lint 与 build 通过工程契约检查及其 61 个 unit 通过git diff --check通过。真实 Provider connectivity 和人工页面检查未运行——这也是实现验收中保留的开放项。九、后果与权衡9.1 收益单一预算即可解释行为自动压缩只由一个压力预算summary_threshold驱动配置者不再需要理解第二比例确定性压缩省钱压缩成功降到阈值以下时不消耗摘要模型调用证据不丢模型视图是完整结果的有损投影但文档标识、来源、URL、分数等关键字段稳定保留可恢复由 Workdir 中的完整原文和 SHA-256 保证状态一致checkpoint 保存摘要PostgreSQL Message 仍保存完整聊天记录两者不是同一个事实源但各有用途前者供模型续跑后者供用户回看审计。9.2 代价与边界手动请求会占用 API 请求、数据库连接和 Conversation 行锁直到摘要结束响应丢失后重试可能再次摘要无 request-id 级幂等或后台恢复摘要 event 只替换模型可见历史system prompt 与 tools 仍由 graph 每轮注入大文件写入参数的截断只发生在压缩视图中不修改持久化消息。9.3 已删除的旧能力本次重构删除了旧实现中的intake_compression_request、compressionRun 类型、相关的 schema/worker/队列/SSE 分支以及AgentGraphRuntime。如果你在升级前见过这些概念升级后它们已不存在。9.4 重新引入独立持久任务的条件决策文档给出了明确的重入条件当产品明确要求手动压缩可排队、可取消、API 或 worker 崩溃后可恢复或拥有可查询的持久终态时再评估引入独立的持久任务生命周期。在此之前当前空闲时同步执行 409 互斥的轻量形态就是有意保留的设计。十、调优实践建议基于以上机制实际部署中可参考以下调优思路均可在 Agent 的 Context 配置中设置需管理员权限summary_threshold是主旋钮调低会更快触发压缩、节省后续模型调用成本但摘要更频繁调高则保留更多原始上下文适合需要模型看到长上下文的代码生成/长文档任务。默认 100K 是兼顾大多数场景的起点。summary_tool_result_token_limit默认 300决定多少 token 以上的工具结果会被落盘 预览。知识库检索和网页搜索类任务建议保持默认或调大预览上限以保留更多证据正文纯脚本类任务可适度调小以省上下文。summary_keep_messages默认 10控制摘要后保留的最近消息数。交互密集的对话可适当加大避免最近几条关键指令被过早卷入摘要。summary_prompt如需面向特定领域如代码审计、医疗、金融定制摘要风格可替换为自定义提示词但必须保留{messages}占位符并遵循保留路径与标识符、不编造事实的基本约束。把 85% 提示当作运维信号当状态面板出现建议先压缩再开始下一次运行时说明下一轮输入将逼近压缩阈值此时手动压缩可以在本轮任务结束后立即回收上下文预算避免下一轮运行时在自动压缩链路上多绕一圈。参考文档与源码决策实现文档 · summary middleware · context compression service · agent state repository · context 配置 · token usage middleware · chat_router 压缩端点 · 前端提示逻辑 · 前端压缩 UI · 单元测试 test_summary_middleware.py / test_context_compression_service.py · 集成测试 test_context_compression_router.py【免费下载链接】Yuxi可私有部署的多租户知识智能体平台统一 RAG、知识图谱、多智能体、MCP/Skills、沙盒与权限管理。Self-hosted knowledge agent platform for RAG, knowledge graphs and multi-agent workflows.项目地址: https://gitcode.com/GitHub_Trending/yu/Yuxi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考