PERSONAL LAB / ai/agent开发
AI Agent 开发

12 框架与协议

岗位要求里 LangChain、LangGraph、Dify、MCP 这些名字出现得很多,但面试官真正想听的,通常不是”我会用某某框架”,而是”它帮你解决了什么、代价是什么、你为什么选或不选它”。这一篇把常见的框架、平台和协议放在一张地图上讲清各自的位置,重点讲 LangGraph 和 MCP 两个我项目里真用过的。

这篇要讲清楚:框架到底替你做了哪些事;LangChain、LangGraph、LlamaIndex、OpenAI Agents SDK、Claude Agent SDK、Dify / Coze 分别适合什么;LangGraph 的状态、节点、边、流式输出怎么用;MCP 的架构、三种能力、两种传输方式,以及 2026-07-28 版改成无状态之后的变化;A2A 协议和 MCP 的分工;什么时候用框架、什么时候自己写。

怎么读:

前置:04 工具调用(MCP 基础)、05 Agent 循环07 状态持久化(LangGraph 检查点)。

第一部分 速记页

问题一句话答案
框架替你做了什么模型接口统一、工具注册和参数解析、循环调度、状态和检查点、流式输出、链路追踪
框架的代价多一层抽象,出问题要读框架源码;版本变化快;控制流分散,不如一个 for 循环直观
LangChain组件库 + create_agent,v1 的 Agent 构建在 LangGraph 之上,用中间件加功能
LangGraph把 Agent 写成状态图:状态、节点、边;强项是检查点、中断恢复、人工审批
LlamaIndex偏”数据”:连接器、索引、查询引擎,做 RAG 起家,也有 Agent 和工作流
OpenAI Agents SDK轻量的多 Agent 框架:Agent、交接(handoff)、护栏、会话、追踪
Claude Agent SDK把 Claude Code 的循环、内置工具、上下文管理、权限、钩子、子 Agent 做成 Python / TypeScript 库
Dify / Coze可视化低代码平台,拖拽搭工作流、知识库、Agent,适合业务人员和快速上线
LangGraph 三要素状态(TypedDict)、节点(函数,返回状态更新)、边(普通边和条件边)
reducer规定状态字段怎么合并,比如 add_messages 是追加消息而不是覆盖
流式模式values 全量状态、updates 增量、messages 逐 token、custom 自定义事件等
MCP 是什么应用和工具服务之间的开放协议,基于 JSON-RPC
MCP 三个角色host(应用)、client(每个连一个 server)、server(提供能力)
MCP 三种能力tools 由模型控制、resources 由应用控制、prompts 由用户控制
MCP 两种传输stdio(本地子进程)、Streamable HTTP(远程)
2026-07-28 版变化协议无状态,每个请求自带版本和能力;去掉 initialize 握手,用 server/discover 发现能力
A2AAgent 和 Agent 之间通信的协议,Google 发起、捐给 Linux 基金会;核心是 Agent Card 和 Task
MCP vs A2AMCP 给一个 Agent 接工具;A2A 让多个 Agent 协作
Agent Skills一个带 SKILL.md 的文件夹,开头只给模型名字和描述,用到才读全文;Anthropic 2025-10 推出、2025-12 开放成标准(agentskills.io),Codex、Copilot、Cursor、Gemini CLI 等约 40 个产品支持
Skills vs MCPSkills 教模型”怎么做”,靠模型读文件、跑命令;MCP 给模型”能调什么”,按协议调服务。两者常一起用
什么时候自己写单 Agent、线性循环、要完全掌控每一步;手写循环几十行就够
什么时候用框架需要检查点、中断恢复、人工审批、复杂分支、多 Agent,或团队已经统一技术栈
我项目手写 65 行 vs LangGraph 126 行,同配置评测 30/30 vs 29/29 看不出差别;LangGraph 多给的是检查点和续跑

第二部分 易混对照

容易混的两个区别一句话记法
LangChain vs LangGraphLangChain 提供组件和现成的 Agent 构建函数;LangGraph 是底层的图执行引擎,LangChain v1 的 Agent 就跑在它上面成品家具 vs 木工工具
框架 vs 平台框架是代码库,写在自己程序里;平台(Dify、Coze)是可视化的应用,拖拽搭建写代码 vs 拖积木
节点 vs 超级步节点是图里的一个函数;超级步是一轮执行,这一轮能跑的节点都跑完算一个一个工位 vs 一个班次
普通边 vs 条件边普通边固定走向下一个节点;条件边由一个函数根据状态决定去哪单行道 vs 路口
updates vs values 流式模式前者只给每个节点改了什么;后者给每一步之后的完整状态变化量 vs 快照
MCP vs Function CallingFunction Calling 是应用和模型之间怎么表达”要调工具”;MCP 是应用和工具服务之间怎么通信04 篇讲过
MCP host vs clienthost 是整个应用,管权限和模型;client 是 host 里的连接对象,一个 client 只连一个 server公司 vs 对接某个供应商的专员
tools vs resourcestools 由模型决定调不调、会产生动作;resources 是数据,由应用决定要不要放进上下文模型伸手 vs 应用递过去
stdio vs Streamable HTTPstdio 由客户端启动子进程,通过标准输入输出通信,只在本地;HTTP 可以远程、多客户端内线电话 vs 外线电话
MCP vs A2AMCP 连工具和数据;A2A 连另一个完整的 Agent,对方内部怎么做不暴露用工具 vs 找同事
交接 vs 子 Agent 当工具交接是把对话控制权交给另一个 Agent;当工具是调用它、拿回结果、控制权还在自己转接电话 vs 请人帮忙查一下

第三部分 面试口述稿

3.1 “你为什么没用框架,而是自己写的循环?”

一开始自己写,是因为这个任务是单 Agent、线性的:调模型、执行工具、把结果放回去、重复,加上步数上限和卡住检测,六十多行就写完了。每一步做了什么一眼能看清,出问题直接打断点,不用去读框架源码。

后来我用 LangGraph 把同一个循环重写了一遍,为了对比公平,两个版本共用工具、提示词、重复调用检测和验收,只换循环的写法。同样的配置各跑三轮,手写 30 次全过,LangGraph 29 次全过,另外有一次是 API 余额不足崩了,不计入。步数和 token 也看不出差别,这符合预期,因为模型和工具都一样。

差别在工程上:LangGraph 版代码多了一倍,一百二十多行,控制流分散在几个路由函数里;但它每一步自动存检查点,进程被杀之后可以用 thread_id 接着跑。我实测过在第 12 步杀掉进程,续跑从第 13 步开始,已经执行过的工具调用没有重复。

所以我的结论是:简单线性的 Agent 手写更直接;需要中断恢复、人工审批、复杂分支的时候,LangGraph 是有实际价值的。

3.2 “LangGraph 的核心概念是什么?”

它把 Agent 写成一张有状态的图。三个要素:状态、节点、边。

状态是一个类型化的字典,定义了图里流转的所有数据,比如消息列表、当前步数。每个字段可以指定 reducer,决定更新怎么合并,最常用的 add_messages 是把新消息追加进列表,而不是覆盖。

节点是普通函数,接收当前状态,返回要更新的字段。

边决定下一步去哪。普通边是固定的,条件边由一个函数根据状态返回下一个节点的名字。一个典型的 Agent 就是两个节点:调模型的节点和执行工具的节点,模型节点后面接条件边,有工具调用就去工具节点,没有就结束,工具节点再连回模型节点。

