从 Dify 到 LangGraph:为什么开发者还要写代码

上一篇《dify+elk-mcp 实现 AIOps 对话式获取日志数据》用 Dify 拖拖拽拽搭了一套对话式日志查询方案,证明了低代码在 AIOps 场景里确实能跑通。

但这篇文章要换一个视角:当你把这套方案从 demo 推到生产,或者当你和一群开发者坐在一起讨论架构时,为什么大多数人最终还是会回到 LangChain、LangGraph,而不是继续拖拖拽拽?

Dify 在企业里用得不少,因为它降低了非开发者的参与门槛。可一旦进入工程化阶段,开发者会更倾向代码。这不是偏见,而是低代码模式本身有三个隐性天花板决定的。

一、回顾:那篇 Dify+ELK-MCP 方案到底做了什么?

原文的方案流程很清晰:

重试成功

重试失败

简单

深度

用户输入自然语言查询

Dify-LLM DeepSeek-V3

参数提取器

参数提取成功?

返回引导提示

API 参数构建器

生成标准化 API 请求体

HTTP 调用 ELK-MCP

API 请求成功?

失败重试 最多3次

接收日志数据

返回失败提示

LLM 生成分析报告

简单/深度?

返回关键日志

返回结构化报告

它做了三件关键的事:

  1. 自然语言转结构化参数:用 DeepSeek-V3 从“查 kf1 租户 web-scrm 最近 30 分钟错误日志”里提取 tenant_idserviceslog_levelstime_range_minutes 等字段。
  2. API 参数构建:用 Python 代码节点把结构化参数拼成 ELK-MCP 的请求体。
  3. 结果分析:再用 LLM 对返回的日志做 SRE 视角的深度分析,输出报告。

这个流程在 Dify 里跑得很漂亮,但它隐藏了一个前提:流程是固定的、异常是可配置的、人是守在屏幕前的。 当这些前提松动时,代码框架的优势就出来了。

二、Dify 在企业里为什么能火?

在讨论“为什么开发者不用 Dify”之前,得先说清楚它为什么在企业里有市场:

  • 快速验证:2 小时搭出 demo,不用搭后端、不用写前端。
  • Prompt 管理可视化:系统提示词、变量、模型参数都能在一个页面里调。
  • 运营人员能参与:业务方可以自己改提示词、加分支,不需要等排期。
  • 内置 LLMOps:调用日志、token 统计、模型切换都现成的。

这些能力让 Dify 非常适合原型、内部工具、非核心流程。但一旦系统进入生产环境,开始对接 CI/CD、监控告警、权限审计、故障演练,开发者就会发现:低代码省下的时间,会在维护阶段加倍还回来。

三、Dify 的三个隐性天花板

我把低代码模式在生产环境里的限制,总结成三张天花板。每张天花板都对应一个必须从 Dify 走向代码的理由。

天花板 Dify 的表现 带来的问题 代码框架怎么破
版本控制与可测试性 DSL diff 可读性差;代码节点里的 Python 没法单独跑单元测试 无法做 code review、无法写 pytest、无法回滚 LangChain:把每个节点拆成可测试的 Python 函数和类
执行确定性与控制力 循环、分支、重试被包在节点内部,黑盒 流程一复杂就看不清数据怎么流,调试靠点执行日志 LangGraph:State / Node / Edge 显式化,每一步都可控
长任务与异常恢复 节点级错误分支,崩溃后难恢复;长任务中断要重跑 分页查询到一半挂了,只能从头再来;人工审批靠 UI 阻塞 LangGraph:checkpoint 断点续跑、interrupt 人机审批、time travel 回溯

这三张天花板不是 Dify 的 bug,而是低代码模式的固有限制。低代码用“抽象”换“速度”,代码用“显式”换“控制”。当你的流程从“能用”变成“必须稳定运行”时,抽象就开始漏底。

代码框架

生产化天花板

版本控制

确定性

异常恢复

Dify 低代码

快速验证

可视化

运营参与

LangChain

LangGraph

四、LangChain:把 Dify 节点拆成“模型 + 提示 + 解析 + 工具”

