DeerFlow 前端智能体协作体系解析:基于 CLAUDE.md 与 AGENTS.md 的工程规范、测试架构与开发实战
【免费下载链接】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
本篇围绕 frontend/CLAUDE.md 这一前端智能体入口文档展开,讲解 DeerFlow(开源长周期 SuperAgent 框架)前端如何以一份共享的AGENTS.md作为唯一事实来源,统一 Claude Code、Codex 等编码智能体的协作规范。读完你将掌握 DeerFlow 前端完整的技术栈、命令体系、Rstest 双项目单测架构、Playwright E2E 方案、路由重写与环境配置原理,以及基于performance-budgets.json的路线资产预算机制,能够直接照此规范参与该项目的开发与智能体辅助编码。
CLAUDE.md:一个"共享智能体指南"的导入式入口
frontend/CLAUDE.md 全文只有五行,其核心设计是一行导入指令:
The frontend agent guidance lives in [AGENTS.md](https://link.gitcode.com/i/c7cca5fce93011bc93d1aa4a43455f61) so it is shared across coding agents (Claude Code, Codex, and others). Claude Code imports it below. @AGENTS.md这是多智能体协作仓库中一个值得借鉴的模式:CLAUDE.md不再承载任何独立内容,而是通过 Claude Code 的@file导入语法引用同目录的 frontend/AGENTS.md。带来的好处有三点:
- 单一事实来源(Single Source of Truth):
AGENTS.md明确声明 "It is the source of truth; the siblingCLAUDE.mdimports it via@AGENTS.md"。所有编码智能体(Claude Code、Codex 以及其他支持 AGENTS.md 约定的工具)读到的是同一份规范,避免了"CLAUDE.md 一套说法、其他智能体另一套说法"的分叉风险。 - 维护成本减半:规范演进只需修改
AGENTS.md一处,贡献流程中也有对应要求——"Update thisAGENTS.mdwhen architecture, commands, or conventions change"。 - 分层下钻:
AGENTS.md末尾还指明 "More specificAGENTS.mdfiles undersrc/contain the frontend sections split from this file",仓库中确实存在 frontend/src/AGENTS.md,承载了按数据流(Data Flow)、关键模式(Key Patterns)、交互所有权(Interaction Ownership)组织的更细粒度约定,形成"根级概览 + 源码级细则"的两层文档结构。
下文以 frontend/AGENTS.md 的原始内容为骨架,逐节展开,并用仓库中的真实配置文件与源码印证每一条约定的落点。
项目概览与技术栈
AGENTS.md对 DeerFlow 前端的定义是:"a Next.js 16 web interface for an AI agent system"——一个与 LangGraph 后端通信、提供线程(thread)式 AI 对话、流式响应、制品(artifacts)与技能/工具系统的 Web 界面。
官方声明的完整技术栈为:Next.js 16、React 19、TypeScript 5.8、Tailwind CSS 4、pnpm 10.26.2,要求 Node.js 22+ 与 pnpm 10.26.2+。这与 frontend/package.json 完全一致:"next": "^16.2.11"、"react": "^19.0.0"、"typescript": "^5.8.2"、"tailwindcss": "^4.0.15",且文件末尾通过"packageManager": "pnpm@10.26.2"钉死了包管理器版本,配合 pnpm 的 packageManager 校验机制保证团队与 CI 使用同一版本。
核心依赖(AGENTS.md列出的四项)在 frontend/package.json 中均可对应到:
- LangGraph SDK(
@langchain/langgraph-sdk^1.5.3)——Agent 编排与流式通信,是整个聊天界面的"生命线"; - LangChain Core(
@langchain/core^1.1.15)——基础 AI 构件; - TanStack Query(
@tanstack/react-query^5.90.17)——服务端状态管理; - UI 层:Shadcn UI、MagicUI、React Bits 与 Vercel AI SDK 元素,均由注册表(registry)生成,不手工维护。
此外从依赖清单还能看到该前端的"重渲染"特征:streamdown+shiki+katex+rehype-*/remark-*全家桶用于流式 Markdown、代码高亮与公式渲染;@uiw/react-codemirror及多语言包用于制品编辑;@xyflow/react、gsap、motion服务于可视化与动效。这些依赖正是 frontend/src/AGENTS.md 中大量数据流约定(流式 Markdown、制品自动打开、SSE 重放缺口恢复等)的落地基础。
命令体系:一张表看懂前端日常操作
frontend/AGENTS.md 给出的命令表如下(已按 frontend/package.json 的scripts字段逐条核实):
| 命令 | 用途 |
|---|---|
pnpm dev | 启动开发服务器(默认 Webpack) |
pnpm build | 生产构建 |
pnpm check | Lint + 类型检查(提交前必跑) |
pnpm lint | 仅 ESLint |
pnpm lint:fix | ESLint 自动修复 |
pnpm format | Prettier 检查(pnpm format:write写入修复) |
pnpm test | 使用 Rstest 运行单元测试 |
pnpm test:e2e | 使用 Playwright(Chromium)运行 E2E 测试 |
pnpm typecheck | TypeScript 类型检查(tsc --noEmit) |
pnpm start | 启动生产服务器 |
对照package.json,有几处细节值得注意:
pnpm check实际是eslint . --ext .ts,.tsx && tsc --noEmit的串联,一条命令同时把静态检查与类型检查挡在提交之前;pnpm dev并不是直接next dev,而是node scripts/dev.mjs——一个自定义启动脚本,用来控制开发打包器选择(下文详述);- 规范中还有一条未列入表格但同等重要的命令:
pnpm perf:check(node scripts/measure-route-assets.mjs --check),用于执行路线资产预算检查。
开发服务器为何包了一层 dev.mjs
AGENTS.md指出:"Webpack is the default development bundler. UseDEER_FLOW_DEV_BUNDLER=turbowithpnpm devto opt in to Turbopack"。实现位于 frontend/scripts/dev.mjs:
export function getDevBundler(_platform = process.platform, env = process.env) { const override = env.DEER_FLOW_DEV_BUNDLER?.trim(); if (override) { if (override !== "turbo" && override !== "webpack") { throw new Error('DEER_FLOW_DEV_BUNDLER must be either "turbo" or "webpack"'); } return override; } // Keep Webpack as the cross-platform default while #5132's Turbopack // PostCSS worker leak remains unfixed in a stable Next.js release. ... return "webpack"; }从源码可以确认两点:一是DEER_FLOW_DEV_BUNDLER只接受turbo或webpack两个取值,非法值直接抛错,避免拼写错误静默失效;二是默认走 Webpack 的原因被明确写在注释里——上游 Next.js 的 Turbopack PostCSS worker 内存泄漏问题在稳定版修复前,跨平台默认值保守地选择 Webpack,而保留platform参数使未来恢复平台感知默认值只需小改动。这是一种"带逃生舱的默认值"设计:出问题时可一条环境变量切换到 Turbopack 定位是否是打包器自身的问题。
单元测试架构:Rstest 双项目拆分 node 与 DOM
AGENTS.md对测试布局的约定是:单元测试位于tests/unit/,且目录结构镜像src/(例如tests/unit/core/api/stream-mode.test.ts对应src/core/api/stream-mode.ts),通过@/路径别名导入源模块。
真正有信息量的是它对环境拆分的解释,原文要点是:Rstest 将测试拆为两个项目运行——*.test.ts(x)在纯node环境(占套件绝大部分),*.dom.test.ts(x)在happy-dom环境(供renderHook驱动的 hook 测试与组件测试使用);"DOM 环境耗时约为 node 套件的 3 倍,所以不渲染的测试不应进入 DOM 环境";且"行为只存在于真实 React 中的 hook(effect 顺序、卸载清理、store 变更重渲染)应放在.dom.test.*文件里,而不是在 node 测试中 mockreact"。
这份解释与 frontend/rstest.config.ts 的实现对得上号:
const shared = { plugins: [pluginReact()], resolve: { alias: { "@": resolve(__dirname, "src") } }, output: { // Streamdown imports KaTeX CSS as a side effect. Bundle these packages so // Rsbuild processes that CSS import instead of Node trying to load it. bundleDependencies: ["streamdown", "katex"], }, }; export default defineConfig({ projects: [ { ...shared, name: "node", include: ["tests/unit/**/*.test.ts", "tests/unit/**/*.test.tsx"], // A DOM environment costs roughly 3x the runtime of this suite, ... exclude: { patterns: ["**/*.dom.test.*"], override: false }, }, { ...shared, name: "dom", testEnvironment: "happy-dom", include: ["tests/unit/**/*.dom.test.ts", "tests/unit/**/*.dom.test.tsx"], }, ], });三个实现细节直接印证了文档约定:
- 命名即环境:node 项目显式排除
**/*.dom.test.*,dom 项目只 include*.dom.test.*,两者互斥且完全由文件后缀决定,开发者"按后缀选环境",零配置; @/别名在两个项目中一致(都指向src),保证测试导入与生产代码解析同一份路径;bundleDependencies: ["streamdown", "katex"]解决了一个真实痛点——Streamdown 会以副作用方式导入 KaTeX CSS,若不在 Rsbuild 侧打包,Node 环境会试图直接加载 CSS 文件而崩溃。
E2E 测试:Playwright 全量 mock 后端 + 真实页面交互
AGENTS.md描述 E2E 策略为:测试位于tests/e2e/,使用 Playwright Chromium;所有后端 API 通过page.route()网络拦截进行 mock,测试的是真实页面交互(导航、聊天输入、流式响应);配置见playwright.config.ts。
frontend/playwright.config.ts 给出了该策略的完整工程化细节:
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3000"; const skipWebServer = process.env.PLAYWRIGHT_SKIP_WEB_SERVER === "1"; export default defineConfig({ testDir: "./tests/e2e", fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: process.env.CI ? "github" : "html", timeout: 30_000, use: { baseURL, locale: "en-US", trace: "on-first-retry" }, projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], webServer: skipWebServer ? undefined : { command: "pnpm exec next build && pnpm exec next start", url: baseURL, reuseExistingServer: !process.env.CI, timeout: 120_000, env: { SKIP_ENV_VALIDATION: "1", DEER_FLOW_AUTH_DISABLED: "1" }, }, });值得点出的设计:
- E2E 跑的是生产构建而非 dev server:
webServer.command是next build && next start,本地可复用已存在的服务器(reuseExistingServer: !CI),CI 则冷启动、单 worker、失败重试 2 次,并强制forbidOnly防止test.only被误提交——这是典型的"本地快、CI 稳"双模配置; - 用环境变量关掉前端自身的防护:注入
SKIP_ENV_VALIDATION=1(跳过 frontend/src/env.js 的环境变量校验,见下文)与DEER_FLOW_AUTH_DISABLED=1(关闭认证),让 mock 后端的页面可以直接进入受保护路由; PLAYWRIGHT_SKIP_WEB_SERVER=1提供了与已存在服务器协作的逃生舱,trace: "on-first-retry"只在重试时留痕,控制产物体积。
由于后端被page.route()全量拦截,E2E 可以在完全没有 Gateway/LangGraph 后端的情况下验证"导航、输入、流式响应"这条用户路径,这正是AGENTS.md所称 "test real page interactions" 的含义。
架构与源码布局
AGENTS.md给出的端到端链路是:
Frontend (Next.js) ──▶ LangGraph SDK ──▶ LangGraph Backend (lead_agent) ├── Sub-Agents └── Tools & Skills产品形态上,前端是一个有状态聊天应用:用户创建 thread(对话)、发送消息、设置 thread 级/goal完成条件,接收流式 AI 响应;后端编排的 agent 可以产出artifacts(文件/代码)、todos与 goal 状态更新。
src/目录布局约定(并已在仓库目录结构中核实):
app/— Next.js App Router。路由包括/(落地页)、/showcase/[thread_id](白名单内的公开只读演示)、/workspace/chats/[thread_id](认证后的聊天)、/workspace/agents/[agent_name]与/workspace/agents/new(自定义 Agent)、/artifacts/view(无浏览器 chrome 的窗口,用面板自己的渲染器渲染单个 Markdown 制品)、/blog/…、(auth)/{login,setup,auth/callback}认证流、/[lang]/docs/…(多语言文档)、以及/api/…路由处理器(如/api/memory);components/— React 组件,其中ui/与ai-elements/是注册表自动生成的(ESLint 忽略,禁止手改),workspace/是聊天页组件(消息、制品、设置),landing/、docs/分别对应落地页与 MDX 渲染;core/— 业务逻辑核心("the heart of the app")。frontend/src/core 目录实测包含threads/(创建、流式、状态)、api/(LangGraph 客户端单例)、agents/、subagents/、auth/、artifacts/(制品)、channels/(IM 连接)、integrations/(Lark CLI 等第三方集成)、i18n/(en-US、zh-CN)、settings/、memory/、skills/、messages/、mcp/、models/、input-polish/(发送前草稿重写)、voice-input/(浏览器语音识别)、suggestions/、tasks/、todos/、tools/、workspace-changes/(运行级变更文件摘要与 diff 拉取)、config/、notification/、blog/,以及渲染辅助streamdown/与utils/;hooks/— 共享 React hooks;lib/— 工具函数(clsx + tailwind-merge 组合出的cn());content/— 被应用渲染的 MDX 内容(博客、文档);styles/— 使用 Tailwind v4@import语法与主题 CSS 变量的全局 CSS;typings/— 环境类型声明;- 根文件:
env.js(环境变量校验)、mdx-components.ts(MDX 组件映射)。
这个布局体现了清晰的"表现层 / 领域层"分界:app/与components/只负责路由与渲染,一切与 LangGraph、认证、制品相关的状态与副作用都收敛在core/下,而 frontend/src/AGENTS.md 则以"数据流 → 关键模式 → 交互所有权"的结构把每个关键文件的职责钉死(例如"LangGraph client 是core/api/中通过getAPIClient()获得的单例"、"thread 路由必须经core/threads/utils.ts::pathOfThread()构造以正确 percent-encode 自定义 Agent 名与 thread ID"),供智能体与人类共同遵循。
代码风格约定
frontend/AGENTS.md 的四条硬性风格规则:
- 导入顺序强制:builtin → external → internal → parent → sibling,组内字母序,组间空行;类型导入使用内联形式
import { type Foo }; - 未使用变量:以下划线
_前缀显式声明弃用意图; - 类名组合:条件 Tailwind 类一律通过
@/lib/utils的cn()组合,而不是字符串拼接; - 路径别名:
@/*映射到src/*;ui/与ai-elements/来自注册表生成,不要手动编辑。
这些规则与测试侧的@/别名配置(见 frontend/rstest.config.ts)保持一致,意味着测试、构建、Lint 三处对模块解析的理解完全同构。
环境配置:可选的后端 URL、Next 重写与开发源放行
后端地址是可选的,默认走 nginx 代理
AGENTS.md给出的两个环境变量均为可选:
NEXT_PUBLIC_BACKEND_BASE_URL=http://localhost:8001 NEXT_PUBLIC_LANGGRAPH_BASE_URL=http://localhost:8001/api原文要求:标准make dev/ Docker 流程下保持它们不设置,因为 nginx 会对外提供/api/langgraph/*前缀并重写到 Gateway 原生的/api/*路由。
不设置时由谁兜底?答案在 frontend/next.config.js 的rewrites()中——Next.js 自身也实现了一套同构代理:
if (!process.env.NEXT_PUBLIC_LANGGRAPH_BASE_URL) { rewrites.push({ source: "/api/langgraph", destination: `${gatewayURL}/api` }); rewrites.push({ source: "/api/langgraph/:path*", destination: `${gatewayURL}/api/:path*` }); } if (!process.env.NEXT_PUBLIC_BACKEND_BASE_URL) { // /api/agents、/api/skills 逐条重写 ... rewrites.push({ source: "/api/:path*", destination: `${gatewayURL}/api/:path*` }); // 兜底 }从这段源码结构看,有两层设计意图:其一,只有当对应NEXT_PUBLIC_*变量未设置时才注入重写,显式配置永远优先于内置代理;其二,/api/langgraph的重写必须先于/api/:path*兜底规则,注释中特意强调 "this must come AFTER the /api/langgraph rewrite ... so that LangGraph-compatible routes keep their public prefix while Gateway receives its native /api/* paths"——即客户端继续说 LangGraph 方言前缀,Gateway 收到的是原生路径。gatewayURL可经DEER_FLOW_INTERNAL_GATEWAY_BASE_URL覆盖,默认http://127.0.0.1:8001。
环境变量校验与 SKIP_ENV_VALIDATION
frontend/src/env.js 使用@t3-oss/env-nextjs+ Zod 定义校验模式:服务端可选变量GITHUB_OAUTH_TOKEN、NODE_ENV(枚举development/test/production,默认 development);客户端仅暴露带NEXT_PUBLIC_前缀的NEXT_PUBLIC_BACKEND_BASE_URL、NEXT_PUBLIC_LANGGRAPH_BASE_URL、NEXT_PUBLIC_STATIC_WEBSITE_ONLY。两个细节值得注意:skipValidation: !!process.env.SKIP_ENV_VALIDATION提供了跳过开关(Docker 构建场景常用);emptyStringAsUndefined: true把空字符串视为未设置,避免z.string()被空串击穿。这也正是 PlaywrightwebServer注入SKIP_ENV_VALIDATION=1的原因(见上文)。
DEER_FLOW_DEV_ALLOWED_ORIGINS:LAN/代理场景下的开发源放行
AGENTS.md描述了一个具体故障模式:当开发服务器要在 localhost 之外的地址(LAN 地址或代理主机名)访问时,必须把该 host 列入DEER_FLOW_DEV_ALLOWED_ORIGINS(逗号分隔;完整 URL 会被归约为主机名)。它喂给 Next 的allowedDevOrigins,管控/_next/*、字体与 HMR 请求——否则这些请求返回 403,"页面能服务端渲染但永不水合(hydrate),登录表单在内的一切都没有响应"。仅限开发,生产构建会忽略该配置。
解析逻辑在 frontend/src/dev-origins.js,并被 frontend/next.config.js 以allowedDevOrigins: getAllowedDevOrigins()接入。normalizeHost()的防御性细节很能说明问题:
- 剥掉 scheme(
https://)、path、query、hash,因为Next 只按 host 匹配,"仍带 scheme/port/path 的条目匹配不到任何东西",调用者将原样遭遇本想修复的 403; - 兼容方括号 IPv6(
[::1]:3000归约为::1); - 只有恰好一个冒号时才视为
host:port并剥掉端口——裸 IPv6 字面量有多个冒号且无端口可剥,避免了误伤。
函数入口注释直接引用了上文故障模式("a dev stack opened on a LAN address or a proxied hostname serves the SSR HTML but never hydrates"),文档与实现互为镜像。
性能预算:pnpm perf:check 与 performance-budgets.json
AGENTS.md的 "Contributing" 部分定义了路线资产(route asset)预算机制,这是容易被忽略但极具实战价值的部分:
pnpm perf:check从一次普通生产构建测量/login,再以 static-demo 模式构建 fixture 驱动的 workspace 路由;- 在临时本地端口启动生产服务器,测量代表路由所引用的去重后JavaScript 与 CSS 文件总量;
- 详细结果写入
.next/performance-results.json,总量与performance-budgets.json对比; - 超预算时"修复路线所有权或拆分点",禁止在未记录并评审实测回归的情况下抬高上限。
预算数值见 frontend/performance-budgets.json(单位字节):
| 路由 | CSS 上限 | JS 上限 |
|---|---|---|
/login | 170,000 | 850,000 |
/ | 175,000 | 1,050,000 |
/workspace/chats | 190,000 | 1,750,000 |
/workspace/chats/<thread_id> | 190,000 | 4,100,000 |
/en/docs | 270,000 | 4,200,000 |
/blog/posts | 270,000 | 4,200,000 |
这张表本身就是架构决策的化石记录:/login最轻(登录页不携带聊天负载),带真实 thread 的聊天页 JS 预算(4.1 MB)显著高于空的/workspace/chats(1.75 MB),因为前者会装配流式渲染、CodeMirror、子代理面板等聊天专属依赖;而/en/docs与/blog/posts同为重内容路由,共享 4.2 MB 档位。frontend/src/AGENTS.md 中"保持/静态、把富内容 CSS 留在渲染它的路由上"(Static root boundary)的约定,正是维持这套预算可行的前提。
贡献流程小结
AGENTS.md的 Contributing 章节规定了新增功能的五步闭环:
- 遵循既有
src/结构; - 补充 TypeScript 类型与恰当的错误处理;
- 在
tests/unit/(pnpm test)写单元测试、在tests/e2e/(pnpm test:e2e)写 E2E 测试; - 提交前运行
pnpm check; - 当架构、命令或约定变化时,同步更新
AGENTS.md本身——这一步把"文档随代码演进"从口头约定变成了检查项,也是CLAUDE.md这种导入式 shim 模式能长期成立的关键保障。
小结
frontend/CLAUDE.md 用一行@AGENTS.md演示了一个可复制的开源协作模式:入口文档只负责"导入",实质规范收敛到一份跨智能体共享的 frontend/AGENTS.md,再向 frontend/src/AGENTS.md 下钻源码级细则。围绕这份规范,DeerFlow 前端形成了完整可验证的工程闭环——技术栈由 frontend/package.json 的packageManager钉版、pnpm check串联 ESLint 与 tsc、frontend/rstest.config.ts 以文件后缀划分 node/DOM 双测试环境、frontend/playwright.config.ts 以生产构建 + 全量网络拦截支撑 E2E、frontend/next.config.js 的环境变量感知重写消除硬编码后端地址、frontend/src/dev-origins.js 修复 LAN 开发 403、frontend/performance-budgets.json 给每条路由的 JS/CSS 总量设了量化红线。对开发者与编码智能体而言,这套文档即契约:照着它组织代码、写测试、跑检查,就能与仓库现状保持一致。
【免费下载链接】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),仅供参考