Day 03 / 共 20 天 · 阶段1 全景与架构

应用类型总览:一套引擎,五种"应用长相"

Dify 界面上创建应用时让你选"聊天助手 / Agent / 文本生成 / 工作流 / Chatflow"。这些在源码里就是 core/app/apps/ 下五个并列的包:chatagent_chatcompletionworkflowadvanced_chat。今天弄清:①它们由一个 AppMode 枚举统一标识;②每个包都长得很像(Generator + Runner + ConfigManager + ResponseConverter 四件套);③它们其实分成"会话式"和"工作流式"两大家族。搞懂这张分类表,Day04 走请求旅程就不会迷路。

📍 你在 20 天里的位置(阶段1:全景与架构 · D01-04)
D01 项目全景 D02 启动 D03 应用类型 D04 请求旅程 S2 模型运行时 S3 工作流 S4 RAG S5 工具/Agent S6 收官
💡 先用两个类比兜住今天 类比一:五种应用就像同一家饮品店的五款饮品——美式、拿铁、卡布奇诺看着不同,但都用同一台咖啡机、同样的"接单→出杯→递给你"流程。Dify 也是:五种应用长相不同,但共用一套"生成器→运行器→流式输出"骨架,只是配方(配置)和步骤细节不同。类比二:AppMode 枚举就像菜单上的商品编号——收银台(AppGenerateService)扫一眼编号,就知道该走哪条制作线。没有这个编号,后厨就不知道该做哪款。
L01

痛点:为什么不是"一个应用类"打天下

🤔 痛点直觉上你可能觉得"应用不就是收到消息、调模型、返回结果吗,写一个类不就行了?"但真实需求差异巨大:聊天助手要记住上下文、要多轮;文本生成是一问一答、不记忆;Agent要能自己调工具循环;工作流压根不是对话、是跑一张流程图;Chatflow又是"对话 + 工作流"的混合体。硬塞进一个类,会变成满是 if app_type == ... 的意大利面条。
💡 本质:用"多态"替代"大 if"Dify 的解法是每种类型一个独立的包,但都实现相同的接口骨架。收银台(Service)只需按 AppMode 挑对应的类,剩下的交给多态——每个类自己知道怎么干。这样新增一种应用,只要加一个包、在分发处加一个分支,不用去改别人的代码。这是"开闭原则"的经典落地。
大白话与其写一个"什么都能干但到处是判断"的巨无霸函数,不如造五个"各司其职、长相一致"的小类。长相一致(都有 generate、都有 run),所以外面调用它们的代码可以统一;各司其职,所以内部逻辑互不干扰。
L02

AppMode:五种类型的"身份证"

所有类型由一个枚举统一标识(api/models/model.py:364):

# api/models/model.py:364
class AppMode(StrEnum):
    COMPLETION = "completion"        # 文本生成(一问一答,无记忆)
    WORKFLOW = "workflow"            # 工作流(跑流程图,非对话)
    CHAT = "chat"                    # 聊天助手(多轮对话,有记忆)
    ADVANCED_CHAT = "advanced-chat"  # Chatflow(对话 + 工作流 混合)
    AGENT_CHAT = "agent-chat"        # Agent 对话(能自己调工具循环)
    AGENT = "agent"                  # 新版 Agent 应用(基于 Dify Agent 运行时)
    CHANNEL = "channel"
    RAG_PIPELINE = "rag-pipeline"

    @classmethod
    def value_of(cls, value: str) -> "AppMode":
        for mode in cls:
            if mode.value == value:
                return mode
        raise ValueError(f"invalid mode value {value}")
StrEnum它继承字符串枚举,所以 AppMode.CHAT == "chat" 成立——数据库里 App.mode 存的就是 "chat" 这种字符串,取出来能直接和枚举比。
五个主类型本教程大纲说的五种应用就是前五个:completion / workflow / chat / advanced-chat / agent-chat。它们各自对应 apps/ 下一个包(L03)。
value_of()字符串转枚举的工具方法:从请求或数据库拿到 "chat" 字符串,用它转成 AppMode.CHAT,认不出就直接报错(不静默)。Day04 的 controller 就用它。
AGENT / CHANNEL / RAG_PIPELINE较新或特殊的类型(新版 Agent 应用、渠道、RAG 流水线),本教程聚焦前五个经典类型,这几个了解即可。
注释里点明:AGENT 是"基于 Dify Agent 运行时的新应用类型",和老的 agent-chat(ReAct 风格)不同。这解释了为什么会有 apps/agent_chat/apps/agent_app/ 两个目录并存。
L03

