01

从一次点击开始

你在 Dify 里点下"运行",背后发生了什么?

Dify 是什么?用一句话说清楚

想象你是一个厨师,但你不需要从零建造厨房——你只需要说"我要做这道菜",Dify 会把所有厨具、食材通道、出菜窗口都帮你准备好。

Dify 是一个 LLM 应用开发平台:你不用写底层代码,就能把 AI 能力组合成真正能用的产品——聊天机器人、文档问答、自动化工作流。

🔀

工作流编排

像拼乐高一样把 AI 步骤串联成流程,无需写代码

📚

RAG 知识库

上传你的文档,AI 就能基于它们回答问题

🤖

Agent 能力

AI 能主动调用工具、搜索网络、执行多步骤任务

🔌

模型无关

GPT、Claude、Llama、国产模型——随时切换,一行不改

你点下"发送"之后的 7 秒

想象你在 Dify 搭建的客服机器人里发了一条消息:"我的订单什么时候到?"

这 7 秒里,至少有 6 个系统协同工作。让我们逐步追踪这段旅程:

🖥️
浏览器
Nginx
🐍
Flask API
⚙️
Worker
🤖
LLM
点击"下一步"开始追踪请求

为什么 Dify 要这样设计?

你可能会问:为什么需要这么多层?直接调 OpenAI 的 API 不就好了?

💡
关键洞察:为什么生产级≠Demo 级

直接调 API 能做出 Demo,但无法处理:并发 1000 个用户、API 限流重试、对话历史存储、多模型切换、用量计费……这就是 Dify 的价值所在。

🔄
队列 + Worker 解耦

API 收到请求立即返回,Worker 在后台慢慢处理——用户不等待,系统不崩溃

📡
SSE 流式响应

SSE 让 AI 的回答像打字一样逐字出现,而不是等 30 秒后突然弹出一大段

🗄️
PostgreSQL 持久化

每一条消息、每一次工作流运行都存档——可回溯、可分析、可审计

代码翻译:Flask 应用工厂

Dify 后端的入口文件 api/app_factory.py 里有一段很有代表性的代码,它展示了生产级应用如何处理每一个请求:

CODE
def create_flask_app_with_configs() -> DifyApp:
    dify_app = DifyApp(__name__)
    dify_app.config.from_mapping(
        dify_config.model_dump()
    )
    @dify_app.before_request
    def before_request():
        init_request_context()
        RecyclableContextVar
            .increment_thread_recycles()
PLAIN ENGLISH

定义一个"工厂函数"——它每次被调用都会创建并返回一个全新的 Web 应用实例

造一个新的 Dify 应用对象,__name__ 告诉它自己在哪个文件里

从配置文件加载所有设置——数据库地址、密钥、API 限制等

model_dump() 把 Python 对象转成普通字典,方便 Flask 读取

注册一个"钩子"——每一个请求到达之前,都先执行 before_request 里的代码

初始化这次请求的日志上下文,这样后续所有日志都会带上请求 ID

清理线程变量,防止上一个请求的数据污染这一个请求

理解检验

你在 Dify 里发了一条消息,等了 15 秒才收到回答。这个 15 秒主要消耗在哪里?

用户为什么能看到 AI 回答"一个字一个字蹦出来"的效果,而不是等待后一次性显示全部内容?

02

架构全景:五大角色

认识 Dify 的演员阵容——每个服务扮演什么角色

一场管弦乐演出

Dify 的架构像一场管弦乐:指挥(Nginx)站在最前面分配任务,不同乐手(各个服务)各司其职,乐谱(共享配置)确保大家演奏同一首曲子。

没有人"全部都做"——每个服务只做好自己那一件事。这是软件工程里最重要的原则之一:单一职责

🎯
Nginx — 门卫 & 指挥

所有流量入口。把 /console/api 转给 API 服务,把 / 转给 Web 服务

🐍
API Service — 大脑

Python Flask 应用。处理所有业务逻辑:验证、路由、协调其他服务

⚙️
Worker Service — 苦力

Celery 异步工作进程。专门执行耗时任务:调用 LLM、处理文档、发邮件

🖥️
Web Service — 脸面

