首页
/ DeerFlow 子代理卡片实时元数据:在折叠卡片上展示有效模型与累计 Token 用量的完整设计方案

DeerFlow 子代理卡片实时元数据:在折叠卡片上展示有效模型与累计 Token 用量的完整设计方案

2026-09-06 14:48:58作者:范垣楠Rhoda

本文基于 DeerFlow 仓库中的规划文档 plans/subagent-card-runtime-metadata.md 展开,解读"在折叠状态的子代理(subagent)卡片上实时展示有效 LLM 名称与累计 Token 用量"这一特性背后的架构决策、三阶段实施计划与验收标准,并结合仓库源码(SubagentTokenCollectorstatus_contractstep_eventstask_tool 与前端 subtask-result.ts)说明每个设计点是如何落地、如何保持向后兼容的。读完后,你将理解:为什么运行期元数据必须按累计快照而非增量下发、为什么以 task_id 为键、以及终端状态持久化如何做到"不新增数据库迁移即可回放"。

背景:折叠卡片上的两个实时信号

DeerFlow 的 Lead Agent 可以通过 task 工具把子任务委派给子代理并行执行。用户在前端工作区看到每张子代理卡片时可以将其折叠——此时卡片不再展示完整过程,但仍需要回答两个问题:

  1. 这个子代理正在用哪个配置的 LLM 执行?(模型身份,Model Identity)
  2. 它目前消耗了多少 Token?(累计用量,Cumulative Token Usage)

规划文档的来源需求是:Conversation request approved on 2026-07-10 — show live token usage and the effective LLM name on collapsed subagent cards。围绕这一需求,文档给出了完整的架构决策集与分阶段(Phase 1/2/3)的建设内容和验收标准。

架构决策:八条不变量

原文档 "Architectural decisions" 一节是整个方案的灵魂,逐条对应了仓库中的具体实现约束:

  • 运行时身份(Runtime identity):所有元数据更新都按键控于既有的子代理 task_id,因此同一个 Lead Agent 回合内的并行委派彼此隔离。在 frontend/src/core/tasks/subtask-result.ts 中可以看到前端以 SUBAGENT_MODEL_NAME_KEY = "subagent_model_name"SUBAGENT_TOKEN_USAGE_KEY = "subagent_token_usage" 为键从结构化元数据中读取,正是这套键控协议的前端镜像。
  • 用量模式(Usage schema):运行期负载携带累计input_tokensoutput_tokenstotal_tokens。它们是快照(snapshot)而不是增量(delta),因此重放的或乱序到达的流帧不会造成重复计数。这正是前端把快照"作为权威累计值合并进任务状态"这一设计的前提。
  • 更新节奏(Update cadence):"实时"指的是每次子代理 LLM 响应完成后。大多数 Provider 在响应完成前不会暴露权威用量,因此不存在响应中部的用量帧。
  • 模型身份(Model identity):线上契约(wire contract)携带的是为子代理解析出的有效 DeerFlow 模型名。UI 优先展示配置中的显示名(display name),取不到时回退到原始模型名;Provider 的部署标识只作为观测数据,不作为卡片主标签。
  • 实时与持久来源:自定义任务生命周期事件(custom task lifecycle events)驱动运行中更新;终态 ToolMessage 元数据从 checkpoint 化的聊天历史中恢复相同取值;持久化的 subagent.end 事件保留终态快照供审计/调试消费者使用。
  • 兼容性(Compatibility):所有协议新增字段都是可选的。旧 run 渲染时没有运行元数据;缺失的 provider 用量渲染为"不可用"而不是 0;既有 JSON 负载是增量式扩展,不需要数据库迁移
  • 既有总额不动(Existing totals):父 run 与线程(thread)的 Token 记账保持不变;卡片元数据只是一个展示投影(presentation projection),绝不能把用量第二次上报给 RunJournal
  • 特性开关(Feature gate):Token 渲染遵循既有的 token_usage.enabled 配置;即使 Token 渲染被禁用,模型身份仍然可以展示。

Phase 1:实时模型身份

用户故事:用户可以折叠一个正在运行的子代理卡片,并立即看到是哪个配置的 LLM 在执行它。

建设内容

规划要求:把有效的子代理模型名通过 task-start 生命周期事件传递出去,并按 task_id 合并进任务状态;在工作区解析友好模型显示名,在折叠卡片头部渲染,且不挤占既有状态指示器

源码印证

backend/packages/harness/deerflow/tools/builtins/task_tool.py 中可以看到落地路径:工具入口先通过 resolve_subagent_model_name(config, parent_model, app_config=...)(定义于 backend/packages/harness/deerflow/subagents/config.py)解析出 effective_model,随后在发出的 task_started 自定义事件 chunk 中携带 "model_name": effective_model。执行器一侧同样持有该名字——backend/packages/harness/deerflow/subagents/executor.pySubagentExecutor 初始化时 self.model_name = resolve_subagent_model_name(config, parent_model, app_config=app_config),并用它构建实际 create_chat_model(name=self.model_name, ...)。也就是说,"卡片上展示的名字"与"真正发请求的模型"来自同一个解析结果,不会出现展示与执行漂移。