apps/ 目录:每种类型一个"长相一致"的包

ls api/core/app/apps/ 真实输出(节选),能看到五种类型是并列的子目录:

# api/core/app/apps/
chat/                 # 聊天助手
agent_chat/           # Agent 对话
completion/           # 文本生成
workflow/             # 工作流
advanced_chat/        # Chatflow(高级对话)
base_app_generator.py         # 所有 Generator 的基类
base_app_runner.py            # 所有 Runner 的基类
base_app_queue_manager.py     # 事件队列基类
message_based_app_generator.py  # 会话式应用的 Generator 基类

随便点开一个类型(如 chat/),会发现四件套结构(ls api/core/app/apps/chat/):

# api/core/app/apps/chat/
app_generator.py            # Generator:组装请求 + 起线程 + 返回响应
app_runner.py               # Runner:在后台线程里真正调模型
app_config_manager.py       # ConfigManager:把应用配置解析成运行时配置
generate_response_converter.py  # ResponseConverter:把内部事件转成对外格式
app_generator.py入口类,如 ChatAppGeneratorapi/core/app/apps/chat/app_generator.py:36)。负责"接请求参数、组装成生成实体、起后台线程跑 Runner、把响应流式返回"。Day04 精读它。
app_runner.pyChatAppRunnerapi/core/app/apps/chat/app_runner.py:28),在后台线程里真正干活:组织提示词、审核、调模型、把结果塞进队列。
app_config_manager.py把用户在界面上的配置(用哪个模型、什么提示词、开不开检索)解析成运行时能用的 AppConfig
generate_response_converter.py把内部统一的事件,转成该入口需要的格式(比如 service_api 和 web 的响应字段略有不同)。
workflow / advanced_chat 多一个文件这两类目录里还有 generate_task_pipeline.pyls 可见),因为工作流的流式输出更复杂——它要吐"节点开始/结束"等事件,需要专门的 pipeline。这正是 L05 说的"两大家族"的分野。
💡 设计取舍:五个包里四件套几乎重名,是"复制粘贴"吗? 看起来每个类型都有 app_generator.py / app_runner.py,像重复。但它们都继承自公共基类:Generator 继承 BaseAppGeneratorapi/core/app/apps/base_app_generator.py:61)或更具体的 MessageBasedAppGenerator,Runner 继承 AppRunnerapi/core/app/apps/base_app_runner.py:55)。共性(解析用户输入、审核、组织提示词)在基类里写一份,个性(各类型独有的流程)在子类里覆写。这是"模板方法模式":骨架公用、步骤定制,不是复制粘贴。代价是要读懂继承链才知道完整逻辑,收益是共性代码零重复。
L04

共享骨架:Generator 与 Runner 的分工

五种类型共享同一套"两段式"骨架。看 ChatAppGenerator 的继承与 ChatAppRunner 的入口签名,就能理解分工:

# api/core/app/apps/chat/app_generator.py:36
class ChatAppGenerator(MessageBasedAppGenerator):
    def generate(self, session, app_model, user, args, invoke_from, streaming=True):
        # 组装请求、起后台线程、返回响应流(Day04 逐行看)
        ...

# api/core/app/apps/chat/app_runner.py:28
class ChatAppRunner(AppRunner):
    def run(self, session, application_generate_entity, queue_manager, conversation, message):
        # 后台线程里真正干活:组织提示词 → 审核 → 调模型 → 事件入队
        ...
Generator.generate()"前台接单":解析参数、建对话/消息记录、初始化队列,然后另起一个线程让 Runner 去跑,自己立刻返回一个"响应流"给调用方。所以用户能马上开始收到流式输出。
Runner.run()"后厨做菜":在后台线程里组织提示词、做内容审核、调用 LLM,把每一段输出作为事件塞进队列。它不直接 return 给用户,而是"发事件"。
queue_manager 连接两端Generator 起的响应流从队列事件,Runner 往队列事件。队列把"生产"和"消费"解耦——这正是 Day04 的核心机制。
每种应用共享的两段式骨架(会话式为例) Generator.generate 前台:接单+起线程 QueueManager 事件队列(解耦) Runner.run 后厨:调模型(新线程) Runner 往队列「写」事件 Generator 的响应流从队列「读」事件 → 流给用户
图注:Generator 与 Runner 靠队列隔开,一个读一个写。这个"生产者/消费者"结构 Day04 会完整走一遍。
L05

