Skip to content

1、什么是 MCP 和 Tool

MCP和Tool

  • MCP:是一份规范/协议,规定了“容器要怎么长、里面的功能要怎么描述、调用要怎么沟通”。就像 HTTP 规范本身,不是一个服务,而是大家都得遵守的语言。
  • MCP服务器:是规范的一个实现,它作为一个容器,可以对外暴露很多能力(Tool / Resource / Event)。就像一个跑在端口上的 Web 服务器,遵循 HTTP 协议,里面可以有很多 API。
  • Tool:是容器里的功能单元,一般表现为一个“函数”或者“接口”。调用的时候就是传参执行,返回结果。

Tool 目前使用起来是比较简单的,在项目中按照规范写一个函数,将这个函数以 tool 参数的形式传递给大模型,大模型输出函数名之后,系统调用该函数

如果你需要搭建一套工具集的服务,想要提供给多个项目使用,提供给别人使用,提供给支持 MCP 的客户端使用,那么你就可以遵守 MCP 协议搭建 MCP 的服务

我觉得 MCP 和 Tool 本质是差不多的,就是给模型提供“外部能力”的作用,根据自己的情况和能力来选择使用是比较好的

2、工具定义规范

参考:https://www.yuque.com/u29239513/wyl4ok/kazggbpdgpzfv0e7

在定义工具的时候,最重要的就是工具的描述和工具调用需要的参数

  1. 工具的描述:清晰的工具描述可以提高大模型工具调用的成功率
  2. 工具的参数:描述参数工具的时候,要从参数名,参数类型,参数描述等尽可能多的方面讲这个参数表达的更清楚,这样在大模型输出工具参数的时候会准确和很多
  3. 工具的执行函数:这个是一个函数,用于大模型输出合适的工具调用指令之后,系统执行该函数

2.1、工具(Tool)的类型定义:

  • description**🌟****:工具的描述**
  • parameters**🌟****(ToolParameterDefinition):工具的参数定义**
  • category:工具分类
  • version:工具版本
  • handler:工具的执行函数,这个不需要传递给大模型,而是作为系统内部执行的
  • agentAccessible:控制工具是否对代理可见
  • internal:标记为内部工具类
  • Purpose:描述了工具的核心目的和功能定位,它回答”这个工具为什么存在“以及”它解决啦什么问题”,其是一个宏观的功能描述,例如:purpose: 'Perform semantic search over stored memory entries to retrieve relevant knowledge and reasoning traces that can inform current decision-making.'
  • UseCase:描述了工具的具体使用场景和适用条件,它回答了“在什么情况下应该使用这个工具”以及“如何正确的使用这个工具”,其更多的关注应用场景和适用条件,例如:useCase: 'Use when you need to find previously stored knowledge, code patterns, or technical information that may be relevant to answering current questions or solving problems.'
  • renderResultForAssistant:格式化工具结果给 AI(cli 应用使用,例如 ClaudeCode)
  • needsPermissions:权限控制,是否需要用户授权(cli 应用使用,例如 ClaudeCode)
  • isEnabled:工具是否启用(cli 应用使用,例如 ClaudeCode)
  • isReadOnly:工具是否只读(影响并发)(cli 应用使用,例如 ClaudeCode)
  • isConcurrencySafe:是否支持并发(cli 应用使用,例如 ClaudeCode)

descriptionparamenters**handler**是工具定义必须要的参数,其他的参数是根据情况选择的

handler 是工具的执行函数,这个不用作为工具定义信息传递给大模型

2.2、工具参数(ToolParameterDefinition)的类型定义:

  • type:定义参数数据类型,如:'string' | 'number' | 'boolean' | 'object' | 'array'
  • description:提供参数的人类可读描述,帮助 LLM 理解参数的用途
  • minimum | maximum:定义参数数值的最小值和最大值
  • minItems | maxItems:定义数组参数的最小和最大元素数量
  • minLength | maxLength:定义字符串参数的最小和最大长度
  • oneOf:定义参数必须匹配多个 Schema 中的一个
  • additionalProperties:禁止对象参数包含额外属性
  • items:定义数组元素的类型
  • enum:定义参数的可能取值
  • default:定义参数的默认值

typedescription 是必须的,这个是参数定义的基本元素,并且参数命名也可以成为参数的描述