编译时传入检查点保存器,每个超级步结束自动保存状态,同一个 thread_id 可以恢复。输出方面,stream 支持几种模式,updates 看每个节点改了什么,messages 拿逐 token 的模型输出,custom 可以在节点里用 get_stream_writer 发自定义事件,我项目推给前端的进度就是用这个。

3.3 “MCP 是什么?架构是怎样的?”

MCP 是 AI 应用和工具服务之间的开放协议,基于 JSON-RPC。它解决的问题是工具复用:一个工具写成 MCP server,任何支持 MCP 的应用都能接,不用每个应用各写一遍。

架构上三个角色:host 是 AI 应用本身,负责管理连接、权限、调用模型;client 由 host 创建,每个 client 只连一个 server;server 提供具体能力。规范特意要求 server 看不到完整对话,也看不到其他 server,对话历史留在 host 里。

server 能提供三种东西:tools 是模型决定调用的函数;resources 是数据,由应用决定要不要放进上下文;prompts 是提示词模板,由用户选择使用,比如斜杠命令。

传输有两种,stdio 是客户端启动子进程、通过标准输入输出通信,适合本地;Streamable HTTP 是每条消息一个 POST 请求,适合远程服务。

我项目写了一个 MCP server,直接复用 Agent 的工具函数和边界检查,所以任何支持 MCP 的客户端接上它,拿到的都是和我自己 Agent 一样的工具和限制。测试里用 SDK 的客户端连上去,验证了越界访问会返回 isError 结果。

3.4 “MCP 和 A2A 有什么区别?”

MCP 解决的是一个 Agent 怎么接工具和数据;A2A 解决的是 Agent 和 Agent 之间怎么协作。A2A 官方文档自己的说法也是两者互补:用 MCP 给单个 Agent 配工具,用 A2A 让这个 Agent 和其他 Agent 协作。

区别在于对方是不是”不透明的”。MCP 的 server 暴露的是具体函数,调用方知道参数和返回;A2A 里对方是一个完整的 Agent,你只知道它的 Agent Card 上写的能力,发一个任务过去,它内部用什么模型、什么工具你不知道,它可能要执行很久,中间还可能回来问你要补充信息。所以 A2A 的核心对象是 Task,有提交、执行中、需要输入、完成、失败这些状态,支持流式更新和推送通知。

A2A 是 Google 发起的,后来捐给了 Linux 基金会。我项目是单 Agent,没用到。

3.5 “Dify、Coze 这种平台和 LangGraph 怎么选?”

看谁来做、要做多深。

Dify 和 Coze 是可视化平台,拖拽搭工作流、接知识库、配工具,自带对话界面和 API,业务人员也能上手,适合流程比较固定、要快速上线的场景,比如内部知识库问答、简单的客服流程。两者都开源、能自己部署。

局限是遇到平台没提供的能力就很难扩展,复杂的分支、自定义的状态管理、细粒度的评测和调试都受平台限制,版本管理和代码审查也不如代码方便。

LangGraph 这类代码框架灵活,能做任意复杂的控制流,能接进自己的测试、CI、监控,但需要工程师来写和维护。

实际团队里两者经常并存:用平台快速验证想法、做简单场景,验证有价值、复杂度上来之后再用代码重写核心流程。

3.6 “你怎么看框架选型?”

我会问几个问题。

第一,任务结构。单 Agent 线性循环,手写就够;有复杂分支、并行、多 Agent、需要人工审批和中断恢复,框架价值大。

第二,要不要持久化。长任务、要能从中间恢复,自己写检查点要处理的细节很多,这是 LangGraph 最实在的价值。

第三,团队和生态。团队已经统一用某个框架,接入观测平台、评测工具都现成,就跟着用。

第四,锁定成本。模型接口、工具定义尽量保持框架无关,比如工具按 MCP 暴露、观测按 OpenTelemetry 埋点,以后换框架代价小。我项目的工具层就是独立的,手写循环、LangGraph、MCP server 三个入口共用同一套工具函数。

还有一点是别被框架的默认值骗了。我用 LangGraph 时发现很多教程说递归上限默认 25,实际 1.2 版是 10007,框架版本变化快,关键参数要自己实测,并且显式设置。

第四部分 逐个详解

4.1 框架到底替你做了什么

回忆 05 篇手写的 Agent 循环,一个能用的 Agent 至少要处理这些事:

flowchart LR
    subgraph 必须有
        A[调模型<br/>各家接口不同] --> B[解析工具调用<br/>参数 JSON 可能不合法]
        B --> C[执行工具<br/>异常转成结果]
        C --> D[结果放回消息]
        D --> E{停止条件}
        E -->|继续| A
    end
    subgraph 迟早要有
        F[流式输出]
        G[状态持久化 / 续跑]
        H[人工审批 暂停恢复]
        I[链路追踪]
        J[多 Agent 调度]
    end

左边的核心循环,手写几十行就行;右边这些,每一样自己做都要花不少功夫,而且容易出错(07 篇讲过检查点和幂等有多少细节)。框架的价值主要在右边。

框架能帮的自己写的难点
统一不同厂商的模型接口各家消息格式、工具调用格式、token 字段不一样
从函数签名自动生成工具 Schema手写 JSON Schema 繁琐易错
检查点和续跑什么时候存、存什么、崩溃窗口、恢复后不重复执行
暂停等人工确认暂停时不能占着进程,要存盘,恢复时要接上
流式事件模型 token、工具进度、自定义事件混在一起推
追踪自动记录每次模型调用和工具调用

代价:

代价我项目里的体会
多一层抽象,排错要读框架源码LangGraph 同步 stream 在线程池里跑节点,原来的 SQLite 连接跨线程报错,第一次运行(run 162)直接崩了
控制流分散手写是一个 for 循环加几个 break;图版本要在路由函数之间跳着看
代码量不一定变少手写 65 行,LangGraph 126 行(含状态定义、续跑、路由函数)
版本变化快,资料过时递归上限很多教程说默认 25,实测 LangGraph 1.2 是 10007
框架的边角约定LangChain 把 JSON 解析失败的工具调用放在 invalid_tool_calls 里,不回 ToolMessage 下一次请求就会被 API 拒绝

4.2 框架和平台地图

flowchart TD
    subgraph 可视化平台
        DIFY[Dify]
        COZE[Coze]
    end
    subgraph Agent 框架
        LC[LangChain<br/>create_agent + 中间件]
        OAI[OpenAI Agents SDK]
        CAS[Claude Agent SDK]
        LI[LlamaIndex<br/>偏数据和检索]
    end
    subgraph 编排引擎
        LG[LangGraph<br/>状态图 检查点]
    end
    subgraph 协议
        MCP[MCP<br/>Agent ↔ 工具]
        A2A[A2A<br/>Agent ↔ Agent]
    end
    LC -->|构建在其上| LG
    LC -.-> MCP
    OAI -.-> MCP
    CAS -.-> MCP
    LG -.-> MCP

下面每个只讲清”是什么、适合什么”,内容来自各自的官方文档(2026-09 查阅)。

LangChain 是怎么来的,在 07 篇 4.5 节讲过:LangChain 是 Harrison Chase 2022 年 10 月开源的,最早用来把”提示词模板 → 模型 → 解析输出”串成一条链;补充一点,他当时在机器学习公司 Robust Intelligence 工作,一个多月后 ChatGPT 发布,大量开发者涌进来,LangChain 成了当时用得最多的框架之一,2023 年 4 月成立了公司(LangChain 的历史)。

