首页
/ DeerFlow 自动会话标题生成:从 TitleConfig 配置到 TitleMiddleware 中间件的完整实现解析

DeerFlow 自动会话标题生成:从 TitleConfig 配置到 TitleMiddleware 中间件的完整实现解析

2026-09-04 17:17:39作者:戚魁泉Nursing

本篇围绕 DeerFlow 的自动会话标题(Thread Title)生成功能展开:它如何让 Agent 在首轮对话结束后自动为会话写入一个 title,默认以零 LLM 开销的本地 fallback 策略生成标题,同时支持显式配置标题模型走 LLM 精炼。读完你可以掌握该功能的完整配置项(含取值范围与默认值)、触发条件与源码级实现原理、断线/中断边界场景下的持久化兜底,以及客户端从 state.values.title 读取标题的标准姿势。

功能全貌:涉及哪些核心文件

自动标题功能横跨「状态 → 配置 → 中间件 → 入口注册 → 运行器兜底」五个层面,各文件职责如下:

文件 职责
thread_state.py ThreadState 新增 title 字段,作为标题的持久化载体
title_config.py 新建 TitleConfig 配置类,含全局单例的 get/set/load/reset API
title_middleware.py 新建 TitleMiddleware,在首轮对话后触发标题生成
app_config.py from_file() 中把 config.yamltitle 段加载进全局配置
agent.py TitleMiddleware 注册进 lead agent 的中间件列表
worker.py 首轮 run 被中断时补写 fallback 标题,并把标题同步到线程元数据

设计上的一个关键决策是:标题存在 Graph 的 State 里,而不是 thread.metadata。两者的对比如下:

方面 State(采用) Metadata(未采用)
持久化 自动(通过 checkpointer) 取决于实现,不可靠
版本控制 支持时间旅行 不支持
类型安全 TypedDict 定义 任意字典
标准化 LangGraph 核心机制 扩展功能

数据层:ThreadState 增加 title 字段

ThreadState 中,title 被声明为可选字段:

class ThreadState(AgentState):
    sandbox: SandboxStateField
    thread_data: NotRequired[ThreadDataState | None]
    title: NotRequired[str | None]   # 自动生成的会话标题
    artifacts: Annotated[list[str], merge_artifacts]
    # ... 其余字段

使用 NotRequired[str | None] 意味着:旧 checkpoint 中不存在该字段时反序列化不会报错,新字段对历史会话完全向后兼容;而一旦写入,它就和 messagesartifacts 等通道一样被 checkpointer 自动持久化,并支持时间旅行回溯。

配置层:TitleConfig 的五个参数

TitleConfig 是一个 Pydantic 模型,字段与约束如下(约束值直接来自源码的 Field 参数):

参数 类型 默认值 约束 说明
enabled bool True 是否启用自动标题生成
max_words int 6 1 ≤ x ≤ 20 标题最大词数
max_chars int 60 10 ≤ x ≤ 200 标题最大字符数
model_name str | None None None 时走本地 fallback;填模型名才启用 LLM 标题
prompt_template str 内置模板 LLM 路径使用的提示词模板,支持 {max_words}{user_msg}{assistant_msg} 占位符

默认的 prompt_template 为:

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(config)               # 整体替换
load_title_config_from_dict(dict)     # 从 config.yaml 解析出的字典加载
reset_title_config()                  # 恢复 TitleConfig() 纯净默认值(供测试使用)

加载链路在 AppConfig.from_file() 中完成:title 段先解析为 AppConfig.title 字段(L237),随后调用 load_title_config_from_dict(config.title.model_dump()) 同步进全局单例,使中间件即使不持有 AppConfig 实例也能读到配置。

仓库根目录的 config.example.yaml 给出了标准配置段:

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

核心机制:TitleMiddleware 触发时机与双策略生成

注册与钩子

lead agent 的构建流程 中,TitleMiddleware 被实例化并追加到中间件列表,紧随其后注册 MemoryMiddleware

# Add TitleMiddleware
middlewares.append(
    TitleMiddleware(
        app_config=resolved_app_config,
        extensions=resolved_extensions,
    )
)

