backend/docs/HARNESS_APP_SPLIT.md
状态:Draft 作者:DeerFlow Team 日期:2026-03-13
DeerFlow 后端当前是一个单一 Python 包(src.*),包含了从底层 agent 编排到上层用户产品的所有代码。随着项目发展,这种结构带来了几个问题:
src/ 下,边界不清晰本文档提出将后端拆分为两部分:deerflow-harness(可发布的 agent 框架包)和 app(不打包的用户产品代码)。
Harness 是 agent 的构建与编排框架,回答 "如何构建和运行 agent" 的问题:
Harness 是一个可发布的 Python 包(deerflow-harness),可以独立安装和使用。
Harness 的设计原则:对上层应用完全无感知。它不知道也不关心谁在调用它——可以是 Web App、CLI、Slack Bot、或者一个单元测试。
App 是面向用户的产品代码,回答 "如何将 agent 呈现给用户" 的问题:
App 不打包、不发布,它是 DeerFlow 项目内部的应用代码,直接运行。
App 依赖 Harness,但 Harness 不依赖 App。
| 模块 | 归属 | 说明 |
|---|---|---|
config/ | Harness | 配置系统是基础设施 |
reflection/ | Harness | 动态模块加载工具 |
utils/ | Harness | 通用工具函数 |
agents/ | Harness | Agent 工厂、middleware、state、memory |
subagents/ | Harness | 子 agent 委派系统 |
sandbox/ | Harness | 沙箱执行环境 |
tools/ | Harness | 工具注册与发现 |
mcp/ | Harness | MCP 协议集成 |
skills/ | Harness | 技能加载、解析、定义 schema |
models/ | Harness | LLM 模型工厂 |
community/ | Harness | 社区工具(tavily、jina 等) |
client.py | Harness | 嵌入式 Python 客户端 |
gateway/ | App | FastAPI REST API |
channels/ | App | IM 平台集成 |
关于 Custom Agents:agent 定义格式(config.yaml + SOUL.md schema)由 Harness 层的 config/agents_config.py 定义,但文件的存储、CRUD、发现机制由 App 层的 gateway/routers/agents.py 负责。
backend/
├── packages/
│ └── harness/
│ ├── pyproject.toml # deerflow-harness 包定义
│ └── deerflow/ # Python 包根(import 前缀: deerflow.*)
│ ├── __init__.py
│ ├── config/
│ ├── reflection/
│ ├── utils/
│ ├── agents/
│ │ ├── lead_agent/
│ │ ├── middlewares/
│ │ ├── memory/
│ │ ├── checkpointer/
│ │ └── thread_state.py
│ ├── subagents/
│ ├── sandbox/
│ ├── tools/
│ ├── mcp/
│ ├── skills/
│ ├── models/
│ ├── community/
│ └── client.py
├── app/ # 不打包(import 前缀: app.*)
│ ├── __init__.py
│ ├── gateway/
│ │ ├── __init__.py
│ │ ├── app.py
│ │ ├── config.py
│ │ ├── path_utils.py
│ │ └── routers/
│ └── channels/
│ ├── __init__.py
│ ├── base.py
│ ├── manager.py
│ ├── service.py
│ ├── store.py
│ ├── message_bus.py
│ ├── feishu.py
│ ├── slack.py
│ └── telegram.py
├── pyproject.toml # uv workspace root
├── langgraph.json
├── tests/
├── docs/
└── Makefile
两个层使用不同的 import 前缀,职责边界一目了然:
# ---------------------------------------------------------------
# Harness 内部互相引用(deerflow.* 前缀)
# ---------------------------------------------------------------
from deerflow.agents import make_lead_agent
from deerflow.models import create_chat_model
from deerflow.config import get_app_config
from deerflow.tools import get_available_tools
# ---------------------------------------------------------------
# App 内部互相引用(app.* 前缀)
# ---------------------------------------------------------------
from app.gateway.app import app
from app.gateway.routers.uploads import upload_files
from app.channels.service import start_channel_service
# ---------------------------------------------------------------
# App 调用 Harness(单向依赖,Harness 永远不 import app)
# ---------------------------------------------------------------
from deerflow.agents import make_lead_agent
from deerflow.models import create_chat_model
from deerflow.skills import load_skills
from deerflow.config.extensions_config import get_extensions_config
App 调用 Harness 示例 — Gateway 中启动 agent:
# app/gateway/routers/chat.py
from deerflow.agents.lead_agent.agent import make_lead_agent
from deerflow.models import create_chat_model
from deerflow.config import get_app_config
async def create_chat_session(thread_id: str, model_name: str):
config = get_app_config()
model = create_chat_model(name=model_name)
agent = make_lead_agent(config=...)
# ... 使用 agent 处理用户消息
App 调用 Harness 示例 — Channel 中查询 skills:
# app/channels/manager.py
from deerflow.skills import load_skills
from deerflow.agents.memory.updater import get_memory_data
def handle_status_command():
skills = load_skills(enabled_only=True)
memory = get_memory_data()
return f"Skills: {len(skills)}, Memory facts: {len(memory.get('facts', []))}"
禁止方向:Harness 代码中绝不能出现 from app. 或 import app.。
| 方面 | 打包(放 packages/ 下) | 不打包(放 backend/app/) |
|---|---|---|
| 命名空间 | 需要 pkgutil extend_path 合并,或独立前缀 | 天然独立,app.* vs deerflow.* |
| 发布需求 | 没有——App 是项目内部代码 | 不需要 pyproject.toml |
| 复杂度 | 需要管理两个包的构建、版本、依赖声明 | 直接运行,零额外配置 |
| 运行方式 | pip install deerflow-app | PYTHONPATH=. uvicorn app.gateway.app:app |
App 的唯一消费者是 DeerFlow 项目自身,没有独立发布的需求。放在 backend/app/ 下作为普通 Python 包,通过 PYTHONPATH 或 editable install 让 Python 找到即可。
┌─────────────────────────────────────┐
│ app/ (不打包,直接运行) │
│ ├── fastapi, uvicorn │
│ ├── slack-sdk, lark-oapi, ... │
│ └── import deerflow.* │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ deerflow-harness (可发布的包) │
│ ├── langgraph, langchain │
│ ├── markitdown, pydantic, ... │
│ └── 零 app 依赖 │
└─────────────────────────────────────┘
依赖分类:
| 分类 | 依赖包 |
|---|---|
| Harness only | agent-sandbox, langchain*, langgraph*, markdownify, markitdown, pydantic, pyyaml, readabilipy, tavily-python, firecrawl-py, tiktoken, ddgs, duckdb, httpx, kubernetes, dotenv |
| App only | fastapi, uvicorn, sse-starlette, python-multipart, lark-oapi, slack-sdk, python-telegram-bot, markdown-to-mrkdwn |
| Shared | langgraph-sdk(channels 用 HTTP client), pydantic, httpx |
backend/pyproject.toml(workspace root):
[project]
name = "deer-flow"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["deerflow-harness"]
[dependency-groups]
dev = ["pytest>=8.0.0", "ruff>=0.14.11"]
# App 的额外依赖(fastapi 等)也声明在 workspace root,因为 app 不打包
app = ["fastapi", "uvicorn", "sse-starlette", "python-multipart"]
channels = ["lark-oapi", "slack-sdk", "python-telegram-bot"]
[tool.uv.workspace]
members = ["packages/harness"]
[tool.uv.sources]
deerflow-harness = { workspace = true }
在拆分之前,需要先解决 client.py 中两处从 harness 到 app 的反向依赖:
_validate_skill_frontmatter# client.py — harness 导入了 app 层代码
from src.gateway.routers.skills import _validate_skill_frontmatter
解决方案:将该函数提取到 deerflow/skills/validation.py。这是一个纯逻辑函数(解析 YAML frontmatter、校验字段),与 FastAPI 无关。
CONVERTIBLE_EXTENSIONS + convert_file_to_markdown# client.py — harness 导入了 app 层代码
from src.gateway.routers.uploads import CONVERTIBLE_EXTENSIONS, convert_file_to_markdown
解决方案:将它们提取到 deerflow/utils/file_conversion.py。仅依赖 markitdown + pathlib,是通用工具函数。
LangGraph Server 只需要 harness 包。langgraph.json 更新:
{
"dependencies": ["./packages/harness"],
"graphs": {
"lead_agent": "deerflow.agents:make_lead_agent"
},
"checkpointer": {
"path": "./packages/harness/deerflow/agents/checkpointer/async_provider.py:make_checkpointer"
}
}
# serve.sh / Makefile
# PYTHONPATH 包含 backend/ 根目录,使 app.* 和 deerflow.* 都能被找到
PYTHONPATH=. uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001
无需变更(只做 URL 路由,不涉及 Python 模块路径)。
Dockerfile 中的 module 引用从 src. 改为 deerflow. / app.,COPY 命令需覆盖 packages/ 和 app/ 目录。
分 3 个 PR 递进执行:
src/skills/validation.py,从 gateway/routers/skills.py 提取 _validate_skill_frontmattersrc/utils/file_conversion.py,从 gateway/routers/uploads.py 提取文件转换逻辑client.py、gateway/routers/skills.py、gateway/routers/uploads.py 的 importpackages/harness/ 目录,创建 pyproject.tomlgit mv 将 harness 相关模块从 src/ 移入 packages/harness/deerflow/git mv 将 app 相关模块从 src/ 移入 app/src.* → deerflow.*(所有 .py 文件、langgraph.json、测试、文档)src.gateway.* → app.gateway.*、src.channels.* → app.channels.*pyproject.tomllanggraph.json、Makefile、Dockerfileuv sync + 全部测试 + 手动验证服务启动CLAUDE.md、README.md| 风险 | 影响 | 缓解措施 |
|---|---|---|
| 全局 rename 误伤 | 字符串中的 src 被错误替换 | 正则精确匹配 \bsrc\.,review diff |
| LangGraph Server 找不到模块 | 服务启动失败 | langgraph.json 的 dependencies 指向正确的 harness 包路径 |
App 的 PYTHONPATH 缺失 | Gateway/Channel 启动 import 报错 | Makefile/Docker 统一设置 PYTHONPATH=. |
config.yaml 中的 use 字段引用旧路径 | 运行时模块解析失败 | config.yaml 中的 use 字段同步更新为 deerflow.* |
测试中 sys.path 混乱 | 测试失败 | 用 editable install(uv sync)确保 deerflow 可导入,conftest.py 中添加 app/ 到 sys.path |
pip install deerflow-harnessdeerflow-sandbox、deerflow-mcp)