Skip to content

Harness 与 App 分层边界

本章目标:

  • 讲清 DeerFlow 后端为何拆成 deerflow.*(可发布 harness)与 app.*(不发布应用层)双层,以及二者的目录归属。
  • 理解依赖单向规则:app → deerflow 允许,deerflow → app 禁止,以及 app/__init__.pydeerflow/__init__.py 等 import 约定。
  • 掌握 tests/test_harness_boundary.py 如何用 AST 扫描在 CI 中强制这条边界,违规会怎样导致 CI 失败。

TL;DR

DeerFlow 后端被切成两层:packages/harness/deerflow/(可独立打包发布的 agent 框架 deerflow-harness)与 app/(不发布的 FastAPI Gateway + IM 通道应用层)。依赖方向严格单向——应用层可以 from deerflow... 引用框架,但框架代码绝不允许 from app...。这条边界不靠人工 review,而是由 tests/test_harness_boundary.py 用 Python ast 模块静态扫描 harness 包内每个 .py 文件强制执行,在 backend-unit-tests.ymlmake test 中随每个 PR 自动运行;一旦 harness 出现对 app.* 的 import,该测试断言失败,CI 红灯阻止合并。这样做是为了让 harness 保持可发布、可独立复用,与具体的 Web 应用形态解耦。

Overview

为什么要费力维护一条"框架不能反向依赖应用"的边界?核心动机是可发布性解耦

deerflow-harness 是一个有独立 pyproject.toml、独立 name = "deerflow-harness"、独立依赖清单的 Python 包 backend/packages/harness/pyproject.toml:1-39,通过 hatchling 只把 deerflow 包打进 wheel backend/packages/harness/pyproject.toml:51-56。它包含构建和运行 agent 所需的一切:agent 编排、工具、沙箱、模型、MCP、skills、配置。它本身不依赖 FastAPI,也不知道"Gateway"或"IM 通道"的存在。

如果让 harness 反向 import app.*(例如某个 agent 工具偷偷 from app.gateway.routers.uploads import ...),会发生三件坏事:

  1. 打包污染:app/ 不在 wheel 内,发布出去的 deerflow-harness 会因找不到 app 模块在导入时崩溃。
  2. 依赖循环:app 依赖 deerflow(deer-flowdependencies 第一项就是 deerflow-harness backend/pyproject.toml:7-8),若 deerflow 又依赖 app,就形成环,无法把 harness 当作纯框架复用。
  3. 解耦失效:嵌入式 DeerFlowClient 走的是同一套 deerflow 模块、不带 FastAPI;一旦 harness 牵连 app,就再也无法在无 HTTP 服务的场景下嵌入使用。

backend/docs/rfc-extract-shared-modules.md 的设计原则一节把这条规则写成显式约束:共享业务逻辑必须下沉到 harness 层(纯逻辑、无 FastAPI 依赖),Gateway 做 HTTP 适配、Client 做本地适配,"满足 test_harness_boundary.py 约束:harness 永不 import app" backend/docs/rfc-extract-shared-modules.md:42-46。该 RFC 在"备选方案"中明确否决了"把所有逻辑放进 client、让 Gateway import 它"的做法,理由正是会破坏 harness/app 边界 backend/docs/rfc-extract-shared-modules.md:170-175

Architecture

两层在仓库中的物理位置与包归属:

物理目录import 前缀包名 / 是否发布Source
Harness(框架层)backend/packages/harness/deerflow/deerflow.*deerflow-harness,通过 hatchling 打包发布backend/packages/harness/pyproject.toml:1-56
App(应用层)backend/app/app.*属于 deer-flow 包,不单独发布backend/pyproject.toml:1-24
边界测试backend/tests/test_harness_boundary.py仅在 CI / make test 运行backend/tests/test_harness_boundary.py:37-46

Harness 层(deerflow.*)目录包含:agents/(LeadAgent、中间件、memory、ThreadState)、sandbox/subagents/tools/mcp/models/skills/config/community/reflection/runtime/persistence/uploads/utils/client.py。App 层(app.*)仅包含两块:app/gateway/(FastAPI Gateway API 与路由)、app/channels/(Feishu / Slack / Telegram / DingTalk IM 集成)。

uv workspace 把 harness 声明为子成员包,源解析为本地 workspace 包(开发期可编辑安装),而 deer-flowdeerflow-harness 列为首要依赖:

langgraph.json 把图入口指向 harness 的 deerflow.agents:make_lead_agent,而 auth 路径指向 app 层的 ./app/gateway/langgraph_auth.py:auth——这正体现"app 引用 harness 入口,harness 不知道 app"的方向。

Components / Subsystems