原文里最关键的两个节点是 LLM 参数提取器HTTP 请求。它们在 Dify 里是两个方块,但在 LangChain 里至少要拆成四块积木:

Dify 节点 LangChain 组件 拆分原因
LLM 参数提取器 ChatModel + PromptTemplate + OutputParser 模型、提示、解析三个职责分离,才能单独测试和版本化
API 参数构建器 RunnableLambda / Python 函数 把 Python 沙箱代码变成可导入、可单元测试的模块
HTTP 调用 ELK-MCP Tool 任何外部调用都抽象成工具,Agent 可以决定是否调用
失败重试 with_retry / fallbacks 重试策略写成代码,而不是节点配置里的下拉框

4.1 参数提取:从 Dify 节点到 Pydantic schema

Dify 的参数提取器最终输出 7 个字段。LangChain 里对应的做法是先定义一个 Pydantic model,再用 JsonOutputParser 约束模型输出:

from pydantic import BaseModel, Field
from langchain_core.output_parsers import JsonOutputParser
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

# 1. 定义输出 schema:对应 Dify 参数提取器的 7 个输出变量
class LogQueryParams(BaseModel):
    """对应 Dify 参数提取器的输出变量。"""
    time_range_minutes: int = Field(default=15)
    log_levels: list[str] = Field(default=["ERROR"])
    services: list[str] = Field(default=[])
    keyword: str = Field(default="")
    tenant_id: str = Field(default="all")
    sort_field: str = Field(default="@timestamp")
    sort_order: str = Field(default="desc")

# 2. 模型 + 提示 + 解析:对应 Dify 的 LLM 节点 + 参数提取器
llm = ChatOpenAI(model="deepseek-v3", temperature=0)
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是日志分析助手,只返回 JSON。"),
    (
        "human",
        PARAMETER_EXTRACTION_PROMPT,  # 上一篇文章里的完整提示词
    ),
])
parser = JsonOutputParser(pydantic_object=LogQueryParams)

extract_chain = prompt | llm | parser

这一步的关键变化是:输出结构被显式定义了。 Dify 的参数提取器也要求模型输出 JSON,但 schema 是写在提示词里的;LangChain 把 schema 提到了代码层面,Pydantic 会在解析失败时抛出明确异常,而不是让下游节点拿到一个缺字段的字典。

4.2 API 参数构建器:从 Dify 代码节点到可测试函数

原文里用 Python 代码节点做索引模式映射、租户识别、查询体构建。在 LangChain 里,这就是一个很普通的 Python 函数:

from langchain_core.runnables import RunnableLambda

def build_api_payload(params: LogQueryParams) -> dict:
    """把结构化参数转成 ELK-MCP 的请求体。"""
    return {
        "index_patterns": [f"{params.tenant_id}_log_*"],
        "filter": [
            {"terms": {"log_level": params.log_levels}},
            {"terms": {"service_name": params.services}},
            {"term": {"tenant_id": params.tenant_id}},
        ],
        "sort": [{params.sort_field: {"order": params.sort_order}}],
        "from": 0,
        "size": 50,
    }

# 现在可以写单元测试了
def test_build_api_payload():
    params = LogQueryParams(
        time_range_minutes=30,
        log_levels=["ERROR"],
        services=["web_scrm"],
        tenant_id="kf1",
    )
    payload = build_api_payload(params)
    assert payload["filter"][2]["term"]["tenant_id"] == "kf1"

这个函数可以被 pytest 单独测试、可以被 IDE 自动补全、可以被类型检查器校验。Dify 代码节点里的 Python 虽然也能写逻辑,但它生活在一个沙箱里,无法导入私有库、无法被版本控制工具完整理解。

4.3 工具与重试:从 Dify HTTP 节点到 Tool

Dify 的 HTTP 节点是一种特殊节点。LangChain 没有“HTTP 节点”这个概念,任何外部调用都被抽象成 Tool

from langchain_core.tools import tool
import requests

