Dify API 后端开发规范深度解析:从 api/AGENTS.md 看分层架构、命令体系与工程边界
api/AGENTS.md 是 Dify 后端(api 目录)的官方开发指南,它用短短一页篇幅定义了后端改动的四条主线:标准化的命令入口(make lint / make type-check / make test)、严格的模块分层边界(controller → service → core → libs)、统一的配置/存储/出站 HTTP 访问点,以及 Celery 异步任务的幂等纪律。读完后你将能在不破坏现有契约的前提下安全地修改 Dify 后端代码,并理解每个约定背后的源码级实现。
一、文档定位:改后端代码前的“本地契约”
api/AGENTS.md 的开篇就定下了基调:修改后端行为之前,先阅读所在模块、类、函数的 docstring 和“非显而易见的注释”。这些注释被定义为本地契约(local contracts)——只在它们所负责的行为发生变化时才更新,并要与当前代码保持一致。
这不是套话,而是与 Dify 的实际工具链配套的约定。后端仓库用 import-linter 把分层规则写成了机器可执行的契约(见下文 api/.importlinter),用自定义 lint 脚本核对响应文档与序列化器的一致性(见 lint_response_contracts.py)。docstring 是人工维护的契约,工具链是机器维护的契约,二者共同构成修改后端代码前的必读上下文。
二、命令体系:所有检查都从仓库根目录发起
文档给出的四个命令入口全部要求从仓库根目录执行:
make lint # 格式化 + lint
make type-check # 类型检查
make test # 单元测试
make test TARGET_TESTS=./api/tests/<path> # 定向测试
再看 Makefile 中这些目标的真实构成,能发现每个目标背后的完整流水线:
2.1 make lint:五步组合拳
lint 目标(Makefile)依次执行:
ruff format ./api—— 代码格式化;ruff check --fix ./api—— 静态检查并自动修复;api-contract-lint—— 运行 api/dev/lint_response_contracts.py,用 AST 比对控制器中@ns.response(..., Model)声明的响应模型与实际返回的dump_response(Model, ...)/Model.model_dump()是否匹配,防止 Swagger 文档与真实返回结构漂移;lint-imports—— 执行 api/.importlinter 中定义的导入分层契约;dotenv-linter ./api/.env.example ./web/.env.example—— 保证前后端 env 样例文件的变量命名一致性。
其中 import-linter 的分层契约值得单独看:api/.importlinter 声明了 backend-layers 契约,四层依次为 controllers → services → core → libs,且 unmatched_ignore_imports_alerting = error——即历史遗留的违规导入被精确列入 ignore_imports 基线,任何新的越层导入都会直接报错,而基线列表则随着旧依赖被删除而不断收缩。这正是文档中“分层边界”约定的强制落地手段。
2.2 make type-check:双引擎类型检查
type-check 目标先跑 ./dev/pyrefly-check-local,再跑 mypy(排除 tests/、migrations/、conftest 等目录,开启 --check-untyped-defs)。类型检查是提交前的硬门槛之一,配合 api/pyproject.toml 中 [tool.pyrefly] 的 python-version = "3.12.0" 配置,类型检查锚定在后端要求的 Python 版本上。
2.3 make test 与定向测试
无参数的 make test 会分两段跑:第一段用 pytest -n auto(xdist 并行)覆盖 api/tests/unit_tests、各 vdb 与 trace provider 的单测(跳过 controllers 目录);第二段单独带覆盖率跑 api/tests/unit_tests/controllers(Makefile)。
定向测试则是日常开发的主力:
make test TARGET_TESTS=./api/tests/unit_tests/controllers/console/test_workflow.py
指定 TARGET_TESTS 后 Makefile 会跳过默认套件,直接 uv run --project api --dev pytest $(TARGET_TESTS),这保证了在只验证单个模块时不必付出全量测试的时间成本。
2.4 直接运行 Python:一律走 uv run --project api
文档明确要求:直接执行 Python 命令时统一使用 uv run --project api 前缀,不要手动激活环境或裸跑 python。Dify 的依赖管理基于 uv workspace——api/pyproject.toml 声明 name = "dify-api"、requires-python = "~=3.12.0",并把 providers/vdb/*、providers/trace/* 组织为 workspace 成员,通过 vdb-all、trace-all 等依赖组按需安装向量库/trace 插件。uv run --project api 保证命令总是在该 workspace 解析出的正确解释器与依赖闭包中执行。
文档同时划了两条红线:依赖 Docker 的集成测试套件归 CI 所有(对应 make test-all 中 --start-middleware、testcontainers 等 Docker 依赖步骤),不要为日常 Agent 工作启动长时间运行的服务。本地开发只做纯单测,Docker 支撑的重型验证交给 CI。
三、架构与边界:一条模块只有一类职责
api/AGENTS.md 的“Architecture And Boundaries”一节是全文核心,每条边界规则都能在仓库中找到对应实现。
3.1 职责分层:controller 传输、service 编排、core 领域策略
原文要求:传输解析与序列化留在 controller,编排放在 service,领域策略放在 core/ 或其领域属主;libs/ 保持业务无关,复用现有属主,不要为新问题新造抽象。
这套划分与目录结构严格对应,并被 api/.importlinter 的 backend-layers 契约固化为 controllers → services → core → libs 的单向依赖层。以 API 契约这一文档引用的场景为例:API_SCHEMA_GUIDE.md 给出的完整模式就是“controller 做校验与序列化、service 做业务”的样板——
@console_ns.expect(console_ns.models[DraftWorkflowNodeRunPayload.__name__])
def post(self, app_model: App, node_id: str):
payload = DraftWorkflowNodeRunPayload.model_validate(console_ns.payload or {})
result = service.run(..., inputs=payload.inputs, query=payload.query)
return dump_response(WorkflowRunNodeExecutionResponse, result)
controller 方法体里只有三件事:解析并校验 Pydantic 模型、调用 service、用 dump_response 序列化返回,业务逻辑完全不越界。
3.2 API 契约改动前必读:controllers/API_SCHEMA_GUIDE.md
文档点名:在改动 controller schema、生成的 API 契约或 SystemFeatureModel 之前,必须先读 api/controllers/API_SCHEMA_GUIDE.md。该指南确立了后端 API 的完整规范:
- 模型规范:请求体用
Payload后缀、查询参数用Query后缀、响应继承fields.base.ResponseModel并以Response结尾; - Swagger 注册:统一用
controllers.common.schema的register_schema_models/register_response_schema_models/query_params_from_model,禁止为迁移后或新增端点再引入 Flask-RESTX 的fields.*字典、@marshal_with、以及给 GET 查询参数用@ns.expect这类反模式; - 序列化:响应统一走
dump_response(...)(内部model_dump(mode="json")),保证 SQLAlchemy 模型、datetime、Pydantic alias 等序列化行为一致; - 验证方式:改 schema 后运行 tests/unit_tests/controllers/common/test_schema.py 等定向测试,并用 api/dev/generate_swagger_specs.py 生成 OpenAPI JSON 人工核对。
api/AGENTS.md 的这条引用把“规范文档 → 修改流程”的依赖关系写死了:先读契约,再动 schema。
3.3 /system-features:最小化未认证引导白名单,不是配置注册表
原文特别强调:把 /system-features 当作最小化的、未认证的引导(bootstrap)白名单,而不是通用配置注册表。这一契约的实现可对照 api/services/entities/feature_entities.py 中的 SystemFeatureModel:
class SystemFeatureModel(FeatureResponseModel):
"""Non-sensitive bootstrap snapshot exposed before Console or Web authentication."""
deployment_edition: DeploymentEdition
enable_app_deploy: bool = False
sso_enforced_for_signin: bool = False
enable_marketplace: bool = False
enable_email_code_login: bool = False
is_allow_register: bool = False
license: LicenseStatusModel = LicenseStatusModel()
branding: BrandingModel = BrandingModel()
...
从字段构成看,它只包含部署版本、SSO 强制登录、注册开关、授权与品牌这类渲染初始界面和选择认证流程所必需的信息,且 docstring 明确其为“非敏感引导快照”。API_SCHEMA_GUIDE.md 进一步给出了新增字段的五条准入标准:Console 与 Web 双方都有已命名的生产消费方、且必须在认证前拿到、值随部署变化且无法由已有公开契约推导、非敏感且语义稳定、每次根引导下发比消费方自查询更划算——五条同时满足才可新增。后端内部策略(上传限制、集成开关)、租户/工作区等认证后状态、推测性字段一律禁止塞入。修改这个模型前读两份文档,正是 api/AGENTS.md 要求这么做的原因。
3.4 多租户隔离:按完整属主链圈定读写,全层传递 tenant_id
文档要求:租户拥有的读写必须按完整属主链(complete owner chain)圈定范围,并在所有受影响的层传递 tenant_id;在经过 payload 或异步边界之后,从已校验的数据库状态重建可信的内部引用。
这条规则的含义是:绝不能把请求体或异步任务载荷中的 ID 直接当作可信凭据——跨了 payload/异步边界之后,必须回到数据库重新校验属主关系。这与 api/.importlinter 的分层约束相呼应:controller 负责边界处的校验,service 负责按属主链执行查询,core/ 负责承载租户策略本身。
3.5 事务纪律:显式、有界,事务内不做外部 I/O
写事务必须显式且有界;除非存在已文档化的一致性契约,否则不得在打开的事务内执行外部 I/O。
从源码结构看,Dify 大量外部依赖(模型 API 调用、向量库写入、对象存储、SSRF 客户端)都具备网络延迟特征,把这类调用放进数据库事务会长时间持有连接。该条规则等价于要求“先完成外部调用,再开短事务落库”,是后端改动评审中的硬性检查点。
3.6 三个统一访问点:配置、存储、出站 HTTP
文档给出三个“必须走指定入口”的强约束,每个入口在仓库中都有明确的属主实现:
(1)配置:一律读 configs.dify_config。 api/configs/init.py 只有三行——from .app_config import DifyConfig; dify_config = DifyConfig()。全后端通过这一个单例访问 api/configs/app_config.py 解析出的配置,而不是各自 os.getenv。这也是为什么 api/.importlinter 的 root_packages 包含 configs:它是被共享的基础层。
(2)存储:一律经 extensions.ext_storage.storage。 api/extensions/ext_storage.py 中的 Storage 扩展在 Flask 初始化时按 dify_config.STORAGE_TYPE 通过 get_storage_factory 分发到具体后端——S3、OpenDAL(local 亦复用 OpenDAL 的 fs scheme)、Azure Blob、阿里 OSS、Google Storage、腾讯 COS、OCI、华为 OBS、百度 BOS、火山 TOS、Supabase 等十余种。业务代码拿到的永远是同一个 storage 句柄,存储后端可替换而调用方零感知。
(3)出站 HTTP:一律走 core.helper.ssrf_proxy。 api/core/helper/ssrf_proxy.py 的模块 docstring 明确其定位——这是“SSRF 防护的通用出站 HTTP 客户端”,用于 HTTP Request 节点、provider/API 集成、认证发现、自定义工具调用等常规外部请求;而远程文件下载/探测则要求改用 core.file.remote_fetcher,以便 Dify 签名的文件 URL 先经 DB + 存储解析再回退到 SSRF 客户端。从源码看,该客户端基于 httpx 连接池实现,池参数(SSRF_POOL_MAX_CONNECTIONS、keepalive 上限与过期时间)均来自 dify_config,并按 verify 与否维护两套池 key,还支持 SSRF_PROXY_HTTP_URL / SSRF_PROXY_HTTPS_URL 的按 scheme 代理挂载(如 docker/ssrf_proxy 部署的代理)。自建 requests.Session 绕过这条通道,是评审中要直接拒绝的做法。
3.7 Pydantic v2 与异常翻译
请求/响应模型使用 Pydantic v2;复用领域特定异常,并在 controller 边界翻译。
结合 API_SCHEMA_GUIDE.md 的示例可以看到“翻译”的具体形态:service 返回 None 时,controller 负责抛 NotFound,而不是让序列化层或上层去兜底:
workflow_run = service.get_workflow_run(...)
if workflow_run is None:
raise NotFound("Workflow run not found")
return dump_response(WorkflowRunDetailResponse, workflow_run)
即 service 用领域异常表达领域失败,controller 是唯一把领域异常翻译成 HTTP 语义的位置,响应模型保持纯 DTO。
3.8 异步任务:复用既有 Celery 属主,重试必须幂等
最后两条规则约束异步工作:
- 异步任务使用既有的 Celery task 与 queue 属主,不要把不相干的任务塞进 workflow 专属 service。 从 api/tasks 目录结构可见任务按领域组织(
document_indexing_task.py、workflow_execution_tasks.py、clean_workflow_runs_task.py等),每个任务文件就是其队列语义的属主。Celery 本身在 api/pyproject.toml 中声明(celery>=5.6.3,<6.0.0),应用装配见 api/extensions/ext_celery.py。 - 可能被重试或重投的 Celery 任务必须保持副作用幂等,并记录受影响资源标识。 这一条直接对应 Celery 的 at-least-once 投递语义:broker 重投、worker 崩溃重启都会让同一任务跑两遍。幂等 + 日志中带资源 ID(如 document id、dataset id、workflow run id)意味着重复执行不产生脏数据,且事后可按 ID 追溯。
四、把规范跑起来:一份可复制的工作流
综合 api/AGENTS.md 与仓库工具链,一次合规的后端改动应当这样走:
# 1. 准备环境(首次):Docker 中间件 + api 依赖 + 数据库迁移
make dev-setup
# 2. 改代码前:读目标模块 docstring;若涉及 API 契约,先读 api/controllers/API_SCHEMA_GUIDE.md
# 3. 定向验证改动
make test TARGET_TESTS=./api/tests/unit_tests/<your_path>
# 4. 提交前全量检查
make lint
make type-check
make test
要点回顾:
- 所有命令从仓库根目录发起,Python 命令一律
uv run --project api前缀; - 分层违规(如 controller 直接 import libs 之下的领域模块)会被
lint-imports拦截; - 响应文档与序列化器不一致会被
api-contract-lint拦截; - Docker 支撑的集成测试(
--start-middleware、testcontainers)交给 CI,本地不启动长驻服务。
五、小结
api/AGENTS.md 虽然篇幅不长,但它是 Dify 后端工程纪律的“宪法”:命令体系保证了检查手段统一,import-linter 分层契约 + 响应契约 lint 把架构边界从文档变成了可执行规则,dify_config / ext_storage.storage / core.helper.ssrf_proxy 三个统一入口收敛了配置、存储与出站网络三类横切关注点,多租户属主链、事务有界性与 Celery 幂等则是数据正确性的三道防线。理解这份指南并对照其引用的 API_SCHEMA_GUIDE.md、api/.importlinter、Makefile 等文件,是安全修改 Dify 后端代码的完整前提。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00