# 工具管理
## 一、什么是 MCP 和 Tool
![MCP和Tool](./image/image%20(10).png)

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

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

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

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

## 二、工具定义规范
参考：[https://www.yuque.com/u29239513/wyl4ok/kazggbpdgpzfv0e7](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）

> `description` 和 `paramenters`、`**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：定义参数的默认值

> `type` 和 `description` 是必须的，这个是参数定义的基本元素，并且参数命名也可以成为参数的描述
>



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

```typescript [toolDefinition.ts]
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 [toolExecutionFunction.ts]
//主要参数-参数对象
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? }
	};
};
```



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



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

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

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

### 3.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 '✅ 修正后执行成功';
  }
}

```

### 3.2、工具错误重试机制
将错误信息加入到上下文（Context）中之后，LLM 会参考错误信息进行“自我修复”

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

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

如果不加限制你会发现，代理会一直循环，一遍又一遍的重复同样的错误，导致这一个的原因有多种：

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

那么相应的解决方案是：

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



## 四、工具的定位
### 4.1、Agent 系统的核心
Excalidraw 文件：[https://gcntfv628ebr.feishu.cn/file/Be6EbeZvmoN6BTxijSecQGuIn5g](https://gcntfv628ebr.feishu.cn/file/Be6EbeZvmoN6BTxijSecQGuIn5g)

![系统核心设计](./image/image%20(11).png)

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



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

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

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



Excalidraw 文件：[https://gcntfv628ebr.feishu.cn/file/TK9Lb7mI7o2RrWx7yhLc8tlGn5d](https://gcntfv628ebr.feishu.cn/file/TK9Lb7mI7o2RrWx7yhLc8tlGn5d)

![LLM系统设计核心](./image/image%20(12).png)

Agent 系统的核心步骤

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

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

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

例如：

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

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

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



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

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



控制流有三种模式

+ **请求信息补充**：模型请求更多的信息，这个时候会中断循环等待人类回复
+ **获取信息**：例如：请求 git 标签，获取标签之后追加到上下文中，然后返回给模型，还有bash工具会将大结果写入到临时文件中，将摘要+临时文件路径返回给LLM，后续LLM如果有需要可以去读取完整的工具内容
+ **请求审批：当出现“高风险操作”的时候，模型会请求用户进行审批，根据用户的选择来执行不同的流程**



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

### 4.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 结构，代码进行提取




## 五、工具的分类
工具大致可以归为三类

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

