ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

deer-flow 上下文压缩可见性设计:在会话历史中为 SummarizationMiddleware 标注摘要位置

deer-flow 上下文压缩可见性设计:在会话历史中为 SummarizationMiddleware 标注摘要位置 deer-flow 上下文压缩可见性设计:在会话历史中为 SummarizationMiddleware 标注摘要位置【免费下载链接】deer-flowAn open-source long-horizon SuperAgent harness that researches, codes, and creates. With the help of sandboxes, memories, tools, skill, subagents and message gateway, it handles different levels of tasks that could take minutes to hours.项目地址: https://gitcode.com/GitHub_Trending/de/deer-flow当长对话触发 deer-flow 的SummarizationMiddleware做上下文压缩时,用户会看到早期消息变少了,却完全不知道压缩发生过。本文基于仓库中的设计文档 2026-04-11-summarize-marker-design.md(状态:设计已批准、实现推迟到后续 PR),完整讲解该摘要标记(summarize marker)方案的调研过程、关键技术论证、验证结果与推荐实现。读完后你将掌握:如何在 LangGraph 中间件中可靠地派发自定义事件、事件存储(RunJournal/RunEventStore)如何承接这些事件,以及前后端如何协作在正确的时间线位置渲染N 条消息被压缩的内联卡片。一、目标:让压缩发生过这件事在 UI 中可见设计文档开篇明确了目标:当SummarizationMiddleware在 run 执行中途运行时,在会话历史 UI 上显示一个summarization happened here标记,让用户理解为什么早期消息看起来被压缩或丢失了。需要理解的前置背景是:该 spec 建立在同日的姊妹文档 2026-04-11-runjournal-history-evaluation.md 之上。后者解决的问题是:SummarizationMiddleware 会原地改写 checkpoint 中的channel_values[messages],导致/history接口返回的已是被 summary 替换后的消息,而 append-only 的run_events(事件存储)中仍保存着原始消息。事件存储版/history修复已经找回了原始消息——而本文介绍的 spec 要在这基础上再加一个可见标记,标在压缩实际发生的 seq 位置上,并可选地展示生成的摘要文本。二、现状调研:middleware 事件类别在生产库中是死的2.1 数据事实:零条 middleware 记录设计文档对backend/.deer-flow/data/deerflow.db的run_events表做了全量扫描:类别行数trace76message34lifecycle8middleware0没有任何一行的event_type包含summariz或middleware。结论很直接:middleware 事件类别在生产环境是dead(从未被写入过)。2.2 根因:journal.py 中的两条死代码路径位置状态journal.py:343-362——on_custom_event(summarization, ...)会写一条 trace 事件 一条categorymiddleware事件Dead。只有当有人调用adispatch_custom_event(summarization, {...})时才会触发。而上游 LangChain 的SummarizationMiddleware(.venv/.../langchain/agents/middleware/summarization.py:272)从不派发任何自定义事件——它的before_model/abefore_model只是原地改写 messages 并返回{messages: new_messages},回调永远不会被触发。journal.py:449——record_middleware(tag, *, name, hook, action, changes)辅助函数Dead。grep 显示 harness 中零调用方,属于猜测性添加,从未被接线。在当前仓库中,record_middleware依然存在于 RunJournal(用于记录标题生成、摘要、HITL 审批等中间件的状态变更),而 test_run_journal.py 中有一条明确的测试:RunJournal does not implement on_custom_event — no crash expected—— 即 RunJournal 当前对on_custom_event的默认行为是 no-op。这印证了设计文档的判断:摘要事件没有任何现成的捕获链路。2.3 摘要确实在跑,只是没有任何日志设计文档给出了一个具体的无声压缩证据:Thread3d5dea4a-0983-4727-a4e8-41a64428933a:run_eventsseq1 → 原始 human 消息写一份关于deer-flow的详细技术报告(事件存储本身没问题);run_eventsseq43 → 一条llm_requesttrace,其messages[0]字面内容包含Here is a summary of the conversation to date:—— 证明 SummarizationMiddleware 确实在 run 中途注入过摘要;该 thread 没有任何categorymiddleware行 → 没有任何东西被捕获供 UI 渲染。从当前源码结构看,压缩行为也确实静默地发生在状态层:DeerFlowSummarizationMiddleware(仓库对上游类的扩展)在acompact_state成功后,通过_amaybe_summarize返回RemoveMessage(idREMOVE_ALL_MESSAGES)加保留尾部消息加summary_text,整个过程只改写 state,不派发任何用户可见事件(summarization_middleware.py)。值得补充的是,当前实现已通过middleware:summarize这个 config tag 为摘要模型做 RunJournal 归因(见 summarization_middleware.py 与 test_summarization_middleware.py),但这解决的是token 用量归属问题,不是历史 UI 可见性问题——两条关注点在本文方案中恰好汇合。三、候选方案:三条路线及取舍方案 A:子类化 SummarizationMiddleware 并派发自定义事件包装上游类,重写abefore_model,在调用super()之后执行await adispatch_custom_event(summarization, {...})。RunJournal 现成的on_custom_event捕获路径就能接住它。方案 B:纯前端 diff 启发式(被否决)比较event_store.count_messages()与渲染消息数,从差值推断发生过压缩。被否决:无法精确定位压缩在时间线中的位置,也无法展示摘要文本,只能给出一枚模糊的 badge。方案 C:方案 A 前端在中间件事件 seq 位置渲染内联卡片(推荐终态)后端与方案 A 相同,前端额外在该中间件事件的 seq 位置渲染一张[N messages condensed]内联卡片。这是设计文档推荐的最终形态,下节先解决方案 A 的一个结构性存疑,再给出完整实现。四、关键技术论证:traceFalse不阻碍adispatch_custom_event这是全文最有价值的部分。一个独立 subagent 曾论证方案 A结构性不可行,理由是:RunnableCallable(traceFalse)会跳过set_config_context,因此var_child_runnable_config永远不会被设置,adispatch_custom_event会抛出RuntimeError(Unable to dispatch an adhoc event without a parent run id)。设计文档判定这是错的,并给出三步机制解释:RunnableCallable.__init__(langgraph/_internal/_runnable.py:293-319)会检查函数签名。如果参数表里显式声明config: RunnableConfig,该参数会被记录进self.func_accepts。ainvoke的traceTrue和traceFalse两个分支执行同一个 kwarg 注入循环(_runnable.py:349-356):if kw config: kw_value config。传给ainvoke的 config(来自 Pregel 的task.proc.ainvoke(task.input, config),pregel/_retry.py:138)本身就是绑定了 callbacks 的 task config。中间件内部把这份config显式传给adispatch_custom_event(..., configconfig)后,函数完全不依赖var_child_runnable_config.get()。LangChain 官方文档(langchain_core/callbacks/manager.py:2574-2579)甚至明确写着If using python 3.10 and async, you MUST specify the config parameter——正是这条路径。一句话总结:traceFalse只影响这一层 runnable 是否创建新的子 callback scope,不影响外层 config(含 callbacks,其中就有 RunJournal)向下传递给函数。deer-flow 的自定义事件双发机制(callback dispatch 同时暴露为astream_events(versionv2)的on_custom_event)在 STREAMING.md 中有完整说明,这正是该事件能被 journal 层捕获的管道基础。五、最小复现验证:五条预测全部成立设计文档用一个独立最小复现脚本/tmp/verify_summarize_event.py落地验证:一个最小AgentMiddleware子类,签名abefore_model(self, state, runtime, config: RunnableConfig);在其中调用await adispatch_custom_event(summarization, {...}, configconfig);create_agent(modelFakeChatModel, middleware[probe]);agent.ainvoke({...}, config{callbacks: [RecordingHandler()]})。运行结果:INFO verify: ProbeMiddleware.abefore_model called INFO verify: config keys: [callbacks, configurable, metadata] INFO verify: config.callbacks type: AsyncCallbackManager INFO verify: config.metadata: {langgraph_step: 1, langgraph_node: probe.before_model, ...} INFO verify: on_custom_event fired: namesummarization run_id019d7d19-1727-7830-aa33-648ecbee4b95 data{summary: fake summary, replaced_count: 3} SUCCESS: approach A is viable (config injection adispatch work)五条预测全部成立:✅config: RunnableConfig签名在traceFalse下依然触发自动注入;✅config.callbacks是带parent_run_id的AsyncCallbackManager;✅adispatch_custom_event(..., configconfig)无错执行;✅RecordingHandler.on_custom_event收到事件;✅ 收到的run_id是绑定到运行中 graph 的合法 UUID。附带发现:config.metadata里带有langgraph_step和langgraph_node,可以放进中间件事件的 metadata,帮助前端把标记放到时间线上的正确位置。六、推荐实现(方案 C):后端零侵入 前端内联卡片6.1 后端:新包装中间件设计文档给出的落点是在backend/packages/harness/deerflow/agents/lead_agent/agent.py新增包装中间件(与当前仓库的实际中间件位置 summarization_middleware.py 同属 lead agent 构建链):from langchain.agents.middleware.summarization import SummarizationMiddleware from langchain_core.callbacks import adispatch_custom_event from langchain_core.runnables import RunnableConfig class _TrackingSummarizationMiddleware(SummarizationMiddleware): Wraps upstream SummarizationMiddleware to emit a summarization custom event on every actual summarization, so RunJournal can persist a middleware:summarize row to the event store. The upstream class does not emit events of its own. Declaring config: RunnableConfig in the override lets LangGraphs RunnableCallable inject the Pregel task config (with callbacks and parent_run_id) regardless of traceFalse on the node. async def abefore_model(self, state, runtime, config: RunnableConfig): before_count len(state.get(messages) or []) result await super().abefore_model(state, runtime) if result is None: return None new_messages result.get(messages) or [] replaced_count max(0, before_count - len(new_messages)) summary_text _extract_summary_text(new_messages) await adispatch_custom_event( summarization, { summary: summary_text, replaced_count: replaced_count, }, configconfig, ) return result def _extract_summary_text(messages: list) - str: Pull the summary string out of the HumanMessage the upstream class injects as Here is a summary of the conversation to date:.... for msg in messages: if getattr(msg, type, None) human: content getattr(msg, content, ) text content if isinstance(content, str) else if text.startswith(Here is a summary of the conversation to date): return text return 用法:把_build_middlewares里现有的SummarizationMiddleware()实例化替换为相同参数的_TrackingSummarizationMiddleware(...)。Journal 侧改动:零。journal.py:343-362的on_custom_event(summarization, ...)本就会同时写 trace 行和categorymiddleware行——这正是为什么让上游类多派一个事件能以极小成本盘活整条持久化链路。History 辅助函数改动:扩展 threads.py 中的_get_event_store_messages,把categorymiddleware行以伪消息形式暴露给前端:# In the per-event loop, after the existing message branch: if evt.get(category) middleware and evt.get(event_type) middleware:summarize: meta evt.get(metadata) or {} messages.append({ id: fsummary-marker-{evt[seq]}, type: summary_marker, replaced_count: meta.get(replaced_count, 0), summary: (raw or {}).get(content, ) if isinstance(raw, dict) else , run_id: evt.get(run_id), })标记使用哨兵type(summary_marker),不与任何 LangChain 消息类型冲突,下游遍历 messages 的消费者可以显式跳过或专门渲染它。由于标记携带seq(如summary-marker-{seq}中的事件序号),前端能把它插到压缩实际发生的时间线位置——这正是方案 B(diff 启发式)做不到的。6.2 前端:两处小改动,反馈逻辑零改动frontend/src/core/messages/utils.ts:扩展消息分组逻辑,识别type summary_marker,将其作为独立分组(如assistant:summary-marker)产出;frontend/src/components/workspace/messages/message-list.tsx:在分组渲染 switch 中加分支,渲染一张醒目的内联卡片,显示N messages condensed,并提供可折叠面板展示摘要全文;反馈(feedback)逻辑无需改动:标记没有feedback字段,点赞按钮自然不会在标记上渲染。这与姊妹文档中发现的 feedback 映射问题(runIdByAiIndex依赖 AI 消息序号对齐)不冲突——标记不是 AI 消息,不进入该序号序列。七、风险清单与缓解措施设计文档列出四条风险,每条都给了缓解方案,值得作为包装上游中间件这类改造的通用检查表:同步路径遗漏。上游类同时有before_model和abefore_model,包装器只重写了异步版。若 deer-flow 某条代码路径走同步流程,那些压缩将不被捕获。缓解:同步版before_model也用dispatch_custom_event(同步变体)按同样模式重写。_extract_summary_text的脆弱性。它依赖上游类注入HumanMessage的前缀Here is a summary of the conversation to date,上游模板一改检测就断。缓解:改为挑出 super() 之后新出现的、原 state 里不存在的第一个 HumanMessage,对措辞变化更鲁棒,代价是一个小的 diff 辅助函数。replaced_count在并发修改下可能不准。若链上其他中间件也在 super() 返回前改过state[messages],朴素的before_count - len(new_messages)算术就错了。缓解:检查上游派发的RemoveMessage(idREMOVE_ALL_MESSAGES),直接从原始输入列表计数。(对照当前 summarization_middleware.py 的实现,_maybe_summarize返回的正是RemoveMessage(idREMOVE_ALL_MESSAGES)加保留消息,该信号确实可用。)History 契约变化。在/history响应里引入非 LangChain 类型条目(typesummary_marker)可能破坏前端对条目盲目 cast 为Message的代码。缓解:前端按上述显式分支处理,并在合并前做端到端类型检查。八、范围之外与后续动作设计文档明确划出的边界:其他中间件(Title、Guardrail、HITL)同样不派发自定义事件。若要为它们做标记,对每个重复同样的包装器模式——不属于本设计范围;老 thread 无法补标记。补丁上线前的历史数据无法追溯(除非重跑 graph)。旧 thread 只会展示事件存储找回的消息,不带标记;Standard mode(make dev)另有 follow-up。该模式下 agent 跑在 LangGraph Server 内而非 Gateway 内嵌运行时,RunJournal可能未接线,自定义事件会发出但无人捕获,需单独跟踪。后续动作:先落地summarize 消息丢失修复(journalCommand解包 事件存储版/history 内联 feedback,已在rayhpeng/fix-persistence-new分支验证),随后以独立 PR 实施本文的 marker 设计。九、结语:一个让静默行为显形的工程范式这篇 spec 的价值不止于加一个 UI 标记,它演示了一条完整链路:用数据扫描定位dead path(2.1/2.2)→ 用真实 thread 数据证明行为发生但不可见(2.3)→ 用最小复现脚本证伪一个看似合理的结构性论断(§4/§5)→ 给出后端零侵入、前端两文件的推荐实现(§6)→ 逐条给出风险与缓解(§7)。对 deer-flow 而言,它补上了事件存储体系中 middleware 类别从0 行到可查询、可渲染的最后一块;对读者而言,其中config: RunnableConfig显式签名 adispatch_custom_event(..., configconfig)绕过var_child_runnable_config的用法,是任何基于 LangGraph 的自定义事件捕获都可复用的关键技巧。【免费下载链接】deer-flowAn open-source long-horizon SuperAgent harness that researches, codes, and creates. With the help of sandboxes, memories, tools, skill, subagents and message gateway, it handles different levels of tasks that could take minutes to hours.项目地址: https://gitcode.com/GitHub_Trending/de/deer-flow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表