本节要纠正的一个流行误解:上一轮规划曾把"节点式 vs MCP"并列成"两种范式对比"——这是一次范畴错误,它把一个协议层的东西(MCP)和一个编排层的东西(节点图)放在同一维度比较。§1 先把区分讲透,后续所有对比都建立在"编排器实现决定范式,MCP 是共用工具层"这一正确框架上。

阅读路径:§1 范畴纠正 → §2–§3 两种编排范式原理 → §4 MCP 共用工具层(附最小 server) → §5 同一任务两种实现对照 → §6 四维对比表 → §7 选型决策树 → §8 混合模式(推荐主线+架构图) → §9 可观测性与失败定位 → §10 与系列其它节的接口。


1. 范畴纠正:编排 ≠ 协议

一个能"用模型+工具完成任务"的 AI 系统,无论长什么样,都拆得出三种角色:

  • LLM / 模型:提供推理与生成能力(可被调用多次,也可能一次不调、纯编排数据)。
  • 工具 / Tool:外部能力单元——查库、读文件、调 API、跑一段模型推理。
  • 编排器 / Orchestrator:决定"何时调哪个模型/工具、按什么顺序、失败怎么办"。它的实现方式决定了范式。

关键在于:MCP(Model Context Protocol)不是编排器,它是工具层的一套 client-server 协议。它规定"一个工具如何被描述(schema)、如何被发现、如何被调用、返回什么格式",但不规定谁来决定调用顺序。决定调用顺序的,仍然是上面那个"编排器"。因此同一个 MCP 工具,既被"节点图编排器"按固定拓扑调用,也被"Agent 编排器"在 loop 里临时决定调用——MCP 处在被两种范式共享的下一层。把它们并列比较,等于问"Python 和 for 循环哪个更好",层次不同,无法对齐。

1.1 概念层次(表格 1)

层次它回答的问题典型成员可变性
协议层
(工具接口契约)
"一个工具怎么被描述、被发现、被调用?"MCP 函数调用/OpenAPI、gRPC、REST与编排范式正交,谁都能接
编排范式层
(谁决定调用顺序)
"任务步骤由图固定,还是由模型临时决定?"声明式节点图 · 过程式 Agent loop · 状态机决定系统的可控性/动态性取舍
具体框架层"用哪个产品把范式落地?"ComfyUI / n8n / Dify(A 型)· Hermes / LangGraph / AutoGPT / Claude Code(B 型)换框架通常不换范式

一句话记住:「协议」管工具长什么样,「范式」管谁来决定下一步,「框架」管你实际装的是哪个软件。三者不同层,别拿不同层的东西 PK。本节后面说的"两种范式",专指中间那一层,与 MCP 不在同一维度。


2. 范式 A · 声明式节点图(ComfyUI 型)

代表:ComfyUI n8n Node-RED Dify 工作流。心智模型一句话:节点 = 组件,连线 = 数据契约,整张图 = 一段程序。

在 pr02 里我们已经拆过 ComfyUI 的节点流水线——它本质是"声明式软件流水线":你先把执行拓扑写死,运行时只是把数据从上游端口灌进下游端口。它的性质因此非常"传统软件":

  • 静态可推演:不用真跑,光看图就能算出数据流、分支、汇聚点,成本/延迟可静态估算。
  • 可版本化 / 可 diff:图就是一段 JSON,Git 里能逐行 diff,"这次改了哪个节点的哪个参数"一目了然。
  • 失败精确归因:某条边断了、某个节点抛异常,错误直接钉在那一个节点上(§9 详述)。
  • 缺点:动态性差:分支一多、组合一爆炸,图会变得难以维护;无法在运行时"灵机一动"改路线。

3. 范式 B · 过程式 Agent 自主编排(Hermes 型)

代表:Hermes LangGraph 状态机 AutoGPT Claude Code。心智模型:把 LLM 放进一个 while 循环——每一轮都把"当前上下文 + 可用工具 schema"喂给模型,模型输出"要么调某工具(带参数)、要么给出最终答案",执行工具后把观察结果再喂回,直到收敛或触发 max_steps/预算护栏。核心是"下一步的决策权在运行期交给了模型"。

  • 动态性强:能处理"事先没画全"的开放任务,工具选择/参数都是临时定的。
  • 缺点:可控性弱——同一个输入,两次跑可能走不同路径。
  • 可观测性依赖 trace 中间件:失败不发生在"某个节点",而是发生在"第 7 步模型做了一个不太对的选择",得靠逐轮 trace 还原(§9,pr03 已建 traces.csv schema)。
  • 成本与延迟方差大:步数不固定,token 消耗和墙钟时间都是随机变量,难以静态预算。

4. 共用工具层:MCP