Next.js 前端。用户看到的所有界面——工作流画布、聊天界面、配置面板

🗄️
PostgreSQL + Redis — 记忆

PostgreSQL 存持久数据(消息、工作流、用户);Redis 存临时状态和任务队列

服务之间怎么"说话"?

当你在 Dify 里创建一个新的聊天应用时,这几个服务是这样沟通的:

代码住在哪里?

了解文件结构,你就知道该让 AI 把新功能写在哪里:

api/ 后端 Python Flask 应用
controllers/ 处理 HTTP 请求的入口函数(路由层)
core/ 所有核心业务逻辑:工作流、RAG、模型管理
models/ 数据库表结构定义(每个文件对应几张表)
services/ 业务服务层(controllers 调用 services,services 调用 core)
web/ 前端 Next.js 应用
app/ 页面路由(Next.js 14 App Router 结构)
app/components/ 可复用 UI 组件:聊天框、节点卡片、工具栏等
docker/ 生产部署配置
docker-compose.yaml 一键启动所有服务的配置文件
💡
给 AI 的正确指令

知道文件结构后,你就能说"在 api/controllers/console/app/ 下新增一个处理 XXX 的路由"——而不是模糊地说"加一个 API"。AI 听懂精确指令,代码质量提升 10 倍。

代码翻译:Docker Compose 服务定义

Dify 的 docker/docker-compose.yaml 定义了所有服务。这是 API 服务的配置片段:

CODE
x-shared-env: &shared-api-worker-env
  CELERY_WORKER_AMOUNT:
    ${CELERY_WORKER_AMOUNT:-4}
  APP_MAX_EXECUTION_TIME:
    ${APP_MAX_EXECUTION_TIME:-1200}
  DB_HOST:
    ${DB_HOST:-db_postgres}
PLAIN ENGLISH

定义一个"共享配置块"——用 & 标记,API 和 Worker 两个服务都会引用它

后台 Worker 进程数量:从环境变量读取

如果没有设置环境变量,默认启动 4 个 Worker 进程

单个应用最大运行时间(秒)

默认 1200 秒(20 分钟)——防止一个任务跑太久把系统卡死

数据库主机地址:

默认连接名叫 db_postgres 的容器(Docker 内部网络会自动解析这个名字)

理解检验

用户上传了一个 500 页的 PDF 让 Dify 建立知识库。这个处理工作主要由哪个服务执行?

你想让工作流画布上的 LLM 节点显示当前使用的模型名称。你应该让 AI 修改哪里的代码?

03

工作流:可视化流程引擎

那些节点和连线背后,藏着什么样的执行引擎?

工作流是一张乐谱

你在画布上拖出来的工作流,本质上是一张"乐谱"——它描述了"应该发生什么、以什么顺序、在什么条件下"。但它本身不执行任何事情。

真正的演奏者,是 api/core/workflow/ 里的执行引擎——它读取乐谱(DSL),然后驱动每一个节点按顺序"演奏"。

1
你在画布上设计

拖入节点(LLM / 代码 / 工具 / 知识库),用箭头连接,设定条件分支

2
Dify 序列化成 JSON DSL

保存时,整个画布被转成一个 JSON 对象存进数据库——节点列表 + 边的列表

3
执行引擎读取并运行

每次触发时,引擎从数据库加载 JSON,构建有向图,按 拓扑顺序逐节点执行

4
结果通过 SSE 实时推送

每个节点完成都触发一个事件,流式推送给前端——你看到节点一个个变绿

节点类型:工作流的词汇表

工作流里的每种节点类型对应 api/core/workflow/nodes/ 下的一个目录。了解它们,你才能知道该组合哪些"词汇"来描述你的业务流程:

🤖

LLM 节点

调用大语言模型生成文本,支持 Prompt 模板和上下文变量注入

🔍

知识检索节点

从知识库里搜索相关段落,把结果注入到后续 LLM 节点的上下文

代码节点

在沙箱里执行 Python 或 JavaScript 代码——处理数据、格式化输出

🔀

条件分支节点

If/Else 逻辑——根据前一步的结果走不同的路径

🔧

工具节点

