DeerFlow 引用渲染机制重构:从 JSONL<citations>块到原生 Markdown 引用的全链路拆解
【免费下载链接】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
本文基于仓库中的按文件变更总结 CODE_CHANGE_SUMMARY_BY_FILE.md,完整解析 DeerFlow 一次「引用(citations)机制瘦身」变更:后端 Gateway 的 artifacts 下载接口如何移除引用剥离逻辑、Agent 提示词如何删除<citations_format>协议、前端 5 个组件与整个core/citations模块如何被统一的MarkdownContent组件替代,并结合当前仓库源码说明这次重构落地后的最终形态与架构意义。读完本文,你可以掌握一次典型「删除一个跨端渲染协议」的重构涉及的全部触点,以及如何在仓库中验证每一处变更的最终状态。
变更背景:旧机制为什么被移除
旧版 DeerFlow 的引用链路是一套跨前后端约定的私有格式:
- 提示词协议层:Lead Agent 的系统提示词(prompt.py)中包含一整段
<citations_format>区块,要求模型在web_search后输出<citations>JSONL 块(每行一个含id/title/url/snippet的 JSON 对象),并在正文中使用Title完整 Markdown 链接;子代理general_purpose的提示词(general_purpose.py)同样内嵌了该协议。 - 后端剥离层:Gateway 的 artifacts 路由(artifacts.py)提供
_extract_citation_urls与remove_citations_block两个函数——下载 Markdown 产物时正则解析<citations>JSONL 块,剥离块本身、[cite-N]标记以及引用 URL 对应的外链,再返回「净化」后的内容。 - 前端解析层:整个
core/citations目录(index.ts、use-parsed-citations.ts、utils.ts)负责解析 JSONL、构建citationMap、生成cleanContent;SafeCitationContent组件承载引用解析 + loading 占位(「正在整理 N 个引用...」)逻辑,被消息列表、消息组、子任务卡片、产物详情等多处复用。
这套机制的问题在于:JSONL 块与渲染层强耦合,后端下载路径要为 Markdown 单独走「读文件 → 正则清洗 → 手动Response」分支,前端要在多组件中重复解析。本次变更的方向是彻底放弃 JSONL 协议与清洗逻辑,统一退化为原生 Markdown 链接渲染,净删 894 行、新增 62 行。
后端:artifacts 下载路径去清洗化
变更对象是 artifacts.py(路由前缀/api/threads/{id}/artifacts,见 CLAUDE.md 的 API 表格)。diff 涉及三处:
- 导入调整:删除
import json与import re;Response从fastapi改为从fastapi.responses导入(保留二进制 inline 返回用)。 - 删除两个清洗函数:
# 被删除(变更前) def _extract_citation_urls(content: str) -> set[str]: """Extract URLs from <citations> JSONL blocks. Format must match frontend core/citations/utils.ts.""" urls: set[str] = set() for match in re.finditer(r"<citations>([\s\S]*?)</citations>", content): for line in match.group(1).split("\n"): line = line.strip() if line.startswith("{"): try: obj = json.loads(line) if "url" in obj: urls.add(obj["url"]) except (json.JSONDecodeError, ValueError): pass return urls def remove_citations_block(content: str) -> str: """Remove ALL citations from markdown (blocks, [cite-N], and citation links). Used for downloads.""" # 依次移除 <citations> 块、未闭合块、[cite-N] 标记、以及引用 URL 对应的外链注释中明确写有「Format must match frontend core/citations/utils.ts」——这正是前后端私有格式耦合的直接证据:前端正则一旦变化,后端下载清洗就会静默失配。
get_artifact下载分支简化:删除is_markdown判断以及「markdown 时读文件 +remove_citations_block+ 手工构造Response」的分支,?download=true一律走FileResponse:
# 变更后:所有下载统一由 FileResponse 流式返回 if request.query_params.get("download"): return FileResponse( path=actual_path, filename=actual_path.name, media_type=mime_type, headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"}, )值得强调的是,当前仓库中该文件已经比 diff 记录的版本进一步演进:现在get_artifact把所有响应统一交给FileResponse流式处理,_read_artifact_payload只返回处置计划(file/inline_file),并支持 RFC 9110 的Range请求、由 Gateway 计算 SHA-256 作为ETag(供浏览器在非安全上下文跳过crypto.subtle),活跃内容类型(HTML/XHTML/SVG)强制附件下载(见 artifacts.py)。这次重构正是该演进的第一步——下载路径不再读取、改写文件内容,为后续纯流式返回扫清了障碍。同时 CLAUDE.md 中 API 表的描述同步从「download with citation removal」改为「file download」。
提示词层:删除<citations_format>协议
变更对象是 Lead Agent 提示词(prompt.py)与 general-purpose 子代理提示词(general_purpose.py):
- Lead Agent:删除
<citations_format>...</citations_format>整段(原约 243–266 行),内容包括 JSONL 块要求、Short Title完整链接的「CRITICAL」格式规则与示例;删除critical_reminders中「Web search citations」一条;删除apply_prompt_template中面向子代理编排模式的「Citations when synthesizing」提醒行。 - general_purpose 子代理:删除其提示词中的
<citations_format>整段;output_format第 2 条由「Key findings or results (with citation links when from web search)」改为「Key findings or results」。
从源码结构看,当前仓库中 Lead Agent 提示词的引用规范已经演进为内联citation:TITLE链接格式(prompt.py 中的<citations>区块:「Use Markdown link formatcitation:TITLEimmediately after the claim」,并要求报告末尾汇总 Sources 一节)——JSONL 结构化块被完全放弃,引用回归为对 Markdown 渲染器而言零特殊化的普通链接。
前端:消息与产物渲染统一为MarkdownContent
这是本次变更中触点最多的一层。核心动作是:新建一个无引用解析逻辑的纯 Markdown 渲染组件MarkdownContent,用它替换所有SafeCitationContent调用点,然后整目录删除core/citations与两个引用专用组件。
新增markdown-content.tsx,删除safe-citation-content.tsx与inline-citation.tsx
新增文件 markdown-content.tsx 在 diff 中是纯 Markdown 渲染薄封装(MessageResponse+streamdownPlugins,空内容返回null)。当前仓库中的版本已扩展为带流式体验的完整渲染层:useSmoothStreamingContent以 50ms 节拍、每次最少 64 字符做平滑揭示,StreamingPre/StreamingCode处理流式代码块,SafeMessageResponse+streamdownPluginsWithoutRawHtml规避 raw HTML(见 markdown-content.tsx)。其对外 API 恰与旧SafeCitationContent兼容(content/isLoading/rehypePlugins/className),因此各调用点只是组件名替换、props 不变。
被删除的两个组件:
safe-citation-content.tsx(原约 85 行):引用解析、loading 占位、renderBody/loadingOnly、cleanContent/citationMap,已被MarkdownContent替代;inline-citation.tsx(原约 289 行):createCitationMarkdownComponents,把[cite-N]/URL 渲染为可点击引用,仅产物预览使用。
artifact-file-detail.tsx:产物详情去引用化
变更对象 artifact-file-detail.tsx:
- const parsed = useParsedCitations(language === "markdown" ? (content ?? "") : ""); - const cleanContent = language === "markdown" && content ? parsed.cleanContent : (content ?? ""); - const contentWithoutCitations = language === "markdown" && content - ? contentWithoutCitationsFromParsed(parsed) : (content ?? ""); + const displayContent = content ?? "";- 预览分支不再经
SafeCitationContent+renderBody间接层,直接<ArtifactFilePreview filepath threadId content={displayContent} language />; - 复制按钮由
clipboard.writeText(contentWithoutCitations)改为写原始displayContent;代码视图CodeEditor的value从cleanContent改为displayContent; ArtifactFilePreview签名删除cleanContent/citationMap两个 prop,内部不再调用createCitationMarkdownComponents,直接Streamdown渲染content;- 同时删除
React命名空间导入及core/citations、SafeCitationContent、useThread等全部相关 import。
消息侧四个组件的等价替换
| 组件 | 变更点 |
|---|---|
| message-group.tsx | 两处推理内容(ChainOfThoughtStep的label)由SafeCitationContent改为MarkdownContent;内部ToolCall去掉rehypePluginsprop 及useThread/fileContent;write_file分支删除 Markdown 预览块(isMarkdown+SafeCitationContent),只保留ChainOfThoughtStep+ path |
| message-list-item.tsx | 删除removeAllCitations导入,复制按钮clipboardData由removeAllCitations(...)包裹改为直接取消息原始内容;正文渲染改为MarkdownContent(保留 KaTeX rehype 插件) |
| message-list.tsx | import 与两处渲染由SafeCitationContent改为MarkdownContent,props 不变 |
| subtask-card.tsx | task.result的渲染由SafeCitationContent改为MarkdownContent |
删除core/citations目录与 i18n 键
core/citations三个文件全部删除,其导出面即旧机制的全部 API 边界:
index.ts(13 行):contentWithoutCitationsFromParsed、extractDomainFromUrl、isExternalUrl、parseCitations、removeAllCitations、shouldShowCitationLoading、syntheticCitationFromLink、useParsedCitations及Citation/ParseCitationsResult/UseParsedCitationsResult类型;use-parsed-citations.ts(28 行):useParsedCitations(content)hook;utils.ts(226 行):<citations>/[cite-N]解析、buildCitationMap、removeAllCitations、contentWithoutCitationsFromParsed。
i18n 三处同步删除citations.loadingCitations/loadingCitationsWithCount命名空间(「Organizing citations...」/「正在整理 N 个引用...」),涉及 types.ts、en-US.ts、zh-CN.ts;三份前端文档(AGENTS.md、CLAUDE.md、README.md)的目录树中删去core/citations/一行,CLAUDE.md 的描述句删去「andcitations」;utils.ts 中externalLinkClassNoUnderline的注释从「For streaming / loading state when link may be a citation (no underline)」改为中性的「Link style without underline by default (e.g. for streaming/loading)」,导出值未变。
技能与 Demo 数据同步
- github-deep-research/SKILL.md:第 6 条原则由「Cite inline - Reference sources near claims」改为「Reference sources - Add source references near claims where applicable」,措辞去引用化。
- market-analysis/SKILL.md:核心能力、输入表、Data Citation、Citations & References 小节与完成检查项全部改为「references」表述,去掉
[[N]](URL)内联引用格式要求,改为「使用 External Search Findings 时以 Markdown 链接引用来源」,References 章节仍按 GB/T 7714-2015 格式化。 - Demo 线程数据(research_deerflow_20260201.md 与同目录
thread.json):删除报告开头的 9 行<citations>…</citations>JSONL 块,正文从# DeerFlow Deep Research Report开始;thread.json中write_file的args.content同步去掉 JSONL 块,另有一处present_files的filepaths由单行数组改为多行格式、文件末尾换行统一,消息顺序与结构未动。
当前仓库中已可验证 demo 产物不再含<citations>块——前端不再依赖它,Demo 数据也随之「去协议化」。
变更统计与架构意义
原文档给出的 diff 统计如下:
| 项目 | 数量 |
|---|---|
| 修改文件 | 18 |
| 新增文件 | 1(markdown-content.tsx) |
| 删除文件 | 5(safe-citation-content.tsx、inline-citation.tsx、core/citations/* 共 3 个) |
| 总行数变化 | +62 / -894 |
从这次变更可以提炼出三条对 DeerFlow(以及任何 LLM 应用前端)有参考价值的架构结论:
- 提示词协议应尽量退化为标准格式。JSONL
<citations>块让「模型输出 → 后端下载清洗 → 前端解析渲染」三方都背上了同一份私有 schema 的维护成本(后端函数注释甚至要求「与前端 utils.ts 保持一致」)。删除后,引用就是普通 Markdown 链接,渲染链路零特殊化。 - 单一渲染入口收敛多组件。5 个调用点共享的
SafeCitationContent(解析 + loading + renderBody 回调)被一个 props 兼容的MarkdownContent替换后,消息、推理、子任务、产物预览全部走同一条 Streamdown 渲染路径,后续演进(如当前仓库中平滑流式揭示、流式代码块、raw HTML 防护)只需改动一处。 - 下载与渲染解耦。Gateway 不再为 Markdown 下载维护一套正则清洗逻辑,
FileResponse流式返回成为唯一路径——「给用户看什么」由前端决定,「交付什么字节」由后端决定,职责边界清晰。
需要说明的适用前提:本文描述的是 CODE_CHANGE_SUMMARY_BY_FILE.md 记录的变更集;其中部分文件(artifacts 路由、Lead Agent 提示词、MarkdownContent组件、i18n 键)在后续提交中又叠加了新能力(流式下载与 ETag、内联citation:TITLE引用规范、引用来源证据面板 sources.ts 与 citation-sources-panel.tsx),当前仓库代码是「本次瘦身 + 后续演进」的叠加结果,引用相关能力并未消失,而是换成了与标准 Markdown 渲染兼容的实现。
【免费下载链接】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),仅供参考