普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月5日首页

「JS全栈AI Agent学习」四、MCP:给AI工具世界造一个USB接口

作者 霪霖笙箫
2026年4月5日 14:27

📌 系列简介:「JS全栈AI Agent学习」系统学习 21 个 Agent 设计模式,篇数随学习进度持续更新。 ⏱️ 预计阅读时间:15 分钟 📖 原书地址adp.xindoo.xyz 前端转 JS 全栈,正在学 AI,理解难免有偏差,欢迎批评指正 ~


🗺️ 系列导航

主题 状态
第一篇 提示链 · 路由 · 并行化
第二篇 反思 · 工具使用 · 规划
第三篇 多智能体 · 记忆管理 · 学习适应
本篇 MCP 协议

前言

我有一个 my-resume 项目——最初只是一个静态展示页面,放了简历、项目经历、技术栈。 后来想把它改造成真正的全栈项目:NestJS 后端 + 数据库 + 前端交互 + AI 能力 + 部署上线,三端齐备,一条龙。

在这个改造过程中,我开始认真思考 AI 能力怎么集成进来。

第一步,我给项目写了一个读 PDF 的 Tool——用户上传简历 PDF,AI 解析内容,然后做各种分析。 写完挺好用的,但写完之后我意识到一个问题:

这个 Tool,只能在我自己的项目里用。

换个框架要重写,换个 AI 平台要重写 …… 这个问题,就是这篇文章的起点。


一、从一个 PDF Tool 说起

我在 LangChain.js 里注册了一个 PDF 阅读工具,大概长这样:

const pdfTool = new DynamicTool({
  name: "read_pdf",
  description: "读取PDF文件并转换为文本,保留原始布局和排版结构。当任务涉及阅读、解析PDF文件内容时触发,仅上传文件不触发。",
  func: async (filePath: string) => {
    return await extractPdfText(filePath);
  }
});

Agent 靠 description 判断要不要调用这个工具——语义匹配,不是关键词匹配。

这里有个细节值得单独说:description 写得好不好,直接决定 Agent 调用的精准度

写不好 description 的时候,不妨问自己:如果这是一份需求文档,自己刚拿到知道什么时候该用这个接口吗?

工具本身没问题。但问题来了——

假设不只是我自己用,而是 100 个开发者都需要这个 PDF Tool。 假设不只是 LangChain.js,还要支持 Claude、GPT-4、自己搭的 Agent…… 每个人都要复制代码、手动注册、适配不同框架——

这个成本,随着工具数量和使用方数量的增加,会指数级爆炸。


二、VSCode 插件的类比——以及它哪里不够用

第一反应是:这不就像 VSCode Extensions 吗?

VSCode 插件市场解决了类似的问题:

  • 开发者按规范写插件,发布到市场
  • 用户订阅安装,开箱即用
  • 统一管理,不用到处复制代码

这个思路是对的。规范 + 发布 + 订阅 + 协作,MCP 都有。

但我多想了一秒,感觉哪里不太对——

VSCode 插件是这样工作的:

插件开发者 → 发布到 Marketplace → 用户安装 → VSCode 加载插件

插件运行在哪里?运行在 VSCode 里。VSCode 是唯一的宿主。

但 AI Tool 的场景完全不同:

你的 PDF Tool,可能被 Claude 调用,可能被 GPT-4 调用,可能被你自己用 LangChain.js 写的 Agent 调用。 这三个"宿主",是三个完全不同的系统。

所以需要的不是"插件市场",而是一个跨系统的通信协议

VSCode Extensions MCP
解决什么 如何扩展 VSCode 的功能 如何让任何 Agent 调用任何工具
宿主 只有 VSCode Claude / GPT / LangChain / 任何框架
本质 插件规范 通信协议
类比 乐高积木的形状规范 USB 接口标准

USB 出现之前:鼠标有鼠标接口,键盘有键盘接口,打印机有打印机接口。 USB 出现之后:一个接口,接任何设备。

MCP 就是 AI 工具世界的 USB。


三、MCP 是什么

MCP(Model Context Protocol,模型上下文协议),是 Anthropic 在 2024 年底提出并开源的一个开放标准。

它要解决的问题,用一张图说清楚:

