基于 HTTP 构建 MCP Tools 的完整工程解析

基于 HTTP 构建 MCP Tools 的完整工程解析

一、项目目标

本文基于以下代码实现,完整讲解:

  • 如何将 HTTP API 封装为 AI Tool
  • 如何构建 MCP(Model Context Protocol)风格的 Tool 执行体系
  • 如何实现多轮 Tool 调用(Agent Loop)
  • 如何进行工程化扩展

二、整体架构

复制代码
User -> LLM -> ToolSpecification -> ToolCall -> HttpToolExecutor -> HTTP API -> Result -> LLM

核心流程:

  1. 用户输入问题
  2. LLM 判断是否需要调用 Tool
  3. 生成 ToolExecutionRequest
  4. 执行 HTTP 请求
  5. 返回结果给 LLM
  6. LLM 继续推理或输出最终结果

三、核心代码解析

1. 主程序 StreamApp2

作用:驱动整个 Tool 调用流程

关键逻辑:

(1)加载 Tool 配置
java 复制代码
package com.nbsaas.boot.tools;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nbsaas.boot.AiConfig;
import com.nbsaas.boot.tools.http.HttpToolDefinition;
import com.nbsaas.boot.tools.http.HttpToolExecutor;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.data.message.*;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class StreamApp2 {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    public static void main(String[] args) throws Exception {
        // 从 classpath 加载 tools.json
        List<HttpToolDefinition> toolDefs = loadToolDefinitions("tools.json");
        HttpToolExecutor httpExecutor = new HttpToolExecutor(toolDefs);

        OpenAiStreamingChatModel model = AiConfig.getOpenAiStreamingChatModel();
        List<ToolSpecification> toolSpecs = httpExecutor.buildToolSpecifications();

        List<ChatMessage> messages = new ArrayList<>();
        messages.add(UserMessage.from("北京今天天气怎么样,适合出行吗"));

        chatLoop(model, toolSpecs, httpExecutor, messages, 1, 5);
    }

    // ========== 核心循环 ==========
    private static void chatLoop(
            OpenAiStreamingChatModel model,
            List<ToolSpecification> toolSpecs,
            HttpToolExecutor httpExecutor,
            List<ChatMessage> messages,
            int round,
            int maxRounds) throws InterruptedException {

        if (round > maxRounds) {
            System.err.println("[警告] 已达到最大工具调用轮次 " + maxRounds + ",强制终止");
            return;
        }

        System.out.println("\n===== 第 " + round + " 轮请求 =====");

        CountDownLatch latch = new CountDownLatch(1);

        ChatRequest request = ChatRequest.builder()
                .messages(messages)
                .toolSpecifications(toolSpecs)
                .build();

        // 用数组包装,让 lambda 内可修改
        final boolean[] hasToolCall = {false};

        model.chat(request, new StreamingChatResponseHandler() {

            @Override
            public void onPartialResponse(String partial) {
                System.out.print(partial);
            }

            @Override
            public void onCompleteResponse(ChatResponse response) {
                AiMessage aiMessage = response.aiMessage();

                // 1. 把 AI 回复加入历史
                messages.add(aiMessage);

                // 2. 检查是否有工具调用
                if (aiMessage.hasToolExecutionRequests()) {
                    hasToolCall[0] = true;
                    System.out.println("\n[检测到工具调用,开始执行...]");

                    for (ToolExecutionRequest req : aiMessage.toolExecutionRequests()) {
                        System.out.printf("  → Tool: %-20s 参数: %s%n",
                                req.name(), req.arguments());

                        // 3. 执行 HTTP Tool
                        String result = httpExecutor.execute(req);
                        System.out.printf("  ← 结果: %s%n", result);

                        // 4. 把结果加入历史
                        messages.add(ToolExecutionResultMessage.from(req, result));
                    }
                } else {
                    System.out.println("\n[最终回答完成]");
                }

                latch.countDown();
            }

            @Override
            public void onError(Throwable error) {
                System.err.println("\n[错误] " + error.getMessage());
                error.printStackTrace();
                latch.countDown();
            }
        });

        latch.await();

        // 5. 有工具调用结果则继续下一轮
        if (hasToolCall[0]) {
            chatLoop(model, toolSpecs, httpExecutor, messages, round + 1, maxRounds);
        }
    }

    // ========== 加载 tools.json ==========
    private static List<HttpToolDefinition> loadToolDefinitions(String filename) throws Exception {
        InputStream is = StreamApp2.class.getClassLoader().getResourceAsStream(filename);
        if (is == null) {
            throw new IllegalArgumentException("找不到配置文件: " + filename);
        }
        List<HttpToolDefinition> defs = objectMapper.readValue(
                is, new TypeReference<List<HttpToolDefinition>>() {});
        System.out.println("[初始化] 加载了 " + defs.size() + " 个 HTTP Tool:");
        defs.forEach(d -> System.out.println("  - " + d.getName() + " → " + d.getUrl()));
        return defs;
    }
}

