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

相关推荐
不爱洗脚的小滕3 小时前
【RAG】召回(Retrieval)与重排(Rerank)核心技术要点汇总
langchain·aigc·ai编程·rag
红尘散仙4 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
win4r4 小时前
MiniMax M3 深度体验:这可能是国产模型里最接近“全能工程师”的一次
aigc·ai编程·claude
卷毛的技术笔记5 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆5 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
adrninistrat0r5 小时前
Java调用链MCP分析工具
java·python·ai编程
喵个咪6 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6166 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364576 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao6 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端