围绕这条边界,有四个协作部件:

  • harness 包定义(packages/harness/pyproject.toml):声明 name = "deerflow-harness"requires-python = ">=3.12"、完整的 LangGraph / LangChain / 模型 / 沙箱依赖,以及 ollama / postgres / pymupdf 可选依赖组 backend/packages/harness/pyproject.toml:1-49[tool.hatch.build.targets.wheel] packages = ["deerflow"] 决定 wheel 内只有 deerflow 顶层包 backend/packages/harness/pyproject.toml:55-56

  • 包入口 __init__.py:backend/packages/harness/deerflow/__init__.pybackend/app/__init__.py 均为空文件(0 字节),仅起到声明 Python 包的作用,不做任何 re-export,因此 import 约定靠子模块全路径(from deerflow.agents import ...from app.gateway.app import app)而非顶层快捷别名。

  • app 层消费者:app.gateway.*app.channels.* 大量通过全路径 import 引用 harness,例如 from deerflow.config.app_config import get_app_configfrom deerflow.runtime.user_context import set_current_userfrom deerflow.config.paths import get_paths backend/app/gateway/services.py:22-23backend/app/channels/feishu.py:15-17。这些都是被允许的方向。

  • 边界守卫测试:tests/test_harness_boundary.py 是这条规则的唯一强制点。它不依赖运行时,纯静态分析:遍历 harness 包所有 .py,用 ast 解析每个 import,只要发现模块名等于 app 或以 app. 开头即记为违规 backend/tests/test_harness_boundary.py:37-46

Data Flow

下面是 test_harness_boundary.py 在一次 CI 运行中扫描 AST、检测违规的完整流程:

CI 触发链:.github/workflows/backend-unit-tests.ymlpushmain 与每个非草稿 PR 时运行 .github/workflows/backend-unit-tests.yml:3-7,在 backend 工作目录执行 make test .github/workflows/backend-unit-tests.yml:38-40,而 make testPYTHONPATH=. uv run pytest tests/ -v(见 backend/Makefile),边界测试随整个 tests/ 套件一并被收集执行。

Implementation Details

整个防火墙的核心不到 20 行——ast 收集 import 与断言部分:

python
def _collect_imports(filepath: Path) -> list[tuple[int, str]]:
    source = filepath.read_text(encoding="utf-8")
    try:
        tree = ast.parse(source, filename=str(filepath))
    except SyntaxError:
        return []
    results: list[tuple[int, str]] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                results.append((node.lineno, alias.name))
        elif isinstance(node, ast.ImportFrom):
            if node.module:
                results.append((node.lineno, node.module))
    return results

backend/tests/test_harness_boundary.py:18-34

解读关键点:

一个真实细节:该检测只匹配模块路径前缀,因此 harness 代码里出现注释或字符串中的 "app config"(如 aio_sandbox_provider.pyfile_conversion.py 里的 docstring)不会被误判——它只解析 AST 的 import 节点,不做文本扫描。

Common Pitfalls / Tips

  • 违规直接让 CI 失败、阻止合并:test_harness_boundary.pymake test 在每个 PR 跑;任意 harness 文件新增 from app.../import app... 都会让 test_harness_does_not_import_app 断言失败,CI 红灯。修复方式是把需要共享的逻辑下沉为 harness 内的纯模块(参考 RFC 的 deerflow.skills.installer / deerflow.uploads.manager 模式 backend/docs/rfc-extract-shared-modules.md:59-116),让 Gateway/Client 各自做适配,而不是让 harness 反向依赖。
  • 顶层 __init__.py 是空的:不要指望 from deerflow import xxxfrom app import xxx 这类顶层别名——harness 与 app 的 __init__.py 均为 0 字节,必须走完整子模块路径 import。
  • 方向是单向不是双向:app → deerflow 是常态且被允许(app/gatewayapp/channels 大量这样做),只有 deerflow → app 被禁止。把共享代码"为了省事"放进 app 再让 harness 引用,是该 RFC 明确否决的反模式 backend/docs/rfc-extract-shared-modules.md:170-175
  • 测试套件的循环导入对策:tests/conftest.py 通过在任何测试触发前向 sys.modules 注入 deerflow.subagents.executor 的 mock 来打破生产代码里 deerflow.subagents.__init__ → executor → agents.thread_state → ... → executor 的循环 backend/tests/conftest.py:26-45;这与边界测试本身无关(边界测试是纯静态、不真正导入 harness),但说明 harness 内部模块图较密,新增跨子系统 import 时要留意循环风险。
  • harness 自身依赖要写进 harness 的 pyproject:harness 新增第三方依赖应加到 packages/harness/pyproject.tomldependencies,而不是 backend/pyproject.toml——否则发布出去的 deerflow-harness wheel 会缺依赖 backend/packages/harness/pyproject.toml:6-39

References

页面关系
01-项目概览.md上游背景:DeerFlow 整体定位与可发布 harness 的目标,本章是其分层规则的细化
03-系统整体架构.md平行视角:系统整体架构(Gateway / Frontend / Runtime),本章聚焦其中后端两层的边界约束
13-Gateway-API与路由体系.md下游消费者:app.gateway 是"app → deerflow"方向的典型应用层,展示边界的实际遵守方式
36-测试策略与质量保障.md延伸:本章的 test_harness_boundary.py 导入防火墙在 36 章作为 CI 强制项详述

公司内部参考 · 由 claude-wiki-gen 基于源码自动生成的二次分析