DeerFlow 子代理卡片实时元数据:在折叠卡片上展示有效模型与累计 Token 用量的完整设计方案
【免费下载链接】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
本文基于 DeerFlow 仓库中的规划文档 plans/subagent-card-runtime-metadata.md 展开,解读"在折叠状态的子代理(subagent)卡片上实时展示有效 LLM 名称与累计 Token 用量"这一特性背后的架构决策、三阶段实施计划与验收标准,并结合仓库源码(SubagentTokenCollector、status_contract、step_events、task_tool与前端subtask-result.ts)说明每个设计点是如何落地、如何保持向后兼容的。读完后,你将理解:为什么运行期元数据必须按累计快照而非增量下发、为什么以task_id为键、以及终端状态持久化如何做到"不新增数据库迁移即可回放"。
背景:折叠卡片上的两个实时信号
DeerFlow 的 Lead Agent 可以通过task工具把子任务委派给子代理并行执行。用户在前端工作区看到每张子代理卡片时可以将其折叠——此时卡片不再展示完整过程,但仍需要回答两个问题:
- 这个子代理正在用哪个配置的 LLM 执行?(模型身份,Model Identity)
- 它目前消耗了多少 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_tokens、output_tokens、total_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.py 中SubagentExecutor初始化时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_id、caller、真实产出的model_name(从response_metadata读取,用于父日志按真实模型分桶,而不是用 Lead Agent 的模型)、input_tokens、output_tokens、total_tokens,以及稀疏存在的cache_read_tokens(仅当 Provider 报告了缓存命中才写入,与父日志按模型分桶的稀疏结构一致); total_tokens缺失时回退为input + output,两者都非正数的响应直接跳过,不伪造 0。
在 executor.py 中,每次 LLM 响应完成后调用collector.snapshot_records(),将最新累计记录写入共享的SubagentResult(update_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快照。
两个值得注意的工程细节:
make_subagent_additional_kwargs在生产边界校验:status不在枚举内或stop_reason不在{token_capped, turn_capped, loop_capped}内会直接抛ValueError——拼写错误必须在生产端就失败,而不是以"缺失元数据"的形式悄悄漏给消费者;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.py 的subagent_run_event负责把task_*自定义流块映射为RunEventStore的持久化 kwargs:
task_started→subagent.start;task_running→subagent.step(经build_subagent_step截断到SUBAGENT_STEP_MAX_CHARS = 8192,防止一次大write_file产生无界行);task_completed/task_failed/task_cancelled/task_timed_out→subagent.end,其content在task_id与status之外,额外携带可选的model_name与usage:model_name经非空字符串校验后写入;usage经normalize_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.ts 从additional_kwargs读取subagent_model_name/subagent_token_usage(经由normalizeTokenUsage,见 frontend/src/core/messages/usage.ts),与实时事件流合并到同一任务模型上,完成"live 与 durable 收敛"。
验收标准(原文档完整保留)
- 完成、失败、取消、超时的卡片都保留其最终模型与用量;
- 重新加载线程时从常规消息历史恢复元数据,不需要每张卡片一次请求;
- 持久化的
subagent.end事件包含相同的终态快照,供审计/调试使用; - 不带元数据的遗留卡片、以及没有用量的 Provider,保持可读并显式显示"不可用"状态;
- 右侧线程 Token Usage 总额保持不变,且子代理用量仍只被计数一次;
- 后端测试、前端单测、类型检查、格式化与相关回归套件全部通过。
兼容性与边缘设计:为什么"全部可选"是硬约束
这份规划最值得沉淀的经验是它对兼容性的系统性处理,仓库中多处可见其对应实现:
- 增量式 JSON 扩展,零迁移:
subagent.end的content只是在既有{task_id, status}上追加可选键;ToolMessage 的additional_kwargs同理。旧数据、旧前端读取时看不到新字段即按"不可用"渲染,没有任何读路径依赖新字段存在。 - 缺失 ≠ 0:
normalize_token_usage返回None的语义是"Provider 没报",前端据此渲染 collecting/unavailable 状态;SubagentTokenCollector侧也跳过total_tokens <= 0的响应。两处一致避免了"伪造的零"污染成本曲线。 - 累计快照 + run_id 去重:因为线上是累计值,合并策略天然幂等——重复帧、重放帧、乱序帧都不会改变"取最新累计值"的语义;
_counted_run_ids去重与前端"替换而非累加"的合并逻辑互为补充,把双计风险分别堵在生产端与消费端。 - 投影不记账:卡片消费的是
SubagentResult上共享的快照与subagent.end事件,正式记账只发生在子代理结束时向RunJournal的一次性移交(record_external_llm_usage_records),右侧线程总额因此不受卡片渲染开关影响。 - 遗留值归一化:
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下发 → 前端以替换语义合并并渲染 → 终态时戳入 ToolMessageadditional_kwargs并持久化进subagent.end事件 → 刷新后从历史事件恢复同一份快照。贯穿全程的四条不变量——累计而非增量、全字段可选、缺失渲染为不可用、投影不做第二次记账——使整个特性可以在不触碰数据库 schema、不破坏旧 run 回放的前提下平滑上线。
【免费下载链接】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),仅供参考