MCP 是一套 client-server 协议:编排器(无论 A 还是 B)作为 client,通过 MCP 连接到若干 server,server 对外暴露工具。约定很朴素:

  • 一个工具 = 一份 schema + 一个执行体:schema 描述"叫什么/收什么参数/返回什么",让 client 不看实现即可发现并调用;执行体是 server 侧真正干活的函数。
  • schema 让"工具描述"与"谁调用它"解耦——所以同一次工具定义,既能被节点图的一个节点调用,也能被 Agent loop 里的一次 decision 调用。

下面给一个最小 MCP server:暴露一个"查询服务器显存"的工具 query_vram。两种范式接入的都是它。

Python
# server.py —— 最小 MCP server(伪 API,真实签名以官方 SDK 为准)
from mcp.server import Server, Tool      # 库名/接口以官方为准
import subprocess

app = Server("vram-tool")                # 一个工具型 MCP server

# 1) 工具描述(schema):给 client 看的接口契约,不含实现
@app.list_tools
async def list_tools:
    return [
        Tool(
            name="query_vram",
            description="查询指定 GPU 当前已用/总显存 (MiB)",
            inputSchema={
                "type": "object",
                "properties": {
                    "gpu_index": {"type": "integer", "default": 0}
                },
                "required": []
            },
        )
    ]

# 2) 工具执行体:client 决定何时调用;server 只负责按参数返回结果
@app.call_tool
async def call_tool(name: str, args: dict):
    if name != "query_vram":
        raise ValueError(f"unknown tool: {name}")
    idx = args.get("gpu_index", 0)
    out = subprocess.check_output(
        ["nvidia-smi", "--query-gpu=memory.used,memory.total",
         "--format=csv,noheader,nounits", "-i", str(idx)]
    ).decode.strip
    used, total = [int(x) for x in out.split(",")]
    return {"gpu_index": idx, "used_mib": used, "total_mib": total,
            "free_mib": total - used}

if __name__ == "__main__":
    app.run_stdio                       # stdio 传输;SSE/streamable-http 亦可

同一个 query_vram,两种接法:在范式 A 里它是图上一个 Custom Node,端口类型写死、上游是谁一目了然;在范式 B 里它出现在 tools_schema 列表里,由 LLM 在某一步"决定要不要查显存、查第几号卡"。工具没变、契约没变,变的只是"谁决定调用它"——这就是"MCP 被共用"的具体含义。


5. 同一任务,两种实现的对照

把抽象落到一个具体任务上。任务定义:"把用户提供的图片打标签并生成一段中文标题。"我们对同一批 20 张图,分别用两种范式实现,再横向比较。

5.1 实现 A · ComfyUI 节点工作流

固定拓扑:Load Image → Vision Model(打标)→ Caption Prompt → LLM 生成中文标题 → Save。下面是 Save (API Format) 风格的骨架 JSON(节点 class_type 以你实际导入的工作流为准):

JSON · workflow_api.json(实现 A)
{
  "1": {
    "class_type": "LoadImage",
    "inputs": { "image": "input/photo_01.jpg" }
  },
  "2": {
    "class_type": "VisionClassifier",
    "inputs": {
      "image": ["1", 0],
      "model": "siglip-large",
      "top_k": 8
    }
  },
  "3": {
    "class_type": "StringConcatenate",
    "inputs": {
      "template": "标签: {tags}。请用不超过 20 字为这张图写一个中文标题。",
      "values": { "tags": ["2", 0] }
    }
  },
  "4": {
    "class_type": "QueryLLM",
    "inputs": {
      "prompt": ["3", 0],
      "model": "qwen2.5-7b",
      "temperature": 0.3,
      "max_tokens": 64
    }
  },
  "5": {
    "class_type": "SaveText",
    "inputs": { "text": ["4", 0], "path": "out/title_01.txt" }
  }
}

决策全在设计期定死:top_k=8、temperature=0.3、标题 ≤20 字都是图上的常量;换图只改 LoadImage.image,拓扑不动。

5.2 实现 B · Hermes Agent 提示词 + 3 工具

把同样的能力拆成 3 个工具(其中 vlm_tag 可以就是一个 MCP server),交给 Agent 自己组织:

YAML · agent.yaml(实现 B)
name: img-title-agent
model: qwen2.5-7b
max_steps: 6                      # 预算护栏,防止 loop 漂移
system_prompt: |
  你的任务:给定一张用户图片,先读出它的内容标签,
  再据此写一段不超过 20 字的中文标题。
  规则:
  1) 先调用 load_image 拿到图片;若失败最多重试 1 次。
  2) 用 vlm_tag 获取候选标签,若标签明显互相矛盾,
     可再调一次 vlm_tag 换 top_k。
  3) 结合标签调用 write_title 生成标题,要求自然、不堆词。
  4) 只有当标题可读时才输出最终答案,否则说明失败原因。