MCP 出现之前:
─────────────────────────────────────────
PDF Tool (Claude版)    ──→ Claude
PDF Tool (OpenAI版)    ──→ GPT-4
PDF Tool (LangChain版) ──→ 你的 Agent
同一个功能,写三遍

MCP 出现之后:
─────────────────────────────────────────
                       ──→ Claude
PDF Tool (MCP版) ────────→ GPT-4
                       ──→ 你的 Agent
写一次,到处用

一句话定义:MCP 是 AI 工具世界的 USB 接口标准——工具只写一次,任何兼容 MCP 的 Agent 都能调用。


四、MCP 协议定义了什么

既然是"通信协议",那它需要定义哪些东西,两端才能"说上话"?

类比 HTTP 协议:它定义了请求方法(GET/POST)、状态码(200/404)、Header 格式……

MCP 对应地定义了四件事:

你的直觉推导 MCP 里的概念 说明
支持哪些平台/连接方式 Transport 层 定义通信方式,解决"怎么连上"
名称和描述 Tool Definition name + description,Agent 靠这个决定要不要调用
怎么使用(说明书) Input Schema 用 JSON Schema 定义入参,Agent 知道要传什么
备注/版本/返回格式 Output Schema / Metadata 返回值格式、版本号、错误码定义

真实的 MCP Tool 定义长这样:

{
  name: "read_pdf",
  description: "读取PDF文件并转换为文本,保留原始布局和排版结构",
  inputSchema: {
    type: "object",
    properties: {
      filePath: {
        type: "string",
        description: "PDF文件的路径"
      },
      pageRange: {
        type: "string",
        description: "可选,指定页码范围,如 '1-5'"
      }
    },
    required: ["filePath"]
  }
}

看到这个结构,有没有觉得很眼熟?

这和写 TypeScript 函数签名,本质上是同一件事:

// TypeScript 函数签名
function readPdf(filePath: string, pageRange?: string): string { ... }

// MCP inputSchema = 把函数签名用 JSON 描述出来,让 Agent 能"读懂"

MCP 的 inputSchema,就是把函数类型定义翻译成 Agent 可以理解的格式。


五、MCP 架构:三个角色

MCP 基于客户端-服务器架构,有三个核心角色:

你的 Agent(MCP Client)
      │
      │  ① 发现:这里有哪些工具?
      ▼
 MCP Server(工具提供方)
      │
      │  ② 返回:工具列表 + 每个工具的 Schema
      ▼
你的 Agent 决策:
  "这个任务需要用 read_pdf"
      │
      │  ③ 调用:传入参数
      ▼
 MCP Server 执行工具
      │
      │  ④ 返回结果
      ▼
Agent 继续处理
角色 是什么 类比
MCP Client 你的 Agent,发起调用方 浏览器
MCP Server 工具提供方,暴露工具能力 Web 服务器
Transport 层 两者之间的通信方式 HTTP / WebSocket

这里有一个值得单独说的设计细节:

第 ① 步是"发现",不是"被告知"。

传统的 Tool Use,是你在代码里明确告诉 Agent:"你有这些工具"——静态注册,写死在代码里。

MCP 的方式是 Agent 主动去问 Server:"你有什么工具?"——动态发现,运行时查询。

这个差异的实际意义是:MCP Server 可以独立部署、独立更新,Agent 不需要改一行代码,就能感知到工具的变化。工具加了新功能、下线了旧接口,Agent 侧完全无感。


六、Transport 层:同步等待,还是流式返回?

三个角色清楚了,还有一个问题没解决:Agent 调用工具,该怎么等结果?

读 PDF 可能要几秒,查数据库可能要几百毫秒,调用外部 API 可能更慢。 如果 Agent 傻等,整个系统就卡住了。

最理想的设计是:既能根据情况异步请求,也能支持同步读取,最后统一输出。

这在计算机里有个专门的名字:流式响应(Streaming)

MCP 定义了两种标准传输方式:

方式一:stdio(标准输入输出)
─────────────────────────────────────────
Agent ──写入 stdin──▶ MCP Server
Agent ◀──读取 stdout── MCP Server

适合:本地工具,同一台机器上运行
类比:命令行管道  ls | grep pdf