@tool
def query_elk_mcp(payload: dict) -> dict:
    """调用 ELK-MCP 日志查询 API,失败时抛异常。"""
    resp = requests.post(
        "http://elk-mcp:999/api/log/query",
        json=payload,
        headers={"Authorization": "Bearer token"},
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()

# 对应 Dify 的失败重试机制:3 次指数退避
safe_query = query_elk_mcp.with_retry(
    stop_after_attempt=3,
    wait_exponential_jitter=True,
)

# 也可以加兜底:主模型失败时换本地小模型
fallback_llm = ChatOpenAI(model="gpt-4o-mini")
robust_extract = extract_chain.with_fallbacks(
    [prompt | fallback_llm | parser]
)

4.4 LCEL:把节点连成一条可观测的链

LangChain 的 | 运算符叫 LCEL(LangChain Expression Language)。它看起来只是把几个组件串起来,但背后做了三件事:

  1. 自动类型传播:上一步输出类型会约束下一步输入。
  2. 流式支持chain.stream() 可以逐 token 输出。
  3. 可观测性:配合 LangSmith,每一步的输入输出都能被 trace。
# 原文工作流的核心骨架,现在变成一条链
chain = robust_extract | RunnableLambda(build_api_payload) | safe_query

# 流式运行,观察每一步的输入输出
for event in chain.stream({"query": "查询 kf1 最近 15 分钟 ERROR 日志"}):
    print(event)

这一步的目标不是把 Dify 替换掉,而是让你看清:每个节点都不是魔法,都是可以被拆解、被测试、被版本化的普通代码。

五、LangGraph:为什么 LangChain 之后必须上图

原文里的失败重试是节点级的:HTTP 请求失败,自动重试 3 次。这个机制对偶发网络抖动有效,但对更复杂的生产场景就不够了。

日志查询助手.yml 里的真实工作流比原文更复杂:它不只是调一次 API,而是要先初始化分页查询,拿到 total_pages,再一页一页拉回来,每页都让 LLM 做摘要,最后生成完整报告,还要把结果写入 MySQL。这才是生产环境的常态。

这种场景下,LangChain 的线性链会遇到四个死胡同:

  1. 无法表达循环:分页查询必须 for page in range(total_pages),链式语法 A | B | C 没有回头路。
  2. 没有显式状态:第 10 页的摘要需要引用第 1 页的上下文,链里得手动 threading。
  3. 崩溃后从头再来:如果拉到第 25 页时进程挂了,LangChain 没有内置机制让你从第 25 页恢复。
  4. 无法人机审批:如果“写入 MySQL”是高风险操作,链不能原路暂停等人点“同意”。

LangGraph 的答案是:把工作流建模成有状态的图。

LangGraph 图结构

合法

不合法

失败

checkpoint 保存

Prompt

LLM

Parser

Tool

Output

重试

错误处理

断点恢复 / 人工审批

LangChain 链式结构

失败只能从第 1 步重来

Prompt

LLM

Parser

Tool

Output

把简单的事情做标准化,把复杂的事情做模块化。LangGraph 就是帮我把“循环 + 状态 + 恢复”做成标准模块的工具。

六、LangGraph 核心概念的精确定义:从 Dify 变量到 State / Node / Edge / Checkpointer / Interrupt

LangGraph 的五个核心概念,本质上都是把 Dify 里隐式的东西显式化。

State(状态)

Dify 的 conversation_variables 和节点输出就是状态,但弱类型、隐式传递。LangGraph 要求你显式定义:

from typing import TypedDict, Annotated
import operator

class LogState(TypedDict):
    """共享状态:对应 Dify 的 conversation_variables + 节点输出。"""
    query: str
    params: dict
    session_id: str
    total_pages: int
    current_page: int
    summary: Annotated[str, operator.add]  # 追加,不是覆盖
    approved: bool

Node(节点)

节点就是函数:def node(state: State) -> dict。它对应 Dify 画布上的一个方块。

def paginate(state: LogState):
    """对应 Dify 的 HTTP 分页查询节点。"""
    page = state["current_page"] + 1
    # 实际调用 ELK-MCP /paginate/get
    return {"current_page": page}

def summarize(state: LogState):
    """对应 Dify 的 LLM 逐页摘要节点。"""
    return {"summary": f"第 {state['current_page']} 页摘要...\n"}

Edge(边)

普通边是固定跳转,条件边根据状态路由。Dify 的连线 + if-else 节点,就是 LangGraph 的边。

from langgraph.graph import END

def should_continue(state: LogState):
    """对应 Dify 的条件分支节点。"""
    if state["current_page"] < state["total_pages"]:
        return "paginate"
    return "approve"

Checkpointer(检查点)

每个 super-step 结束后自动持久化状态。Dify 的执行日志只能“看历史”,LangGraph 的 checkpoint 能“回到历史并继续跑”。

from langgraph.checkpoint.memory import MemorySaver

app = builder.compile(checkpointer=MemorySaver())

# 崩溃后修复问题,用同一个 thread_id 从最近检查点恢复
app.invoke(
    None,
    config={"configurable": {"thread_id": "ops-001"}}
)

Interrupt(中断)

interrupt() 让图在任意节点暂停,等人输入。Dify 里实现这个通常靠“直接回复”节点 + 下一条用户消息,上下文管理是隐式的;LangGraph 把它变成显式原语:

from langgraph.types import interrupt, Command

def human_approval(state: LogState):
    """在写入 MySQL 前暂停,等待人工确认。"""
    decision = interrupt(
        {
            "message": "即将写入 MySQL,是否继续?",
            "summary": state["summary"],
        }
    )
    return {"approved": decision == "yes"}

# 用户通过 UI 回复后恢复
app.invoke(
    Command(resume="yes"),
    config={"configurable": {"thread_id": "ops-001"}}
)

这五个概念组合起来,就能把 日志查询助手.yml 里那条带分页、带审批、带落库的工作流,写成可控、可恢复、可测试的代码。

开始

extract_params
参数提取

init_query
初始化分页

还有下一页?

paginate
拉取一页

summarize
LLM 摘要

human_approval
人工审批

store
写入 MySQL

结束

七、Dify 与 LangGraph 的逐节点映射

把 Dify 的工作流翻译成 LangGraph,其实是一张一一对应的表:

Dify 概念 LangGraph 概念 为什么要这样对应
开始节点 图的 entry point 工作流的入口
LLM 节点 调用 ChatModel 的 Node 模型调用被包成一个函数
参数提取器 OutputParser 所在的 Node 把模型输出转成结构化状态
代码节点 Python 函数 Node 自定义计算逻辑
HTTP 请求节点 Tool 调用 Node 外部 API 调用
if-else 条件分支 条件边 add_conditional_edges 根据状态决定下一步
循环节点 带返回边的 Node 用条件边控制是否继续循环
等待节点 普通 Node(sleep)或外部事件触发 显式控制等待
直接回复节点 输出 Node 或 interrupt 返回用户或暂停等人
conversation_variables State 所有节点共享的强类型状态
执行日志 Checkpointer + get_state_history 不只是看历史,还能恢复和分叉

LangGraph 概念

Dify 节点

LLM

代码

HTTP

if-else

循环

直接回复

变量

Node 调用 ChatModel

Node Python 函数

Node 调用 Tool

条件边

循环边

Output Node / Interrupt

State

八、生产级能力对比:异常边界、人机交互、可观测性

学习框架不能只看“正常流程怎么走”,更要看“出错时怎么办”。

能力 Dify LangChain LangGraph
异常边界 节点级错误分支 链级 try/except + retry 节点级 + checkpoint 恢复
重试机制 HTTP/LLM 节点可配 with_retry / fallbacks 可在节点/条件边里自定义
崩溃恢复 依赖执行日志,难恢复 checkpointer 断点续跑
状态可见性 执行日志 + 变量面板 需手动打印 get_state / get_state_history
人机审批 UI 阻塞式 无原生 interrupt + Command(resume)
调试能力 节点单独运行 LangSmith trace time travel + breakpoint

8.1 异常处理:Dify 的节点级 vs LangGraph 的图级

Dify 的错误处理是节点级的。每个 HTTP、LLM、代码节点都要单独开错误分支,复杂流程会画得很乱。而且它没有“全局异常边界”——只要有一个关键节点没处理错误,整个流程就停了,外部系统收不到统一的状态回调。

LangGraph 把异常处理提升到了“工作流级别”:

from typing import TypedDict

class LogState(TypedDict):
    error: str
    retry_count: int

def paginate(state: LogState):
    """带错误处理的节点。"""
    try:
        # 调用 ELK-MCP /paginate/get
        return {"retry_count": 0}
    except Exception as e:
        return {"error": str(e), "retry_count": state["retry_count"] + 1}

def route_on_error(state: LogState):
    """根据是否有错误决定下一步。"""
    if state["error"] and state["retry_count"] < 3:
        return "paginate"  # 重试
    if state["error"]:
        return "fallback"  # 超过次数,走兜底
    return "summarize"

配合 checkpointer,即使进程重启,你也能从失败的那一步继续重试,而不是从头再来。

8.2 人机交互:Dify 的阻塞式提问 vs LangGraph 的 interrupt

在 Dify 里实现人机交互,通常是在关键节点放一个“直接回复”节点,把问题抛给用户,等用户下一条消息进来再继续。这个模式有两个问题:

  1. 上下文耦合:用户回复的内容必须被正确解析并映射到下一步的变量,否则流程会跑偏。
  2. 无法跨会话恢复:如果用户关闭了窗口,下次再进来,Dify 不会记得上次停在哪个审批节点。

LangGraph 的 interrupt()工作流级的暂停。它保存状态、等待输入、恢复执行,完全解耦于 UI。你可以在前端做一个“待审批”列表,用户点“同意”后,后端用 Command(resume="yes") 继续。

8.3 Time Travel:Dify 做不到的时光旅行

假设最终生成的 SRE 报告有问题,你想回到“意图识别”那一步,把 serviceskf11 改成 kf12,再看看结果会不会更好。在 LangGraph 里:

# 拿到完整执行历史
history = list(app.get_state_history(config))

# 找到意图识别那一步的 checkpoint
target = next(
    c for c in history if "extract" in c.values.get("steps_completed", [])
)

# 修改状态
new_state = {**target.values, "services": ["kf12"]}

# 从那个检查点 fork 一条新线程继续
new_config = {"configurable": {"thread_id": "ops-001-variant"}}
app.update_state(new_config, new_state)
app.invoke(None, new_config)

在 Dify 里,你只能手动修改输入重新跑。LangGraph 让你可以“从历史的任意时刻分叉”。

九、我的学习路径:Dify → LangChain → LangGraph

理解了三个天花板之后,学习顺序就不再是“学完 A 学 B”,而是缺什么补什么

开始

流程是否固定?

是否需要循环、状态持久化或人工审批?

LangChain / LCEL

LangGraph

LangGraph 多步 Agent

阶段 1:用 Dify 证明概念

目标:2 小时跑通对话式日志查询。
重点:理解节点、变量、分支;用原文的简化流程即可。

阶段 2:用 LangChain 拆积木

目标:把参数提取、API 构建、HTTP 调用重写成可测试的代码。
重点:学会 ChatModel / PromptTemplate / OutputParser / Tool / RunnableLambda 的分工。

阶段 3:用 LangGraph 补控制

目标:把分页循环、异常恢复、人工审批加进去。
重点:定义 State、写 Node、画 Edge、加 Checkpointer、用 Interrupt。

低代码优先:快速 demo 专业编排:复杂生产 简单脚本:临时任务 高度定制:精细控制 LangGraph LangChain Dify 易用性低 易用性高 控制力低 控制力高 "Dify / LangChain / LangGraph 的易用性 vs 控制力"

写在最后

Dify 和代码框架不是替代关系,而是同一个流程在不同成熟度下的不同形态

  • Dify 让业务方 2 小时验证想法。
  • LangChain 让开发者把每个节点拆成可测试的组件。
  • LangGraph 让复杂流程具备生产级控制能力。

企业里 Dify 用得多,是因为它降低了启动成本;开发者回归代码,是因为代码降低了长期维护成本。理解了这一点,就不会再纠结“选哪个框架”,而是会看当前阶段缺什么能力。

踩过的坑,比读过的书更有价值。希望这篇续集能帮你把“为什么从 Dify 走向代码”这个问题,看得更清楚一点。


项目已开源:

欢迎 Star 和 Issue 反馈。

我是 magicCzc,一个把 AIOps 当信仰的运维开发工程师。
GitHub:https://github.com/magicCzc

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