验收标准(原文档完整保留)

  • 运行中的折叠卡片在 task-start 事件到达时立即显示其有效模型;
  • 使用不同模型的并行子代理在各自的卡片上显示正确的模型;
  • 配置了显示名的模型优先展示显示名;未知模型回退到原始标识;
  • 不带模型字段的旧任务事件仍能正常渲染。

Phase 2:实时累计 Token 用量

用户故事:用户观察一个折叠且正在运行的子代理卡片,能在每次子代理 LLM 调用完成后看到 Token 总量增加。

建设内容

规划要求:在子代理运行期间发布采集器(collector)最新的累计用量快照,并把它挂到任务进度事件上;前端把快照合并进任务状态作为权威累计值,然后在模型标签旁渲染格式化后的总量。同时要保留父 run 的既有记账路径,不新增任何一次记账写入

源码印证:SubagentTokenCollector 如何产出快照

快照的生产者是 backend/packages/harness/deerflow/subagents/token_collector.py 中的 SubagentTokenCollector

  • 它是一个 LangChain BaseCallbackHandler,每次子代理执行创建独立实例,caller 标识归属;
  • on_llm_end 中用 _counted_run_ids 集合按 run_id 去重,保证同一次 LLM 调用的重复回调不会双计——这对应架构决策中"重放/乱序帧不会双计"的底线;
  • 每条记录携带 source_run_idcaller、真实产出的 model_name(从 response_metadata 读取,用于父日志按真实模型分桶,而不是用 Lead Agent 的模型)、input_tokensoutput_tokenstotal_tokens,以及稀疏存在的 cache_read_tokens(仅当 Provider 报告了缓存命中才写入,与父日志按模型分桶的稀疏结构一致);
  • total_tokens 缺失时回退为 input + output,两者都非正数的响应直接跳过,不伪造 0

executor.py 中,每次 LLM 响应完成后调用 collector.snapshot_records(),将最新累计记录写入共享的 SubagentResultupdate_token_usage_records);下一个 task_running 事件携带该快照,折叠卡片即可无记账副作用地更新。子代理结束后,记录经 RunJournal.record_external_llm_usage_records 一次性移交父日志完成唯一一次正式记账——这正是"卡片是投影、不做第二次记账"的实现保障。

验收标准(原文档完整保留)

  • 首次完成的子代理 LLM 调用在用量可用时,把折叠卡片从"采集中"更新为非零总量;
  • 后续调用用新的累计总量替换卡片快照,而不是把总量再加一次;
  • 重放的、重复的或更早的进度事件绝不双计或使显示总量减小;
  • 并发子代理按 task_id 保持相互独立的总量;
  • 省略用量元数据的 Provider 显示"不可用/采集中"状态,绝不显示伪造的 0。

Phase 3:终端持久化与边缘路径

用户故事:无论完成、失败、取消、超时还是页面刷新,用户看到的最终模型与 Token 用量都一致。

建设内容

规划要求:把最终模型与累计用量戳入既有结构化任务 ToolMessage 元数据和持久化的 subagent.end 事件;让历史重建逻辑学会读取这些可选元数据,实时快照与终态历史收敛到同一个任务模型上;覆盖所有终态状态,并安全地容忍遗留/畸形元数据。

源码印证一:ToolMessage 元数据契约

backend/packages/harness/deerflow/subagents/status_contract.py 定义了跨前后端的结构化结果元数据契约,其中与本特性直接相关的字段:

  • subagent_model_name(可选):本次委派 run 使用的有效 DeerFlow 模型标识;
  • subagent_token_usage(可选):Provider 报告时的最终累计 input_tokens / output_tokens / total_tokens 快照。

两个值得注意的工程细节:

  1. make_subagent_additional_kwargs 在生产边界校验:status 不在枚举内或 stop_reason 不在 {token_capped, turn_capped, loop_capped} 内会直接抛 ValueError——拼写错误必须在生产端就失败,而不是以"缺失元数据"的形式悄悄漏给消费者;
  2. normalize_token_usage两个元数据表面的唯一共享校验器(终态 ToolMessage 元数据与持久化的 subagent.step / subagent.end 事件),要求三个键全部为非负 int(显式拒绝 bool),任何非 Mapping 或畸形输入返回 None——Provider 没有用量时字段整体缺席,前端据此渲染"不可用",而不是 0。

枚举值本身由跨语言共享夹具 contracts/subagent_status_contract.json 钉住(completed / failed / cancelled / timed_out / polling_timed_out),Python 侧 SUBAGENT_STATUS_VALUES 与 TypeScript 侧通过契约测试互相锁定。

源码印证二:subagent.end 事件保留终态快照

