Dify 私有部署一周:低代码平台到底"低"掉了什么
一个 8 年 Java 后端,把 Dify 用 Docker Compose 拉起来,接火山方舟、建知识库、调检索参数,最后跟自己前三周手写的 LangGraph4j 硬碰硬对比的一周复盘。
核心结论一句话:Dify 工作流是 DAG,Agent 才是循环。低代码没有降低复杂度,只是把复杂度挪了个位置——从代码挪到了配置,从工程师挪到了业务方。
缘起:手写了三周,为什么要回头看低代码
Week 1 用 OkHttp 手撕 ReAct 的 while 循环,Week 2 换 Spring AI 一行 .tools().call(),Week 3 用 LangGraph4j 把循环画成图、用 MCP 把工具拆成协议。三周下来抽象层次一路往上爬,惯性会让人觉得"接下来该更深"。
结果 Week 4 反着来:装 Dify。理由很务实——
- 面试要"能落地"的东西。四个 Demo 里三个是"跑在 IDE 里的 main 方法",缺一个"能打开链接演示"的应用。
- 岗位技能云连续 6 天把 Agent 框架挂榜首,但招聘 JD 里 Dify / Coze 出现频率同样在涨。企业要的不是"最先进",是"下周就能上线"。
- 想验证一个判断:低代码平台是不是只是把我手写的那套东西包了个 UI?
一周跑完,第三个问题有了答案,而且比预想的有意思。
一、部署:三个坑,两个跟 Dify 无关
环境是 Windows + WSL2 Ubuntu,Docker Desktop 在 Windows 侧。第一个判断是代码放哪:
cd ~ && mkdir -p ~/agent-learning && cd ~/agent-learning
git clone https://github.com/langgenius/dify.git
cd dify/docker && cp .env.example .env
docker compose up -d放 WSL2 家目录,不放 /mnt/f/。WSL2 内 Linux 文件系统的 IO 是 Windows 挂载点的 3-5 倍,Dify 首次启动要拉 8 个镜像(api / web / worker / db / redis / weaviate / nginx / sandbox)约 3-4 GB,跑在挂载点上是自找麻烦。
坑 1:docker.sock: no such file or directory
failed to connect to the docker API at unix:///var/run/docker.sock这个报错分两种,排查路径完全不同:
| 报错 | 病因 | 修法 |
|---|---|---|
no such file or directory | Docker Desktop 的 WSL Integration 没开 | Settings → Resources → WSL Integration 勾 Ubuntu → 重开终端 |
permission denied | 当前用户不在 docker 组 | usermod -aG docker $USER |
ls -la /var/run/docker.sock 一条命令就能分清——文件不存在是集成问题,文件存在但读不了才是权限问题。
坑 2:打开 8080 看到 Spring Boot 的 Whitelabel Error Page
这个坑很值得说,因为报错本身就是最好的线索。Dify 是 Python + Node 栈,它不可能返回 Spring 的错误页。看到 Whitelabel 的第一反应不该是去翻 Dify 容器日志,而是——8080 被 Week 1/2/3 忘关的 Spring Boot Demo 占了。
sed -i 's/^EXPOSE_NGINX_PORT=.*/EXPOSE_NGINX_PORT=8888/' .env
docker compose down && docker compose up -d顺手记一条 Docker Compose 的硬规矩:改了 .env 或 docker-compose.yml,restart 不生效,必须 down + up -d。restart 只重启进程不重读配置,这个坑能让人怀疑人生半小时。
坑 3:Dify 不认模型名,只认 endpoint ID
接火山方舟的时候卡了一下:Settings → Model Provider → 火山方舟,填的不是 doubao-seed-1-6 这种模型名,而是控制台"在线推理 → 已接入的推理点"里的 ep-xxxxxxxx。Chat 和 Embedding 各要一个,而且加 Embedding 模型时模型类型必须选 Text Embedding,选成 LLM 后面建知识库会找不到向量模型。
二、知识库:RAG 三要素里,参数是最不重要的那个
Day 3 的实验是把 Week 1/2/3 三篇周记扔进知识库,做一个能查自己博客的 Chatbot——一鱼两吃,既测 RAG 又当博客搜索。
2.1 父子分段:解决"检索要小、生成要大"的矛盾
先用默认的通用分段(\n\n 切、500 tokens、重叠 50),效果不理想:
- Python 代码块被切碎,
def和函数体分到两段 - 中文破折号
——被当成分段边界 - 段落偏短,信息密度不够
换 Dify 的父子分段后明显好转:
父段:1000 tokens,用 \n\n 切 → 保留完整章节,喂给 LLM 生成
子段:200 tokens,用 \n 切 → 精准匹配,用于向量检索这是对"段落粒度矛盾"的一个工程解。检索希望段落小(越小越精准),生成希望段落大(上下文越足越连贯),传统单层分段只能二选一。父子分段用子段索引、父段回填,类比就是:子段是目录,父段是正文。
实测效果最能说明问题:问"Checkpoint 和 State 是什么关系",三个子段命中,但 LLM 拿到的是父段的完整上下文,才能输出"State 是工作内存,Checkpoint 是虚拟机快照"这种精确回答。如果只给它 200 tokens 的碎片,答案会浮在表面。
2.2 Score 0.7 的教训:向量分数不是对错标签
调参跑了三组:
| 组 | TopK | Score | 命中题 | 半命中题 | 越界题 |
|---|---|---|---|---|---|
| A 宽松 | 5 | 0.3 | ✅ 双源融合 | ✅ 三源召回 | ✅ 拒答 |
| B 基线 | 3 | 0.5 | ✅ 正常 | ✅ 引用偏少 | ✅ 拒答 |
| C 严格 | 2 | 0.7 | ✅ 命中 | ❌ 引用大幅减少 | ✅ 拒答 |
C 组的失败很有教育意义。中文 embedding 的分数分布本来就偏低,真正强相关的段落一般落在 0.5-0.7,卡到 0.7 意味着只有跟问题字面高度重合的段落能过关。
举个具体例子:文档里"OkHttp 客户端"和"用 OkHttp 发请求"表达同一件事,向量分可能一个 0.72 一个 0.58。卡 0.7 → 后者被误杀 → 用户问"用了什么 HTTP 库",召回的段落里恰好没有关键词,LLM 就答不上来。
结论:优先控 TopK,Score 放宽甚至去掉,把相关性判断交给 LLM,配合 Prompt 明确"没找到就说没找到"。最终定 TopK=5, Score=0.3——文档少、质量高的场景下,宁可多召回让 LLM 自己甄别。
2.3 意外收获:好知识库不是查找器,是知识关联引擎
三题回归里最惊喜的是 Q2。问的是 Week 3 的 Checkpoint,答案里主动带出了 Week 2 的 ChatMemory 做对比:
ChatMemory = 聊天日志(只有对话历史),Checkpoint = 完整执行现场(State + 程序计数器)
这不是 Prompt 让它做的。是 embedding 把两篇不同博客里的相关概念一起召回了,LLM 自然做了对比。这个效果有三个前提:
- 内容本身有交叉引用——我在 Week 3 博客里主动写了跟 ChatMemory 的对比
- 分段策略保留了上下文——父子分段功不可没
- Score 没卡太严——0.7 的时候这个效果直接消失
所以 RAG 三要素的重要性排序是:好内容 > 好分段 > 好参数。垃圾进垃圾出,参数只是放大器。
2.4 越界题的三段式范式
问"怎么用 Rust 写一个 MCP Server"(博客里完全没有),系统的回答是:
我在博客里没找到相关内容。博客 Week 3 里 MCP Server 的示例用的是 Python 的 FastMCP……如果你需要,我可以根据 MCP 官方规范给你一个 Rust 版的最小实现示例。
承认边界 → 说明边界在哪 → 提供替代路径。这三段式比干巴巴一句"不知道"有用得多,而且是可复现的:Prompt 约束 + LLM 对话能力 + RAG 上下文叠加的结果。Prompt 里那一句就够:
- 只用提供的上下文回答问题
- 如果上下文没有相关内容,说"我在博客里没找到相关内容",不要编造
- 引用来源用 [1] [2] 标注三、硬碰硬:Dify Workflow vs LangGraph4j
Day 4 没写代码,纯建判断力。四个维度对比下来,最根本的差异只有一句话:Dify 工作流是 DAG,Agent 是循环。
3.1 分支控制:河道分流 vs 回旋镖
LangGraph 里循环是一等公民:
.addConditionalEdges("llm",
edge_async(state -> state.nextAction()),
Map.of("tool", "tool", "end", END))
.addEdge("tool", "llm"); // ← 这条回环边就是 ReAct 的 whileDify 工作流里没有回环边。它是有向无环图,条件分支只能"if true 走 A,if false 走 B,最后汇聚到终点"。
Dify 工作流的条件分支是河道分流,不是回旋镖——水流出去就回不来了。 所以像"LLM 生成 → 判断质量 → 不合格重新生成"这种需求,工作流模式做不了,必须用 Agent 模式(内置 ReAct 循环)。
3.2 状态管理:reducer vs 扁平覆盖
| 维度 | LangGraph4j | Dify Workflow |
|---|---|---|
| 状态定义 | SCHEMA 声明式 reducer,逐字段定合并策略 | 模板注入,节点输出→输入传递 |
| 生命周期 | Checkpoint 持久化,threadId 恢复 | 单次对话内有效,无跨对话状态 |
| 复杂状态 | 同一 State 混用 append 和覆盖两种策略 | 无 reducer 概念,赋值即覆盖 |
| 中断干预 | interrupt() + Command(resume=...) | 不支持中间插入 |
LangGraph 的 State 能做到 messages 用 Channels.appender 追加、nextAction 默认覆盖,同一个状态对象里混用两种合并策略。Dify 的变量是扁平覆盖,表达不了这个。这就是为什么 Dify 适合单次执行的管道,多轮 Agent 对话得靠 LangGraph 的 Checkpoint。
3.3 可观测性:这一局 Dify 赢
必须承认,Dify 在这里是压倒性的:
| 能力 | Dify | LangGraph4j |
|---|---|---|
| 调用链追踪 | 内置 Trace 面板 + 耗时瀑布图 | 需自接 LangSmith / Langfuse |
| Token 成本 | 应用级看板,按模型/日期拆解 | 手动统计 |
| 错误处理 | 节点错误高亮 + 自动重试 + 降级兜底 | 自己写 try/catch |
| Prompt 版本 | 内置版本管理 + 一键切模型 | 改代码重新部署 |
但代价是 Agent 模式内部是黑盒。你看不到 ReAct 循环里每一步的 state 变化,排查"第 3 次工具调用时 messages 列表是什么"这种问题,还是得回到 LangGraph 的 stream()。
产品化的可观测性开箱即用但有天花板,自建的可观测性要成本但没有上限。
四、抽象层次不是高低,是分工
一周下来最值钱的认知是这张图:
Layer 3: Dify 产品层 ───── "业务方操作"
UI 拖拽 / 知识库管理 / Trace 面板 / 一键部署
Layer 2: LangGraph 框架层 ─ "工程师编排"
StateGraph / Checkpoint / ToolNode / 条件边
Layer 1: 手写代码层 ─────── "完全控制"
while(true) / if(toolCall) / OkHttp / 自己拼 Prompt三层不是替代关系,是同一件事的不同封装粒度,选哪层取决于团队构成,不是取决于哪个更先进。
选型上我现在的判断标准:
- 业务方要自己调 Prompt / 管知识库 → Dify,没有第二选项
- 简单 RAG 问答、固定流程管道、3 天出 Demo → Dify
- 多轮 Agent、复杂状态、要 HITL 中断 → LangGraph
- 生产环境要排查幻觉/死循环 → LangGraph,透明性是刚需
- Java 微服务深度集成 → LangGraph4j,技术栈统一
- 两个一起用:Dify 做业务接入层(飞书对接 + 知识库 + Prompt 迭代),LangGraph 做 Agent 引擎,Dify 的 HTTP 节点调 LangGraph 的 REST API
五、面试角度:低代码经历怎么讲才不像"只会拖拽"
这是本周想清楚的最实用的一件事。低代码平台经历讲不好会显得很水,讲好了是加分项。区别在于能不能说出它的边界。
三个高频问题的答法:
Q:Dify 能替代 LangGraph 吗? "Dify 的工作流是 DAG,Agent 模式封装了 ReAct 循环但它是黑盒——不能在中间插入人工审批,也不能自定义状态合并策略。LangGraph 让循环变回'一条普通边 + 一个条件边',控制权在代码里。"
Q:Dify 的 Agent 模式内部大概怎么实现的? "它的 /chat-messages 接口返回 event: agent_message 和 event: agent_thought,事件流跟 LangGraph 的 stream() 很像。内部大概率是 LLM 推理 → 判断是否需要工具 → 执行 → 观察 → 再推理,加一个最大迭代次数和超时保护。"
Q:给团队做选型,什么指标决定用哪个? "团队角色、业务复杂度、可观测性需求、技术栈、交付速度,五个维度打分。选型不是选框架,是选团队分工。"
关键是从底层原理讲到产品分工。手写过 while 循环的人讲 Dify 的黑盒问题,和只用过 Dify 的人讲,可信度完全不同。
总结:这周站到了哪一层
Week 1 手写循环,Week 2 框架封装,Week 3 图编排 + 协议解耦,Week 4 产品化平台。四层都站过之后,对"低代码"的看法变了:
它没有降低复杂度,只是把复杂度从代码挪到了配置,把决策权从工程师挪到了业务方。 该处理的问题一个都没消失——分段策略要选、检索参数要调、循环能力的缺失要用 Agent 模式绕、黑盒排查要接受。只不过这些问题从"写 200 行 Java"变成了"点几个下拉框",从而让不写代码的人也能参与。
对 8 年 Java 老兵的价值也很清楚:我不是"会用 Dify",我是"知道 Dify 在什么地方会撞墙,撞墙之后能退回代码层继续走"。这个才是差异化。
Week 4 的遗留债:Dify 接飞书还没做,Agent 工作流的实操还差一半。Week 5 的方向是 RAG 进阶(Rerank / HyDE / Ragas 评估),把"能跑"往"能量化"推。
参考:Dify 官方 docker-compose 部署文档 · 火山方舟在线推理接入点配置 · LangGraph4j 1.8.20 · 笔记
week4-day2-Dify-Setup.md/week4-day3-Dify-RAG.md/week4-day4-dify-vs-langgraph.md