1.1 框架分层:引擎/编排/SDK/协议四层定位图
卷一《AI Agent 开发入门》从零手写了 minagent,卷二《AI Agent 工程实战》把它做成了 ResearchCopilot 服务。走到本卷,问题换了一个:市面上那些「Agent 框架」到底各自负责哪一层,能不能拿来重写我们已经跑通的引擎?
在回答之前必须先有一张地图——否则你会在 langgraph、pydantic_ai、agents、mcp 这些名字之间反复迷路,把「编排」当成「引擎」,把「协议」当成「SDK」。这类混淆不是小事:它会让你的技术选型在错误的粒度上发生,比如「用 LangGraph 替换 PydanticAI」——而这两个根本不在同一层,谈不上替换。
本节把「框架全景」推进到一张可落地的四层定位图:引擎层 / 编排层 / SDK 层 / 协议层,每层给出代表库、职责边界与本机实测的模块归属,并把卷一的 minagent 与卷二的 ResearchCopilot 钉到图上。后续每一章都会回到这张图,说明「这一章在动哪一层」。
1.1.1 为什么必须先分层
一个常见的错误提问是「PydanticAI 和 LangGraph 哪个更好」。这个问法本身就不成立——它们根本不在同一层。
PydanticAI 是「让一个 Agent 跑起来」的 SDK;LangGraph 是「让多个步骤按图编排、可检查点恢复」的执行器。你可以用 LangGraph 编排三个 PydanticAI Agent,也可以完全不用 LangGraph 而直接用 PydanticAI 的循环。把它们并列比较,就像比较「发动机」和「变速箱」——它们协同工作,但不互相替代。
分层的价值在于:它把「选型」拆成了「分层选型」。你不再问「用哪个框架」,而是分四次问:
| 层 | 你要决定的问题 | 典型答案 |
|---|---|---|
| 引擎层 | 我用谁的 loop 内核跑单次推理? | 自研 minagent / 某 SDK 内置循环 |
| 编排层 | 多步骤、多 Agent 怎么串、怎么恢复? | LangGraph / 自研状态机 |
| SDK 层 | 单个 Agent 的提示、工具、输出模型怎么写? | PydanticAI / Claude Agent SDK / OpenAI Agents SDK / smolagents |
| 协议层 | Agent 与外部工具、与别的 Agent 怎么通信? | MCP / A2A |
四层是可以自由组合的。这正是本卷的主线:用 SDK 层重写卷二的问答路径,用编排层补上持久化,用协议层接上外部能力。
还有一层现实收益:分层让「替换」变得可控。当你想把 PydanticAI 换成 Claude Agent SDK,只要两者都在 SDK 层、都被 1.2 的统一 Protocol 包住,业务代码就不用动。如果当初把编排逻辑也写进了 SDK 调用里,替换就会牵一发动全身。
1.1.2 四层定位图
下面这张图是本节的核心产出。左列是层的名字,中列是职责,右列是本机已装、本卷要用的代表库。
┌──────────────────────────────────────────────────────────────────────┐
│ 协议层 Protocol 「怎么对话」——跨进程/跨厂商的线上约定 │
│ MCP (mcp 2.3.0 / fastmcp 4.1.0) A2A │
│ · Agent ↔ 工具/资源 · Agent ↔ Agent │
├──────────────────────────────────────────────────────────────────────┤
│ SDK 层 Agent SDK 「单个 Agent 怎么写」——提示/工具/输出模型 │
│ PydanticAI 2.55.0 · Claude Agent SDK 0.2.165 │
│ OpenAI Agents SDK 0.23.1 · smolagents 1.26.0 │
│ · 自带 run 循环(引擎层的一种实现) · 工具注册与权限 │
├──────────────────────────────────────────────────────────────────────┤
│ 编排层 Orchestration 「多步骤怎么跑」——图/状态机/检查点/恢复 │
│ LangGraph 1.2.14 (langchain-core 1.6.9) │
│ · StateGraph 节点与边 · checkpoint 持久化 · 中断与恢复 │
├──────────────────────────────────────────────────────────────────────┤
│ 引擎层 Engine 「一次推理循环怎么转」——messages↔ModelResponse │
│ 卷一的 minagent · 裸 provider SDK (anthropic 1.13.0 / openai 3.28.0)│
│ · 组装 messages · 调模型 · 解析 tool_calls · 回灌结果 · 收敛判断 │
└──────────────────────────────────────────────────────────────────────┘
↑ 下层不知道上层;上层通过接口调用下层(依赖倒置)
把两卷的产物钉上去,图就完整了:
| 产物 | 落在哪一层 | 说明 |
|---|---|---|
卷一 minagent | 引擎层 | 手写 loop:run(model, registry, user_input, ...) -> RunResult |
卷二 ResearchCopilot | 编排层 + 服务壳 | REST/队列/多租户在服务层;多步研究流程是编排 |
| PydanticAI | SDK 层(内含引擎) | Agent.run() 内部就是一个 loop |
| LangGraph | 编排层 | 它调 SDK 或裸模型,自己不做提示工程 |
| MCP / A2A | 协议层 | 只定义线上格式,不定义 loop |
关键洞察:SDK 层是「引擎层 + 提示/工具/输出」的打包。卷一你手写的循环,在 PydanticAI 里被封装成 Agent.run();在 Claude Agent SDK 里被封装成一次 query() 会话。理解这一点,本卷后面每一章「读源码」就有了统一的坐标——你要读的,无非是「这个 SDK 把引擎层的哪几件事替我做了」。
本卷的章节安排,本身就是沿这张图从上到下、再从下到上走一遍:
| 章节 | 主要动哪一层 | 一句话 |
|---|---|---|
| 第 2–4 章 | SDK 层 | 用四个 SDK 分别重写问答路径并同题对照 |
| 第 5 章 | 引擎层 + 编排层 | 读源码,把 SDK 藏起来的循环挖出来 |
| 第 6 章 | 编排层 | 补持久化与确定性重放 |
| 第 7 章 | SDK 层(扩展) | 计算机使用与浏览器 Agent |
| 第 8 章 | 协议层 | A2A 跨 Agent 协作 |
| 第 10–11 章 | 横切 | 可观测、成本、安全、平台化 |
1.1.3 真机佐证:import 各框架看归属
分层不能只靠嘴说。下面这段脚本在本机 /tmp/agentbook/venv3 里真实执行,逐个 import 并用 importlib.metadata 读版本、用 __file__ 读模块归属,用实测输出给分层背书:
import importlib
import importlib.metadata as md
LAYERS = {
"协议层": [("mcp", "mcp"), ("fastmcp", "fastmcp")],
"SDK 层": [("pydantic_ai", "pydantic-ai"), ("agents", "openai-agents"),
("smolagents", "smolagents"), ("claude_agent_sdk", "claude-agent-sdk")],
"编排层": [("langgraph", "langgraph"), ("langchain_core", "langchain-core")],
"引擎层": [("anthropic", "anthropic"), ("openai", "openai")],
}
for layer, pkgs in LAYERS.items():
print(f"== {layer} ==")
for modname, dist in pkgs:
mod = importlib.import_module(modname)
ver = md.version(dist)
path = getattr(mod, "__file__", None) or "<namespace package>"
leaf = path.split("site-packages/")[-1] if "site-packages/" in path else path
print(f" {dist:18s} v{ver:8s} {modname:16s} -> {leaf}")
真跑输出(Python 3.14.6):
== 协议层 ==
mcp v2.3.0 mcp -> mcp/__init__.py
fastmcp v4.1.0 fastmcp -> fastmcp/__init__.py
== SDK 层 ==
pydantic-ai v2.55.0 pydantic_ai -> pydantic_ai/__init__.py
openai-agents v0.23.1 agents -> agents/__init__.py
smolagents v1.26.0 smolagents -> smolagents/__init__.py
claude-agent-sdk v0.2.165 claude_agent_sdk -> claude_agent_sdk/__init__.py
== 编排层 ==
langgraph v1.2.14 langgraph -> <namespace package>
langchain-core v1.6.9 langchain_core -> langchain_core/__init__.py
== 引擎层 ==
anthropic v1.13.0 anthropic -> anthropic/__init__.py
openai v3.28.0 openai -> openai/__init__.py
两处细节值得记下来,它们是实测才能发现的:
openai-agents的 import 名是agents,不是openai_agents;claude-agent-sdk的 import 名是claude_agent_sdk(连字符转下划线)。凭记忆写 import 是本节最容易翻车的点,写错会直接ModuleNotFoundError。langgraph是 namespace package(__file__为None),它的实体在子模块里:langgraph.graph、langgraph.checkpoint。所以第 5.1 节读源码时要直接定位到langgraph/graph/,不能靠langgraph.__file__。
红线提醒:
claude_agent_sdk与agents的运行会真实发起网络请求、消耗用户额度。本节只做import与inspect,未执行任何query()/Runner.run()调用。第 3、4 章也遵守同一约束。
1.1.4 每层的典型失败模式
分层还有一个实用价值:它告诉你「出了问题该去哪一层找」。每一层都有自己的经典故障,认得出层,就找得到根因。
| 层 | 典型失败模式 | 症状 | 排查入口 |
|---|---|---|---|
| 引擎层 | 收敛判断错 / 工具结果回灌格式错 | 循环不结束,或提前结束 | 打印 Step 轨迹 |
| 编排层 | 检查点丢失 / 边条件写反 | 重启后从头跑,或走错分支 | 看 checkpoint 存储 |
| SDK 层 | 输出模型校验失败 / 工具 schema 不匹配 | 重试耗尽报错 | 看 SDK 的 retry 日志 |
| 协议层 | 握手失败 / 版本不兼容 | 连不上 Server / 字段缺失 | 看传输层报文 |
举个真实场景:服务重启后任务从头跑了一遍。这是编排层的检查点没落盘,不是引擎层的循环 bug。如果一开始没分层,你可能花一天去读 Agent.run 的源码,而问题其实在 checkpoint 的存储配置上。分层不是学术分类,是排障的第一把手术刀。
再比如「模型明明返回了工具调用,却没被执行」——这是引擎层的 tool_calls 解析或回灌出了问题,和 SDK、编排都无关。先定层,再定因,能省掉大量无效搜索。
1.1.5 怎么判断一个库属于哪一层
面对一个没见过的新库,用下面三个问题快速定位:
- 它有没有自己的 run 循环? 有 → 至少是引擎层或 SDK 层。没有、只是定义数据格式 → 协议层。
- 它管不管「多步骤的状态与恢复」? 管 → 编排层。不管、只关心单次交互 → SDK 层。
- 它能不能脱离任何具体模型厂商? 能、只定义线上约定 → 协议层。绑死某家 → 多半是 SDK 层。
用这三问过一遍:mcp 只定义工具调用格式、没有循环、与厂商无关 → 协议层。langgraph 有节点/边/检查点 → 编排层。pydantic_ai 有 Agent.run 但不管跨步骤恢复 → SDK 层。anthropic 只有 messages.create 一次调用、没有循环 → 引擎层的零件。
再补一条经验法则:一个库如果「换掉它你就要重写循环」,它是引擎/SDK 层;如果「换掉它你只是改流程编排」,它是编排层;如果「换掉它你只是改通信地址」,它是协议层。替换成本,就是层的指纹。
一个常见的误判:把 fastmcp 当成「又一个 Agent 框架」。它没有循环、不组装 messages、只把 Python 函数暴露成 MCP 工具——它是协议层的一个实现库,不是 SDK 层。用上面的三问过一遍就清楚了。
1.1.6 同一个任务在四层分别长什么样
抽象讲多了会飘,用同一个任务「回答一个问题」过一遍四层,分层立刻具体起来。下面是四段伪代码骨架(只为对照结构,不是本节要跑的代码),注意每一层关心的东西完全不同:
# 引擎层(卷一 minagent):只关心「循环怎么转」
result = run(model, registry, user_input="研究 X", max_steps=8)
# ↑ 组装 messages、调模型、解析 tool_calls、回灌、判收敛,全在这一个函数里
# SDK 层(PydanticAI):只关心「一个 Agent 怎么写」
agent = Agent(model, output_type=Report, tools=[search])
result = await agent.run("研究 X") # 循环被 SDK 藏起来了
# 编排层(LangGraph):只关心「多步骤怎么连、怎么恢复」
graph = StateGraph(State)
graph.add_node("plan", plan_node); graph.add_node("search", search_node)
graph.add_edge("plan", "search")
app = graph.compile(checkpointer=checkpointer) # 崩溃可从检查点续跑
# 协议层(MCP):只关心「工具怎么被外部 Agent 调用」
@server.tool()
def search(q: str) -> str: ... # 只定义线上接口,不含任何循环
对着这四段,前面几节的话就落地了:
- 引擎层那段把「循环」写死在调用里,换模型要动
run内部。 - SDK 层那段用
Agent.run把循环封掉,你拿到的是「一个能跑的 Agent」。 - 编排层那段根本不碰循环,它把 SDK 或引擎当节点用,价值在
checkpointer。 - 协议层那段连「谁在调、调几次」都不关心,它只把
search暴露成一个标准接口。
同一个「研究 X」,在四层里被切成四种不同的关注点。 这也是为什么「换框架」要先问「换哪一层」——引擎层和 SDK 层可以互相替换(都能跑单次循环),但它们都不能替掉编排层的检查点,也不能替掉协议层的线上约定。
1.1.7 与站内选型矩阵的分工
站内 /posts/llm/ 下已有 llm-agent-frameworks 一篇,那是一张选型矩阵:把各框架的特性列成表,帮你「挑一个」。本卷和它不是重复关系,而是接力关系:
| 维度 | 站内 llm-agent-frameworks | 本卷《AI Agent 高级编程》 |
|---|---|---|
| 视角 | 横向对比(选哪个) | 纵向拆解(怎么跑起来) |
| 深度 | 特性清单、优劣势 | 源码调用链、可运行重写 |
| 产出 | 一张对照矩阵 | 每节一段能跑的真实代码 |
| 落点 | 决策辅助 | 把卷二 ResearchCopilot 真重写一遍 |
换句话说:那篇帮你「决定用 PydanticAI」,本卷帮你「用 PydanticAI 把服务重写出来,并说清它内部发生了什么」。如果你还没做过选型,先读那篇;如果你已经定了框架要落地,本卷才是你要的。MCP 的协议规范、Server 实现、传输与 OAuth 一律不在本卷范围,遇到就链 /posts/mcp/。
1.1.8 协议层内部:MCP 管工具、A2A 管协作
协议层里有两个名字经常被混为一谈,值得先分清,第 8 章会展开:
| 协议 | 连接的是 | 解决什么 | 本卷落点 |
|---|---|---|---|
| MCP | Agent ↔ 工具/资源 | 让工具一次实现、到处复用 | 第 8.2 节与 /posts/mcp/ 分工 |
| A2A | Agent ↔ Agent | 让多个 Agent 互相委派任务 | 第 8.1 / 8.3 节 |
一句话区分:MCP 是「Agent 怎么用工具」,A2A 是「Agent 怎么找别的 Agent 帮忙」。二者都只定义线上格式,都不含循环。本卷不重讲 MCP 的协议规范(站内 /posts/mcp/ 已覆盖),只在第 8 章讲 A2A 与二者的分工。把它们放在定位图最上层,是因为它们不关心你下层用什么框架——这正是协议的价值:跨厂商、跨语言、跨进程。
1.1.9 这套框架替你做了什么、你又让渡了什么
分层图最终要落到一个务实的判断上。用 SDK 层替掉手写引擎,收益和代价是对称的:
框架替你做了:run 循环的收敛判断、工具 schema 生成与调用解析、输出模型校验与失败重试、消息历史管理、流式与取消。这些卷一你写了一整章的东西,在 SDK 里是一行 Agent(...).run(...)。
你让渡了:对循环内部细节的直接控制权。想改「什么时候算收敛」,在 minagent 里改三行;在 SDK 里可能要读源码找 hook 点,甚至根本改不动。这就是锁定度——本卷第 1.3 节会把它量化成打分维度。
所以本卷的姿势是:先用框架把东西跑起来,再读源码把让渡掉的控制权拿回来。第 5 章整章都在干这件事。分层图不是终点,是你决定「在哪一层用框架、在哪一层保留自研」的起点。
阅读导航:上一节:目录 · 下一节:1.2 统一抽象:一个 Protocol 适配多家并写适配层 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。