tools:
  - name: load_image
    description: "读取本地/URL 图片,返回可被视觉模型消费的句柄"
    source: builtin
  - name: vlm_tag
    description: "对图片打标签,参数 image_handle、top_k"
    source: mcp://vlm-server          # ← 与实现 A 用的是同一份视觉能力
  - name: write_title
    description: "根据标签+风格生成中文标题"
    source: mcp://llm-writer

工具 vlm_tag、write_title 各自带 inputSchema(image_handle/top_k、tags/max_chars),与实现 A 用的是同一份视觉与文本能力。这里"先做什么、标签矛盾要不要重打、什么时候算完成"全是运行期由模型决定——同一张图两次跑,步数可能不一样。

5.3 一批 20 张图的横向对照(示意)

下表是这一对照实验的预期趋势示意(非某次实测真值,仅建立直觉):

观测项实现 A(节点图)实现 B(Agent)
成功率(流程完整跑通)高且稳定(≈拓扑固定,20 张走同一条路)略低且波动(个别图 loop 早停或超 max_steps)
平均步数恒定 = 图中节点数(此处 5)随机变量(示例:3–6 步,看模型是否重打标签)
可观测性天然:每节点输入输出可截图/落盘需 trace 中间件逐轮记录才看得清
出错定位难度低:错误钉在具体节点(如 class_type 不匹配)高:要回放"第几步模型选错工具/参数"
对"开放图"的适应性弱:标签异常时只会照流程硬跑强:能自主重打/调整再决定
成本方差低(可静态估算 token 与时间)高(步数不定 → 延迟/费用波动)

注意:这里 vlm_tag 与实现 A 的 VisionClassifier 用的是同一份视觉能力(通过 MCP 暴露)。两者的差异不来自工具,只来自编排器决定调用的方式。这正是 §1 那个区分的实证回收。


6. 四维对比表(表格 2)

把两种范式放到同一张评分表上,逐维给"A 优 / B 优 / 平"与一句解释。不给"谁绝对更好"的判断——取舍取决于任务性质(§7)。

维度评级解释
开发成本
(简单固定流程)
A 优拖几个节点连线即成,不用写 loop 与护栏;但复杂分支下 A 反而更贵。
可控性 / 可预测性A 优拓扑写死,行为可静态推演;B 每次可能不同路。
成功率 · 简单任务A 优路径固定,几乎不偏离;B 的多余决策反而引入不确定。
成功率 · 复杂/开放任务B 优需要临场选工具/重试/改策略时,B 的动态性救场;A 的图会组合爆炸。
可观测性A 优节点即观测点;B 必须额外挂 trace 中间件才等价。
复用性平都靠工具层复用;共用 MCP 时,两边复用的是同一批工具。
出错定位A 优A 错在节点、可精确归因;B 错在 loop 状态漂移,需回放 trace。
成本 / 延迟预算A 优A 可静态估算(步数恒定);B 步数是随机变量,方差大。
动态适应 / 自进化空间B 优B 可接记忆、反思、工具自选择;A 要把新情形画进图才行。

读表要点:A 在"确定性相关"维度(可控/可观测/定位/预算)几乎全线占优;B 只在"开放性相关"维度(复杂任务成功率、动态适应)占优;复用性因共享 MCP 而打平——这就是选型的底层结构。


7. 选型决策树

把 §6 的取舍变成可执行的问题串。按顺序问自己,命中即停:

  • 任务是否是固定流程? → 是 → 范式 A(流程已知,没必要让模型现编)。
  • 是否需要在运行时动态选择工具/参数? → 是 → 范式 B(决策权该交给模型)。
  • 是否需要人机协作回路(中途介入/审批)? → 是 → B 更适合在 loop 里插一轮人工确认。
  • 是否需要精确的成本与延迟预算? → 是 → A(步数恒定才能静态估算 SLA)。
  • 是否需要长上下文记忆 / 自进化 / 反思? → 是 → B。
  • 以上都不明显、任务既有稳定骨干又有偶发开放分支 → 看 §8 混合模式。

一个经验法则:"重复一千次都不变"→ A;"每一次都可能要临场应变"→ B。拿不准时,先问"能不能提前把图全画出来"——画得出来就别上 Agent,省下的都是方差。


8. 混合模式:外层 Agent + 内层节点图(推荐主线)

生产系统里最常见、也是本节推荐的主线:让 Agent 处理开放任务与用户交互,一旦命中"稳定高频的子流程",就把它委托给一段 ComfyUI/n8n 工作流跑。这不是"选边站",而是把 §6 两张优点分层组合——外层享受 B 的动态性与协作回路,内层享受 A 的确定性与可观测性,两者用 MCP 共享工具、避免重复实现。"稳定高频子流程"最好固化、交给节点图收益最大;"开放交互与临场决策"留给 Agent。

