1 Function Calling 深入理解
1.1 Function Calling 的本质
Function Calling 是 LLM 厂商提供的一种能力,让模型能够"表达"它想调用某个函数。
核心要点:LLM 自己不执行函数,它只是告诉你"我觉得应该调用这个函数,参数是这些"。
ini
完整的 Function Calling 流程(5步):
┌─────────┐ ┌─────────┐
│ 你的代码 │ │ LLM API │
└────┬────┘ └────┬────┘
│ │
│ ① 发送: messages + tools 定义 │
│ ──────────────────────────────────────→ │
│ │
│ ② 返回: "我想调用 get_weather, │
│ 参数 city='北京'" │
│ ←────────────────────────────────────── │
│ │
│ ③ 你的代码执行 get_weather("北京") │
│ 结果: "晴,25°C" │
│ │
│ ④ 发送: 工具执行结果 │
│ ──────────────────────────────────────→ │
│ │
│ ⑤ 返回: "北京今天天气晴朗, │
│ 气温25°C,适合外出" │
│ ←────────────────────────────────────── │
│ │
1.2 完整代码实现(OpenAI 版)
java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
// ====== 第一步: 定义工具 ======
// 工具的实际实现
record WeatherInfo(String city, int temp, String condition, int humidity, String unit) {}
record Order(String id, String user, String status, int amount) {}
record NotificationResult(boolean success, String channel) {}
public class FunctionCallingAgent {
private static final ObjectMapper mapper = new ObjectMapper();
// 引用模块一中定义的 ClaudeClient
private final ClaudeClient client;
public FunctionCallingAgent(ClaudeClient client) {
this.client = client;
}
/** 获取天气(模拟实现) */
private JsonNode getWeather(JsonNode args) {
String city = args.get("city").asText();
String unit = args.has("unit") ? args.get("unit").asText() : "celsius";
// 实际中你会调用真正的天气 API
var data = Map.of(
"北京", new WeatherInfo("北京", 25, "晴", 30, unit),
"上海", new WeatherInfo("上海", 28, "多云", 65, unit)
);
WeatherInfo info = data.getOrDefault(city,
new WeatherInfo(city, 0, "未知", 0, unit));
return mapper.valueToTree(info);
}
/** 搜索订单(模拟实现) */
private JsonNode searchOrders(JsonNode args) {
String userId = args.get("user_id").asText();
String status = args.has("status") ? args.get("status").asText() : null;
var orders = List.of(
new Order("ORD001", "U123", "shipped", 299),
new Order("ORD002", "U123", "delivered", 599),
new Order("ORD003", "U456", "pending", 99)
);
var result = orders.stream()
.filter(o -> o.user().equals(userId))
.filter(o -> status == null || o.status().equals(status))
.collect(Collectors.toList());
return mapper.valueToTree(result);
}
/** 发送通知(模拟实现) */
private JsonNode sendNotification(JsonNode args) {
String channel = args.get("channel").asText();
String message = args.get("message").asText();
System.out.printf(" 📤 发送通知到 %s: %s%n", channel, message);
return mapper.valueToTree(new NotificationResult(true, channel));
}
// 工具映射表(名称 → 函数)
private final Map<String, Function<JsonNode, JsonNode>> toolsMap = Map.of(
"get_weather", this::getWeather,
"search_orders", this::searchOrders,
"send_notification", this::sendNotification
);
// ====== 第二步: 定义工具的 JSON Schema ======
private List<ObjectNode> buildToolsSchema() {
var getWeatherTool = mapper.createObjectNode()
.put("name", "get_weather")
.put("description", "获取指定城市的实时天气信息,包括温度、天气状况和湿度");
var weatherProps = mapper.createObjectNode();
weatherProps.set("city", mapper.createObjectNode()
.put("type", "string").put("description", "城市名称,如 '北京'、'上海'"));
weatherProps.set("unit", mapper.createObjectNode()
.put("type", "string").put("description", "温度单位,默认摄氏度")
.set("enum", mapper.createArrayNode().add("celsius").add("fahrenheit")));
getWeatherTool.set("input_schema", mapper.createObjectNode()
.put("type", "object")
.set("properties", weatherProps));
((ObjectNode) getWeatherTool.get("input_schema"))
.set("required", mapper.createArrayNode().add("city"));
var searchOrdersTool = mapper.createObjectNode()
.put("name", "search_orders")
.put("description", "根据用户ID查询订单列表。可按状态筛选。");
var orderProps = mapper.createObjectNode();
orderProps.set("user_id", mapper.createObjectNode()
.put("type", "string").put("description", "用户ID,如 'U123'"));
orderProps.set("status", mapper.createObjectNode()
.put("type", "string").put("description", "订单状态筛选(可选)")
.set("enum", mapper.createArrayNode()
.add("pending").add("shipped").add("delivered").add("cancelled")));
searchOrdersTool.set("input_schema", mapper.createObjectNode()
.put("type", "object")
.set("properties", orderProps));
((ObjectNode) searchOrdersTool.get("input_schema"))
.set("required", mapper.createArrayNode().add("user_id"));
var sendNotifTool = mapper.createObjectNode()
.put("name", "send_notification")
.put("description", "向指定渠道发送通知消息");
var notifProps = mapper.createObjectNode();
notifProps.set("channel", mapper.createObjectNode()
.put("type", "string").put("description", "通知渠道")
.set("enum", mapper.createArrayNode().add("email").add("slack").add("sms")));
notifProps.set("message", mapper.createObjectNode()
.put("type", "string").put("description", "通知内容"));
sendNotifTool.set("input_schema", mapper.createObjectNode()
.put("type", "object")
.set("properties", notifProps));
((ObjectNode) sendNotifTool.get("input_schema"))
.set("required", mapper.createArrayNode().add("channel").add("message"));
return List.of(getWeatherTool, searchOrdersTool, sendNotifTool);
}
// ====== 第三步: Agent 循环 ======
/** 完整的 Function Calling Agent */
public String runAgent(String userMessage, int maxTurns) throws Exception {
String system = """
你是一个客服助手,可以查询天气、搜索订单、发送通知。
请根据用户需求选择合适的工具。如果需要多个步骤,依次执行。""";
var messages = new ArrayList<>(List.of(
new Message("user", userMessage)
));
var tools = buildToolsSchema();
for (int turn = 0; turn < maxTurns; turn++) {
// 调用 LLM
ChatResponse response = client.chatWithTools(system, messages, tools);
// 分离 text block 和 tool_use block
var toolUseBlocks = response.content().stream()
.filter(b -> b instanceof ContentBlock.ToolUseBlock)
.map(b -> (ContentBlock.ToolUseBlock) b)
.toList();
// 情况 1: LLM 直接回答(不需要工具)
if (toolUseBlocks.isEmpty()) {
return response.content().stream()
.filter(b -> b instanceof ContentBlock.TextBlock)
.map(b -> ((ContentBlock.TextBlock) b).text())
.collect(Collectors.joining());
}
// 情况 2: LLM 想调用工具
// 先把 assistant 消息加入历史(包含完整 content blocks)
messages.add(new Message("assistant", mapper.writeValueAsString(response.content())));
// 可能一次调用多个工具(并行工具调用)
var toolResults = new ArrayNode(mapper.getNodeFactory());
for (var toolUse : toolUseBlocks) {
String funcName = toolUse.name();
JsonNode funcArgs = toolUse.input();
System.out.printf(" 🔧 调用工具: %s(%s)%n", funcName, funcArgs);
// 执行工具
String resultStr;
try {
JsonNode result = toolsMap.get(funcName).apply(funcArgs);
resultStr = mapper.writeValueAsString(result);
} catch (Exception e) {
resultStr = mapper.writeValueAsString(Map.of("error", e.getMessage()));
}
System.out.printf(" 📋 结果: %s%n",
resultStr.substring(0, Math.min(200, resultStr.length())));
// 把工具结果传回 LLM
toolResults.add(mapper.createObjectNode()
.put("type", "tool_result")
.put("tool_use_id", toolUse.id())
.put("content", resultStr));
}
messages.add(new Message("user", mapper.writeValueAsString(toolResults)));
}
return "达到最大轮次限制";
}
// ====== 测试 ======
public static void main(String[] args) throws Exception {
var client = new ClaudeClient();
var agent = new FunctionCallingAgent(client);
// 简单查询
System.out.println("=".repeat(50));
System.out.println(agent.runAgent("北京今天天气怎么样?", 10));
// 多工具调用
System.out.println("\n" + "=".repeat(50));
System.out.println(agent.runAgent("帮我查一下用户 U123 的订单,然后把结果通过 Slack 通知我", 10));
}
}
1.3 并行工具调用(Parallel Tool Calls)
LLM 有时会一次请求调用多个工具(当它认为这些调用是独立的):
css
用户: "告诉我北京和上海的天气"
LLM 的响应:
tool_calls: [
{ id: "call_1", function: { name: "get_weather", arguments: {"city": "北京"} } },
{ id: "call_2", function: { name: "get_weather", arguments: {"city": "上海"} } }
]
→ 两个工具调用可以并行执行,提升速度!
java
import java.util.concurrent.CompletableFuture;
/** 并行执行多个工具调用 */
List<Map.Entry<ContentBlock.ToolUseBlock, JsonNode>> executeToolsParallel(
List<ContentBlock.ToolUseBlock> toolCalls) throws Exception {
// 为每个工具调用创建一个异步任务
var futures = toolCalls.stream()
.map(call -> CompletableFuture.supplyAsync(() ->
Map.entry(call, toolsMap.get(call.name()).apply(call.input()))
))
.toList();
// 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
return futures.stream().map(CompletableFuture::join).toList();
}
1.4 Claude API 的 Tool Use
Claude 的工具调用与 OpenAI 类似但格式略有不同:
java
// Claude 的工具定义格式(使用 Jackson ObjectNode)
var tools = List.of(
mapper.createObjectNode()
.put("name", "get_weather")
.put("description", "获取指定城市的天气信息")
.<ObjectNode>set("input_schema", mapper.createObjectNode() // 注意: Claude 用 input_schema,不是 parameters
.put("type", "object")
.<ObjectNode>set("properties", mapper.createObjectNode()
.set("city", mapper.createObjectNode()
.put("type", "string")
.put("description", "城市名称")))
.set("required", mapper.createArrayNode().add("city")))
);
/** Claude API 的 Tool Use Agent */
public String runClaudeAgent(String userMessage) throws Exception {
var messages = new ArrayList<>(List.of(
new Message("user", userMessage)
));
while (true) {
ChatResponse response = client.chatWithTools(
null, messages, tools
);
// 检查是否有工具调用
var toolUseBlocks = response.content().stream()
.filter(b -> b instanceof ContentBlock.ToolUseBlock)
.map(b -> (ContentBlock.ToolUseBlock) b)
.toList();
if (toolUseBlocks.isEmpty()) {
// 没有工具调用,提取文本回答
return response.content().stream()
.filter(b -> b instanceof ContentBlock.TextBlock)
.map(b -> ((ContentBlock.TextBlock) b).text())
.findFirst().orElse("");
}
// 把 assistant 消息加入历史
messages.add(new Message("assistant",
mapper.writeValueAsString(response.content())));
// 执行工具并返回结果
var toolResults = mapper.createArrayNode();
for (var block : toolUseBlocks) {
JsonNode result = toolsMap.get(block.name()).apply(block.input());
toolResults.add(mapper.createObjectNode()
.put("type", "tool_result")
.put("tool_use_id", block.id())
.put("content", mapper.writeValueAsString(result)));
}
messages.add(new Message("user",
mapper.writeValueAsString(toolResults)));
}
}
// 使用
public static void main(String[] args) throws Exception {
var client = new ClaudeClient();
var agent = new FunctionCallingAgent(client);
System.out.println(agent.runClaudeAgent("北京天气怎么样?"));
}
1.5 OpenAI vs Claude Tool Use 对比
| 维度 | OpenAI | Claude |
|---|---|---|
| 工具定义字段 | parameters |
input_schema |
| 工具调用返回 | tool_calls 数组 |
content 中的 tool_use block |
| 结果返回 role | role: "tool" |
role: "user" + tool_result type |
| 并行调用 | 支持 | 支持 |
| 强制调用 | tool_choice: "required" |
tool_choice: {"type": "any"} |
| 指定工具 | tool_choice: {"type":"function","function":{"name":"xxx"}} |
tool_choice: {"type":"tool","name":"xxx"} |
2 MCP 协议------Agent 工具的未来标准
2.1 为什么需要 MCP?
问题场景:假设你开发了 3 个 Agent(客服、运维、数据分析),它们都需要访问数据库、搜索日志、发送通知。
没有 MCP 的情况:
markdown
Agent 1 (客服) Agent 2 (运维) Agent 3 (数据分析)
│ │ │
├─→ 自己写数据库工具 ├─→ 自己写数据库工具 ├─→ 自己写数据库工具
├─→ 自己写日志工具 ├─→ 自己写日志工具 ├─→ 自己写日志工具
└─→ 自己写通知工具 └─→ 自己写通知工具 └─→ 自己写通知工具
问题:
- 每个 Agent 都要重复实现相同的工具 ❌
- 工具接口不统一,难以维护 ❌
- 换个 Agent 框架,所有工具要重写 ❌
有 MCP 的情况:
arduino
Agent 1 (客服) Agent 2 (运维) Agent 3 (数据分析)
│ │ │
└──── MCP Client ──┼──── MCP Client ──┘
│ │
▼ ▼
┌─── MCP Protocol ──────────────────┐
│ │
▼ ▼ ▼ ▼
数据库 日志 通知 文件
MCP Server MCP Server MCP Server MCP Server
优势:
- 工具只写一次,所有 Agent 共用 ✅
- 接口标准统一 ✅
- 社区共享,拿来即用 ✅
2.2 MCP 架构详解
MCP 的能力分为三个层面:Server 端能力 (Server 暴露给 Client)、Client 端能力 (Client 暴露给 Server)和跨功能能力。
arduino
一、Server 端能力(Server → Client)
┌──────────────────────────────────────────┐
│ MCP Server │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Tools(工具) │ │
│ │ Agent 可以调用的函数 │ │
│ │ │ │
│ │ 例: search_logs, query_database │ │
│ │ → Agent 主动调用,获取信息或执行操作 │ │
│ │ → 通过 tools/list 发现, tools/call │ │
│ │ 调用 │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Resources(资源) │ │
│ │ Agent 可以读取的数据 │ │
│ │ │ │
│ │ 例: file:///logs/app.log │ │
│ │ 例: db://users/schema │ │
│ │ → 类似文件系统,提供数据访问 │ │
│ │ → 通过 resources/list 发现, │ │
│ │ resources/read 读取 │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Prompts(提示模板) │ │
│ │ 预定义的提示词模板 │ │
│ │ │ │
│ │ 例: "分析日志错误" 模板 │ │
│ │ → 帮助用户快速构建有效提示 │ │
│ │ → 通过 prompts/list 发现, │ │
│ │ prompts/get 获取 │ │
│ └─────────────────────────────────────┘ │
└──────────────────────────────────────────┘
二、Client 端能力(Client → Server,即 Server 可以反向请求 Client)
┌──────────────────────────────────────────┐
│ MCP Client │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Sampling(采样) │ │
│ │ Server 请求 Client 的 LLM 生成内容 │ │
│ │ │ │
│ │ 场景: Server 想用 LLM 但不想自己 │ │
│ │ 集成 LLM SDK,借用 Client 的 LLM │ │
│ │ → 通过 sampling/createMessage 请求 │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Elicitation(用户交互) │ │
│ │ Server 向用户请求额外信息或确认 │ │
│ │ │ │
│ │ 场景: 执行危险操作前要求用户确认, │ │
│ │ 或需要用户补充缺失的参数 │ │
│ │ → 通过 elicitation/create 请求 │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Logging(日志) │ │
│ │ Server 向 Client 发送日志消息 │ │
│ │ │ │
│ │ 场景: 调试信息、运行状态监控、 │ │
│ │ 错误报告等 │ │
│ │ → 通过 notifications/message 发送 │ │
│ └─────────────────────────────────────┘ │
└──────────────────────────────────────────┘
三、跨功能能力
┌──────────────────────────────────────────┐
│ Notifications(通知) │
│ 实时事件通知,无需响应 │
│ │
│ 例: Server 的工具列表发生变化时,主动 │
│ 通知 Client 刷新(tools/list_changed) │
├──────────────────────────────────────────┤
│ Tasks(任务) [实验性] │
│ 支持长时间运行的操作 │
│ │
│ 例: 批量数据处理、多步工作流,支持 │
│ 延迟获取结果和状态跟踪 │
└──────────────────────────────────────────┘
能力协商 :Client 和 Server 在初始化握手时会互相声明自己支持哪些能力。例如 Client 声明支持 sampling 后,Server 才能请求 LLM 补全;Server 声明支持 tools 后,Client 才会去发现和调用工具。
2.3 MCP 通信协议
MCP 使用 JSON-RPC 2.0 协议进行通信:
css
MCP Client MCP Server
│ │
│ ── initialize ──────────────────→ │ 握手(声明双方支持的能力)
│ ←── capabilities ─────────────── │
│ │
│ ── tools/list ──────────────────→ │ 发现工具
│ ←── [工具列表] ───────────────── │
│ │
│ ── tools/call ──────────────────→ │ 调用工具
│ {name: "search", args: {...}} │
│ ←── {result: ...} ────────────── │
│ │
│ ── resources/list ──────────────→ │ 发现资源
│ ←── [资源列表] ───────────────── │
│ │
│ ── resources/read ──────────────→ │ 读取资源
│ {uri: "file:///..."} │
│ ←── {content: ...} ───────────── │
│ │
│ ── prompts/list ────────────────→ │ 发现提示模板
│ ←── [模板列表] ───────────────── │
│ │
│ ── prompts/get ─────────────────→ │ 获取提示模板
│ ←── {messages: [...]} ────────── │
│ │
│ ←── sampling/createMessage ──── │ Server 请求 LLM 补全
│ ── {content: ...} ──────────────→ │ (需 Client 声明支持 sampling)
│ │
│ ←── elicitation/create ───────── │ Server 请求用户输入
│ ── {response: ...} ─────────────→ │ (需 Client 声明支持 elicitation)
│ │
│ ←── notifications/message ────── │ Server 发送日志(无需响应)
│ │
│ ←── notifications/tools/ │ Server 通知工具列表变更
│ list_changed ────────────── │ (无需响应,Client 可重新拉取)
2.4 编写一个 MCP Server
java
/**
* 一个简单的 MCP Server 示例
* 功能: 提供天气查询和待办事项管理
*
* 基于 HTTP 实现,使用 com.sun.net.httpserver
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;
import java.io.*;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class McpToolsServer {
private static final ObjectMapper mapper = new ObjectMapper();
// 内存中的待办事项(模拟数据库)
private static final List<Map<String, Object>> todos = new CopyOnWriteArrayList<>();
// ====== 定义工具 ======
/** 返回所有可用工具 */
private static ArrayNode listTools() {
var tools = mapper.createArrayNode();
tools.add(mapper.createObjectNode()
.put("name", "get_weather")
.put("description", "获取指定城市的天气信息")
.<ObjectNode>set("inputSchema", mapper.createObjectNode()
.put("type", "object")
.<ObjectNode>set("properties", mapper.createObjectNode()
.set("city", mapper.createObjectNode()
.put("type", "string")
.put("description", "城市名称")))
.set("required", mapper.createArrayNode().add("city"))));
tools.add(mapper.createObjectNode()
.put("name", "add_todo")
.put("description", "添加一个待办事项")
.<ObjectNode>set("inputSchema", mapper.createObjectNode()
.put("type", "object")
.<ObjectNode>set("properties", mapper.createObjectNode()
.<ObjectNode>set("title", mapper.createObjectNode()
.put("type", "string")
.put("description", "待办事项标题"))
.set("priority", mapper.createObjectNode()
.put("type", "string")
.put("description", "优先级")
.set("enum", mapper.createArrayNode()
.add("high").add("medium").add("low"))))
.set("required", mapper.createArrayNode().add("title"))));
tools.add(mapper.createObjectNode()
.put("name", "list_todos")
.put("description", "列出所有待办事项")
.set("inputSchema", mapper.createObjectNode()
.put("type", "object")
.set("properties", mapper.createObjectNode())));
return tools;
}
/** 处理工具调用 */
private static ObjectNode callTool(String name, JsonNode arguments) {
var result = mapper.createObjectNode();
var content = mapper.createArrayNode();
switch (name) {
case "get_weather" -> {
String city = arguments.get("city").asText();
// 模拟天气数据
var weather = Map.of("北京", "晴 25°C", "上海", "多云 28°C");
String text = weather.getOrDefault(city, city + ": 暂无数据");
content.add(mapper.createObjectNode()
.put("type", "text").put("text", text));
}
case "add_todo" -> {
var todo = new LinkedHashMap<String, Object>();
todo.put("id", todos.size() + 1);
todo.put("title", arguments.get("title").asText());
todo.put("priority", arguments.has("priority")
? arguments.get("priority").asText() : "medium");
todo.put("done", false);
todos.add(todo);
content.add(mapper.createObjectNode()
.put("type", "text")
.put("text", "已添加待办: " + todo.get("title")));
}
case "list_todos" -> {
if (todos.isEmpty()) {
content.add(mapper.createObjectNode()
.put("type", "text").put("text", "暂无待办事项"));
} else {
var sb = new StringBuilder();
for (var t : todos) {
sb.append((boolean) t.get("done") ? "✅" : "⬜")
.append(" [").append(t.get("priority")).append("] ")
.append(t.get("title")).append("\n");
}
content.add(mapper.createObjectNode()
.put("type", "text").put("text", sb.toString().strip()));
}
}
default -> content.add(mapper.createObjectNode()
.put("type", "text").put("text", "未知工具: " + name));
}
result.set("content", content);
return result;
}
// ====== JSON-RPC 请求处理 ======
private static ObjectNode handleJsonRpc(JsonNode request) {
String method = request.get("method").asText();
var response = mapper.createObjectNode();
response.put("jsonrpc", "2.0");
response.set("id", request.get("id"));
switch (method) {
case "initialize" -> {
var caps = mapper.createObjectNode();
caps.set("tools", mapper.createObjectNode());
response.set("result", mapper.createObjectNode()
.put("protocolVersion", "2024-11-05")
.set("capabilities", caps));
}
case "tools/list" -> response.set("result",
mapper.createObjectNode().set("tools", listTools()));
case "tools/call" -> {
var params = request.get("params");
response.set("result", callTool(
params.get("name").asText(),
params.get("arguments")));
}
default -> response.set("error", mapper.createObjectNode()
.put("code", -32601).put("message", "Method not found: " + method));
}
return response;
}
// ====== 启动 Server ======
public static void main(String[] args) throws Exception {
int port = 3000;
var server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/mcp", exchange -> {
if ("POST".equals(exchange.getRequestMethod())) {
JsonNode request = mapper.readTree(exchange.getRequestBody());
ObjectNode response = handleJsonRpc(request);
byte[] body = mapper.writeValueAsBytes(response);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, body.length);
try (var os = exchange.getResponseBody()) { os.write(body); }
} else {
exchange.sendResponseHeaders(405, -1);
}
});
server.start();
System.out.println("MCP Server running on port " + port);
}
}
2.5 配置 MCP Server
在 Claude Code 中使用:
json
// ~/.claude/settings.json 或项目的 .mcp.json
{
"mcpServers": {
"my-tools": {
"command": "python",
"args": ["/path/to/my_mcp_server.py"],
"env": {
"API_KEY": "xxx"
}
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "your-github-token"
}
}
}
}
在 Cursor 中使用:
json
// .cursor/mcp.json
{
"mcpServers": {
"my-tools": {
"command": "python",
"args": ["my_mcp_server.py"]
}
}
}
2.6 常用的社区 MCP Server
| MCP Server | 功能 | 安装方式 |
|---|---|---|
| @modelcontextprotocol/server-github | GitHub 操作(PR、Issue、代码搜索) | npx -y @modelcontextprotocol/server-github |
| @modelcontextprotocol/server-filesystem | 文件系统读写 | npx -y @modelcontextprotocol/server-filesystem |
| @modelcontextprotocol/server-postgres | PostgreSQL 查询 | npx -y @modelcontextprotocol/server-postgres |
| @modelcontextprotocol/server-slack | Slack 消息收发 | npx -y @modelcontextprotocol/server-slack |
| @modelcontextprotocol/server-puppeteer | 浏览器自动化 | npx -y @modelcontextprotocol/server-puppeteer |
| @modelcontextprotocol/server-brave-search | Brave 搜索引擎 | npx -y @modelcontextprotocol/server-brave-search |
2.7 MCP vs Function Calling 的关系
vbscript
关系: MCP 是 Function Calling 的"标准化 + 生态化"升级
Function Calling:
- 每个 Agent 自己定义工具
- 工具和 Agent 代码耦合
- 不同 Agent 之间无法共享工具
MCP:
- 工具独立成 Server
- Agent 通过 MCP 协议发现和调用工具
- 一个 MCP Server 可被多个 Agent 使用
- 社区共享,生态丰富
层次关系:
MCP Server 内部的工具 → 通过 MCP 协议暴露 → Agent 的 MCP Client 接收
→ 转化为 LLM 的 Function Calling tools 定义 → LLM 选择并"调用"
→ Agent 代码通过 MCP Client 转发到 MCP Server 执行
3 工具设计最佳实践
3.1 命名规范
java
// ❌ 差的命名
"do_stuff" // 太模糊
"tool1" // 无意义
"handleUserRequest" // 太宽泛
"getDataAndProcess" // 一个工具做太多事
// ✅ 好的命名
"search_orders" // 动词_名词,清晰
"get_weather" // 简短直接
"create_jira_ticket" // 包含具体系统名
"query_database" // 说明操作类型
3.2 描述的写法
工具描述是 LLM 决定是否使用该工具的核心依据。
java
// ❌ 差的描述
"处理数据" // 太模糊,LLM 不知道什么时候该用
"查询" // 查询什么?
// ✅ 好的描述
"根据订单ID查询订单详情,返回订单状态、金额、收货地址和物流信息。当用户询问订单相关问题时使用此工具。"
// ✅ 更好的描述(包含使用场景)
"""
搜索公司内部知识库中的技术文档。
使用场景: 当用户询问内部系统的技术细节、架构设计、部署流程等问题时。
不适用: 一般性的编程问题(这些请直接回答)。
返回: 最相关的 3 篇文档摘要及链接。
"""
3.3 参数设计
java
// ❌ 差的参数设计
mapper.createObjectNode()
.put("type", "object")
.set("properties", mapper.createObjectNode()
.set("data", mapper.createObjectNode()
.put("type", "string"))); // 什么 data?
// ✅ 好的参数设计
var props = mapper.createObjectNode();
props.set("user_id", mapper.createObjectNode()
.put("type", "string")
.put("description", "用户唯一标识,格式: U + 数字,如 'U12345'"));
props.set("date_range", mapper.createObjectNode()
.put("type", "string")
.put("description", "查询时间范围。格式: 'YYYY-MM-DD:YYYY-MM-DD',如 '2024-01-01:2024-01-31'"));
props.set("status", mapper.createObjectNode()
.put("type", "string")
.put("description", "用户状态筛选")
.set("enum", mapper.createArrayNode() // 枚举值帮助 LLM 正确填写
.add("active").add("inactive").add("suspended")));
props.set("limit", mapper.createObjectNode()
.put("type", "integer")
.put("description", "返回结果最大数量,默认 10,最大 100")
.put("default", 10));
var schema = mapper.createObjectNode()
.put("type", "object")
.set("properties", props);
((ObjectNode) schema)
.set("required", mapper.createArrayNode().add("user_id")); // 只有真正必要的才 required
3.4 错误处理
java
/** 工具应该返回清晰的错误信息,帮助 LLM 理解问题并采取补救措施 */
public String searchDatabase(String query) {
var mapper = new ObjectMapper();
try {
var results = db.execute(query);
if (results.isEmpty()) {
// ✅ 返回有意义的空结果说明
return mapper.writeValueAsString(Map.of(
"status", "no_results",
"message", "未找到匹配 '" + query + "' 的记录",
"suggestion", "可以尝试更宽泛的搜索条件"
));
}
return mapper.writeValueAsString(Map.of("status", "success", "data", results));
} catch (SecurityException e) {
// ✅ 清晰的权限错误
return toJson(Map.of(
"status", "error",
"error", "permission_denied",
"message", "当前用户无权执行此查询,需要管理员权限"
));
} catch (java.util.concurrent.TimeoutException e) {
// ✅ 超时信息
return toJson(Map.of(
"status", "error",
"error", "timeout",
"message", "查询超时(>30s),可能是查询条件太宽泛,建议缩小范围"
));
} catch (Exception e) {
// ✅ 通用错误但不暴露内部细节
return toJson(Map.of(
"status", "error",
"error", "internal",
"message", "查询执行失败: " + e.getClass().getSimpleName()
));
}
}
private String toJson(Map<String, String> map) {
try {
return new ObjectMapper().writeValueAsString(map);
} catch (Exception e) {
return "{\"error\":\"serialization_failed\"}";
}
}
3.5 工具粒度
makefile
原则: 一个工具做一件事
❌ 粒度太大(万能工具):
"database_manager" → 能建表、查询、更新、删除、备份...
问题: LLM 难以正确选择参数,各种操作混在一起
❌ 粒度太小(碎片化):
"open_connection" → "prepare_query" → "execute" → "close_connection"
问题: LLM 需要记住调用顺序,增加出错概率
✅ 粒度合适:
"query_users" → 查询用户
"create_user" → 创建用户
"update_user" → 更新用户
"search_orders" → 搜索订单
每个工具独立完成一个完整的业务操作
4 实战练习
练习 1: 构建一个完整的工具集
java
/**
* 目标: 为一个"项目管理 Agent"设计和实现工具集
* 包含:
* - 查询项目列表
* - 创建任务
* - 更新任务状态
* - 搜索文档
*/
// 请自己尝试实现以下工具的 JSON Schema 和执行函数
// 然后用模块二中学到的 ReAct Agent 把它们串起来
// 1. list_projects: 查询所有项目
// 2. create_task: 在指定项目中创建任务
// 3. update_task_status: 更新任务状态
// 4. search_docs: 在知识库中搜索文档
练习 2: 写一个 MCP Server
java
/**
* 目标: 实现一个笔记管理 MCP Server
* 功能:
* - add_note: 添加笔记
* - search_notes: 搜索笔记
* - list_notes: 列出所有笔记
* - delete_note: 删除笔记
*
* 然后配置到 Claude Code 中使用
*/
本模块学习检查清单
- 能完整描述 Function Calling 的 5 步流程
- 能用 OpenAI 或 Claude API 实现带工具调用的 Agent
- 理解并行工具调用(Parallel Tool Calls)
- 理解 MCP 的架构(Server 端: Tools / Resources / Prompts,Client 端: Sampling / Elicitation / Logging)
- 能编写一个简单的 MCP Server
- 能在 Claude Code 或 Cursor 中配置 MCP Server
- 掌握工具设计的 5 个核心原则(命名/描述/参数/错误处理/粒度)