下面是一个工具定义完整的案例:

typescript
export const memoryOperationTool: InternalTool = {
	name: 'memory_operation',
	category: 'memory',
	internal: true,
	description:
		'Process extracted knowledge and determine memory operations (ADD, UPDATE, DELETE, NONE) using LLM-powered intelligent reasoning and similarity analysis with existing memories.',
	version: '2.0.0', // version
	parameters: {
		type: 'object',
		properties: {
			extractedFacts: {
				type: 'array',
				description:
					'Array of knowledge facts already extracted from interactions, containing technical details, code patterns, or implementation information.',
				items: {
					type: 'string',
				},
			},
			existingMemories: {
				type: 'array',
				description: 'Array of existing memory entries to compare against for similarity analysis.',
				items: {
					type: 'object',
					properties: {
						id: {
							type: 'string',
							description: 'Unique identifier of the existing memory',
						},
						text: {
							type: 'string',
							description: 'Content of the existing memory',
						},
						metadata: {
							type: 'object',
							description: 'Optional metadata for the memory',
						},
					},
					required: ['id', 'text'],
				},
			},
		},
		required: ['extractedFacts'],
	},
	handler: memoryOperationHandler
	},
};

//工具调用函数
const memoryOperationHandler = async (
	args: MemoryOperationArgs,
	context?: InternalToolContext
): Promise<MemoryOperationResult> => {
	// TODO: Implement intelligent reasoning logic to determine memory operations
	// Example placeholder return structure
	return {
		operations: [], // List of { operation: 'ADD' | 'UPDATE' | 'DELETE' | 'NONE', fact, memoryId? }
	};
};

2.3、工具执行函数定义

上面工具定义和工具参数定义这些都在作为 tool 参数传递给 LLM ,本质上就是 LLM 根据传递的工具描述和用户输入挑选出来一个合适的工具,但是大模型不负责执行工具函数,它只负责输出工具名称和工具参数

对于工具执行函数的定义,可以简单的根据工具参数去定义的,但是还是有一些常规定义方法,可以参考

工具的执行函数,会定义两个参数对象

  • arge:函数执行需要的参数对象
  • context |metadata:函数的元信息

一般来说,arge 是刚刚传入工具参数的类型,是工具执行函数的基本参数,而 context 或者 metadata 是属于辅助参数,大部分情况是用于监控和统计的,是可选的,没有这个也不影响函数的主功能执行

例如上面的定义案例中的memoryOperationHandler

typescript
//主要参数-参数对象
export interface MemoryOperationArgs {
	extractedFacts: string[];
	existingMemories?: {
		id: string;
		text: string;
		metadata?: Record<string, any>;
	}[];
}

//辅助参数-元信息
export interface InternalToolContext {
    toolName: string;
    startTime: number;
    sessionId: string | undefined;
    userId?: string;
    metadata: Record<string, any> | undefined;
    
    // 服务依赖
    services?: {
        embeddingManager?: EmbeddingManager;
        vectorStoreManager?: VectorStoreManager;
        llmService?: ILLMService;
        knowledgeGraphManager?: KnowledgeGraphManager;
    };
}

//工具执行函数
const memoryOperationHandler = async (
	args: MemoryOperationArgs,
	context?: InternalToolContext
): Promise<MemoryOperationResult> => {
	// TODO: Implement intelligent reasoning logic to determine memory operations
	// Example placeholder return structure
	return {
		operations: [], // List of { operation: 'ADD' | 'UPDATE' | 'DELETE' | 'NONE', fact, memoryId? }
	};
};

三、工具的定位

3.1、Agent 系统的核心

Excalidraw 文件:https://gcntfv628ebr.feishu.cn/file/Be6EbeZvmoN6BTxijSecQGuIn5g

系统核心设计

之前的系统的设计,都是围绕一个“有向无环图(DAG)”的理念来设计的,核心模块主要是由循环和条件判断组成,复杂的系统也只是多个循环和多个条件判断组成,在此基础上增加可观察性,模块化,重试,管理等功能,成为一个完整的系统

而有了 LLM 之后,设计 Agent 的系统时,我们可以暂时放下“有向无环图(DAG)”的想法,我们不需要为每一个步骤和边缘情况编写代码,只需要给 Agent 一个目标和一系列的转换,让 LLM 实时做出决策来确定路径