中间件覆写了 after_model()aafter_model() 两个钩子(L278-L288),在每次模型响应后触发。同步钩子只生成本地 fallback 标题;异步钩子则额外承担 LLM 标题路径。整体工作流程为:

用户发送首条消息
  ↓
Agent 处理并返回回复
  ↓
TitleMiddleware.after_model() / aafter_model() 触发
  ↓
检查:是否首次对话?是否已有 title?
  ↓
默认从首条用户消息生成本地 fallback title
  ↓
如果显式配置 title.model_name,才调用 LLM 生成更精炼的 title
  ↓
返回 {"title": "..."} 更新 state
  ↓
Checkpointer 自动持久化(如果配置了)
  ↓
客户端从 state.values.title 读取

触发条件 _should_generate_title

_should_generate_title 定义了「是否该生成标题」的全部判据:

  1. config.enabledFalse → 不生成;
  2. state 中已有 title → 不生成(每个 thread 只生成一次);
  3. 消息数检查:正常路径要求至少 1 条用户消息 + 1 条助手回复(len(messages) >= 2);
  4. 精确计数:用户消息(排除动态上下文提醒消息)恰好为 1 条,且助手消息至少 1 条。

其中第 3 点有一个细节:allow_partial_exchange=True 时把门槛降为「至少 1 条用户消息」,专门服务于首轮 run 被取消、AI 回复尚未落 checkpoint 的中断场景——此时允许仅凭首条用户消息生成 fallback 标题。

本地 fallback 策略(默认路径)

model_nameNone(默认配置)时,_generate_title_result 直接调用 _fallback_title,其规则是:

  • 用户消息为空 → 返回 "New Conversation"
  • 用户消息长度超过 min(max_chars, 50) → 截断并追加省略号 ...(预留省略号空间,保证结果恰好满足 max_chars 上限);
  • 否则原样返回用户消息作为标题。

这条路径不发起任何 LLM 调用,因此默认配置下标题生成对流式回复收尾的延迟贡献几乎为零。

LLM 标题路径(显式配置 model_name 时)

异步钩子走 _agenerate_title_result,关键实现点:

  1. 无用户文本时短路:仅含附件的首轮没有用户自撰文本,直接返回 fallback,避免让标题模型从助手回复中「脑补」标题;
  2. Prompt 构建_build_title_prompt 取首条用户消息与首条 AI 回复,各截断到 500 字符后填入 prompt_template;AI 回复会先经 _strip_think_tags 用正则 <think>...</think> 剥除推理模型的思维块(兼容 minimax、DeepSeek-R1 等);
  3. 模型调用:通过 create_chat_model(name=config.model_name, thinking_enabled=False, ...) 创建标题模型,并经 observe_system_model_call(..., SystemOperationKind.TITLE, ...) 包装,使扩展系统能观测到这是一次系统级模型调用;
  4. 输出清洗_parse_title 归一化内容类型、再剥 think 标签、strip 引号,最后按 max_chars 截断;
  5. 异常兜底:模型创建或调用抛任何异常时仅记录 debug 日志,回落到本地 fallback 标题——LLM 路径的失败永远不会阻断主流程。

另外,_get_runnable_config 继承父级 RunnableConfig 并追加 middleware:title 标签与 TAG_NOSTREAM:前者让 RunJournal 把这次 LLM 调用识别为 middleware:title 而非 lead_agent,后者确保标题生成产生的 token 流不会混入主对话的流式输出

配置与客户端使用

后端配置

启用/禁用:

# config.yaml
title:
  enabled: true  # 设为 false 禁用

自定义参数:

title:
  enabled: true
  max_words: 8      # 标题最多 8 个词(取值范围 1-20)
  max_chars: 80     # 标题最多 80 个字符(取值范围 10-200)
  model_name: null  # null = 快速本地 fallback;填模型名才启用 LLM 标题

配置持久化(可选)

本地开发时如需持久化 title,可配置 checkpointer:

# checkpointer.py
from langgraph.checkpoint.sqlite import SqliteSaver

checkpointer = SqliteSaver.from_conn_string("deerflow.db")
// langgraph.json
{
  "graphs": {
    "lead_agent": "deerflow.agents:lead_agent"
  },
  "checkpointer": "checkpointer:checkpointer"
}

客户端读取