调用内置或自定义工具:Google Search、DALL-E、HTTP 请求等

🔁

循环节点

对列表中的每个元素重复执行一段子流程(类似 for 循环)

代码翻译:工作流触发入口

当你点击"运行",请求到达 api/core/app/apps/workflow/app_generator.pyWorkflowAppGenerator。这里是真正"发动引擎"的地方:

CODE
class WorkflowAppGenerator(BaseAppGenerator):
  def generate(
    self,
    app_model: App,
    workflow: Workflow,
    invoke_from: InvokeFrom,
    inputs: Mapping[str, Any],
    stream: bool,
  ) -> ...:
PLAIN ENGLISH

定义一个"工作流应用生成器"类,继承自基础生成器

核心方法:generate,负责触发一次工作流执行

(self 表示这是类的实例方法)

要运行哪个应用(数据库里的 App 记录)

要运行哪个工作流版本(草稿 or 已发布版本)

谁触发的:用户通过界面、API 调用还是自动化触发

用户传入的变量(比如 {article: "我要总结的文章内容"})

是否流式返回——true 时用 SSE 推送,false 时等全部完成后返回

变量是怎么在节点之间传递的?

工作流最神奇的地方是:前一个节点的输出可以作为后一个节点的输入。Dify 用"变量池"来实现这一点:

💡
变量池的工作原理

每次工作流运行,Dify 维护一个"共享变量池"。每个节点执行完毕后,把自己的输出写入池子。下一个节点从池子里读取它需要的变量,用 {{节点名.输出字段}} 语法引用。

1

用户输入:topic = "AI 发展"

2

LLM 节点读取 {{sys.query}},输出 outline

3

第二个 LLM 节点读取 {{llm1.outline}},生成正文

4

代码节点格式化 {{llm2.text}} 输出 Markdown

理解检验

你设计了一个有 8 个节点的复杂工作流并保存了。下次再打开时,Dify 是怎么恢复这个工作流的?

你想搭一个工作流:如果 LLM 判断用户意图是"投诉",就走投诉处理流程;否则走普通客服流程。你需要用哪种节点类型?

04

RAG 管道:让 AI 读你的文档

知识库是怎么建立的?AI 是怎么在你的文档里"找到"答案的?

为什么 AI 需要"读"你的文档?

LLM 的知识在训练时就固定了——它不认识你公司的产品手册,不知道你昨天发布的公告,也不了解你独特的业务规则。

RAG 解决这个问题的方式非常聪明:不是"训练 AI 学你的知识"(很贵),而是"回答时临时把相关文档页给 AI 看"(很便宜)。

📖
类比:开卷考试 vs. 闭卷考试

微调(fine-tuning)让 AI 记住知识 = 闭卷考试。RAG 给 AI 相关页面 = 开卷考试。对于私有知识库,开卷考试便宜 100 倍,效果还往往更好。

从上传到回答:RAG 的完整管道

Dify 的 RAG 分两个阶段:建索引(你上传文档时)和检索(用户提问时)。

1
提取文本 — ExtractSetting

从 PDF/Word/网页中提取纯文本,支持 20+ 种格式。代码在 api/core/rag/extractor/

2
切块 — TextSplitter

把长文档切成小段落(默认 500 token/段)。代码在 api/core/rag/splitter/

3
向量化 — EmbeddingModel

每个段落被转成一个数字向量(向量),存入向量数据库

4
检索 — 用户提问时

用户的问题也被向量化,在数据库里找"最相似的段落"——余弦相似度计算

5
注入 Prompt — 生成答案

把检索到的段落塞进 Prompt:"基于以下资料回答:[检索结果]",LLM 生成答案

代码翻译:索引构建器

api/core/indexing_runner.py 里的 IndexingRunner 负责整个建索引流程。看看它的错误处理有多严谨:

CODE
def _handle_indexing_error(
    self,
    document_id: str,
    error: Exception
) -> None:
    document = db.session.get(
        DatasetDocument, document_id)
    if document:
        document.indexing_status = 
            IndexingStatus.ERROR
        document.error = str(error_message)
        db.session.commit()
PLAIN ENGLISH