LangChain

  • v1 的核心是 create_agent:把模型、工具、提示词、**中间件(middleware)**组合成一个 Agent。中间件用来逐步加功能,官方举的例子有护栏、重试、路由、自定义工具策略
  • 官方文档明确说 LangChain 的 Agent 构建在 LangGraph 之上,因此自带持久化、人工介入这些能力
  • 官方给的选择建议:想要开箱即用、带上下文压缩和子 Agent 的,用 Deep Agents;想要高度可定制的,用 LangChain;需要把确定性流程和 Agent 行为组合在一起的复杂场景,用 LangGraph
  • 岗位里出现最多(00 篇统计 54 个岗位里 16 次),面试时至少要能说清它和 LangGraph 的关系

LangGraph:见 4.3 节,我项目用过。

LlamaIndex

  • 官方定位是面向自有数据构建 Agent 和工作流的数据框架,核心是”上下文增强”:让模型能用上私有数据
  • 组件:数据连接器(接 API、数据库、PDF)、索引、查询引擎和对话引擎、Agent、事件驱动的工作流
  • 配套的云服务有文档解析 LlamaParse 等
  • 适合 RAG 为主、数据源多、文档解析复杂的项目

OpenAI Agents SDK

  • 轻量的多 Agent 框架,几个核心概念:Agent(带指令和工具的模型)、交接(handoff)(把任务委派给另一个 Agent)、护栏(guardrails)(校验输入输出)、会话(sessions)(跨轮保存上下文)、追踪(tracing)
  • 默认用 OpenAI 模型,也可以配置其他厂商
  • 适合需要几个角色分工、互相转交的场景,13 篇多 Agent 会用到”交接”这个概念

Claude Agent SDK

  • 把 Claude Code 背后的 Agent 循环、内置工具(读写编辑文件、执行命令、搜索网页)、上下文管理做成 Python 和 TypeScript 库
  • 还提供钩子(在循环的关键节点运行自定义代码)、子 Agent、MCP、权限控制(哪些工具自动执行、哪些要批准)、会话(可以恢复或分叉)
  • 和前面几个的区别:它自带一整套”能干活”的工具和循环,适合要做编码、文件处理类 Agent,不想自己实现工具循环的场景

Dify

  • 开源的大模型应用开发平台,许可证是在 Apache 2.0 基础上加了附加条件的 Dify 开源许可证
  • 可视化工作流、支持大量商业和开源模型、RAG 流水线、带内置工具的 Agent、监控和 API
  • 可以用 Docker 自己部署

Coze(扣子)

  • 字节跳动的 Agent 开发平台,开源版叫 Coze Studio,Apache 2.0 许可证,后端用 Go
  • 可视化工作流、插件、知识库、Agent 搭建和调试

平台和框架的取舍:

可视化平台代码框架
上手快,业务人员也能用需要工程师
灵活性受平台能力限制任意控制流
调试平台界面断点、日志、自己的测试
评测和 CI平台提供什么就是什么能接进自己的流程
版本管理平台导出 / 版本功能git、代码审查
适合流程固定、快速验证、内部工具核心业务、复杂逻辑、要长期维护

4.3 LangGraph 核心概念

LangGraph 为什么出现(LangChain 早期的 AgentExecutor 把循环包死了,改不动、存不了档,2024 年 1 月团队发布了允许有环的图),在 07 篇 4.5 节讲过,这里不重复。

三要素:

概念是什么我项目里
状态(State)一个 TypedDict,定义图里所有流转的数据消息、项目路径、步数、重复调用计数、报错类型、状态、是否通过、验收日志
节点(Node)普通函数,参数是当前状态,返回要更新的字段agent(调模型)、tools(执行工具)、verify(独立验收)
边(Edge)普通边:固定去某个节点;条件边:一个函数根据状态返回下一个节点名after_agentafter_tools 两个路由函数

TypedDict:Python typing 模块里的一种类型写法,声明”这个字典有哪些键、每个键是什么类型”。它运行时就是普通字典,类型信息给编辑器和框架看。

reducer(合并函数):节点只返回”要改哪些字段”,框架把它合并进状态。默认是覆盖;想要别的合并方式,用 Annotated[类型, 合并函数] 指定:

class State(TypedDict):
    messages: Annotated[list, add_messages]
    step: int
  • step 没有指定,节点返回 {"step": 3} 就直接覆盖
  • messages 指定了 add_messages,节点返回 {"messages": [新消息]}追加到已有列表后面,而不是把整个列表换掉。add_messages 还会按消息 ID 去重,ID 相同的新消息会替换旧的
  • Annotatedtyping 里的写法,给类型附加额外信息。Annotated[list, add_messages] 意思是”类型是 list,附带信息是 add_messages”,LangGraph 读到这个附带信息就知道怎么合并

我项目的图:

flowchart TD
    START([START]) --> agent
    agent -->|after_agent:模型没调工具| verify
    agent -->|after_agent:有工具调用| tools
    tools -->|after_tools:卡住或到步数上限| verify
    tools -->|after_tools:否则| agent
    verify --> END([END])

对应的代码(agent/graph_agent.py):

def after_agent(state: State):
    return "verify" if state["status"] == "agent_done" else "tools"

def after_tools(state: State):
    return "verify" if state["status"] == "stuck" or state["step"] >= core.MAX_STEPS else "agent"

g = StateGraph(State)
g.add_node("agent", agent)
g.add_node("tools", tools)
g.add_node("verify", verify)
g.add_edge(START, "agent")
g.add_conditional_edges("agent", after_agent, ["tools", "verify"])
g.add_conditional_edges("tools", after_tools, ["agent", "verify"])
g.add_edge("verify", END)
return g.compile(checkpointer=checkpointer)
  • add_conditional_edges 的第三个参数列出这个路由函数可能返回的节点名,框架用它画图、检查
  • compile(checkpointer=...) 编译成可执行的图,传入检查点保存器(07 篇讲过)

和手写版的对照:

手写循环里的LangGraph 里的
for step in range(1, MAX_STEPS + 1)tools → agent 的边形成循环,after_tools 检查步数
if not msg.tool_calls: breakafter_agent 返回 "verify"
卡住检测后 breaktools 节点设 status = "stuck"after_tools 路由到 verify
局部变量 messagesseenState 里的字段,每步自动存检查点
yield 事件给前端节点里 get_stream_writer()(事件),外面 stream_mode="custom"
进程退出状态就没了--resume <thread_id> 续跑

流式模式官方文档):

模式输出什么
values每一步之后的完整状态
updates每个节点返回的更新
messages模型逐 token 的输出和元数据
custom节点里通过 get_stream_writer() 写出的自定义数据
checkpoints检查点事件(需要检查点保存器)
tasks任务开始、结束事件
debug以上能拿到的全部信息

stream_mode 可以传一个列表同时要多种。文档里还介绍了 version="v2" 的统一输出格式,每块是一个带 type 字段的字典;下面的实验用的是默认格式,同时要多种模式时每块是 (模式, 数据) 元组。

4.4 动手实验 1:一个最小的 LangGraph Agent

用 LangGraph 自带的 ToolNodetools_condition,配一个写死剧本的假模型,看图怎么跑、流式输出长什么样。保存为 lg_min.py,用项目虚拟环境运行。

from typing import Annotated, TypedDict

from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.tools import tool
from langgraph.config import get_stream_writer
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition


@tool
def run_command(command: str) -> str:
    """执行一条构建命令"""
    if "build" in command and "-B" not in command:
        return "[exit=0] 编译完成"
    return "[exit=1] 找不到 zlib" if "ZLIB" not in command else "[exit=0] 配置完成"


SCRIPT = [
    AIMessage("", tool_calls=[{"name": "run_command", "args": {"command": "cmake -B build"}, "id": "c1"}]),
    AIMessage("", tool_calls=[{"name": "run_command", "args": {"command": "cmake -B build -DZLIB_ROOT=deps"}, "id": "c2"}]),
    AIMessage("", tool_calls=[{"name": "run_command", "args": {"command": "cmake --build build"}, "id": "c3"}]),
    AIMessage("编译成功"),
]


class State(TypedDict):
    messages: Annotated[list, add_messages]
    step: int


def agent(state: State):
    step = state["step"] + 1
    msg = SCRIPT[step - 1]
    get_stream_writer()(f"第 {step} 步,模型回复 {len(msg.tool_calls)} 个工具调用")
    return {"messages": [msg], "step": step}


g = StateGraph(State)
g.add_node("agent", agent)
g.add_node("tools", ToolNode([run_command]))
g.add_edge(START, "agent")
g.add_conditional_edges("agent", tools_condition)
g.add_edge("tools", "agent")
graph = g.compile()

inputs = {"messages": [HumanMessage("编译这个项目")], "step": 0}
for mode, chunk in graph.stream(inputs, stream_mode=["updates", "custom"]):
    if mode == "custom":
        print("custom ", chunk)
    else:
        for node, update in chunk.items():
            last = update["messages"][-1]
            print("updates", node, "->", last.type, (last.content or last.tool_calls[0]["args"]["command"])[:30])

final = graph.invoke(inputs)
print("消息数", len(final["messages"]), "步数", final["step"])
print(graph.get_graph().draw_mermaid())

实际输出(Python 3.13,langgraph 1.2.11):

custom  第 1 步,模型回复 1 个工具调用
updates agent -> ai cmake -B build
updates tools -> tool [exit=1] 找不到 zlib
custom  第 2 步,模型回复 1 个工具调用
updates agent -> ai cmake -B build -DZLIB_ROOT=dep
updates tools -> tool [exit=0] 配置完成
custom  第 3 步,模型回复 1 个工具调用
updates agent -> ai cmake --build build
updates tools -> tool [exit=0] 编译完成
custom  第 4 步,模型回复 0 个工具调用
updates agent -> ai 编译成功
消息数 8 步数 4
---
config:
  flowchart:
    curve: linear
---
graph TD;
	__start__([<p>__start__</p>]):::first
	agent(agent)
	tools(tools)
	__end__([<p>__end__</p>]):::last
	__start__ --> agent;
	agent -.-> __end__;
	agent -.-> tools;
	tools --> agent;
	classDef default fill:#f2f0ff,line-height:1.2
	classDef first fill-opacity:0
	classDef last fill:#bfb6fc

逐段讲。

定义工具。

  • @tool:LangChain 的装饰器,把普通函数变成模型能调用的工具。它读取函数名当工具名、文档字符串当描述、类型注解生成参数 Schema
  • 文档字符串(docstring):函数定义下面第一行用三引号写的字符串,这里是 """执行一条构建命令"""。用 @tool 时必须写,否则工具没有描述会报错
  • 函数体是假的:根据命令文字返回写死的结果,模拟”第一次配置找不到 zlib,加上 ZLIB_ROOT 后成功”

假模型的剧本。

  • AIMessage(内容, tool_calls=[...]):LangChain 的”模型回复”消息类型。tool_calls 里每一项是一个字典,有 nameargsid,这是 LangChain 统一的格式,不管底层是哪家模型
  • 前三条各要求调一次工具,第四条没有工具调用,表示结束

agent 节点。

  • 参数 state 是当前状态,返回的字典是更新。返回 {"messages": [msg]},由于 add_messages,消息被追加
  • get_stream_writer():拿到一个”写入函数”,调用它写的内容会出现在 custom 模式的流里。它只能在图运行时的节点里调用

建图。

  • ToolNode([run_command]):LangGraph 预置的工具节点。它读最后一条消息的 tool_calls,执行对应工具,把每个结果包装成 ToolMessage(带着对应的 tool_call_id)返回
  • tools_condition:预置的路由函数。最后一条消息有工具调用就返回 "tools",没有就返回 END。所以这里 add_conditional_edges 不用写第三个参数
  • add_edge("tools", "agent"):工具执行完回到模型,形成循环

运行。

  • graph.stream(inputs, stream_mode=["updates", "custom"]):返回一个生成器,一边运行一边产出数据。同时要两种模式时,每次产出 (模式名, 数据) 元组,用 for mode, chunk in ... 拆开
  • updates 模式的数据是 {节点名: 这个节点返回的更新},所以要 chunk.items() 再拆一层
  • last.type:消息类型,ai 是模型消息,tool 是工具结果
  • last.content or last.tool_calls[0]["args"]["command"]or 在左边为空字符串时取右边。模型消息有工具调用时内容是空的,就显示命令
  • graph.invoke(inputs):不要流式,直接跑完返回最终状态。这里重新跑了一遍,所以剧本又从头开始
  • graph.get_graph().draw_mermaid():导出图的 mermaid 代码,虚线表示条件边

看输出。

  1. custom 事件总是出现在同一节点的 updates 之前,因为它是节点运行过程中写出的,而 updates 是节点返回之后才有的。我项目用 custom 推送工具开始执行这类进度,就是为了让前端在工具跑完之前就能看到
  2. 消息数 8 = 1 条用户消息 + 4 条模型消息 + 3 条工具结果
  3. 导出的 mermaid 图和 4.3 节画的结构一致:从 agent 出发有两条虚线(条件边)

自己改一改:

  1. stream_mode 改成 "values"(单个字符串,不是列表),看每次输出的内容变成什么
  2. 把剧本第一条改成一次要两个工具调用(tool_calls 列表里放两个),看 ToolNode 怎么处理
  3. 编译时加上 checkpointer=InMemorySaver()(从 langgraph.checkpoint.memory 导入),调用时传 config={"configurable": {"thread_id": "t1"}},跑完后用 graph.get_state(config).values["step"] 查状态

4.5 MCP 架构

04 篇讲了 MCP 在工具调用里的位置、和 Function Calling 的分工。这里讲架构和协议本身,内容来自 MCP 规范 2026-07-28 版

三个角色:

flowchart LR
    subgraph HOST["Host 进程(AI 应用)"]
        H[Host<br/>管权限 调模型<br/>持有完整对话]
        C1[Client 1]
        C2[Client 2]
        C3[Client 3]
        H --> C1
        H --> C2
        H --> C3
    end
    C1 --> S1[Server:文件和 Git<br/>本地]
    C2 --> S2[Server:数据库<br/>本地]
    C3 --> S3[Server:外部 API<br/>远程]
角色职责
Host创建和管理多个 client;控制连接权限和生命周期;执行安全策略和用户授权;协调模型调用;汇总各 client 的上下文
Client由 host 创建,每个只连一个 server;在每个请求上附带协议版本和能力
Server通过三种原语提供能力;可以是本地进程,也可以是远程服务

规范列出的设计原则里,有一条和安全直接相关:server 不能读到完整对话,也不能”看到”其他 server。server 只拿到必要的上下文,完整对话留在 host 里,跨 server 的交互由 host 控制。这意味着一个恶意或被攻破的 MCP server,接触不到别的 server 的数据,也读不到用户和模型的全部聊天。