// 获取 thread title
const state = await client.threads.getState(threadId);
const title = state.values.title || "New Conversation";

// 显示在对话列表
<li>{title}</li>

注意:Title 位于 state.values.title,而非 thread.metadata.title——这是使用 State 方案带来的直接约定。

边界场景:首轮 run 中断后的标题兜底

这是该功能中最精细的一段实现。当首轮 run 在 checkpointer 写入可用 checkpoint 前被取消时,worker.py 会在 interrupted-run cleanup 中调用 _ensure_interrupted_title,其设计要点:

  • 保持 finalizing 状态:run 在 cleanup 期间保持 finalizing,避免同线程的新 run 在 fallback title 写入期间覆盖 checkpoint;
  • 幂等:写入前先读 latest checkpoint 的 channel_values.title,已有标题则直接返回,重复调用不会重写;
  • title-only 更新:若同线程状态已经前进(后续消息已写入),只对最新 snapshot 做 title-only 更新,避免把旧消息重新变成 latest;
  • 版本推进:写入时同步 bump channel_versions["title"] 并在 metadata.writes 中声明 runtime_interrupt_title,保证 checkpointer 的通道版本一致性。

标题落 checkpoint 后,worker 还会把它同步到线程元数据的展示名(worker.py L1524-L1535):

# Sync title from checkpoint to threads_meta.display_name
title = ckpt.get("channel_values", {}).get("title")
if title:
    await thread_store.update_display_name(thread_id, title)

从源码结构看,这形成了一个「State 为权威来源、metadata 为展示副本」的双写结构:State 里的 title 随 checkpoint 持久化且可时间旅行,display_name 则供会话列表等 UI 快速读取。

测试与验证

仓库提供了 tests/test_title_generation.py,覆盖配置类与中间件初始化:

# 运行标题生成测试
pytest tests/test_title_generation.py -v

# 运行所有测试
pytest

测试内容包含:默认配置值断言(enabled=Truemax_words=6max_chars=60model_name=None)、自定义配置、越界校验(max_words 传 0 或 21、max_chars 传 5 或 201 均抛 ValueError)、全局 get/set、中间件初始化与 state_schema 校验。文件末尾留有集成测试 TODO(mock Runtime、checkpointer 持久化、并发标题生成等)。

故障排查

Title 没有生成?

  1. 检查配置:title.enabled: true
  2. 确认是首次对话(1 个用户消息 + 1 个助手回复),且 state 中尚无 title;
  3. 如果显式配置了 title.model_name,检查标题模型是否可用;未配置时会走本地 fallback,不会静默失败。

Title 生成但看不到?

  1. 确认读取位置:state.values.title(不是 thread.metadata.title);
  2. 检查 API 响应是否包含 title;
  3. 重新获取 state(标题在模型响应后的钩子中写入,过早读取可能拿不到)。

Title 重启后丢失?

  1. 本地开发需要配置 checkpointer(见上文 SqliteSaver 示例);
  2. 托管部署环境下持久化由平台自动完成;
  3. 检查数据库确认 checkpointer 工作正常。

中断首轮后仍显示默认标题?

  1. worker 的 interrupted-run cleanup 会补写 fallback 标题(见「边界场景」一节),确认 run 的取消路径是否进入了该分支;
  2. 确认取消发生在 checkpoint 写入前——若已有可用 checkpoint 且其中含 title,兜底逻辑幂等返回,不会覆盖。

性能影响与优化建议

  • 默认延迟:默认 title.model_name: null 不发起额外 LLM 调用,仅从首条用户消息生成本地 fallback 标题;
  • 显式 LLM 标题延迟:只有配置 title.model_name 时,首轮回复后才会等待一次标题模型调用;
  • 并发安全:在 after_model() / aafter_model() 中更新 state,不需要客户端额外请求;
  • 资源消耗:每个 thread 只生成一次。

优化建议:

  1. 默认保持 model_name: null,避免流式回复结束前的额外 LLM 等待;
  2. 如需更精炼标题,再显式配置较快的标题模型;
  3. 适当调小 max_wordsmax_chars,并保持 prompt 简洁(用户/助手消息各截断 500 字符,prompt 体积有天然上限)。

延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384