说明:

  • 从 classpath 加载 tools.json
  • 支持动态扩展 Tool

(2)构建 Tool 执行器
java 复制代码
package com.nbsaas.boot.tools.http;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import okhttp3.*;
public class HttpToolExecutor {

    private final Map<String, HttpToolDefinition> definitions;
    private final OkHttpClient httpClient;
    private final ObjectMapper objectMapper;

    public HttpToolExecutor(List<HttpToolDefinition> definitions) {
        this.definitions = definitions.stream()
                .collect(Collectors.toMap(HttpToolDefinition::getName, d -> d));
        this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
                .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                .build();
        this.objectMapper = new ObjectMapper();
    }

    // ========== 对外执行入口 ==========
    public String execute(ToolExecutionRequest req) {
        HttpToolDefinition def = definitions.get(req.name());
        if (def == null) {
            return "错误:未找到 HTTP Tool [" + req.name() + "]";
        }
        try {
            Map<String, Object> arguments = parseArguments(req.arguments());
            return "POST".equalsIgnoreCase(def.getMethod())
                    ? doPost(def, arguments)
                    : doGet(def, arguments);
        } catch (Exception e) {
            return "HTTP Tool 执行失败 [" + req.name() + "]:" + e.getMessage();
        }
    }

    // ========== POST 请求 ==========
    private String doPost(HttpToolDefinition def, Map<String, Object> arguments) throws Exception {
        String bodyStr = objectMapper.writeValueAsString(arguments);
        RequestBody requestBody = RequestBody.create(
                bodyStr.getBytes(StandardCharsets.UTF_8),
                MediaType.get("application/json")
        );
        Request request = new Request.Builder()
                .url(def.getUrl())
                .post(requestBody)
                .build();
        return executeRequest(request);
    }

    // ========== GET 请求 ==========
    private String doGet(HttpToolDefinition def, Map<String, Object> arguments) throws Exception {
        HttpUrl baseUrl = HttpUrl.parse(def.getUrl());
        if (baseUrl == null) {
            throw new IllegalArgumentException("无效的 URL: " + def.getUrl());
        }
        HttpUrl.Builder urlBuilder = baseUrl.newBuilder();
        arguments.forEach((k, v) -> urlBuilder.addQueryParameter(k, String.valueOf(v)));
        Request request = new Request.Builder()
                .url(urlBuilder.build())
                .get()
                .build();
        return executeRequest(request);
    }

    // ========== 公共请求执行 ==========
    private String executeRequest(Request request) throws IOException {
        try (Response response = httpClient.newCall(request).execute()) {
            ResponseBody body = response.body();
            String bodyStr = body != null ? body.string() : "";
            if (!response.isSuccessful()) {
                throw new IOException("HTTP " + response.code() + ": " + bodyStr);
            }
            return bodyStr.isEmpty() ? "空响应" : bodyStr;
        }
    }

    // ========== 参数解析 ==========
    private Map<String, Object> parseArguments(String arguments) throws Exception {
        if (arguments == null || arguments.isBlank()) {
            return Map.of();
        }
        return objectMapper.readValue(arguments, new TypeReference<>() {});
    }

    // ========== 动态构建 ToolSpecification ==========
    public List<ToolSpecification> buildToolSpecifications() {
        return definitions.values().stream()
                .map(this::buildSpecification)
                .collect(Collectors.toList());
    }

    private ToolSpecification buildSpecification(HttpToolDefinition def) {
        JsonObjectSchema.Builder schemaBuilder = JsonObjectSchema.builder();
        List<String> requiredParams = new ArrayList<>();

        if (def.getParams() != null) {
            for (HttpToolParam param : def.getParams()) {
                switch (param.getType().toLowerCase()) {
                    case "integer", "int", "long" ->
                            schemaBuilder.addIntegerProperty(param.getName(), param.getDescription());
                    case "boolean", "bool" ->
                            schemaBuilder.addBooleanProperty(param.getName(), param.getDescription());
                    default ->
                            schemaBuilder.addStringProperty(param.getName(), param.getDescription());
                }
                if (param.isRequired()) {
                    requiredParams.add(param.getName());
                }
            }
        }

        return ToolSpecification.builder()
                .name(def.getName())
                .description(def.getDescription())
                .parameters(schemaBuilder.required(requiredParams).build())
                .build();
    }
}

HttpToolExecutor httpExecutor = new HttpToolExecutor(toolDefs);

说明:

  • 统一执行所有 HTTP Tool
  • 类似 RPC 调度中心

(3)构建 Tool Specification
java 复制代码
List<ToolSpecification> toolSpecs = httpExecutor.buildToolSpecifications();

说明:

  • 将 Tool 转换为 LLM 可识别结构(JSON Schema)

