
DeerFlow Record/Replay E2E无密钥双层次前后端契约验证体系与回放实现全解【免费下载链接】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-flowDeerFlow 是一个长时程 SuperAgent 框架前端Next.js与后端FastAPI 网关 LangGraph harness之间的 SSE/JSON 契约极易在两侧各自“绿灯”的情况下悄然漂移。DeerFlow 通过一套确定性、无 API 密钥的 Record/Replay 端到端体系解决这个问题录制一次真实模型运行随后用确定性的ReplayChatModel在无密钥环境下回放分别验证后端 SSE 协议形状Layer 1 后端 golden与真实前端渲染语义Layer 2 全栈渲染。读完本文你将理解其按“调用方 规范化对话哈希”匹配录制的底层原理、波动字段归一化策略、跨栈契约场景多 run 渲染顺序回归的构造方法以及完整的录制—构建—回放—CI 工作流。为什么需要 Record/Replay拒绝“假绿灯”DeerFlow 前端原本就有一套基于 mock 的 e2e 测试frontend/tests/e2e/由 playwright.config.ts 驱动它手工编写后端的 JSON/SSE 响应。问题在于一旦后端修改了 schema 或 SSE 事件格式mock 不会随之变化测试依然通过——文档 REPLAY_E2E.md 称之为“fake green”假绿灯。Record/Replay 体系的答案是回放一次录制下来的真实运行recorded real run对打真实后端Layer 1乃至真实前端Layer 2让契约漂移直接把构建打红而不是靠人工维护的 mock 掩盖。整个体系只有两个消费端但共享同一次录制产物——一份 fixture。两层验证架构总览Layer 1 — 后端 goldenSSE 协议形状测试入口是 tests/test_replay_golden.py它做四件事用 ReplayChatModel 替换真实模型通过 StarletteTestClient驱动真实的 FastAPI 网关app.gateway.app:create_app无任何网络模型调用走完整真实链路注册账号 → 创建 thread →POST /api/threads/{id}/runs/stream取回 SSE 事件流把事件流降维为“事件名 顶层 payload 键集合”的形状序列与提交的 golden 文件逐条比对额外检查replay_provider.replay_misses()只要有哈希未命中就响亮失败fail loud。对应测试代码中的断言test_replay_golden.pyassert events[0][event] metadata # 首事件必须是 metadata assert events[-1][event] end # 末事件必须是 end运行完成 misses replay_provider.replay_misses() assert not misses, replay miss: the fixture is stale vs the current ... if os.environ.get(DEERFLOW_WRITE_GOLDEN): events_path.write_text(...) # DEERFLOW_WRITE_GOLDEN1 时重写 golden return golden json.loads(events_path.read_text(encodingutf-8))[events] assert events golden # SSE 形状序列必须与 golden 完全一致其中“形状降维”由 sse_event_shapes 完成——只保留event名与sorted(data.keys())不记录易变值因此 golden 跨机器、跨天稳定同时仍能捕获事件序列与 payload 结构漂移。仓库中已提交的 fixture 与 golden 是可直接查看的样例tests/fixtures/replay/write_read_file.ultra.json录制产物含scenario/mode/model/prompt/context/turns字段tests/fixtures/replay/write_read_file.ultra.events.jsonLayer 1 golden即 SSE 形状序列。该场景的录制 prompt 是一个完整的工具调用闭环“使用你自己的文件工具创建/mnt/user-data/outputs/note.txt并读回内容不要委派 subagent、不要澄清提问”在ultra模式thinking plan subagent 全开下跑通write_file → 自动标题 → read_file → 最终回答的图执行路径。golden 中的典型事件序列为metadata → values(键: artifacts, delegations, messages, skill_context, viewed_images, …) → … → end直观展示了网关流式契约的样子。这个测试还特意重置了进程级单例app_config、paths、persistenceengine等见 _reset_process_singletons让测试专属的DEER_FLOW_HOME与DEER_FLOW_CONFIG_PATH生效——这是“在进程内驱动真实网关”的必要前提。Layer 2 — 全栈渲染语义渲染Layer 2 位于 frontend/tests/e2e-real-backend/由 playwright.real-backend.config.ts 编排真实 Next.jspnpm build pnpm start 真实网关回放模型端口 8011 Chromium。它断言“回放出的内容在浏览器里真的渲染出来了”——例如回放 run 的自动标题与后续建议是否正确呈现real-backend-render.spec.ts 直接从共享 fixture 读取prompt保证输入哈希命中录制轮次。两个层次互补、互不覆盖Layer 1 守协议shape快、无浏览器Layer 2 守语义render真实渲染栈。文档还特别指出Layer 2 同时承载跨栈契约场景——即“后端改动悄悄破坏前端假设、而双方单元测试都保持绿灯”的危险类别。该配置的关键编排细节playwright.real-backend.config.ts第一个 webServer 以cwd: ../backend启动 run_replay_gateway.pyreplay 网关/health就绪探针并注入DEERFLOW_ENABLE_TEST_SEED1挂载测试专用 seeder见下文与DEER_FLOW_AUTH_DISABLED1第二个 webServer 启动前端注入DEER_FLOW_INTERNAL_GATEWAY_BASE_URLhttp://127.0.0.1:8011。故意不设置NEXT_PUBLIC_*让前端走 next.config 的同源代理而非跨源直连——跨源 fetch 会丢失鉴权 cookieworkers: 1、forbidOnly: CI 时启用、CI 下重试 1 次报告在 CI 用githubreporter。目录内另有 auth-disabled-contract.spec.ts无 cookie 契约与 multi-run-order.spec.ts跨栈场景下一节详述。Replay 核心原理按哈希匹配录制轮次整个体系的灵魂是 tests/replay_provider.py。其设计可以用一句话概括每次模型调用按“调用方身份 输入消息规范化哈希”查表取回录制的输出未命中则响亮报错绝不静默通过。为什么按输入哈希而不是按轮次序号一次真实运行会由多个调用方发起模型调用lead agent 自身的轮次、TitleMiddleware自动标题、memory、可能的 subagent。它们交错出现数量与顺序本就不该成为回放依赖。按输入消息的规范化哈希匹配意味着每次调用都能取回“正是为该输入录制过的输出”与调用顺序、由哪个中间件发起无关。匹配键由两层哈希组成hash_replay_input / hash_input_keyinput_hash sha256(json({caller: ..., conversation_hash: ...})) conversation_hash sha256(规范化后的对话消息投影)调用方callerlead_agent、middleware:title、suggest_agent、subagent:*等来自 LangGraph 回调的 tags / run_namecaller_identitytags 优先因为图中间件与 subagent 已用 tags 作显式调用方标记。两个不同模型调用方即使对话文本完全相同也不会争抢同一个回放桶兼容别名title_agent被归一到middleware:title_CALLER_NAME_ALIASESL88-L93因为部分执行路径不保留 tag 直至模型回调旧 fixture 兼容早期录制的input_hash恰好就是 conversation hash回放时先查 caller 键、再回退 legacy 键_match存量 fixture 无需重录即可迁移。为什么 system prompt 被排除出匹配键这是一个关键设计决策_canonical_messages 的 docstring 与文档均有说明lead-agent 的 system prompt 是活文档、频繁被编辑例如 PR #3195 就新增过 “File Editing Workflow” 段落。若把它算进哈希任何人一改提示词所有 fixture 立即过期、无关 PR 全线红。真正稳定的契约是对话流本身用户输入 → 工具调用 → 工具结果 → 回答它才能唯一标识一个被录制的轮次。caller 仍保留在键中防止不同调用方互相污染。同一函数还排除了所有additional_kwargs.hide_from_ui的框架注入隐藏消息动态上下文提醒、memory 注入等它们携带每会话易变数据且非用户创作内容。波动字段归一化让 fixture 跨机器、跨天可回放哈希前的文本规范化_normalize_text 与各正则定义 L137-L164覆盖了所有“两次运行间无意义但字节不同”的部分波动来源归一化方式前端逐请求注入的system-reminder当前日期、星期、动态上下文后端直连路径没有整块删除re.DOTALL删除 reminder 后残留的空角色行User: \n折叠为空InputSanitizationMiddleware的--- BEGIN/END USER INPUT ---边界包装剥离传输层变换非语义变化UUIDthread/run/user idUUIDISO 时间戳 / 纯日期TS/DATEmacOS/Linux/DEER_FLOW_HOME的临时与 home 绝对路径PATH消息投影本身也丢掉了id、response_metadata、usage_metadata、tool_call_id等易变字段只保留 role、规范化文本、tool-call 的 nameargs。由于录制端scripts/record_gateway.py与回放端共用同一套hash_messagesrecord_gateway 直接from replay_provider import caller_identity, hash_messages, hash_replay_inputrecord 与 replay 的哈希一致性是构造上保证的而非约定。未命中必须响亮失败ReplayChatModel未命中时抛出带诊断信息的KeyError含调用方、已知哈希列表、规范化输入前 800 字符L341-L351——这是“回放运行偏离录制”的信号图结构变了、新的波动字段漏过归一化、或非确定性工具结果污染了下游输入。但这里有个陷阱网关的 LLM 错误处理中间件会把该异常吞掉并包装成一条正常的 assistant 错误消息导致 SSE事件形状完全不变——Layer 1 的 shape golden 靠形状根本抓不到 miss。因此 provider 维护了一个进程级 miss 记录表Layer 1 测试显式检查replay_misses()并失败L94-L106Layer 2 则天然失败——录制轮次压根渲染不出来。其他实现细节同样服务于“像真模型一样行为”bind_tools是返回self的 no-op录制轮次自带真实tool_callsagent 照常分发_stream以单个 chunk 输出整条录制消息并触发on_llm_new_token保证流式语义可用。模型块配置只换use不改模型名_replay_fixture.py 是 record/replay 共享的配置与驱动层其头注点明了铁律录制与回放必须走完全相同、影响提示词的配置否则系统提示词不同、输入哈希永不匹配。它把这一致性收敛到构造上——唯一差异是models[].use# 回放块REPLAY_MODEL_BLOCK_replay_fixture.py L31-L36 models: - name: scenario-model display_name: Scenario Model use: replay_provider:ReplayChatModel model: replay supports_thinking: true # 录制块real_model_block - name: scenario-model use: langchain_openai:ChatOpenAI model: 真实模型名 api_key: $OPENAI_API_KEY base_url: $OPENAI_API_BASE模型name在两侧保持一致提示词里可能出现模型名只有use指向不同类回放 fixture 路径经DEERFLOW_REPLAY_FIXTURE环境变量注入。build_config_yaml 生成的完整网关配置把一切影响提示词的内容全部钉死sandbox固定为LocalSandboxProvidertools固定为ls/read_file/write_file三件套file:read/file:write组skills指向空的home/skills目录配合 prepare_hermetic_extras 生成的空extensions_config.jsonmcpServers与skills均为空对象——确保开发者机器上 gitignored 的自定义 skills、MCP 服务器绝不泄漏进提示词memory 与 summarization 全部禁用enabled: false/injection_enabled: false因为它们是后台、去抖时序的模型调用跨运行不可复现。注意标题title仍启用——默认title.model_name: null路径走的是本地状态更新而非被录制的模型调用数据库用临时目录下的 sqlite。前端四种模式到运行上下文的映射也集中在此MODE_CONTEXT镜像core/threads/hooks.ts的前端映射flash/thinking/pro/ultra分别对应(thinking_enabled, is_plan_mode, subagent_enabled)的flash(F,F,F)、thinking(T,F,F)、pro(T,T,F)、ultra(T,T,T)。已提交 fixture 的context字段即{is_bootstrap: false, mode: ultra, thinking_enabled: true, is_plan_mode: true, subagent_enabled: true}回放时原样带回。drive_gateway 则封装了与 React 前端 LangGraph SDK 完全一致的 wire 路径真实注册随机邮箱→ 取csrf_tokencookie → 建 thread → 以assistant_id: lead_agent、stream_mode: [values]、recursion_limit: 100贴近网关默认过紧的 limit 会在协议中性的中间件新增图步骤时产生假 golden 漂移发起流式 POST。跨栈契约场景多 run 渲染顺序multi-run-order这是 Layer 2 独有的价值展示也是文档中最精彩的回归故事。背景issue #3352——上下文压缩context compression之后刷新线程历史消息渲染顺序错乱。根因是前后端失步后端RunManager.list_by_thread自 PR #2932 起按newest-first返回 runs而前端core/threads/hooks.ts遍历 runs 并把每页加载结果 prepend 到头部——一旦 checkpoint 不再保留较早消息压缩后的状态时间顺序就被反转。整个 bug 存活期间后端顺序测试一直是绿的前端的回归单测又在 mock 里硬编码了“后端 newest-first”的假设。只有真实前端打真实后端才能抓住这种失步。为什么不用录制对话来复现#3352 只在 checkpoint 不再持有旧消息时复现。因此该场景不录制任何对话而是用测试专用 seeder直接构造出“≥2 个 run、每个 run 有消息事件、且刻意没有 checkpoint”的前置条件——迫使前端走 per-run 重载路径让顺序 bug 可观测。seed_runs_router.py 实现了/api/test-only/seed-runs端点其安全性设计值得强调只由 run_replay_gateway.py 在DEERFLOW_ENABLE_TEST_SEED1时挂载生产 app 永远不会 import 它“so it cannot ship”它通过网关自身的app.state.run_store/app.state.run_event_store写入user_id默认取请求鉴权上下文——与随后读回这些 runs 的浏览器会话一致事件形状严格镜像runtime/journal.py真实运行写入的内容event_type为llm.human.input/llm.ai.response、categorymessage、content为message.model_dump()、metadata.callerlead_agentL37-L98每个 run 的消息一次 batch写入保证 seq 单调、run1 消息先于 run2。于是真实调用链list_by_thread → /runs/{id}/messages → 前端 prepend全链路在线运行。multi-run-order.spec.ts 的流程是同域注册 → 以两个不同created_at播种 run-1ALPHA较旧与 run-2OMEGA较新→page.goto加载线程触发 per-run 重载 → 断言两个标记各渲染一次且ALPHA 的 boundingBox.y 小于 OMEGA 的第一 run 必须渲染在第二 run 上方。失败信息直接点名失步双方“backend list_by_thread ordering and frontend history rebuild are out of sync (#3352)”。把 PR #3354 的前端修复回滚该 spec 立刻变红。录制新场景需要真实密钥仅限开发机录制走真实前端驱动保证捕获的输入与浏览器实际发出的完全一致包括前端注入的 system-reminder、标题/建议调用等且 fixture 本身不含任何 API 密钥。完整三步来自 REPLAY_E2E.md# 1. 用真实模型网关 真实前端录制捕获所有模型调用 OPENAI_API_KEY... OPENAI_API_BASEopenai-compatible-endpoint/v1 \ DEERFLOW_RECORD_OUT/tmp/rec/turns.jsonl RECORD_MODELmodel \ bash -c cd frontend pnpm exec playwright test -c playwright.record.config.ts # 2. 把捕获拼成 fixture cd backend uv run python scripts/build_fixture_from_jsonl.py \ --jsonl /tmp/rec/turns.jsonl --meta /tmp/rec/turns.jsonl.meta.json \ --out tests/fixtures/replay/scenario.mode.json --model model # 3. 重新生成提交的 golden DEERFLOW_WRITE_GOLDEN1 PYTHONPATH. uv run pytest tests/test_replay_golden.py各环节的仓库落点录制网关scripts/record_gateway.py以真实模型运行网关通过回调把每次模型调用的(input_hash, output)追加为 JSONL 行。它注册了一个Capture回调处理器on_chat_model_start记录输入与caller_identityon_llm_end配对写出。环境变量RECORD_PORT默认 8012、RECORD_MODEL默认 gpt-5.5、DEERFLOW_RECORD_OUT控制行为录制编排playwright.record.config.ts 驱动 spec tests/e2e-record/record-write-read-file.spec.ts该配置明确标注“手动执行、需要密钥、永不进 CI”且仅在对应环境变量确实设置时才透传避免把空串传给录制网关拼装scripts/build_fixture_from_jsonl.py读取 JSONL turns 与 spec 写出的.meta.json侧车{scenario, mode, prompt}输出含scenario / mode / model / prompt / context / turns的 fixture并打印每个 turn 的caller、哈希前缀与 tool_calls 便于人工核对。运行验证无需任何密钥日常与 CI 的回放完全无密钥cd backend PYTHONPATH. uv run pytest tests/test_replay_golden.py # Layer 1 cd frontend pnpm exec playwright test -c playwright.real-backend.config.ts # Layer 2Layer 2 也可以单独调试回放网关run_replay_gateway.py 支持独立运行cd backend uv run python scripts/run_replay_gateway.py --port 8011该脚本用build_config_yaml(REPLAY_MODEL_BLOCK)生成临时配置覆盖而非 setdefaultDEER_FLOW_HOME防止外层环境变量泄漏进来扰动影响提示词的路径与 skills设置DEERFLOW_REPLAY_FIXTURE指向默认 fixturewrite_read_file.ultra.json并把backend/与backend/tests/放进PYTHONPATH使配置中的use: replay_provider:ReplayChatModel可解析。CI 接线契约两侧任一变更即触发.github/workflows/replay-e2e.yml 让两个层次在契约任一侧发生变化时都运行——这正是“后端改动无法悄悄破坏前端”的制度保证。触发路径包括frontend/**、backend/app/gateway/**、backend/packages/harness/**、replay fixtures 以及回放基础设施文件本身replay_provider.py、_replay_fixture.py、seed_runs_router.py、test_replay_golden.py、run_replay_gateway.pybackend-replay-goldenLayer 1Python 3.12 uv版本钉在 0.11.1与 backend/Dockerfile 的UV_IMAGE标签一致由backend/tests/test_ci_uv_version_pin.py守护uv sync --group dev后执行PYTHONPATH. uv run pytest tests/test_replay_golden.py -v超时 15 分钟fullstack-replay-renderLayer 2Node 24 corepack 钉住 pnpm 10.26.2安装 Playwright Chromium 后执行pnpm exec playwright test -c playwright.real-backend.config.ts超时 25 分钟门禁策略DOM 断言是门禁渲染截图与 Playwright HTML 报告作为 CI artifactreplay-render保留 7 天上传供人工审查而不是硬断言跨 OS 的视觉基线两个 job 均跳过 draft PR并对同一 PR 启用cancel-in-progress并发组。已知限制与扩展点文档REPLAY_E2E.md明确列出三条限制也即体系的扩展接口视觉回归基线是 OS 相关的因此只作为本地开发门禁gitignoreCI 以 artifact 形式上传渲染结果供人工审查fixture 与录制时的提示词耦合如果新的环境依赖内容进入系统提示词需要扩展 replay_provider.py 中的归一化新增波动字段正则或在 build_config_yaml 中把该内容钉死agent 图若改变了模型调用次数必须重新录制该场景——回放在哈希未命中时会响亮报错并指向分歧点KeyError中附调用方与已知哈希列表。从源码结构看这套限制恰好对应了三个清晰的维护面replay_provider.py的_normalize_text正则族处理“新波动字段”、_replay_fixture.py处理“新提示词输入”、fixture 目录处理“新图行为”。小结DeerFlow 的 Record/Replay E2E 体系用一份录制喂饱两层验证Layer 1 以进程内真实网关 SSE 形状 golden 守护协议结构Layer 2 以真实 Next.js 真实网关 Chromium 守护渲染语义并额外承载了双方单测都抓不到的跨栈失步场景#3352 多 run 顺序。其可移植性来自三个构造性保证record/replay 共用同一哈希函数、匹配键刻意排除易变的 system prompt、回放配置把所有影响提示词的输入skills、MCP、memory、summarization、临时路径钉死为空或禁用状态——最终实现“无 API 密钥、跨机器、跨天、跨提示词编辑”的确定性回放任何未命中都以响亮失败而非静默假绿灯收场。【免费下载链接】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),仅供参考