方式二:HTTP + SSE(Server-Sent Events)
─────────────────────────────────────────
Agent ──HTTP POST──▶ MCP Server
Agent ◀──SSE 流式──── MCP Server

适合:远程工具,跨网络调用
类比:流式 AI 回复

SSE 的工作方式,画出来是这样的:

Agent 发出请求 ──────────────────────────▶ MCP Server
                                               │
                                          开始执行工具
                                               │
                ◀── 流式返回(边处理边推送)──── │
                ◀── 流式返回 ────────────────── │
                ◀── 流式返回 ────────────────── │
                ◀── [DONE] ──────────────────── │

Agent 不需要傻等,数据来一点处理一点,最后统一完成

做过前端 AI 应用的同学,这个原理一眼就熟——

你在页面上做的流式渲染 AI 回复,用的就是同一套机制: ReadableStream → 一块一块读 → 渲染到页面

MCP 的 SSE 传输,和你前端写的流式 AI 回复,本质上是同一件事。


七、动手:把 PDF Tool 升级成 MCP Server

理论讲完,直接上代码。

my-resume 项目里的 PDF Tool,改造成一个标准的 MCP Server:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

// 创建 MCP Server 实例
const server = new Server({
  name: "resume-tools",   // 工具集名称
  version: "1.0.0",       // 版本号
});

// ① 声明工具列表
// Agent 来问"你有什么工具"时,返回这个
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "read_pdf",
      description:
        "读取PDF文件并转换为文本,保留原始布局和排版结构。" +
        "当任务涉及阅读、解析PDF内容时触发,仅上传文件不触发。",
      inputSchema: {
        type: "object",
        properties: {
          filePath: {
            type: "string",
            description: "PDF文件路径",
          },
          pageRange: {
            type: "string",
            description: "可选,页码范围,如 '1-5'",
          },
        },
        required: ["filePath"],
      },
    },
  ],
}));

// ② 处理工具调用
// Agent 决定调用某个工具时,走这里
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "read_pdf") {
    const { filePath, pageRange } = request.params.arguments as {
      filePath: string;
      pageRange?: string;
    };

    const result = await extractPdfText(filePath, pageRange);

    return {
      content: [{ type: "text", text: result }],
    };
  }

  throw new Error(`未知工具: ${request.params.name}`);
});

// ③ 启动(stdio 模式,本地调用)
const transport = new StdioServerTransport();
await server.connect(transport);
console.log("MCP Server 已启动,等待 Agent 连接...");

看这个结构,有没有觉得很像你写 NestJS Controller

NestJS:  @Controller@Get/@Post         → 处理请求 → 返回响应
MCP:      Server     → ListTools/CallTool → 处理请求 → 返回结果

这个类比不是巧合——两者解决的是同一类问题:定义接口、处理请求、返回结果。只是服务的对象从"HTTP 客户端"变成了"AI Agent"。

关于 my-resume 项目的思考

my-resume 的全栈改造里,MCP 的三个角色可以这样对应:

MCP Server = 我写的工具层(NestJS 后端)
  → read_pdf:解析用户上传的简历 PDF
  → get_resume_data:从数据库读取结构化简历数据
  → search_projects:搜索项目经历

MCP Client = 调用工具的 Agent(前端发起,后端编排)
  → 接收用户指令:"帮我优化这段工作经历"
  → 决策:先调用 get_resume_data 拿到原始数据
  → 再交给 LLM 处理,返回优化建议

Transport = stdio(本地)或 HTTP+SSE(部署后远程调用)

Server 提供能力,Client 使用能力,Transport 是中间的管道。

这个分层思路,和 NestJS 的 Controller / Service / Repository 分层,逻辑上是一脉相承的。


八、MCP vs 工具函数调用:别混淆

学到这里,有一个容易混淆的地方值得单独说清楚。

MCP 和 LangChain 里的 Tool Use(工具函数调用)看起来很像,但有本质区别:

特性 工具函数调用(Tool Use) MCP
标准化 各平台专有,格式不统一 开放标准,跨平台互通
工具发现 你明确告诉 Agent 有哪些工具 Agent 主动查询,动态发现
可重用性 与特定应用/框架耦合 独立部署,任何兼容方都能用
架构 一对一(LLM ↔ 工具) 客户端-服务器(多对多)

一句话区分:

工具函数调用 = 给 AI 一套专用工具箱,工具是定制的,只能在这个项目里用。

MCP = 造一个标准插座,任何符合规格的工具都能插进来,任何兼容的 Agent 都能用。


九、核心洞察

洞察 一句话
MCP 是什么 AI 工具世界的 USB 接口标准
解决什么问题 工具只写一次,任何 Agent 都能用
三个角色 Client(Agent)· Server(工具)· Transport(通道)
两种传输 stdio(本地)· HTTP+SSE(远程+流式)
和 VSCode 插件的本质区别 插件规范 vs 跨系统通信协议
和 NestJS 的类比 Controller/Service 分层 ≈ Server/Handler 分层
你已经在用的类似概念 流式 AI 回复的 ReadableStream
动态发现 vs 静态注册 MCP 让 Agent 主动问"你有什么",而不是被动被告知

结语

MCP 这章,我觉得是目前为止最"工程感"的一章。

它不是一个新的 AI 能力,而是一个工程规范——解决的是"怎么让 AI 能力可复用、可组合、可跨平台"这个问题。

对于 my-resume 全栈改造来说,这章给了我一个很清晰的架构思路:

不要把 AI 工具写死在业务代码里。把它们抽成 MCP Server,独立部署,独立维护。 今天接 Claude,明天换 GPT-4,后天自己搭 Agent——工具层一行代码不用改。

这个思路,和后端开发里"接口与实现分离"是同一个道理。只不过现在,接口的调用方从"前端页面"变成了"AI Agent"。

学到这里,越来越觉得:AI 工程和软件工程,底层是同一套思维。 分层、解耦、标准化——这些事工程师早就在做了,只不过现在的场景换了。


💬 系列地址:持续更新中

📖 原书地址adp.xindoo.xyz

🛠️ 实战项目:my-resume(静态页面 → NestJS + 数据库 + AI + 部署上线,进行中)

如果这篇对你有帮助,欢迎点赞收藏,我们下篇见 👋

昨天以前首页

「JS全栈AI Agent学习」二、反思、工具使用、规划——让 Agent 从"执行者"变成"自主完成者"

作者 霪霖笙箫
2026年3月31日 22:54

📌 系列简介:「JS全栈AI Agent学习」系列第二篇 ⏱️ 预计阅读时间:15 分钟 🛠️ 技术栈:LangChain.js + TypeScript + NestJS 📖 原书地址adp.xindoo.xyz

上一篇学了提示链、路由、并行化——解决了"怎么执行"的问题。 这一篇的三个模式,解决的是更高阶的问题:怎么做得更好、怎么突破边界、怎么面对复杂任务


🗺️ 系列导航

主题 状态
第1篇 提示链 · 路由 · 并行化
第2篇(本篇) 反思 · 工具使用 · 规划

📖 读这篇,你可以带走什么

# 你会学到 对应章节
1 反思不是"泛泛自评",而是有具体内容的结构化闭环 第四章
2 自我反思 vs 外部评审,怎么选、什么时候混用 第四章
3 反思上限为什么定 3 次——「一鼓作气,再而衰,三而竭」 第四章
4 LLM 不执行代码,它只输出"意图"——这个认知很重要 第五章
5 工具的 description 比实现更重要,写好描述比写好代码更关键 第五章
6 安全防护三层结构:输入校验 → 权限分级 → 人工确认 第五章
7 判断要不要用规划模式,比"任务复不复杂"更精准的标准 第六章
8 [REPLAN] 标记:不可逆操作不能让 AI 自己决定 第六章

前言:执行能力有了,然后呢?

学完第一篇,你的 Agent 已经能拆解任务、分发请求、并行提速了。 但你可能还会遇到这些问题:

  • AI 输出了结果,但质量不稳定,有时好有时差——怎么让它自己发现问题并改进
  • 任务需要读文件、查数据库、调接口,纯文本 LLM 根本做不到——怎么让它真正能做事
  • 任务很复杂,直接丢给 AI 结果天马行空——怎么让它先想清楚再动手

这三个问题,对应本篇的三个模式:反思、工具使用、规划

如果说第一篇解决的是"怎么跑起来",这一篇解决的是——怎么跑得稳、跑得远、跑得对


📖 第四章:反思(Reflection)

