工具与协议层——Agent 如何连接世界

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 个核心原则(命名/描述/参数/错误处理/粒度)

相关推荐
希望永不加班2 小时前
SpringBoot 过滤器(Filter)与请求链路梳理
java·spring boot·后端·spring
0xDevNull2 小时前
Java实现Redis延迟队列:从原理到高可用架构
java·开发语言·后端
糖炒栗子03262 小时前
Go 语言环境搭建与版本管理指南 (2026)
开发语言·后端·golang
恼书:-(空寄3 小时前
Spring 事务失效的 8 大场景 + 原因 + 解决方案
java·后端·spring
我是若尘3 小时前
我的需求代码被主干 revert 了,接下来我该怎么操作?
前端·后端·代码规范
dweizhao3 小时前
这份AI报告,把美股干崩了
后端
与虾牵手4 小时前
Claude Tool Use 怎么用?从零到生产的完整教程(2026)
ai编程·claude
JOEH604 小时前
Java 后端开发中的内存泄漏问题:90% 开发者都会踩的 5 个坑
后端
_野猪佩奇_牛马版4 小时前
多智能体协作 - 使用 LangGraph 子图实现
后端