两种典型触发方式:

  • workflow-as-a-tool(正向):把一整段节点图打包成 MCP 里的一个工具,Agent 在需要时"调用一次工作流",返回一个结果。对 Agent 而言它就是个黑盒工具。
  • workflow 内抛回 Agent(反向):稳定流程里遇到事先无法确定的分支,工作流用一个"handoff 节点"把控制权连同上下文交回外层 Agent,由 Agent 决策后再决定续跑还是换路。
外层 · 范式 B(Agent loop) LLM 决策下一步/是否结束 人机回路审批/澄清 观察/记忆 loop:观测喂回 → 再决策 内层 · 范式 A(稳定子流程) Load图片 Vision打标 Save 失败=某节点报错,可定位 workflow-as-a-tool ▶ ◀ 开放分支抛回 Agent 共用工具层 · MCP(client-server 协议,非编排范式) query_vram vlm_tag write_title … client client
图 8-1 · 混合模式:外层 Agent(范式 B) + 内层节点图(范式 A),两者作为 MCP client 共享同一工具层

9. 可观测性与失败定位对比

两种范式的"失败"长得不一样,定位方式因此不同:

  • 节点图(A):失败 = 某个节点抛异常/某条边类型不匹配,错误直接归属到那一个节点 id。回放即定位。ComfyUI 的 /prompt 校验失败会返回具体节点错误(见 pr02 §6.2)。
  • Agent(B):失败 = loop 状态漂移——没有哪个节点"崩了",而是模型在某一步选错了工具、填错了参数,或陷入反复自我确认。必须有逐轮 trace 中间件才看得见。pr03 已建 traces.csv schema,pr09 提供失败编码。

一段示意性的 Agent 失败 trace(对应 §5 实现 B 的某个坏 case):

CSV · traces.csv 片段(范式 B 失败样例,示意)
step,role,tool,args_summary,observation,latency_ms,tokens,fail_code
1,assistant,vlm_tag,"handle=img_07,top_k=8","[cat,sofa,indoor,...]",210,340,
2,tool,-,-,"ok",-, -,
3,assistant,vlm_tag,"handle=img_07,top_k=16","标签矛盾:sofa vs bed",190,360,
4,assistant,write_title,"tags=[...],max=20","标题含矛盾词'卧室沙发'",120,80,F2
5,assistant,write_title,"重试,同参数","同上",118,82,F2
6,-,-,-,"超步数/预算耗尽,loop 未收敛",-, -,F5

定位方法:用 fail_code(pr09 编码)定位到第 3–5 步——根因是"模型在标签矛盾时反复用同一策略重试",而非任何工具报错。这类失败在范式 A 里不会发生(矛盾只会被硬塞进 write_title)。Agent 的灵活正是它难排障的来源,所以 pr03 的 trace schema 不是可选项。


10. 与本系列其它节的接口

本节不是孤岛,它和 pr 系列其它讲义是"正交 + 复用"关系:

  • pr03(Agent trace schema):§9 的失败定位直接依赖 pr03 的 traces.csv 字段——pr16 讲"为什么需要它",pr03 给"它长什么样"。
  • pr11 / pr12(量化 / 提速):讲模型侧工程,与编排范式正交——两种范式底下都能接同一份被量化/加速过的模型。
  • pr02(ComfyUI 案例):是 §2 / §5.1「范式 A」的落地样本;本节把它从"一个工具"上升为与 Agent 对等的一种编排范式来比较。
  • pr17(SWE-bench 最小复现):属典型范式 B——Agent 在代码库上自主探索、逐步决定编辑/测试/提交,可用来检验 §3 与 §9 的 loop/trace 心智模型。

诚实标注:文中 ComfyUI 的 class_type、Hermes/Agent 配置字段、以及 MCP SDK 的 Server/Tool API 均为示意最小写法,真实接口、方法与版本以各项目官方文档为准。§5.3 与 §6 的评级是结构性趋势判断,非某次实测真值,也不构成"A 优于 B"的绝对结论——选型永远回到 §7 的任务性质。


一页纸总结:MCP 是工具协议,不是编排范式;节点式(设计期写死拓扑)与 Agent 自主(运行期交给模型 loop)才是两种对等的编排范式,二者共享 MCP 工具层。确定性维度(A 优:可控/可观测/定位/预算)与开放维度(B 优:动态/协作/自进化)互补,生产主线是"外层 Agent + 内层节点图 + 中间 MCP"的混合模式;失败定位上 A 归因到节点、B 归因到 trace 里的某一步决策。