Dify Agent 源码拆解:从 FC 到 CoT,工业级 Agent 的 5 个设计决策

0 阅读4分钟

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 完整推理循环

ScreenShot_2026-04-06_210058_907.png

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 AgentLangGraph 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)。