两大家族:会话式 vs 工作流式

五种类型看似并列,其实按"底层是不是跑图"分成两大家族。这从它们的 Generator 基类和目录差异能看出来:

应用类型家族特点Generator 基类线索
chat会话式
(message-based)
多轮对话、有记忆MessageBasedAppGenerator
agent_chat对话 + 自动调工具循环
completion一问一答、无记忆
workflow工作流式
(graph-based)
跑流程图、非对话generate_task_pipeline.py
(专门的图事件流水线)
advanced_chat对话外壳 + 内部工作流
💡 怎么一眼分家族看目录里有没有 generate_task_pipeline.pyworkflow/advanced_chat/ 有(ls 可验证),因为它们底层跑工作流图、要吐"节点开始/结束/输出"这类图事件,需要专门的流水线;chat/agent_chat/completion/ 没有,它们共用 task_pipeline/ 里的通用 easy-ui pipeline(Day04 会遇到)。家族不同 → 流式输出的事件种类不同 → 需要的 pipeline 不同
大白话会话式家族像"点单即出的现成饮品",流程短;工作流家族像"需要多道工序的定制餐",每完成一道工序都要报一声(节点事件)。所以后者要一套更啰嗦的"播报系统"(task pipeline)。advanced_chat 特殊在——它披着聊天的外衣,肚子里却是一张工作流图,所以归工作流家族。
L06

对号入座 + 今日小结

📝 真实值:从"选哪款应用"到"跑哪段代码" 你在界面创建一个"聊天助手",数据库里这条 App 记录的 mode = "chat"。发消息时,Service 用 AppMode.value_of("chat") 得到 AppMode.CHAT,据此选中 ChatAppGeneratorapi/core/app/apps/chat/app_generator.py:36)→ 它起线程跑 ChatAppRunnerapi/core/app/apps/chat/app_runner.py:28)→ 用会话式通用 pipeline 流式返回。换成"工作流"应用,则 mode="workflow"WorkflowAppGenerator → 带图事件的 generate_task_pipeline.py一条从"UI 选择"到"具体源码文件"的清晰链路。

👶 小白:agent-chat 和 chat 都能聊天,源码上最大区别在哪?

👨‍🏫 老师:两者都是"会话式家族"、都继承 MessageBasedAppGenerator,Generator 层几乎一样。区别在 RunnerChatAppRunner 组织好提示词就调一次模型出结果;AgentChatAppRunner 则会进入"调模型→模型说要用工具→执行工具→把结果再喂回模型"的循环,直到模型给出最终答复。也就是说,差异主要沉在 Runner 里的"要不要循环调工具",这也是 Day17 讲 Agent 时的重点。

🧠 今天你应该能回答

  • 五种经典应用类型是哪些?(chat / agent-chat / completion / workflow / advanced-chat)
  • 它们由什么统一标识?(AppMode 枚举,api/models/model.py:364
  • 每个类型包里的"四件套"是什么?(Generator / Runner / ConfigManager / ResponseConverter)
  • Generator 和 Runner 的分工?(前台接单起线程 vs 后厨调模型发事件)
  • 两大家族怎么分?(会话式 vs 工作流式;看有没有 generate_task_pipeline.py
  • 为什么不用一个大类 + if 判断?(多态替代大 if,符合开闭原则;共性走基类)

✋ 10 分钟动手

cd /Users/bitmart/work/codes/github/AI_WORK/dify

# 1. 看类型枚举
sed -n '364,388p' api/models/model.py

# 2. 看五种类型是并列的包 + 每个包的四件套
ls api/core/app/apps/
ls api/core/app/apps/chat/
ls api/core/app/apps/workflow/     # 注意多了 generate_task_pipeline.py

# 3. 看继承链(家族分野)
grep -rn "class .*Generator(" api/core/app/apps/*/app_generator.py
grep -n "class BaseAppGenerator\|class AppRunner" api/core/app/apps/base_app_*.py
明日预告 · Day 04:类型认全了,明天把"一次 chat 对话"从头到尾跑一遍——controllers/.../completion.pyChatApi.postservices/app_generate_service.pyAppMode 分发 → ChatAppGenerator.generate 起线程 → ChatAppRunner 调模型 → task_pipeline 把结果 SSE 流回前端。今天的"两段式骨架"届时会真正动起来。
← Day 02 环境搭建与启动 Day 04 · 一次对话请求的完整旅程 →