Dify Agent 源码拆解:从 FC 到 CoT,工业级 Agent 的 5 个设计决策
这是 Dify 源码拆解系列第二篇。上一篇拆了 RAG Pipeline(TF-IDF 替代 BM25 的归一化取舍、父子分段等),这次拆 Workflow 引擎和 Agent 模块。我之前用 LangGraph 做过一个 7 节点 Multi-Agent 系统,本文聚焦于 Dify Agent 的设计决策和跟 LangGraph 的对比。
大家都知道 Agent 的核心循环是"LLM 思考 → 选工具 → 调工具 → 结果喂回 LLM",但打开一个 10 万星项目的源码,才发现这个循环的每一步都有值得琢磨的工程取舍。
架构概览:三层结构
graphon(外部pip包) ← 通用图引擎,DAG调度 + 节点并发
↓
api/core/workflow/ ← Dify 胶水层,节点工厂 + Layer中间件
↓
api/core/agent/ ← 旧版 Agent,FC/CoT 完整推理循环
graphon 已被抽成独立包,本地看不到。但 workflow 层和旧版 agent 层的代码足以理解核心设计。
决策 1:Layer 中间件模式
self.graph_engine.layer(ExecutionLimitsLayer(max_steps=500, max_time=1200))
self.graph_engine.layer(LLMQuotaLayer())
self.graph_engine.layer(ObservabilityLayer())
横切关注点(执行限制、配额、可观测性)通过 Layer 叠加,不侵入节点业务逻辑。跟 Express/Koa 中间件同一个思路。
跟 LangGraph 对比:LangGraph 没有原生中间件机制。要做执行限制只能在节点内部加 if 判断,关注点耦合。
决策 2:工厂模式 + 依赖注入
node_factory.py 核心是一个类型到依赖的字典映射:
{
"llm": lambda: { model_instance, memory, credentials },
"agent": lambda: { strategy_resolver, message_transformer },
"code": lambda: { code_executor, code_limits },
}
Factory 初始化时准备好所有依赖,create_node 时根据节点类型分发。节点自身不感知依赖来源。
节点注册用 importlib 动态导入:
_import_node_package("graphon.nodes") # graphon 内置节点
_import_node_package("core.workflow.nodes") # Dify 业务节点
新增节点只需要在对应目录加文件,不用改 Factory 代码。(不过新增的节点如果需要依赖注入,还是要在node_init_kwargs_factories字典里加一行)
决策 3:Agent 策略插件化
agent_node.py 是一个调度壳,核心就三行:
strategy = self._strategy_resolver.resolve(...) # 加载策略插件
message_stream = strategy.invoke(...) # 调用插件执行
yield from self._message_transformer.transform(...)
策略接口用 Python Protocol 定义(鸭子类型):
class ResolvedAgentStrategy(Protocol):
def get_parameters(self) -> Sequence[AgentStrategyParameter]: ...
def invoke(self, *, params, ...) -> Generator[ToolInvokeMessage]: ...
插件作者不需要导入这个 Protocol,只要方法签名匹配就能被调用。这解决了跨仓库依赖的问题——策略实现在 dify-official-plugins 仓库,跟主仓库解耦。
决策 4:FC 和 CoT 的核心差异
两者的推理循环结构一样(while 循环 + 退出条件),唯一区别在于"怎么从 LLM 输出中提取工具调用":
FC:模型原生返回结构化 tool_calls 字段
tool_calls = chunk.delta.message.tool_calls # 一行搞定
CoT/ReAct:模型输出自由文本,用 stop word "Observation" 截断,然后 200+ 行状态机逐字符解析 Thought/Action/Action Input
CoT 解析器要处理的边界情况:JSON 嵌套计数、代码块检测、action 前缀识别、Cohere 模型返回 list 而非 dict 的兼容……任何一个没处理好就解析失败。
这就是 FC 比 CoT 稳定的根本原因:FC 把解析复杂度交给模型厂商,CoT 留给自己。行业趋势是越来越多模型支持 FC,CoT/ReAct 逐渐变成 fallback 方案。
决策 5:三层退出保险
while function_call_state and iteration_step <= max_iteration_steps:
# 第一层:LLM 不返回 tool_calls / 输出 Final Answer → 正常退出
# 第二层:最后一轮清空工具列表 → 强制 LLM 直接回答
if iteration_step == max_iteration_steps:
prompt_messages_tools = []
# 第三层:最后一轮还想调工具 → 抛异常
if iteration_step == max_iteration_steps and tool_calls:
raise AgentMaxIterationError(...)
另外 base_agent_runner.py 里还有一个有意思的设计:知识库检索被包装成工具。Agent 不是固定要查知识库,而是根据用户问题自己判断"我需不需要查"。底层走的就是上一篇拆过的 RAG Pipeline。
对比总结
| 维度 | Dify Agent | LangGraph Agent |
|---|---|---|
| 中间件 | Layer 洋葱模型 | 无原生支持 |
| 节点创建 | 工厂 + 依赖注入 + 动态注册 | 手动 add_node |
| 策略扩展 | Protocol 接口 + 插件热插拔 | 硬编码 StateGraph |
| 推理模式 | FC / CoT 可切换 | 固定模式 |
| 退出机制 | 三层保险 | 单一 max_rounds |
| 工具格式 | 统一 PromptMessageTool(OpenAI 格式) | 直接调用 |
| 持久化 | PostgreSQL 存每轮思考链 | 只存最终结果 |
Dify 源码拆解系列到这里就告一段落了。两篇文章覆盖了 RAG Pipeline 和 Workflow/Agent 两大核心模块。如果你也在学 Agent 开发,希望这些笔记能给你一些参考。
目前在找 AI Agent / LLM 应用方向的实习。有 Dify 源码级理解和 LangGraph 多智能体系统实战经验,开源项目 AgentFlow(github.com/iuyup/AgentFlow)。