DeerFlow 自动对话标题生成机制:TitleMiddleware 触发条件、双路径策略与持久化实现
DeerFlow 自动对话标题生成机制TitleMiddleware 触发条件、双路径策略与持久化实现【免费下载链接】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-flowDeerFlowdeer-flow的长时任务对话线程在首次交互后需要自动生成一个可读的会话标题用于前端对话列表展示与线程检索。本文围绕backend/docs/AUTO_TITLE_GENERATION.md的完整设计展开标题在after_model钩子中如何判定首轮对话、本地 fallback 与 LLM 生成两条路径如何取舍、ThreadState.title经由 checkpointer 的持久化机制以及 DeerFlow 运行层针对中断 run 的补偿写入逻辑。读完后你将掌握该功能的配置方式、源码级实现细节、客户端读取方法以及标题缺失/丢失场景的排查手段。功能定位与触发时机自动 Thread Title 生成功能在用户首次提问并收到回复后自动触发由TitleMiddleware在 LangChain Agent 中间件链的after_model/aafter_model钩子中完成。它不是独立服务而是 DeerFlow 主 Agentlead agent中间件链的一环从 lead_agent 组装代码 可以看到TitleMiddleware在TokenUsageMiddleware之后、MemoryMiddleware之前被追加到链上源码注释明确 TitleMiddleware generates title after first exchange, MemoryMiddleware queues conversation for memory update (after TitleMiddleware)。在自定义 Agent 工厂 factory.py 中它对应第 8 号位8. TitleMiddleware (auto_title feature)支持按auto_title特性开关启用或替换为自定义中间件实例。核心判定逻辑见 title_middleware.py 的_should_generate_title方法配置必须启用config.enabled为真否则直接返回Nonestate中已存在title时不重复生成幂等必须是首轮对话恰好 1 条用户消息且至少 1 条助手回复。源码对messages通道做了防御性处理——读取半初始化 checkpoint 时messages可能为None会归一为空列表以避免len()报错用户消息的识别排除了动态上下文提醒类消息is_dynamic_context_reminder即只有真正的用户输入计入首轮判定。中断 run 的补偿路径文档描述的常规路径之外DeerFlow 还处理了一个边界场景首轮 run 在助手消息写入 checkpoint 之前被取消。此时after_model永远不会执行标题就会缺失。run worker 中的_ensure_interrupted_title函数在 run 结束阶段补偿它读取线程 checkpoint 的channel_values若已有title则短路返回幂等否则调用middleware._generate_title_result(..., allow_partial_exchangeTrue)。allow_partial_exchangeTrue放宽了消息数门槛min_messages从 2 降为 1允许仅有 1 条用户消息的部分对话状态从而把本地 fallback 标题持久化进 checkpoint。该写入还带有 stale-snapshot 防护写入前会比较 checkpoint 身份标识若期间有其他写者更新了快照则重试最多 3 次避免覆盖并发写入。双路径标题生成本地 fallback 优先LLM 可选TitleMiddleware的策略是默认快、显式才慢默认路径未配置title.model_name不发起任何 LLM 调用直接从首条用户消息生成 fallback 标题。这避免了在流式回复结束前额外等待一次模型往返显式路径配置了title.model_name构造 title prompt 并异步调用配置的标题模型失败时模型不可用、超时、返回空等任意异常回退到本地策略日志仅记录 debug 级别。从源码实现看title_middleware.py 的_agenerate_title_resultLLM 路径还有几个值得注意的细节附件-only 首轮保护若首轮用户消息没有任何文本仅附件代码直接走 fallback不讓标题模型基于助手回复脑补标题prompt 构造_build_title_prompt取首条用户消息与首条助手消息各截断到前 500 字符填入prompt_template助手消息会先经_strip_think_tags去除推理模型的think.../think块针对 minimax、DeepSeek-R1 等推理型模型输出归一化_parse_title模型输出先做结构化内容归一化、剥离 think 标签、去除首尾引号最后按max_chars截断调用可观测性LLM 调用通过observe_system_model_call包裹并标记为SystemOperationKind.TITLE且_get_runnable_config会继承父 RunnableConfig 并追加run_nametitle_agent与middleware:title、TAG_NOSTREAM标签使 RunJournal 能将其识别为中间件调用而非 lead_agent 调用同时TAG_NOSTREAM保证这次调用不打扰前端流式输出。内容归一化与 fallback 截断文档强调先把 LangChain message content 里的结构化 block/list 内容归一化为纯文本再拼到 title prompt 里避免把 Python/JSON 的原始 repr 泄漏到标题生成模型。对应实现是_normalize_content字符串原样返回列表递归归一后用换行拼接字典优先取text字段其次递归content字段无法解析则返回空串。_fallback_title的截断规则上限取min(max_chars, 50)超长时预留 3 个字符的省略号位body min(fallback_chars, max_chars - len(...))保证与模型路径的max_chars约束完全一致用户消息为空时返回New Conversation。另外若用户消息的additional_kwargs中带有ORIGINAL_USER_CONTENT_KEY原始用户内容标记会优先用get_original_user_content_text还原用户原始文本保证标题反映的是用户真正输入而非经过改写的消息。存储机制为什么是 ThreadState 而非 metadataTitle 存储在ThreadState.title中而非 thread metadata。ThreadState 定义 中该字段声明为class ThreadState(AgentState): sandbox: SandboxStateField thread_data: NotRequired[ThreadDataState | None] title: NotRequired[str | None] # ✅ Title stored here artifacts: Annotated[list[str], merge_artifacts] # ... 其余字段省略TitleMiddleware自身只声明了一个最小兼容 schemaTitleMiddlewareState(AgentState)其中title: NotRequired[str | None]与ThreadState的title通道对齐从而让中间件的返回值{title: ...}直接落入线程状态通道。选择 State 而非 Metadata 的对比原文档表格特性StateMetadata持久化✅ 自动通过 checkpointer⚠️ 取决于实现版本控制✅ 支持时间旅行❌ 不支持类型安全✅ TypedDict 定义❌ 任意字典可追溯✅ 每次更新都记录⚠️ 只有最新值标准化✅ LangGraph 核心机制⚠️ 扩展功能持久化行为与部署方式部署方式持久化说明LangGraph Studio (本地)❌ 否仅内存存储重启后丢失LangGraph Platform✅ 是自动持久化到数据库自定义 Checkpointer✅ 是需配置 PostgreSQL/SQLite checkpointer如果需要在本地开发时也持久化 title配置 checkpointer# 在 langgraph.json 同级目录创建 checkpointer.py from langgraph.checkpoint.postgres import PostgresSaver checkpointer PostgresSaver.from_conn_string( postgresql://user:passlocalhost/dbname )然后在 langgraph.json 中引用{ graphs: { lead_agent: deerflow.agents:lead_agent }, checkpointer: checkpointer:checkpointer }需要说明的前提DeerFlow 实际部署gateway / Docker Compose走的是内置持久化栈ThreadState中的messages通道可配置为 delta 快照模式见 thread_state.py 的get_thread_state_schema/adapt_state_schema_for_modetitle作为普通状态字段随 checkpoint 一并落库因此生产形态下标题天然持久化上表的本地 Studio 丢失主要针对纯 LangGraph Studio 内存场景。配置项详解config.yaml 配置仓库根目录 config.example.yaml 中的实际配置块第 1734 行附近# # Title Generation Configuration # # Automatic conversation title generation settings title: enabled: true max_words: 6 max_chars: 60 model_name: null # null fast local fallback; set a model name to use LLM title generation代码级配置配置由 title_config.py 管理TitleConfig是一个 Pydantic 模型各字段的取值范围有硬约束比文档的示例更完整字段类型默认值约束说明enabledboolTrue—是否启用自动标题生成max_wordsint61 x 20生成的标题最大词数仅约束 LLM promptmax_charsint6010 x 200标题最大字符数LLM 路径与 fallback 路径统一执行model_namestr \| NoneNone—None表示走本地 fallback填模型名才启用 LLM 标题生成prompt_templatestr见下—LLM 标题生成的 prompt 模板含{max_words}、{user_msg}、{assistant_msg}占位符默认 prompt 模板为Generate a concise title (max {max_words} words) for this conversation. User: {user_msg} Assistant: {assistant_msg} Return ONLY the title, no quotes, no explanation.全局单例通过get_title_config()/set_title_config()读写load_title_config_from_dict()在AppConfig.from_file()时由配置字典加载reset_title_config()供测试还原默认值。代码中覆盖示例from deerflow.config.title_config import TitleConfig, set_title_config set_title_config(TitleConfig( enabledTrue, max_words8, max_chars80, ))注意TitleMiddleware的配置解析优先级_get_title_config构造时显式传入的title_config参数 构造时传入的app_config.title 全局get_title_config()单例。lead agent 组装时始终注入app_config因此文件配置路径在 DeerFlow 运行时是主路径。端到端工作流程用户首条消息 → 首轮完整回复 →after_model判定 → 双路径生成 → state 写入 → checkpointer 持久化 → 客户端读取实现层面同步与异步钩子的分工title_middleware.pyoverride def after_model(self, state: TitleMiddlewareState, runtime: Runtime) - dict | None: # 同步钩子只做本地 fallback绝不在同步上下文发起 LLM 调用 return self._generate_title_result(state) override async def aafter_model(self, state: TitleMiddlewareState, runtime: Runtime) - dict | None: # 异步钩子才走LLM 生成 失败回退完整路径 from deerflow_extension_api import task_store_from_runtime return await self._agenerate_title_result( state, task_storetask_store_from_runtime(runtime), )客户端使用获取 Thread Title// 方式1: 从 thread state 获取 const state await client.threads.getState(threadId); const title state.values.title || New Conversation; // 方式2: 监听 stream 事件 for await (const chunk of client.runs.stream(threadId, assistantId, { input: { messages: [{ role: user, content: Hello }] } })) { if (chunk.event values chunk.data.title) { console.log(Title:, chunk.data.title); } }在对话列表中显示 Title// 在对话列表中显示 function ConversationList() { const [threads, setThreads] useState([]); useEffect(() { async function loadThreads() { const allThreads await client.threads.list(); // 获取每个 thread 的 state 来读取 title const threadsWithTitles await Promise.all( allThreads.map(async (t) { const state await client.threads.getState(t.thread_id); return { id: t.thread_id, title: state.values.title || New Conversation, updatedAt: t.updated_at, }; }) ); setThreads(threadsWithTitles); } loadThreads(); }, []); return ( ul {threads.map(thread ( li key{thread.id} a href{/chat/${thread.id}}{thread.title}/a /li ))} /ul ); }优势与注意事项设计优势原文档总结可靠持久化— 使用 LangGraph 的 state 机制自动随 checkpointer 持久化完全后端处理— 客户端无需额外逻辑自动触发— 首次对话后自动生成且首轮 run 被中断时由 run worker 补偿写入本地 fallback 标题可配置— 支持自定义长度、prompt 模板、模型容错性强— LLM 路径任何异常都回退到本地策略标题生成永不阻塞或阻断主流程架构一致— 与现有 SandboxMiddleware 等中间件采用相同的 state 更新模式。使用时的注意事项读取方式Title 在state.values.title而非thread.metadata.title性能默认配置model_name: null零额外 LLM 开销只有显式配置title.model_name时首轮回复后才会额外等待一次 LLM title 生成该调用带TAG_NOSTREAM不会混入前端事件流并发安全middleware 在 agent 首次完整回复后更新 state不需要客户端额外请求幂等判定state 已有 title 即返回None与 run worker 的 stale-snapshot 重试共同保证并发安全Fallback 策略默认使用用户消息前若干字符上限min(max_chars, 50) 省略号作为 titleLLM 调用失败时同样回退该策略用户消息为空时固定为New Conversation。测试与验证在 backend/tests 目录下两个测试文件分别覆盖核心逻辑与端到端生成原文档给出的验证命令cd backend uv run pytest tests/test_title_middleware_core_logic.py tests/test_title_generation.pytest_title_middleware_core_logic.py 覆盖_should_generate_title的首轮判定、allow_partial_exchange的中断路径含中文消息 请帮我写测试 的 fallback 断言等核心分支test_title_generation.py 验证标题生成的完整流程。此外test_run_worker_rollback.py 中有大量针对中断 run 补偿标题路径的桩测试mock_generate_title_result(state, allow_partial_exchangeTrue)可用于理解 run 回滚场景下标题持久化的各种边界。故障排查Title 没有生成检查配置是否启用get_title_config().enabled True注意 middleware 若注入了app_config实际读的是app_config.title确认是首轮对话只有恰好 1 个用户消息排除动态上下文提醒消息且至少 1 个助手回复时才会触发若显式配置了title.model_name检查标题模型是否可用未配置时走本地 fallback该路径不依赖任何外部模型只要用户消息有文本就必然生成若 run 在中途被取消确认 run worker 的中断补偿_ensure_interrupted_title已执行——它在无 checkpoint 或无法从消息派生文本时返回None不会写入空标题。Title 生成但客户端看不到确认读取位置应该从state.values.title读取而非thread.metadata.title检查 API 响应确认 state 中包含 title 字段尝试重新获取 stateclient.threads.getState(threadId)。Title 重启后丢失检查是否配置了 checkpointer本地开发需要确认部署方式LangGraph Platform 会自动持久化查看数据库确认 checkpointer 正常工作channel_values.title应存在。相关源码文件索引文件作用thread_state.pyThreadState定义title字段所在title_middleware.pyTitleMiddleware完整实现判定、归一化、双路径生成、钩子title_config.pyTitleConfig模型与全局配置管理config.example.yamltitle配置块的参考示例第 1734 行附近lead_agent/agent.pylead agent 中间件链中 TitleMiddleware 的注册位置factory.py自定义 Agent 工厂中的auto_title特性开关worker.py_ensure_interrupted_title中断 run 的 fallback 标题补偿写入AUTO_TITLE_GENERATION.md本主题的设计文档【免费下载链接】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),仅供参考
