Dify 第5课:Dify 架构设计深挖

📅 2026/6/21 6:04:20
Dify 第5课:Dify 架构设计深挖
这节课不讲怎么点按钮讲底层怎么跑的。面试被问如果让你设计一个企业级 LLM 应用平台这部分就是你的答案。一、工作流引擎设计DAG vs 顺序链路大多数人的认知工作流就是上一步做完下一步。但 Dify 不一样。Dify 的工作流是 DAG有向无环图引擎不是顺序执行器。顺序链路n8n 那种 A → B → C → D 每次只走一条线串行。 DAG 引擎Dify 的 ┌─ B ─┐ A ─┤ ├─ D └─ C ┘ A 跑完B 和 C 可以同时跑都跑完了才进 D。面试考点Dify 怎么判断一个节点能跑了答每个节点有一个dependencies列表前置节点 ID 列表。只有所有 dependencies 的状态都是succeeded这个节点才会被调度器放进执行队列。并行执行策略Dify 的并行不是真的多线程而是异步并发工作流引擎扫描 DAG找出所有就绪节点把它们同时提交给 LLM / 工具执行一个节点完成后重新扫描 DAG找新的就绪节点重复直到所有节点完成或出错所以它的吞吐瓶颈在哪LLM 调用最慢几百毫秒到几秒外部 API 调用网络延迟代码执行节点如果写在 Python 里的计算很重节点级重试/超时每个节点可以独立配置retry_times失败重试次数retry_interval重试间隔秒timeout超时时间秒面试考点一个节点超时了整个工作流怎么处理默认策略节点超时 → 标记为 failed → 如果有 error_handle 分支就走分支 → 没有就整个工作流失败所以好的工作流设计一定是关键节点配重试 容错分支兜底。二、变量系统实现Dify 的变量系统是面试高频坑很多人搞混。三种变量类型类型作用域生命周期环境变量env全局所有应用共享永久写在配置里会话变量conversation_vars单次会话内从对话开始到结束节点变量node_vars当前节点内节点执行完可能就释放了变量注入机制Dify 是怎么把变量塞进 prompt 的本质是个模板引擎用户的问题是{{sys.query}} 当前上下文{{#context#}} 请根据以上信息回答。运行时的实际过程工作流执行到 LLM 节点解析 prompt 模板提取所有的{{...}}占位符从变量作用域链查找对应的值替换占位符生成最终 prompt发给 LLM作用域链查找顺序重要节点变量 → 会话变量 → 环境变量相同名称时节点变量优先。变量传递的坑面试题A 节点的输出怎么传给 C 节点Dify 的做法节点输出自动写入一个全局的nodes_outputs字典。其他节点通过{{nodes.A.output}}引用。但注意这不是真正的数据流传递是共享状态。如果 B 和 C 都读了 A 的输出它们读到的是同一个值。三、RAG Pipeline 完整链路这节和第 3 课有重叠但第 5 课关注的是每一层的设计考虑。文档上传 │ ├─ 预处理格式解析 → 清洗 → 分段 │ - PDF/Word/HTML 各自有解析器 │ - 分段策略段落、N 元、滑动窗口 │ - 分段大小默认 500 tokens可配 │ ├─ Embedding异步任务 │ - Celery worker 异步执行 │ - 每批 16/32 条减少 API 调用次数 │ - 失败重试3 次递增退避 │ ├─ 写入向量数据库 │ - 支持的引擎Weaviate / Qdrant / Milvus / PostgreSQL(pgvector) │ - 写入完成后标记索引状态为 completed │ └─ 检索时 - 用户 query 也做 embedding - 向量检索 全文检索BM25 - 混合检索后重排序Rerank - Top-K 截断默认 3-5 条面试考点为什么 Embedding 要用 Celery 异步执行因为 Embedding 是 I/O 密集型调 API且可能很慢大批量文档。同步执行会阻塞 Web 进程。Dify 用 Celery Redis 做任务队列API 只负责提交任务Worker 后台慢慢处理。四、Agent Function Calling 执行流程Dify 的 Agent 执行过程从源码角度看是这样的用户输入 │ ├─ 1. 构建 system prompt │ - 工具描述列表name description parameters │ - Agent 策略指令ReAct / FC │ - 历史对话摘要 │ ├─ 2. 调用 LLM │ - 如果是 FC 模式模型决定是否调工具 │ - 如果是 ReAct 模式模型输出 Thought Action │ ├─ 3. 解析 LLM 输出 │ - FC解析 tool_calls 字段 │ - ReAct正则匹配 Action: xxx 和 Action Input: xxx │ ├─ 4. 执行工具 │ - 验证参数JSON Schema │ - 执行工具代码 │ - 获取结果 │ ├─ 5. 结果反馈给 LLM │ - FC以 tool_result 消息返回 │ - ReAct以 Observation 文本返回 │ ├─ 6. 再次调用 LLM如果有必要 │ - 检查迭代次数是否超限 │ - 没超 → 回到第 2 步 │ - 超了 → 生成最终回复 │ └─ 7. 返回最终结果给用户关键词工具的 JSON Schema 验证发生在哪一步第 4 步。不是 LLM 决定调你就调Dify 会校验参数格式。参数不符合 Schema 会返回校验错误LLM 收到错误后修正参数重新调用。五、多租户与权限设计Dify 是 SaaS 产品多租户是必考题。隔离层级层级隔离策略数据范围工作空间Workspace逻辑隔离数据库用 tenant_id 字段区分所有数据应用App工作空间内隔离可以被分享应用配置 数据知识库Dataset应用级隔离文档 向量数据API Key应用级前端/后端 API 区分调用次数 限额数据隔离实现最简单但也最有效所有表加一个 tenant_id 列查询时 where tenant_id xxx。SELECT*FROMdocumentsWHEREtenant_idworkspace_123ANDdataset_idds_456Dify 的 Middleware 层自动注入这个过滤条件应用层代码不需要关心谁在访问。API Key 管理每个应用可以生成多个 API Key分App Secret Key后端使用有管理权限和App Public Key前端 SDK 使用只读支持设置过期时间、IP 白名单、速率限制第5课总结模块一句话面试话术工作流引擎“Dify 基于 DAG 调度节点有 dependencies 依赖检查就绪节点异步并发执行”变量系统“三层作用域节点→会话→环境模板引擎注入作用域链查找”RAG Pipeline“异步 EmbeddingCelery 三段检索向量全文重排序”Agent FC 流程“7 步闭环构建 prompt→调 LLM→解析→执行→反馈→循环→返回”多租户“逻辑隔离tenant_id 列 Middleware 自动注入”好先小测再更新记录。Dify 第5课课后小测5 题1. Dify 的工作流引擎是顺序执行还是 DAG 执行如果一个节点有两个前置节点其中一个失败了这个节点还会执行吗2. 变量作用域查找顺序是什么如果一个节点变量和环境变量同名谁优先3. Dify 的 RAG Pipeline 中Embedding 为什么用 Celery 异步执行而不是同步执行4. Agent Function Calling 执行流程中工具参数校验发生在什么时候是 LLM 说了算还是 Dify 会校验5. 多租户隔离的核心实现手段是什么只用一个 tenant_id 列够不够1. 工作流引擎顺序还是 DAGDAG有向无环图执行。Dify 的 Workflow Engine 在启动时先做拓扑排序生成 DAG然后按依赖关系逐层推进。两个前置节点一个失败了这个节点还执行吗默认不执行。Dify 的节点依赖关系是AND 合并语义——所有前置节点都完成statussuccess下游节点才会触发。只要有一个前置 failure下游节点会被标记为skipped不执行。不过 Dify 提供了错误分支failure branch机制你可以在节点上显式配置错误处理策略继续/终止/降级这时候即使前置失败了也能按你设定的路径走下去。这是通过 Edge 的error_handling字段控制的。2. 变量作用域查找顺序严格来说Dify 变量是通过前缀区分的不存在「同名冲突」的模糊情况引用方式变量类型作用域{{节点名.变量名}}节点输出变量该节点及下游可见{{env.变量名}}环境变量整个应用{{sys.变量名}}系统变量时间/用户ID等全局{{conversation.变量名}}对话变量对话生命周期所以如果你的节点变量叫name环境变量也叫name各走各的引用路径——{{node1.name}}优先匹配节点输出{{env.name}}匹配环境变量不存在模糊查找顺序问题。但如果在Code 节点Python/JS内部编程访问变量作用域是局部变量代码内部定义前置节点注入变量 环境变量代码节点里如果手动定义了跟注入变量同名的局部变量局部变量会覆盖。3. Embedding 为什么用 Celery 异步执行一句话不是不能同步而是不应该同步。对比同步执行Celery 异步用户体验用户 HTTP 请求挂起几秒甚至几十秒秒级返回「任务已提交」前端轮询进度容错网络抖动直接让请求失败Celery 自带重试机制 死信队列资源控制Worker 进程被单个请求占死可以通过 Celery concurrency 控制并发度批量处理单次请求处理所有 chunkOOM 风险一个文档拆成多个 task 分布式处理水平扩展加机器没用模型 API 调用是同步阻塞的加 Worker 节点直接线性提升吞吐关键点Embedding 调用外部 API一次调用可能几百毫秒到几秒。如果一个文档有 100 个 chunk同步执行一个 HTTP 请求就要等 100 次 API 往返。用 Celery 可以把每个 chunk 的 embedding 分散到多个 worker 并行处理吞吐量提升 10x 以上。4. Agent Function Calling 工具参数校验时机LLM 先出参数Dify 再校验。时序是这样的1. LLM 输出 function_call含 parameters 参数 JSON 2. Dify 解析 parameters JSON 3. Dify 拿着 JSON SchemaOpenAPI spec做参数校验 ✅ ← 这里 4. 校验通过 → 执行工具 5. 校验失败 → 把错误信息包装回 LLM告诉 LLM「参数不对重新生成」所以答案是LLM 说了不算Dify 会校验。而且它是先校验后执行。具体来说参数校验用的是工具定义时的JSON Schemarequired、type、enum、pattern等约束校验失败时Dify 不会直接给用户报错而是会发回给 LLM让 TA 重试修正但如果反复失败超过max_iteration限制Agent 才会抛出错误面试加分点虽然 Dify 会校验参数类型和结构但它不做参数值的语义校验——比如传了个「河南省」给一个要求 city_code 的工具Dify 只看 city_code 是不是 string 类型不会检查省名是否合法。这是留给 Tool 内部逻辑的事情。5. 多租户隔离的核心手段一句话多层级隔离光靠一个 tenant_id 列远远不够。Dify 的租户隔离是分层叠加的数据层只看 tenant_id 够不够隔离层级Dify 实现只靠 tenant_id 够应用/工作流SELECT * FROM workflows WHERE tenant_id :tid middleware 自动注入✅ 够对话记录WHERE tenant_id :tid middleware 自动注入✅ 够知识库文档WHERE tenant_id :tid⚠️ 勉强够向量检索每个 tenant 使用独立的Collection/Index❌不够真正的完整隔离策略1. 关系型数据隔离 └─ tenant_id 列 middleware 强制注入 ── 所有查询自动 WHERE tenant_id xxx 2. API Key 隔离 └─ 每个应用有自己的 app_id API Key └─ 不要用万能 API Key 3. 向量数据库隔离最容易出问题的地方 └─ 每个租户独立 Collection/Index └─ 原因向量检索 API 不总是支持 WHERE tenant_id 过滤 └─ 即使用 pgvector加 tenant_id 过滤也会影响 Recall 精度 4. 文件存储隔离 └─ 每个租户独立 Bucket 或前缀目录 5. 模型/额度隔离 └─ 租户级别的 Rate Limiter Quota 控制面试结论光靠 tenant_id 列在关系型数据层面够用配合 middleware 自动注入但在向量数据库层面不够因为向量检索的 filter元数据过滤不是所有向量数据库都可靠。Dify 的做法是关系型数据用 tenant_id 列 中间件过滤向量数据库用独立 Collection双重保障。