三种原语,按”谁来控制”区分:

原语谁控制是什么例子
Prompts用户用户主动选择的交互模板斜杠命令、菜单选项
Resources应用由客户端附加和管理的上下文数据文件内容、git 历史
Tools模型暴露给模型、用来执行动作的函数发 POST 请求、写文件

这张表是理解 MCP 的关键:不是所有东西都做成工具。只读的背景资料更适合做成 resource,由应用决定什么时候放进上下文,不用让模型每次想起来去调用;常用的任务模板做成 prompt,给用户点选。

2026-07-28 版的重要变化:协议无状态。 规范原文的说法是 MCP 是一个无状态协议,每个请求都是自包含的,自带协议版本和能力。和早期版本对比:

早期版本2026-07-28 版
建立连接initialize 握手,建立连接级的会话没有握手;客户端可以先调 server/discover 发现服务端支持的版本和能力
客户端能力握手时声明一次每个请求都在 _meta 里带上
服务端向客户端要东西(让客户端调模型、问用户、查根目录)服务端主动发 JSON-RPC 请求服务端不能主动发请求,而是在回复里返回 InputRequiredResult,客户端补上信息后重发原请求
订阅通知连接上直接推客户端打开 subscriptions/listen

为什么这很重要? 无状态意味着远程 MCP server 可以像普通 Web 服务一样放在负载均衡后面,任意一台机器都能处理任意请求,不用把同一个客户端粘在同一台机器上。网上大量资料讲的还是 initialize 握手那一套,面试时如果说到这一点,要说清楚是哪个版本。规范也写了和旧版本互相兼容的回退办法。

两种传输方式规范传输一节):

传输怎么通信取消请求适合
stdio客户端启动 server 子进程,通过标准输入输出传换行分隔的 JSON-RPC 消息notifications/cancelled 通知本地工具
Streamable HTTP每条消息是一个发往单一 MCP 端点的 HTTP POST,回复是一个 JSON 对象或者这个请求专属的 SSE 流关闭这个请求的响应流远程服务、多客户端

规范强调:协议语义在所有传输上完全一样,传输只管消息怎么打包和送达。所以同一个 server 代码,换个启动参数就能从 stdio 变成 HTTP 服务。

tools 的错误分两类(04 篇讲过,这里再强调一次):工具不存在、请求格式错误走 JSON-RPC 协议错误;参数校验失败、业务失败走带 isError: true 的正常结果,交给模型自己修正。

4.6 动手实验 2:一个 MCP server 的三种原语

用项目虚拟环境里的 mcp SDK(2.2.0),在同一个进程里起一个 server、用客户端连上,把 tools、resources、prompts 都走一遍。保存为 mcp_min.py

import asyncio

from mcp import Client
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

server = MCPServer("demo-build", instructions="演示用的构建工具服务")


@server.tool(description="读取项目里的文件")
def read_file(path: str) -> str:
    if path.startswith("/") or ".." in path:
        raise ToolError(f"路径越界: {path}")
    return f"{path} 的内容"


@server.resource("build://targets", description="支持的目标平台")
def targets() -> str:
    return "aarch64-linux-musl\nx86_64-linux-musl"


@server.prompt(description="交叉编译任务模板")
def cross_build(project: str, target: str = "aarch64-linux-musl") -> str:
    return f"把 {project} 交叉编译到 {target},完成后说明产物位置。"


async def main():
    async with Client(server) as c:
        print("协议版本", c.protocol_version, "服务", c.server_info.name)
        print("能力", sorted(k for k, v in c.server_capabilities.model_dump().items() if v))
        for t in (await c.list_tools()).tools:
            print("工具", t.name, t.input_schema)
        ok = await c.call_tool("read_file", {"path": "CMakeLists.txt"})
        bad = await c.call_tool("read_file", {"path": "../secret"})
        print("调用", ok.is_error, ok.content[0].text, "|", bad.is_error, bad.content[0].text)
        print("资源", [str(r.uri) for r in (await c.list_resources()).resources])
        res = await c.read_resource("build://targets")
        print("读资源", res.contents[0].text.splitlines())
        p = await c.get_prompt("cross_build", {"project": "zlib"})
        print("提示词", p.messages[0].role, p.messages[0].content.text)


asyncio.run(main())

实际输出(Python 3.13,mcp 2.2.0):

协议版本 2026-07-28 服务 demo-build
能力 ['prompts', 'resources', 'tools']
工具 read_file {'type': 'object', 'properties': {'path': {'title': 'Path', 'type': 'string'}}, 'required': ['path'], 'title': 'read_fileArguments'}
调用 False CMakeLists.txt 的内容 | True Error executing tool read_file: 路径越界: ../secret
资源 ['build://targets']
读资源 ['aarch64-linux-musl', 'x86_64-linux-musl']
提示词 user 把 zlib 交叉编译到 aarch64-linux-musl,完成后说明产物位置。

另外 stderr 上还有一行 server 打的 INFO 日志:Tool 'read_file' failed: ... 路径越界: ../secret

这里的路径检查是演示用的简化写法,真实项目要按 11 篇的 resolve + is_relative_to 来写。

逐段讲。

server 端。

  • MCPServer("demo-build", instructions=...):创建 server,instructions 是给客户端(和模型)看的使用说明
  • @server.tool(description=...):注册工具。SDK 读函数的类型注解生成 inputSchema,所以输出里 pathstringrequired
  • raise ToolError(...):SDK 自己的异常类。我看了 SDK 源码里这个类的说明:抛它表示”预料之中的失败”,调用返回 is_error=True,消息原样给模型读;抛其他任何异常都会被当成崩溃,模型只能看到 Error executing tool <工具名>,看不到具体原因。所以项目 mcp_server.py 里的 guarded 要把 tools.ToolError 转成 SDK 的 ToolError,否则模型拿不到”路径越界”这种能指导下一步的信息
  • @server.resource("build://targets", ...):注册资源。第一个参数是资源的 URI(统一资源标识符,一个唯一的地址字符串),build:// 这种写法是自定义的,只要唯一就行
  • @server.prompt(...):注册提示词模板,函数参数就是模板参数,返回的字符串成为一条用户消息

client 端。

  • async def main()异步函数。里面可以用 await 等待耗时的操作(网络、子进程通信)而不阻塞整个程序。异步函数不能直接调用,要交给 asyncio.run(main()) 运行。14 篇会细讲异步
  • async with Client(server) as c:异步上下文管理器,和 with 一样保证连接用完关闭,只是进入和退出时要 await。这里把 server 对象直接传给 Client,SDK 在同一进程里用内存通道连接,不启动子进程,适合测试。项目 tests/test_mcp.py 里两种都测了:内存连接测工具逻辑,StdioServerParameters 启动真实子进程测命令行入口
  • await c.list_tools():列出工具;.tools 是工具列表,每个有 nameinput_schema
  • c.server_capabilities.model_dump()server_capabilities 是一个 Pydantic 模型,model_dump() 转成字典。字典推导里 if v 只保留非空的能力
  • bad.is_errorTrue,内容里带着原因,这就是 4.5 节说的”业务失败走正常结果”

看输出。

  1. 协议版本是 2026-07-28,就是 4.5 节讲的无状态版本
  2. 服务端能力自动包含了 promptsresourcestools,因为三种都注册了
  3. 越界调用没有让客户端抛异常,而是一个 is_error=True 的结果,模型可以读到”路径越界”然后换个路径
  4. 提示词模板渲染成了一条 user 角色的消息,默认参数 target 自动填上了