读这章之前,我就已经这么做了

说实话,读到「反思」这章之前,我就已经在开发里这么做了。

写完一段代码,我习惯问自己三个问题:

  • 这段代码有没有完成功能?
  • 边界有没有考虑完善?
  • 还能不能复用、优化?

这和《论语》里那句话是一个道理:

吾日三省吾身——为人谋而不忠乎?与朋友交而不信乎?传不习乎?

科技循环图表.jpg

反省不是走形式,是要有具体内容的。曾子每天反省三件具体的事,不是泛泛地说"我今天表现怎么样"。

读到反思模式,我的第一反应就是:这不就是把这个习惯自动化了吗?

Agent 执行完任务,不直接输出结果,而是先问自己:

  • 这个结果完成了目标吗?
  • 有没有遗漏的边界?
  • 有没有可以改进的地方?
执行 → 输出结果 → [反思:完成目标了吗?边界全了吗?还能更好吗?]
                                    ↓ 有问题
                              改进后重新执行
                                    ↑_____________|
                                    (循环,直到满意)

两种反思模式

书里把反思分为两种角色结构,用开发类比来理解:

自我反思(Self-Reflection)

同一个 LLM,先执行,再切换角色评审自己的输出。

就像写完代码自己 Review——能发现明显问题,但容易"自我感觉良好",对自己的盲区视而不见。

优点:简单、低成本
缺点:容易陷入"认知偏差",发现不了深层问题
适合:对质量要求适中的场景

外部评审(Critic Agent)

另起一个专门负责批评的 Agent,独立评审执行结果。

就像找另一个同事来 Code Review——他不知道你当时怎么想的,所以能发现你自己看不到的问题。

优点:更客观,能发现执行 Agent 的盲区
缺点:多一次 LLM 调用,成本更高
适合:对质量要求高的场景,如代码生成、方案设计

两者不是非此即彼的——先用自我反思快速迭代,质量卡关了再引入外部评审,是比较务实的做法。

反思结果必须结构化

反思不能只是一段文字感想,必须输出结构化数据,方便程序判断是否需要继续迭代:

interface ReflectionResult {
  score: number;           // 0-10 质量评分
  passed: boolean;         // 是否达到标准(比如 score >= 7)
  issues: string[];        // 发现的具体问题列表
  suggestions: string[];   // 改进建议
  iteration: number;       // 当前第几次反思
}

反思上限定为 3 次——「一鼓作气,再而衰,三而竭」

反思循环必须有终止条件,这个上限不是随意拍的数字,是有逻辑的:

一鼓作气,再而衰,三而竭。——《左传·曹刿论战》

第一次反思:效果最好,能发现最明显的问题,改进幅度最大。 第二次反思:精益求精,发现细节问题,改进幅度变小。 第三次反思:边际收益已经很低,再反思下去基本是形式主义。

const MAX_ITERATIONS = 3;  // 一鼓作气,三而竭

async function reflectiveExecute(task: string): Promise<string> {
  let result = await execute(task);

  for (let i = 0; i < MAX_ITERATIONS; i++) {
    const reflection = await reflect(task, result);

    if (reflection.passed) {
      console.log(`✅ 第 ${i + 1} 次反思通过,评分:${reflection.score}`);
      break;
    }

    console.log(`🔄 第 ${i + 1} 次反思,发现问题:`, reflection.issues);
    result = await executeWithFeedback(task, result, reflection);
  }

  return result;
}

💡 超过上限还没通过,应该升级为人工介入——而不是继续让 AI 自己转圈。 无限循环不是"更努力",是"没有终止条件的 bug"。


📖 第五章:工具使用(Tool Use)

先聊聊我踩过的坑

用过 Codex CLI、Claude 这类工具的人应该都有体会:在 CLI 环境里,AI 默认是读不了你本地文件的.vue.md.pdf,你以为它能理解,其实它只是在"猜"。

这就是纯 LLM 的边界——它只能处理你直接输入的文本,做不到:

❌ 读取本地文件(.vue .md .pdf .xlsx...)
❌ 访问实时网络
❌ 执行代码
❌ 操作数据库
❌ 调用第三方 API
❌ 感知当前时间

Tool Use 就是为了解决这个问题——给 LLM 装上"手"