这样的好处是,你编写的模块更少了,只需将图的“边缘”交给 LLM,让它自己处理节点,甚至会发现有时候 LLM 为问题找到了新的解决方案

这种方式其实就是把循环和条件判断统一交给 LLM 来驱动

Excalidraw 文件:https://gcntfv628ebr.feishu.cn/file/TK9Lb7mI7o2RrWx7yhLc8tlGn5d

LLM系统设计核心

Agent 系统的核心步骤

  1. LLM 决定工作流程中的下一步,输出“工具调用”
  2. 确定性代码执行工具调用
  3. 工具执行结果追加到上下文中
  4. 重复直到下一个步骤确定为“完成”

工具调用的能力,将 Agent 系统中的不确定性的 LLM 流程转换为确定性的代码执行

3.2、控制流

控制流的方式可以为工具调用添加更多的分支控制,让工具调用不仅仅是一次函数执行,而是一次完整的流程调用,所以学会使用控制流来构建工具调用,会发生非常多有趣的事情

例如:

  1. 普通的工具调用:一次执行,查询天气结果,将结果返回给模型

  2. 控制流的工具调用:会检测该工具调用是否是“高风险”,如:我要部署后端服务,这个时候检测到该工具调用时高风险,会进入人工审核判断,

  • 先保存上下文
  • 给用户发审批通知
  • 等待用户的审批结果
  • 用户同意,函数执行

使用控制流,你需要为特定的例子构建独特的控制结构,具体来说,某些工具调用会跳出循环并等待人类或其他长时间的任务(如训练管道)的响应,后续你可能还会使用其他的自定义实现

  1. 工具调用结果进行摘要缓存
  2. LLM 辅助结构化输出
  3. 上下文压缩或内存管理
  4. 日志记录、跟踪和指标统计
  5. 持久暂停和等待

控制流有三种模式

  • 请求信息补充:模型请求更多的信息,这个时候会中断循环等待人类回复
  • 获取信息:例如:请求 git 标签,获取标签之后追加到上下文中,然后返回给模型
  • 请求审批:当出现“高风险操作”的时候,模型会请求用户进行审批,根据用户的选择来执行不同的流程

🌟 工具的调用构建,不要只停留在函数执行,而是要有“控制流”的思维,以此揭开工具调用更多的 LLM 玩法,并且控制流构建合理,可以替代使用 LangGraph 的工作流构建方式

3.3、辅助结构化输出

我们需要大模型输出 JSON 格式的时候,会在提示词里面控制大模型输出结果,例如:

“输出结果一定要是JSON格式,字段要求:xxx、xxxx、xxxx”

这个时候大模型大部分情况下输出的效果还是可以的,如果要让效果更加稳定,我们需要花一点心思在提示词的优化上面。

但是我们还有另外一种思路:使用工具调用的方式来达到结构化输出的目的

在 Function Call 的模式下,输出的结果自然就是 JSON,工具调用的名称和参数的 JSON 格式

🚀 那么是不是可以思考一下,我设置了一个函数,这个函数的执行结果为空,执行代码无,因为这个函数就是用来输出 JSON 格式的参数列表的,

这个参数列表就是我需要的结果,而不是工具调用之后返回的结果


例如现在有一个使用 LLM 进行总结摘要的功能

typescript
//方式A:提示词控制结构化输出
const prompt = `
请总结以下内容,输出必须是 JSON:
{
  "title": "...",
  "key_points": ["...", "..."],
  "sentiment": "positive | neutral | negative"
}
内容:${inputText}
`;

// 模型大概率会返回 JSON 字符串
const raw = await llm.chat(prompt);
const result = JSON.parse(raw);
console.log(result);


//方式B:工具调用辅助结构化输出
// 定义一个“假的工具”,只要参数,不关心执行
const tools = [{
  name: "summarize_text",
  description: "把文本总结为 JSON",
  parameters: {
    type: "object",
    properties: {
      title: { type: "string" },
      key_points: { type: "array", items: { type: "string" } },
      sentiment: { type: "string" }
    },
    required: ["title", "key_points", "sentiment"]
  }
}];

// 让模型调用工具
const res = await llm.chatWithTools({
  user: `请总结:${inputText}`,
  tools,
  tool_choice: "summarize_text" // 强制用工具
});