自己改一改:

  1. read_file 里把 raise ToolError(...) 改成 raise ValueError(...),看 bad.content[0].text 变成什么,体会为什么要用 SDK 的 ToolError
  2. read_file 加一个参数 offset: int = 1,看 input_schema 的变化
  3. 再注册一个资源 build://toolchain,返回 toolchain 文件的内容,然后列出资源确认

4.7 我项目的 MCP server

agent/mcp_server.py(89 行)把 Agent 用的 5 个工具暴露成 MCP server。设计上有三点:

1. 复用同一套工具函数。

flowchart TD
    T["agent/tools.py<br/>list_files / read_file / write_file / run_command<br/>路径检查 白名单 凭据拦截 截断"]
    T --> A["build_agent.py<br/>手写循环"]
    T --> G["graph_agent.py<br/>LangGraph"]
    T --> M["mcp_server.py<br/>MCP server"]
    M --> C1[任意 MCP 客户端]

三个入口共用 tools.py边界检查只写一份。11 篇讲过的”两个工具功能重叠,防护就只有弱的那个那么强”,放到入口层面也一样:如果 MCP 版本自己另写一套检查,迟早会和 Agent 版本不一致。工具描述也是从 tools.SCHEMAS 里读的,保证模型看到的说明一致。

2. 工作目录在启动时固定。

def create_server(workdir, kb_mode="bm25"):
    workdir = Path(workdir).resolve()
    ...
    @server.tool(description=DESCRIPTIONS["read_file"])
    def read_file(path: str, offset: int = 1, limit: int = tools.MAX_LINES) -> str:
        return guarded(tools.read_file, workdir, path, offset, limit)

workdir 不是工具参数,而是 create_server 的局部变量,被里面定义的函数”记住”了。这种写法叫闭包(closure):内层函数可以使用外层函数的变量,外层函数返回后这些变量依然有效。好处是模型没法通过参数改工作目录。

3. 知识库按需加载。

kb = {}

@server.tool(description="交叉编译经验库。...")
def search_knowledge(query: str) -> str:
    if "kb" not in kb:
        kb["kb"] = knowledge.load(kb_mode)
    return kb["kb"].format(kb["kb"].search(query, topk=3))

向量检索要加载 bge-m3 模型,比较慢。第一次调用时才加载,存进字典 kb 里,之后直接用。用字典而不是普通变量,是因为内层函数里给外层变量重新赋值需要 nonlocal 声明,而修改字典的内容不需要。

测试tests/test_mcp.py)验证了:5 个工具都在;正常读文件成功;read_file("../secret.txt")run_command("cmake -E cat ../secret.txt") 都返回 is_error=True 且带原因;写文件成功;知识库能查到 k002;以 --kb none 通过 stdio 启动时没有 search_knowledge,且 .xbuild/zig.cmake 已经准备好。

局限:和 Agent 版本完全一样,11 篇讲的脚本绕过、环境变量泄露在 MCP 版本里同样存在;而且 MCP 客户端是别人的程序,调用前是否需要用户确认、调用记录怎么留,都由客户端决定,server 这边控制不了。

4.8 A2A:Agent 之间的协议

先说为什么会有 A2A。 MCP(2024 年 11 月,来历见 04 篇 4.11 节)解决了”Agent 怎么接工具”。到 2025 年,大公司内部开始有很多 Agent:报销的、招聘的、客服的,分别由不同团队、不同厂商、用不同框架做出来。让它们互相配合,要么每两个之间写一套专门的对接代码,要么把对方当成一个普通工具来调,可对方是个会自己规划、可能要跑几小时、中途还要问用户问题的 Agent,工具调用那种”一问一答”装不下。2025 年 4 月 9 日,Google 在 Cloud Next 大会上发布了 A2A,当时有 50 多家合作方(发布文章),文章里就写明它和 MCP 是互补关系;同年 6 月 23 日 Google 把它捐给了 Linux 基金会(Linux 基金会公告),由多家公司共同管理,不再归 Google 一家。

A2A(Agent2Agent Protocol):让不同团队、不同框架做出来的 Agent 能互相发现、互相委派任务的开放协议。由 Google 发起,后来捐给 Linux 基金会,Apache 2.0 许可证,最新发布的规范版本是 1.0.0(官网,2026-09 查阅)。

和 MCP 的分工,A2A 官网自己的说法是:MCP 管 Agent 到工具的通信,A2A 管 Agent 到 Agent 的通信,两者互补,设计上就是配合使用的。

flowchart LR
    U[用户] --> TA[旅行规划 Agent]
    TA -->|A2A 发任务| FA[机票 Agent<br/>别的公司做的]
    TA -->|A2A 发任务| HA[酒店 Agent<br/>别的框架做的]
    FA -->|MCP| FT[航班查询 API]
    HA -->|MCP| HT[酒店库存数据库]
    TA -->|MCP| CAL[用户日历]

关键区别是”不透明”:通过 MCP 调工具,调用方知道函数名、参数、返回结构;通过 A2A 找另一个 Agent,只知道它声明的能力,它内部用什么模型、什么工具、怎么推理都不暴露。

核心概念规范):

概念是什么
Agent CardAgent 的”名片”:身份、能力、技能、支持的协议、认证要求,发布在一个公开地址上供发现
Task一次委派的工作单元,有唯一 ID、状态、产出、交互历史
Message一次交流,角色是 user 或 agent,由若干 Part 组成
Part内容块:文本、文件或结构化数据
Artifact任务的产出,也由 Part 组成

Task 的状态(规范定义了 8 种):

stateDiagram-v2
    [*] --> SUBMITTED
    SUBMITTED --> WORKING
    SUBMITTED --> REJECTED
    WORKING --> INPUT_REQUIRED
    INPUT_REQUIRED --> WORKING
    WORKING --> AUTH_REQUIRED
    AUTH_REQUIRED --> WORKING
    WORKING --> COMPLETED
    WORKING --> FAILED
    WORKING --> CANCELED
    COMPLETED --> [*]
    FAILED --> [*]
    CANCELED --> [*]
    REJECTED --> [*]

图里的状态转移是按状态名的含义画的示意,规范只列出了状态和哪些是终止状态,没有给这样一张完整的转移图。

为什么要有 INPUT_REQUIRED 被委派的 Agent 执行到一半可能发现信息不够(“您要经济舱还是商务舱?”),需要回头问调用方,这和 07 篇的人工审批暂停是同一类问题。

传输和获取结果:

选项
协议绑定JSON-RPC 2.0、gRPC、HTTP+JSON(REST 风格)
获取进度轮询 GetTask;流式订阅;推送通知(回调调用方的 webhook)

长任务可能跑几小时,调用方不能一直挂着连接,所以推送通知是必要的。

现在要不要学? 面试里被问到的概率比 MCP 低很多,岗位要求里也少见(00 篇用的 54 个岗位里,只有 2 个在加分项或技术清单里提到 A2A)。能说清楚”是什么、和 MCP 什么关系、核心是 Agent Card 和 Task”就够了。

4.9 什么时候用框架,什么时候自己写