定义一个"处理建索引失败"的方法

(self 是类实例)

哪个文档出错了(用 ID 标识)

错误是什么(Python 里所有异常的基类)

返回类型是 None——这个方法只做副作用(更新数据库),不返回值

从数据库里查出这个文档的记录

如果文档还在(没有被删除)

把状态改为 ERROR——前端会显示"处理失败"

记录错误原因——方便用户和管理员排查

提交到数据库——这个修改正式生效

💡
生产级的标志:状态机

注意 IndexingStatus.ERROR 这种模式——文档处理有明确的状态流转:pending → indexing → completed / error。这叫"状态机"。生产代码总是把状态显式化,而不是靠猜测。

切块策略:影响检索质量的隐藏参数

切块大小是 RAG 质量最重要的调参点,也是最容易被忽略的。理解它,你才能在 Dify 里做出正确的配置选择:

✂️
切太小(<100 token)

段落缺乏上下文,检索出来的片段 AI 看不懂——答案支离破碎

适中(300-500 token)

Dify 默认值。一般覆盖一个完整段落,语义连贯,检索精度和上下文长度平衡

📏
切太大(>1000 token)

每个段落包含多个主题,检索出来的内容不够精准,且占用更多 Prompt 空间

理解检验

公司今天更新了一份产品手册,你想让知识库里的 AI 立刻用上新版本。你应该怎么做?

你用 Dify 搭建了一个基于公司文档的 QA 机器人。一个用户问了一个问题,但相关信息根本没有在任何已上传的文档里。AI 最可能会怎么回答?

05

模型层:厂商无关的统一接口

为什么换一个 AI 模型只需要点几下,不需要改代码?

模型层 = USB 标准接口

USB 发明之前,每个设备都有自己的接口——打印机插头、键盘插头、鼠标插头各不相同,换设备就要换接口。USB 统一了标准,任何设备插进去都能用。

Dify 的模型层做的是完全一样的事:不管是 OpenAI、Anthropic、百度千帆还是本地 Ollama,Dify 的工作流代码都用同一套接口调用——底层细节被屏蔽掉了。

🌐

OpenAI / Azure

GPT-4o, GPT-4, o1,最广泛使用的商业模型

🤖

Anthropic

Claude 3.5 Sonnet,长上下文、代码和分析能力强

🇨🇳

国产模型

通义千问、GLM、文心一言——数据合规、国内延迟低

🏠

本地部署

Ollama / LM Studio——数据不出内网,适合高合规要求

代码翻译:ModelInstance 统一模型实例

api/core/model_manager.py 里的 ModelInstance 就是那个"USB 接口"——工作流调用它,不需要知道底层是什么模型:

CODE
class ModelInstance:
  def __init__(
    self,
    provider_model_bundle,
    model: str,
    credentials: dict | None = None
  ) -> None:
    self.provider = 
      provider_model_bundle
        .configuration.provider.provider
    if credentials is None:
      credentials = 
        self._fetch_credentials_from_bundle(
          provider_model_bundle, model
        )
PLAIN ENGLISH

定义"模型实例"类——代表一个准备好被调用的 AI 模型

构造方法:创建实例时调用

(self 是实例自身)

模型的"套餐"——包含厂商配置、认证信息、负载均衡设置

具体的模型名称,比如 "gpt-4o" 或 "claude-3-5-sonnet"

API 密钥,如果不传就自动从数据库读取

记录这个实例用的是哪家厂商(openai / anthropic / xxx)

如果没有传入密钥

就从数据库(provider_model_bundle)里取出

该厂商为该模型配置的 API 密钥

生产级特性:负载均衡 & 限流保护

只配一个 API Key 在生产中会很快触发 限流。Dify 内置了两个生产级保护机制:

⚖️
多 Key 负载均衡

配置多个 API Key,Dify 自动在它们之间轮转请求——把总配额叠加起来

🔄
自动重试 & Fallback

遇到 429/503 等错误,自动等待后重试;如果主模型持续失败,切换到备用模型

⚠️
常见踩坑:只配一个 API Key