backend/packages/harness/deerflow/subagents/step_events.pysubagent_run_event 负责把 task_* 自定义流块映射为 RunEventStore 的持久化 kwargs:

  • task_startedsubagent.starttask_runningsubagent.step(经 build_subagent_step 截断到 SUBAGENT_STEP_MAX_CHARS = 8192,防止一次大 write_file 产生无界行);
  • task_completed / task_failed / task_cancelled / task_timed_outsubagent.end,其 contenttask_idstatus 之外,额外携带可选的 model_nameusagemodel_name 经非空字符串校验后写入;usagenormalize_token_usage 归一化后写入,畸形则整字段缺席;
  • 大块 result / error 文本按 SUBAGENT_STEP_MAX_CHARS 截断并打 result_truncated / error_truncated 标志,保证持久化行有界。

这些事件挂在专门的 subagent 类别下(见 SUBAGENT_EVENT_CATEGORY),因此不会混入 list_messages(线程消息流),只通过 list_events 暴露给前端"展开时按需回填"(fetch-on-expand),list_events 支持按 metadata["task_id"] 过滤加 after_seq 前向游标分页——卡片按单个子代理翻阅步骤时不会被 run 级 limit 截断尾部,且全程无 schema 迁移(过滤复用既有 run 级索引)。

源码印证三:前端读取路径

frontend/src/core/tasks/subtask-result.tsadditional_kwargs 读取 subagent_model_name / subagent_token_usage(经由 normalizeTokenUsage,见 frontend/src/core/messages/usage.ts),与实时事件流合并到同一任务模型上,完成"live 与 durable 收敛"。

验收标准(原文档完整保留)

  • 完成、失败、取消、超时的卡片都保留其最终模型与用量;
  • 重新加载线程时从常规消息历史恢复元数据,不需要每张卡片一次请求
  • 持久化的 subagent.end 事件包含相同的终态快照,供审计/调试使用;
  • 不带元数据的遗留卡片、以及没有用量的 Provider,保持可读并显式显示"不可用"状态;
  • 右侧线程 Token Usage 总额保持不变,且子代理用量仍只被计数一次;
  • 后端测试、前端单测、类型检查、格式化与相关回归套件全部通过。

兼容性与边缘设计:为什么"全部可选"是硬约束

这份规划最值得沉淀的经验是它对兼容性的系统性处理,仓库中多处可见其对应实现:

  1. 增量式 JSON 扩展,零迁移subagent.endcontent 只是在既有 {task_id, status}追加可选键;ToolMessage 的 additional_kwargs 同理。旧数据、旧前端读取时看不到新字段即按"不可用"渲染,没有任何读路径依赖新字段存在。
  2. 缺失 ≠ 0normalize_token_usage 返回 None 的语义是"Provider 没报",前端据此渲染 collecting/unavailable 状态;SubagentTokenCollector 侧也跳过 total_tokens <= 0 的响应。两处一致避免了"伪造的零"污染成本曲线。
  3. 累计快照 + run_id 去重:因为线上是累计值,合并策略天然幂等——重复帧、重放帧、乱序帧都不会改变"取最新累计值"的语义;_counted_run_ids 去重与前端"替换而非累加"的合并逻辑互为补充,把双计风险分别堵在生产端与消费端。
  4. 投影不记账:卡片消费的是 SubagentResult 上共享的快照与 subagent.end 事件,正式记账只发生在子代理结束时向 RunJournal 的一次性移交(record_external_llm_usage_records),右侧线程总额因此不受卡片渲染开关影响。
  5. 遗留值归一化status_contract.py 中的 read_subagent_result_metadata 对历史上已 checkpoint 进线程历史的 max_turns_reached 等已停产状态值做了读侧归一化(映射为 turn_capped),避免历史数据在新版本下"悬空"为 in-progress——这是"容忍遗留/畸形元数据"的具体形态。

关键文件索引

关注点 文件
方案与验收标准 plans/subagent-card-runtime-metadata.md
Token 快照采集 backend/packages/harness/deerflow/subagents/token_collector.py
元数据契约与校验器 backend/packages/harness/deerflow/subagents/status_contract.py
事件构建与 subagent.end 持久化 backend/packages/harness/deerflow/subagents/step_events.py
task_started 携带模型名 backend/packages/harness/deerflow/tools/builtins/task_tool.py
模型解析 backend/packages/harness/deerflow/subagents/config.py
跨语言枚举夹具 contracts/subagent_status_contract.json
前端读取与合并 frontend/src/core/tasks/subtask-result.ts
事件流文档 backend/docs/RUN_EVENT_STREAM.md

小结

这份规划把"折叠卡片上的两个实时信号"拆解为一条清晰的链路:SubagentTokenCollector 按 run_id 去重产出累计快照 → task_started / task_running 自定义事件携带模型名与用量按 task_id 下发 → 前端以替换语义合并并渲染 → 终态时戳入 ToolMessage additional_kwargs 并持久化进 subagent.end 事件 → 刷新后从历史事件恢复同一份快照。贯穿全程的四条不变量——累计而非增量、全字段可选、缺失渲染为不可用、投影不做第二次记账——使整个特性可以在不触碰数据库 schema、不破坏旧 run 回放的前提下平滑上线。

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

项目优选

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