(4)初始化对话
java 复制代码
messages.add(UserMessage.from("北京今天天气怎么样,适合出行吗"));

(5)进入 Agent Loop
java 复制代码
chatLoop(model, toolSpecs, httpExecutor, messages, 1, 5);

2. chatLoop(核心执行引擎)

这是整个系统最核心部分。

执行流程:
  1. 发送请求给 LLM
  2. 流式接收输出
  3. 检测 Tool 调用
  4. 执行 Tool
  5. 回写结果
  6. 递归下一轮

关键代码
java 复制代码
if (aiMessage.hasToolExecutionRequests()) {

说明:

  • 判断 LLM 是否触发 Tool

java 复制代码
String result = httpExecutor.execute(req);

说明:

  • 执行 HTTP 请求

java 复制代码
messages.add(ToolExecutionResultMessage.from(req, result));

说明:

  • 将 Tool 结果返回给 LLM

多轮调用机制
java 复制代码
chatLoop(... round + 1 ...)

说明:

  • 实现 Agent 推理能力
  • 支持复杂任务链

3. HttpToolExecutor

作用:HTTP Tool 执行引擎


(1)Tool 注册
java 复制代码
this.definitions = definitions.stream()
    .collect(Collectors.toMap(HttpToolDefinition::getName, d -> d));

说明:

  • 构建 Tool 索引

(2)统一执行入口
java 复制代码
public String execute(ToolExecutionRequest req)

流程:

  1. 查找 Tool
  2. 解析参数
  3. 执行 HTTP
  4. 返回结果

(3)参数解析
java 复制代码
objectMapper.readValue(arguments, new TypeReference<>() {});

说明:

  • JSON → Map

(4)GET 请求
java 复制代码
urlBuilder.addQueryParameter(k, String.valueOf(v));

(5)POST 请求
java 复制代码
RequestBody.create(json)

(6)执行请求
java 复制代码
httpClient.newCall(request).execute();

说明:

  • 使用 OkHttp

4. ToolSpecification 构建

java 复制代码
schemaBuilder.addStringProperty(...)

说明:

  • 动态生成 JSON Schema

支持类型:

  • string
  • integer
  • boolean

5. HttpToolDefinition

java 复制代码
private String name;
private String description;
private String url;
private String method;
private List<HttpToolParam> params;

说明:

  • Tool 元数据定义

6. HttpToolParam

java 复制代码
private String name;
private String type;
private boolean required;

说明:

  • 参数描述
  • 用于构建 Schema

四、执行流程(完整链路)

Step 1:用户输入

复制代码
北京天气怎么样

Step 2:LLM 判断

生成:

复制代码
ToolExecutionRequest

Step 3:执行 Tool

复制代码
HttpToolExecutor.execute()

Step 4:调用 HTTP API

复制代码
GET /weather?city=北京

Step 5:返回结果

复制代码
{ "temp": 20 }

Step 6:继续推理

LLM 输出最终结果


五、工程价值

优势

  • Tool 完全解耦
  • 支持动态扩展
  • 兼容 MCP
  • 支持 Agent

六、优化建议

1. Tool 网关

增加:

  • 限流
  • 鉴权
  • 熔断

2. 参数校验

引入:

  • JSON Schema Validator

3. 统一返回结构

json 复制代码
{
  "code": 0,
  "data": {}
}

4. 缓存机制

适用于:

  • 天气
  • 配置接口

5. Tool 平台化

升级为:

  • 数据库存储
  • UI 管理

七、总结

本项目本质是:

一个轻量级 MCP Tool 执行引擎

具备:

  • Tool Schema 构建
  • Tool 执行
  • 多轮推理

可扩展为:

  • AI Gateway
  • Agent 平台
相关推荐
编程牛马姐8 小时前
独立站SEO流量增长:提高Google排名的优化方法
前端·javascript·网络
小童不学前端9 小时前
前端如何转 AI 应用开发
ai·ai应用开发
2401_873479409 小时前
如何从零搭建私有化IP查询平台?数据采集、清洗、建库到API发布全流程
服务器·网络·tcp/ip
花千树-0109 小时前
MCP 协议通信详解:从握手到工具调用的完整流程
ai·langchain·aigc·agent·ai agent·mcp
学代码的真由酱9 小时前
HTTPS
网络协议·http·https
GeeLark10 小时前
Android 16 is here. 行业首发
ai·自动化·aigc
FS_Marking10 小时前
CWDM vs DWDM:区别是什么?
网络
samson_www10 小时前
EC2的GRUB引导程序问题
运维·ai
竹之却11 小时前
【Agent-阿程】OpenClaw v2026.4.15 版本更新全解析
人工智能·ai·openclaw
嵌入式小企鹅11 小时前
DeepSeek-V4昇腾首发、国芯抗量子MCU突破、AI编程Agent抢班夺权
人工智能·学习·ai·程序员·算力·risc-v