前端视角的类比:先用原生,再找第三方

读到这章,我想到了一个前端开发的日常习惯:

实现一个功能,不会先去 npm 找包,而是先看浏览器原生支持什么。

  • 需要定位?先看 navigator.geolocation
  • 需要存储?先看 localStorage / sessionStorage
  • 需要网络请求?先用 fetch
  • 原生做不到,再找第三方库;库也不够,再自己造

这就是 KISS(Keep It Simple)DRY(Don't Repeat Yourself) 原则——少造轮子,优先复用已有能力。

Agent 的工具使用,逻辑完全一样:

LLM 原生能力(文本推理)
    ↓ 不够用
调用已有工具(读文件、查数据库、调 API)
    ↓ 还不够
扩展新工具(自己开发 / 接入第三方服务)

核心机制:LLM 不执行代码

这个类比让我想清楚了一件事:工具的边界在哪里——LLM 负责"知道用什么",宿主程序负责"真正去做"。

很多人以为 AI 调用工具是"AI 直接执行代码",其实完全不是:

用户输入
    ↓
LLM 分析:我需要什么工具?参数是什么?
    ↓
输出结构化的"工具调用意图"JSON)  ← LLM 只做这一步
    ↓
宿主程序(你写的代码)实际执行工具   ← 真正执行在这里
    ↓
把执行结果返回给 LLMLLM 基于结果生成最终回答

🎯 这个认知很重要:LLM 只是"描述想做什么",真正执行的是你写的宿主程序。 所以安全控制要在宿主程序里做——靠 LLM 自己约束自己,是不可靠的。

科技架构分工.jpg

白名单 + 分发 + 统一处理——实际项目里用过的设计

这套设计不是读完书推导出来的,是我在实际项目里做过的:

根据文件后缀区分格式,再根据不同格式分发到对应的处理逻辑。

映射到 Agent 工具系统,就是三步:

第一步:确定支持的文件类型白名单

不能什么都支持,要根据实际需求定优先级:

  • 文档类:先支持 .md.pdf.xlsx.pptx 后面再加
  • 图片类:先支持 .jpg.png.webp,SVG 单独处理
  • 视频类:先不做,太消耗 token,等有明确需求再说

第二步:后缀识别 + 分发到对应 Skill

有了白名单,就能写识别逻辑:识别到 .pngimage-skill,识别到 .mddocument-skill,不支持的直接阻断,避免 token 浪费。

第三步:封装公共结果处理器

不同工具的输出格式不同,需要一个统一的结果处理层,把各种格式归一化后再返回给用户。

这个思路,和 LangChain 的 Tool 设计几乎一致:

// Tool 三要素:实现、名称、描述
const readDocumentTool = tool(
  async ({ filePath }) => {
    const ext = path.extname(filePath).slice(1).toLowerCase();
    if (!["md", "txt"].includes(ext)) return "错误:不支持的文件类型";
    return fs.readFileSync(filePath, "utf-8");
  },
  {
    name: "read_document",
    // description 是最重要的:LLM 靠这个决定要不要用这个工具
    description: "读取 .md 或 .txt 文档的文本内容",
    schema: z.object({
      filePath: z.string().describe("文档的绝对路径"),
    }),
  }
);

💡 description 比实现更重要:LLM 选工具完全靠 description。 写得越清晰,选择越准确——这和写函数注释是一个道理,只不过读注释的换成了 AI。

安全设计:三层防护——来自安全公司的经验

书里只提到"安全控制要在宿主程序里做",但没有给出具体结构。

这三层是我自己想到的,来自之前在安全公司做产品的经验——防御要分层,每一层只负责自己那一段

第一层:输入校验(宿主程序)
├── 文件类型白名单过滤
├── 路径合法性检查(防路径穿越攻击)
└── 文件大小限制(防 token 爆炸)

第二层:权限分级(工具内部)
├── 只读操作 → 直接执行
├── 写操作   → 需要用户权限验证
└── 危险操作 → 直接拒绝 + 记录日志

第三层:人工确认(不可逆操作)
└── 涉及不可逆操作 → 暂停执行,等待用户确认