Demo 阶段一个 Key 够用。但产品上线后如果有 100 个并发用户,单个 OpenAI Key 的每分钟限额(TPM)会瞬间打满。在 Dify 里提前配置多 Key 负载均衡,避免上线即崩。

不止 LLM:Dify 管理的 5 种模型

很多人以为 Dify 只管理聊天模型,其实它统一管理了 5 种不同用途的模型——每种对应不同的业务场景:

LLM 大语言模型——对话、生成、推理(GPT-4o 等)
Embedding 文本向量化——用于 RAG 知识库的索引和检索(text-embedding-3 等)
Reranker 重排序模型——对检索结果二次打分,提升 RAG 精准度
TTS / ASR 语音合成 / 语音识别——文字转语音、语音转文字
Moderation 内容审核——过滤有害内容,保护产品安全

理解检验

你用 GPT-4o 搭建了一套工作流,现在想切换到 Claude 3.5 试试效果。需要改多少代码?

你在 Dify 里建了一个知识库,上传了 100 份文档。文档内容被转成向量存储时,用的是哪种模型?

06

调试生产:当事情出错时

学会读懂错误信号,从 AI 的 bug 循环中脱身

Dify 应用的三类故障

大多数 Dify 应用故障都可以归入三个大类。知道类型,就知道去哪里查:

🤖
AI 质量问题

回答跑题、不遵循格式、产生幻觉——根源是 Prompt 设计或上下文不足

基础设施问题

超时、503、429 限流——根源是配置、并发或 API 配额问题

🔗
工作流逻辑问题

节点卡住、变量为空、循环不终止——根源是流程设计或数据格式不匹配

🔍
第一步永远是:看日志

Dify 在 api/logs/ 下记录了所有请求。Docker 部署时用 docker logs dify-api 实时查看。日志里有 stack trace,能精确定位出错位置。

排查对话:一个真实的调试流程

你的工作流突然开始返回 500 错误。跟着这个排查流程走:

代码翻译:错误处理架构

api/core/app/task_pipeline/based_generate_task_pipeline.py 里的错误处理展示了 Dify 如何把底层错误转成对用户友好的信息:

CODE
def handle_error(self, *, event, ...):
  e = event.error
  if isinstance(
    e, InvokeAuthorizationError
  ):
    err = InvokeAuthorizationError(
      "Incorrect API key provided"
    )
  elif isinstance(e, InvokeError):
    err = e
  else:
    description = getattr(
      e, "description", None)
    err = Exception(description)
PLAIN ENGLISH

定义处理错误的方法,* 表示后面的参数必须用名字传入

从事件对象里取出原始错误

判断错误类型——isinstance 检查"这个错误是不是某种类型"

如果是 API 密钥认证失败

包装成统一的认证错误,附上用户能看懂的提示语

如果是其他 LLM 调用错误(超时、限流等)

直接把原始错误往上传——保留完整信息给日志

如果是未知类型的错误

尝试读取 description 字段(如果有的话)

用 description 包装成通用 Exception 传给上层

找 Bug 挑战

下面这段工作流配置有一个常见问题——导致 LLM 节点总是返回空内容。你能找出来吗?

找出问题:这个工作流为什么总输出空字符串?

1 nodes:
2 - type: llm
3 model: gpt-4o
4 prompt: "请总结以下内容:{{user_input}}"
5 output_var: result
6 - type: answer
7 content: "{{result}}"

三条调试黄金法则

这三条法则能帮你系统性地解决 90% 的 Dify 应用问题:

1
单步运行,逐节点验证

工作流复杂时,用 Dify 的"单步调试"模式——每个节点执行后暂停,查看输入输出是否符合预期,再继续

2
变量为空时,往上查

LLM 输出为空?先检查输入变量有没有值。输入变量为空?再往上查是哪个节点没有正确输出——沿着数据流回溯

3
AI 质量差时,先加 System Prompt

AI 乱答?90% 的情况是 System Prompt 不够明确。先在 Prompt 里加上"你是…你只能回答…你必须按以下格式输出"

综合检验

你的工作流最终输出节点总是显示空白。从哪里开始排查最有效率?

日志里出现了 InvokeAuthorizationError。最可能是什么原因?