// 拿到工具的参数 = 我们要的 JSON
const args = JSON.parse(res.tool_calls[0].arguments);
console.log(args);

方式 A:使用提示词控制大模型输出相应的 JSON 结构

方式 B:使用工具调用让大模型输出“工具参数”的 JSON 结构,代码进行提取

四、工具的错误处理

无论你工具定义的多好,LLM 的推理能力多强,都是有可能会出现工具调用错误的,这种错误会让 Agent 系统不够稳定,但是好在 LLM 有“自我修复”的能力,我们可以将错误信息加到 LLM 的上下文中使用起来,发挥 LLM 的“自由修复”能力

目前的工具调用可能出现的问题大概有两种:

  1. 调用工具前进行参数验证 - 用于收集 LLM 输出工具参数错误
  2. 还有一种是工具调用期间的错误事件,例如网络情况-触发重试机制,例如工具挑选错误,也可以将错误信息输入

当触发 LLM 错误修复的机制的时候,要设置一个最大循环修复次数,不然 LLM 会在错误中无限循环

4.1、工具调用参数错误处理机制

当你在设置工具执行函数的逻辑的时候,考虑在工具执行核心逻辑之前,进行参数验证,像 API 接口那样的参数验证,并且对于参数验证出现的错误信息要好好设计,因为这个是 LLM 进行错误修复时的“指路灯”

工具执行错误处理机制:

  1. 验证参数失败:系统检测到未定义的参数或者参数类型错误、参数缺失
  2. 错误反馈:将详细的错误信息返回给 LLM,例如
    1. 参数验证失败-'profile'对象包含未定义的属性'birthday',该属性在schema中不允许。允许的属性有:'name'(必需)、'email'(必需)、'age'(可选)。请移除'birthday'属性并重试
  3. 上下文更新:将错误信息添加到对话历史中
  4. LLM 学习:LLM 从错误信息中学习正确的参数结构
  5. 第二次尝试:LLM 生成修正后的参数
  6. 验证成功:修正后的参数通过验证,执行工具函数
typescript
// 工具错误处理机制核心代码
function validateTool(params) {
  const allowed = ['name', 'email', 'age'];
  const invalid = Object.keys(params).find(p => !allowed.includes(p));
  if (invalid) {
    throw new Error(`参数验证失败-'profile'对象包含未定义的属性'${invalid}',允许的属性有:'name'(必需)、'email'(必需)、'age'(可选)。请移除'${invalid}'属性并重试`);
  }
}

async function executeTool(params) {
  try {
    validateTool(params);
    return '✅ 执行成功';
  } catch (error) {
    console.log('❌ 错误:', error.message);
    
    // LLM从错误中学习并修正参数
    const corrected = { ...params };
    delete corrected.birthday;
    
    console.log('🤖 LLM重试修正后的参数:', corrected);
    validateTool(corrected);
    return '✅ 修正后执行成功';
  }
}

4.2、工具错误重试机制

将错误信息加入到上下文(Context)中之后,LLM 会参考错误信息进行“自我修复”

但是不要陷入到无限的错误修复循环中,要加入限制机制,

例如:限制单个工具最多 3 次尝试,之后程序中断,或者让大模型将错误出现的原因回复给用户,辅助用户进行排查

  • LLM 理解能力限制
  • 上下文混乱
  • 缺乏替代策略:LLM 不知道除了重试还能做什么

那么相应的解决方案是:

  1. 控制流接管:通过确定的程序来主动干预,而不是完全依赖 LLM 判断
  2. 上下文管理:重组或删除错误信息,避免上下文污染,你也可以重新整理错误信息,让错误信息:明确,干净,有效
  3. 使用小而专注的 Agent:减少复杂性,降低错误概率

五、工具的分类

工具大致可以归为三类

  1. 数据类(Data):
    1. 获取上下文与信息,支撑流程决策
    2. 示例:查询数据库、读取 PDF、搜索网页
  2. 动作类(Action)
    1. 在系统中执行操作,推动流程向前
    2. 示例:发邮件、更新 CRM、转交客服请求
  3. 编排类(Orchestration)
    1. 智能体可作为另一个智能体的“工具”使用,实现更高级的协调
    2. 示例:退款智能体、研究智能体、写作智能体