三层各自防一类风险:

  • 危险脚本伪造后缀名:比如把恶意脚本命名成 .md——要在工具内部先做内容校验,不能只看后缀
  • 权限过大的操作:比如删库、清空文件——直接在工具层拒绝,不给 LLM 任何执行机会
  • 有副作用的操作:比如写入数据库、发送邮件——先上报给用户,让用户决定是否执行

ReAct:工具调用的思维框架

工具调用背后有一个经典模式叫 ReAct(Reasoning + Acting):

Thought(思考):我需要先检测文件类型
Action(行动):调用 detect_file_type 工具
Observation(观察):返回 { supported: true, category: "document" }

Thought(思考):是 md 文件,用 read_document 工具
Action(行动):调用 read_document 工具
Observation(观察):返回文件内容

Thought(思考):我已经有了文件内容,可以总结了
Action(行动):生成最终回答(不再调用工具)

LLM 不断地思考 → 行动 → 观察,直到认为任务完成。这个循环,就是工具调用的完整链路。

💡 关键洞察

洞察 说明
先用原生,再找第三方 KISS + DRY,能不造轮子就不造
LLM 不执行代码 它只输出"意图",宿主程序才真正执行
description 决定工具选择 写好描述比写好实现更重要
temperature=0 工具调用场景必须稳定,不能随机
安全分三层 输入校验 → 权限分级 → 人工确认

📖 第六章:规划(Planning)

先有需求,后发现对应模式

工具使用解决了"能做到"的问题,但如果任务足够复杂,直接丢给 AI 执行,结果往往天马行空。

我在实际项目里遇到过这个问题。当前在做的报价系统,如果直接跟 AI 说:

"根据当前项目的设计风格,在 projectList 添加一个入口,实现一个报价页面,并能生成 PDF"

AI 可能会给你一个完全不符合现有架构的方案,或者直接开始写代码,跳过了架构设计和影响范围分析。

说实话,"先规划再执行"这个习惯,我在做这个需求之前就已经这么做了——不是先学了规划模式才这么想,而是工程经验告诉我应该这么做。读到这章,才发现原来这就是规划模式。

我接到复杂需求的拆解方式大概是这样:

  1. 架构设计:先看影响范围,在不影响主流程的情况下设计完整链路
  2. 交互阶段拆分:梳理完整的用户交互流程(报价系统:项目入口 → 报价页 → 生成报价单,3个页面)
  3. 功能模块拆分:每个页面拆分功能模块,明确职责,不用太细,编码时再优化
  4. 组合验证:把所有模块组合起来,验证整体交互是否完整

这个过程映射到 Agent 上,就是规划模式要做的事。

原书的核心判断标准

书里有一句话说得非常准,是判断要不要用规划模式的关键:

「方法」需要被发现,还是已经已知?

  • 方法已知、流程固定 → 用提示链就够了
  • 方法未知、需要动态生成 → 用规划模式

这个标准比"任务复不复杂"更精准。有些任务看起来复杂,但流程是固定的(比如生成报价单),用提示链反而更稳;有些任务看起来简单,但解法不确定(比如调试一个未知 bug),就需要规划模式动态应对。

两种规划策略

这两个名字(静态规划/动态规划)是 AI 伙伴帮我提炼的,但背后的意思是我自己的理解:

静态规划(Plan-then-Execute)

先出完整计划,确认后再执行,执行中不修改计划。

适合:任务边界清晰、步骤可预见
例子:生成报价单(步骤固定:收集信息 → 计算 → 排版 → 导出PDF)
优点:可预测、可审查、token 消耗稳定
缺点:遇到意外情况不够灵活

动态规划(ReAct + Replan)

每步执行后重新评估,必要时修改后续计划。

适合:任务边界模糊、中途可能发现新信息
例子:代码重构(重构过程中可能发现新的依赖问题)
优点:灵活应对变化
缺点:token 消耗大,执行路径不可预测

计划必须结构化

计划不能是一段文字,必须是结构化数据,方便程序处理依赖关系和并行调度:

interface PlanTask {
  id: string;
  name: string;
  description: string;
  dependencies: string[];    // 依赖哪些任务完成后才能执行
  estimatedTokens: number;
  status: "pending" | "running" | "done" | "failed" | "skipped";
  result?: string;
  retryCount: number;
}