flowchart TD
    Q0{业务人员要自己搭?<br/>流程固定、要快速上线?} -->|是| P[Dify / Coze 这类平台]
    Q0 -->|否| Q1{要中断恢复、人工审批<br/>或长时间运行?}
    Q1 -->|是| LG[LangGraph<br/>或基于它的 LangChain]
    Q1 -->|否| Q2{多个 Agent 分工、交接?}
    Q2 -->|是| MA[OpenAI Agents SDK / LangGraph<br/>先看 13 篇想清楚是否真需要]
    Q2 -->|否| Q3{需要现成的文件、命令、编辑工具<br/>做编码类 Agent?}
    Q3 -->|是| CAS[Claude Agent SDK]
    Q3 -->|否| Q4{主要是 RAG、数据源多?}
    Q4 -->|是| LI[LlamaIndex]
    Q4 -->|否| HW[手写循环<br/>几十行,完全可控]

这张图只是起点,实际还要看团队已有技术栈、观测和评测工具的集成。

无论选哪个,都建议保持两层独立:

  1. 工具层独立于框架。工具就是普通函数,加上检查和截断,框架只负责调用。我项目的 tools.py 被三个入口共用,就是这个思路。以后换框架,工具不用重写
  2. 评测独立于框架。评测脚本调的是统一的入口(我项目是 build_agent.build(),靠环境变量 AGENT_IMPL 切换手写或 LangGraph),换实现后同一套评测直接对比

我项目的最终结论eval/REPORT.md):对这种单 Agent、线性循环的任务,手写更直接;LangGraph 真正多给的是检查点和中断恢复,这在长任务(一次困难项目要跑两三分钟、几十万 token)里有实际价值。

还有一个诚实的补充:07 篇讲过,LangGraph 的检查点粒度是节点,工具节点执行到一半被杀,恢复后已执行的工具会再跑一次。框架给了续跑能力,但幂等仍然要自己做

第五部分 对照项目

本篇知识点项目里的位置做到了什么没做到或可以改进的
手写循环agent/build_agent.pybuild_events65 行,步数上限、卡住检测、验收没有检查点,崩了只能重跑
LangGraph 状态图agent/graph_agent.py三节点两条条件边;add_messages;SqliteSaver 检查点;--resume 续跑检查点粒度是节点;代码量翻倍
公平对比两版共用 check_and_run、提示词、工具、验收同配置 30/30 vs 29/29,步数和 token 无差别每配置只 3 轮
流式事件手写版 yield;图版本 get_stream_writer + stream_mode="custom"前端两种实现拿到一样的事件没用 messages 模式做逐 token 输出
无效工具调用tools 节点处理 invalid_tool_calls也回 ToolMessage,避免下次请求被拒
递归上限run_config 显式设 MAX_STEPS × 2 + 10路由写错时防死循环
线程问题Trace 连接 check_same_thread=False修复了 run 162 的崩溃
续跑实测REPORT 的 run 217第 12 步杀进程,从第 13 步续跑,第 18 步通过,工具调用无重复没测节点执行中途被杀的情况
切换实现环境变量 AGENT_IMPL评测脚本不用改
MCP serveragent/mcp_server.py复用 tools.py;工作目录用闭包固定;知识库按需加载;stdio 入口没做 Streamable HTTP;没接真实 MCP 客户端做端到端测试
MCP 错误guarded 转成 SDK 的 ToolError模型能读到越界原因
MCP 测试tests/test_mcp.py内存连接和 stdio 子进程两种
框架无关的工具层tools.py 被三个入口共用边界检查只写一份
A2A、多 Agent 框架单 Agent 任务,没用到

对照开源实现:pi

pi05 篇第五部分介绍过)是 4.9 节决策图最下面”手写”那条路走到底的样子:不用 LangChain、LangGraph,从模型接口到主循环全部自己写,也不接 MCP。源码以 commit 7b4cfd6 为准。

分层。 它拆成三个包,别人可以只拿其中一层用:

做什么对应本系列
ai统一十几家服务商的调用接口、重试、用量和费用02 篇
agent主循环、工具执行、会话存储、上下文压缩05、06、07 篇
coding-agent具体产品:内置工具、系统提示词、扩展、命令行界面04 篇

主循环不知道有哪些具体工具,工具定义在最上面一层。这和本篇 4.9 节”工具层独立于框架”是同一个思路。

不接 MCP,改用”技能”。 README 明确写着不做 MCP,建议写带说明文档的命令行工具。做法是:每个技能是一个目录,里面有一个 SKILL.md(开头写名字和一句话描述),可能还有脚本。系统提示词里只列出每个技能的名字、描述和文件路径;模型判断任务用得上时,自己用 read 工具读 SKILL.md,再按里面的说明用 bash 执行脚本或命令(skills.ts L355-L383)。pi 实现的是公开的 Agent Skills 规范,也能直接加载 Claude Code、Codex 的技能目录。

MCP 工具pi 的技能
模型一开始看到什么每个工具的名字、描述和完整参数 Schema,每次请求都要发每个技能一行:名字、描述、路径
详细说明什么时候进上下文一开始就在模型决定用的时候才读
怎么执行客户端按协议调 server模型用 bash 直接跑脚本或命令行工具
要写什么一个 MCP server一个 Markdown 文件,加上现成或自己写的命令行工具
前提客户端支持 MCP,Agent 不需要 shellAgent 必须能读文件、能执行命令
权限怎么管可以按工具单独控制给了 bash 就等于什么都能跑,没法按技能细分

两者不是谁取代谁。技能省上下文、写起来快,适合本来就有 shell 的编程 Agent;MCP 适合不给模型 shell 的应用(比如聊天客户端),或者需要按工具做权限控制的场景。我项目做 MCP server,是为了让 Claude Desktop、Cursor 这类客户端直接接入同一套带路径检查的工具,这层检查在技能方式下就没处放。

钩子和中间件,自己实现一层。 本篇速记页说 LangChain v1 用中间件给 Agent 加功能,pi 用”扩展”做同样的事。扩展是 TypeScript 模块,可以注册工具和命令,监听几十种事件(extensions.md)。比如 tool_call 事件在工具执行前触发,可以改参数或拦截;示例 permission-gate.ts 大约 30 行,命令里匹配到 rm -rfsudo 就弹窗确认,没有界面时直接拦下。context 事件在每次调模型前触发,可以改要发出去的消息。

面试时怎么用这个例子。 被问”为什么不用框架”时,除了我自己手写 65 行和 LangGraph 126 行的对比,还可以举 pi:一个每天有人在用的编程 Agent,整条链路自己写,好处是每一层都能按需求设计,比如 07 篇的树形会话、06 篇的压缩规则;代价是这些都要自己写、自己维护,光 agent 这一个包的源码就有两万多行。框架省下的就是这部分工作量。

第六部分 追问清单

你刚讲完下一个追问回答方向
手写 vs LangGraph那你为什么还要写 LangGraph 版岗位要求常见;验证框架带来的实际差别;检查点和续跑有价值
两版结果一样那框架没用?结果一样是预期,模型和工具相同;差别在工程能力(续跑、审批)
检查点节点中途被杀会怎样节点内已执行的工具会重复执行;需要幂等
reduceradd_messages 除了追加还做什么按消息 ID 去重,相同 ID 替换
条件边路由函数写错会怎样可能死循环;靠 recursion_limit 兜底
递归上限默认是多少很多资料说 25 是旧版;实测 1.2 是 10007;要显式设置
stream 模式前端要逐字显示模型输出用哪个messages;自定义进度用 custom
LangChain和 LangGraph 什么关系v1 的 create_agent 构建在 LangGraph 之上,用中间件加功能
MCP为什么要做 MCP server工具复用给其他客户端;同一套边界检查
MCP 版本initialize 握手是怎么回事早期版本有;2026-07-28 版改成无状态,用 server/discover,每个请求带能力
MCP 原语resources 和 tools 什么区别谁控制:应用 vs 模型;只读背景资料适合 resource
MCP 安全server 能看到用户的对话吗规范要求不能,完整对话留在 host;但工具结果、描述仍要当不可信
MCP 传输远程部署用什么Streamable HTTP;无状态便于负载均衡
A2A和 MCP 什么区别工具 vs Agent;透明 vs 不透明;Task 有状态、可能要补充输入
平台Dify 做不了什么平台外的能力难扩展;复杂状态、自定义评测、代码审查受限
选型新项目你会怎么选看任务结构、是否需要持久化、团队栈;工具层和评测保持框架无关
MCP不用 MCP,还能怎么给 Agent 接能力技能:说明文档加命令行工具,系统提示词只放名字和描述,用时再读;前提是 Agent 有 shell;pi 就这么做
手写手写能撑多大的项目pi 整条链路自己写,agent 包两万多行;换来完全掌控,代价是全部自己维护

