基于 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 平台
相关推荐
doiito4 小时前
【Agent Harness】为什么我把 JSON‑LD “编译成 DAG” 后,整个 Agent 平台立刻聪明了
ai·rust·架构设计·系统设计·ai agent
王二端茶倒水8 小时前
商业 WiFi 不是免费上网,而是门店数字化的入口
网络协议
xiezhr9 小时前
折腾半小时,终于让AI 能直接帮我写飞书文档了
ai·飞书·ai agent·飞书cli·飞书文档
岳小哥AI10 小时前
Claude Fable和Claude Mythos 5同时发布:注意力机制下愈加强大的AI大模型
ai·ai基础
Artech10 小时前
[MAF预定义的AIContextProvider-04]Mem0Provider——长期记忆基于的云端解决方案
ai·agent·maf·aicontextprovider·chathistorymemoryprovider·mem0provider
哥不是小萝莉20 小时前
一文读懂 OpenAI Codex 源码的原理、架构与未来
ai
AlfredZhao1 天前
AI 编程工作总结:从体验问题到模块能力建设
ai·codex
cup112 天前
[技术复盘] Windows Python 打包实战:Nuitka 环境踩坑总结与 CI 自动化构建全指南
python·ai·环境变量·ci·nuitka·skill
IT王师傅2 天前
从 豆包 到 Codex CLI:一名普通开发者的 AI 工具进化路线
ai·codex cli·openclaw
岳小哥AI2 天前
Siri要接入AI了,苹果手机上一句话让GPT写文案、DeepSeek写代码的时刻来了
ai·ai基础