有了依赖关系,就能做两件事:

  • 拓扑排序:确定执行顺序
  • 并行调度:没有依赖关系的任务同时执行(结合第一篇的并行化模式)

偏差检测与 [REPLAN] 标记——来自另一个项目的 Task 状态管理

执行到一半发现计划不对,怎么办?

[REPLAN] 这个标记是我自己设计的,来自另一个项目里做 Task 状态管理和流转的经验——任务执行过程中,状态不只有"成功/失败",还有"需要重新规划"这种中间态。

// 执行器检测到异常时,输出 [REPLAN] 标记
// 宿主程序捕获后,触发人工干预回调
const needReplan = result.startsWith("[REPLAN]");

if (needReplan && onReplanNeeded) {
  const decision = await onReplanNeeded(result);  // 上报给用户
  // 根据用户决策:continue / replan / abort
}

当 Agent 识别到偏差,有三种处理路径:

  • 继续执行:当前偏差在可接受范围内
  • 调整后继续:修改当前阶段的参数或方向,继续后续步骤
  • 推翻重来:整个计划有问题,重新规划

实际项目里,大多数情况 AI 都会继续执行下去,这是现阶段的局限。但在设计系统时,应该主动埋入这个检测点——不可逆操作不能让 AI 自己决定

规划 vs 提示链

提示链 规划模式
步骤来源 开发者硬编码 LLM 动态生成
流程是否固定 固定,不能变 可根据任务调整
适合场景 方法已知的任务 方法需要被发现的任务
成本 简单、可控、低 token 灵活、强大、高 token

🎯 选择原则

  • 方法已知 → 提示链
  • 方法未知 → 规划模式
  • 两者结合 → 规划模式生成计划,每个子任务内部用提示链执行

🔗 三章串联:自我进化闭环

这三章合在一起,构成了一个完整的自主执行闭环

规划(Planning)
    ↓ 决定做什么、怎么做、分几步
工具使用(Tool Use)
    ↓ 执行时调用外部能力,突破纯文本边界
反思(Reflection)
    ↓ 做完后评估质量,驱动下次更好
    ↑_________________________________|
              (循环迭代)
模式 解决什么问题 核心结构
反思 输出质量不稳定,怎么自我改进? 执行 → 评审 → 改进(循环)
工具使用 纯文本做不到的事,怎么突破? LLM 意图 → 宿主执行 → 结果反馈
规划 复杂任务怎么不跑偏? 先出计划 → 确认 → 执行 → 监控

三者的关系,不是并列的三个工具,而是一个闭环:

规划决定方向,工具使用突破边界,反思保证质量——缺任何一环,Agent 都跑不远。


🛠️ 在 my-resume 项目中的应用

项目场景 对应模式 说明
AI 生成简历后自动评分优化 反思 生成 → 评审 → 改进,最多3轮
读取用户上传的 PDF/图片简历 工具使用 文件类型白名单 + 对应 Skill 处理
报价系统完整链路实现 规划 先拆解阶段和步骤,再按计划执行

边学边做,是我觉得最快的方式。理论看懂了,不等于会用——只有落到真实项目里,才知道哪里还没想清楚。


📝 总结

两篇学完,手里已经有了六个核心模式:

第一篇(基础执行) 第二篇(自我进化)
拆解 提示链:怎么串行拆解 规划:怎么面对复杂任务
分发 路由:怎么分发请求 工具使用:怎么突破边界
提速/质量 并行化:怎么提速 反思:怎么自我改进

这六个模式已经能覆盖大多数 Agent 开发场景。

学到这里,我越来越觉得:Agent 设计和软件系统设计,底层是同一套思维。 拆任务、管流程、做容错、保质量——工程师早就在做了,只不过现在执行者从代码变成了模型。

后续几篇会进入更高阶的领域:多智能体协作、记忆管理、安全防护……

下一篇预告: 当一个 Agent 不够用,需要多个 Agent 协作时,怎么设计? 第三篇将覆盖多智能体、记忆管理、学习适应三个模式。


💬 系列地址:持续更新中

📖 原书地址adp.xindoo.xyz

🛠️ 实战项目:my-resume(NestJS + Drizzle ORM + SQLite)

如果这篇对你有帮助,欢迎点赞收藏,我们下篇见 👋

❌
❌