第七部分 闭卷自测

1. 框架主要替你做了哪些事?代价是什么?各举一个我项目里的例子。

答案

替你做:统一模型接口、自动生成工具 Schema、检查点和续跑、暂停等人工确认、流式事件、追踪。代价:多一层抽象(LangGraph 线程池导致 SQLite 跨线程报错,run 162 崩溃)、控制流分散、代码不一定变少(65 行 vs 126 行)、资料过时(递归上限默认值)、边角约定(invalid_tool_calls 也要回 ToolMessage)。

2. LangChain 和 LangGraph 是什么关系?官方怎么建议在 Deep Agents、LangChain、LangGraph 之间选?

答案

LangChain v1 的 create_agent 构建在 LangGraph 之上,用中间件加功能。开箱即用、要上下文压缩和子 Agent 用 Deep Agents;高度可定制用 LangChain;要把确定性流程和 Agent 行为组合的复杂场景用 LangGraph。

3. LangGraph 的状态、节点、边分别是什么?reducer 起什么作用?

答案

状态是 TypedDict,定义图里流转的数据;节点是函数,接收状态返回更新;边决定下一步,普通边固定,条件边由函数根据状态返回节点名。reducer 决定更新怎么合并进状态,默认覆盖,add_messages 追加消息并按 ID 去重。

4. 实验 1 里,为什么 custom 事件总是出现在同一节点的 updates 之前?消息数为什么是 8?

答案

custom 是节点运行过程中通过 get_stream_writer 写出的,updates 是节点返回之后才产生的。8 = 1 条用户消息 + 4 条模型消息 + 3 条工具结果。

5. ToolNodetools_condition 分别做什么?

答案

ToolNode 读最后一条消息的 tool_calls,执行对应工具,把结果包装成带 tool_call_idToolMessage 返回。tools_condition 是路由函数,最后一条消息有工具调用返回 "tools",否则返回 END

6. MCP 的 host、client、server 各负责什么?client 和 server 是什么对应关系?规范对 server 能看到什么有什么要求?

答案

host 是 AI 应用,管理 client、权限、安全策略、模型调用、上下文汇总;client 由 host 创建,每个只连一个 server;server 提供 tools、resources、prompts。规范要求 server 看不到完整对话,也看不到其他 server,完整对话留在 host。

7. MCP 三种原语分别由谁控制?什么东西适合做成 resource 而不是 tool?

答案

prompts 用户控制、resources 应用控制、tools 模型控制。只读的背景资料(文件内容、git 历史、支持的平台列表)适合做 resource,由应用决定何时放进上下文,不必让模型主动调用。

8. MCP 2026-07-28 版在”状态”上有什么变化?为什么对远程部署有好处?

答案

改成无状态:没有 initialize 握手和连接级会话,每个请求自带协议版本和客户端能力,可以用 server/discover 发现能力;服务端不主动发请求,需要客户端输入时返回 InputRequiredResult。无状态让任意一台服务器都能处理任意请求,便于放在负载均衡后面水平扩展。

9. 实验 2 里如果工具抛的是 ValueError 而不是 SDK 的 ToolError,模型会看到什么?这对项目有什么启示?

答案

SDK 把其他异常当崩溃,模型只看到 Error executing tool <工具名>,看不到原因。所以项目的 guarded 要把 tools.ToolError 转成 SDK 的 ToolError,让模型读到”路径越界”这类能指导下一步的信息。

10. 我项目的 MCP server 为什么要复用 tools.py?工作目录为什么不做成工具参数?

答案

边界检查只写一份,保证 MCP 版和 Agent 版一致,避免两套检查不一致导致的漏洞。工作目录在 create_server 时固定,内层工具函数通过闭包使用,模型无法通过参数修改访问范围。

11. A2A 和 MCP 的核心区别是什么?A2A 的 Task 为什么需要 INPUT_REQUIRED 状态和推送通知?

答案

MCP 连工具,调用方知道函数和参数;A2A 连完整的 Agent,对方内部不透明,只通过 Agent Card 了解能力。被委派的 Agent 执行中可能缺信息,需要回头问调用方,所以有 INPUT_REQUIRED;任务可能跑很久,调用方不能一直挂着连接,所以需要推送通知。

12. 新项目做框架选型,你会考虑哪些问题?无论选哪个,哪两层建议保持独立?

答案

考虑:业务人员是否要自己搭、是否需要中断恢复和人工审批、是否多 Agent、是否要现成的编码工具、是否以 RAG 为主、团队已有技术栈。保持独立:工具层(普通函数加检查,框架只负责调用)和评测(统一入口,换实现直接对比)。

13. pi 不接 MCP,而是用”技能”给 Agent 加能力。两种做法在上下文占用和使用前提上有什么不同?各适合什么场景?

答案

上下文:MCP 工具的名字、描述和完整参数 Schema 每次请求都要发;技能在系统提示词里只占一行(名字、描述、路径),模型决定用时才去读详细说明。前提:技能要求 Agent 能读文件、能执行命令,执行时等于给了 shell,没法按技能细分权限;MCP 不需要 shell,可以按工具控制权限。所以技能适合本来就有 shell 的编程 Agent;MCP 适合不给模型 shell 的应用,或者需要逐个工具控制权限的场景。

延伸阅读

  1. LangGraph 流式输出文档 — 七种流式模式和 v2 输出格式
  2. LangChain 概览create_agent、中间件,以及和 LangGraph、Deep Agents 的关系
  3. MCP 规范 2026-07-28:架构 — 角色、设计原则、无状态的能力协商
  4. MCP 规范 2026-07-28:传输 — stdio 和 Streamable HTTP
  5. A2A 协议规范 — Agent Card、Task 状态、三种协议绑定
  6. OpenAI Agents SDK 文档 — Agent、交接、护栏、会话、追踪
  7. Claude Agent SDK 概览 — 内置工具、钩子、子 Agent、权限
  8. LlamaIndex 框架文档
  9. DifyCoze Studio — 两个开源的可视化平台
  10. pi:SkillsExtensions(commit 7b4cfd6)— 不用框架、不接 MCP 的编程 Agent 怎么做能力扩展和事件钩子
  11. Agent Skills 规范Anthropic:Equipping agents for the real world with Agent Skills(2025-10)— SKILL.md 的格式和三层按需加载

下一篇:13 多 Agent——多个 Agent 分工听起来很美,什么时候真的值得,代价又是什么。

Related · Agent 开发
⎇ main ai/agent开发 23 节 230 notes UTF-8