大模型实操-Spring Boot集成LangChain4j

-- 引题:本文介绍了在 Spring Boot 中通过 MCP 协议集成 LangChain4j 的企业级方案。工具用 Spring AI 的 @Tool 注解声明,由 spring-ai-starter-mcp-server 自动暴露为标准 MCP Server。客户端 McpClientService 支持本地反射调用与远程 SSE 连接双模式,自动合并工具列表。Agent 层绕过 LangChain4j 的 ChatModel,直接用 RestClient 调 LLM API,MCP 的 JSON Schema 原样传递,无需格式转换。

基础知识

1.1 问题

在企业 AI 应用中,LLM 需要调用内部工具(查公告、订会议室、请假等)来完成用户请求。传统的做法是:

  • 把工具写在 LLM 的系统提示词里,让模型自己理解并返回 JSON 格式的调用请求
  • 用 LangChain4j 的 @Tool 注解,框架自动处理函数调用

这些做法在小规模场景够用,但在企业生产环境中面临几个问题:

  1. 工具和 AI 应用强耦合------工具改个参数类型,AI 应用要重新编译部署
  2. 难以跨语言、跨框架复用 ------Java 的 @Tool 注解,Python 的 Agent 用不了
  3. 没有标准协议------每个项目的工具接入方式都不一样

1.2 MCP 协议是什么

MCP(Model Context Protocol)是 Anthropic 提出的开放协议,定义了 AI 应用与工具/数据源之间的标准通信方式。核心概念:

scss 复制代码
┌──────────────┐       MCP 协议       ┌──────────────┐
│  MCP Client   │ ←── JSON-RPC ───→   │  MCP Server   │
│  (AI 应用)    │                     │  (工具提供方)  │
│              │                     │              │
│  tools/list  │                     │  @Tool 方法   │
│  tools/call  │                     │               │
└──────────────┘                     └──────────────┘
  • JSON-RPC 2.0:请求响应格式
  • SSE 传输:服务端推送事件流
  • 工具发现tools/list 返回工具定义(JSON Schema)
  • 工具调用tools/call 执行并返回结果

1.3 整体架构

本文介绍的方案:

arduino 复制代码
┌────────────── langchain4j-agent ──────────────┐
│                                                 │
│  ┌──────────────┐    ┌──────────────────┐       │
│  │ @Tool 服务    │    │ spring-ai-starter│       │
│  │ 公告/会议/请假│───→│ -mcp-server      │       │
│  └──────────────┘    │ (MCP Server)     │       │
│                      └────────┬─────────┘       │
│                               │ SSE              │
│                      ┌────────▼─────────┐       │
│                      │ McpClientService  │       │
│                      │ (MCP Client)     │       │
│                      │ 本地 + 远程合并   │       │
│                      └────────┬─────────┘       │
│                               │                   │
│                      ┌────────▼─────────┐       │
│                      │ DirectAgentService│       │
│                      │ (RestClient 直调   │       │
│                      │  LLM API)        │       │
│                      └──────────────────┘       │
└──────────────────────────────────────────────────┘

2. MCP Server:工具定义

工具服务用 Spring AI 的 @Tool 注解声明,spring-ai-starter-mcp-server 自动扫描并注册到 MCP Server。

2.1 添加依赖

xml 复制代码
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>

2.2 声明工具

java 复制代码
@Service
public class AnnouncementService {

    @Tool(description = "获取公司最新的内部公告(薪酬、考核、福利等)。可按分类过滤,限制返回条数。")
    public String getLatestAnnouncements(
            @ToolParam(description = "公告分类,可选值:薪酬、考核、福利,不填则返回所有") String category,
            @ToolParam(description = "最多返回的条数,默认3") Integer limit) {
        // 业务逻辑
        return "查询结果...";
    }
}
java 复制代码
@Service
public class MeetingService {

    @Tool(description = "预约会议室,需要会议主题、开始时间、结束时间、参会人列表")
    public String bookMeeting(
            @ToolParam(description = "会议主题") String subject,
            @ToolParam(description = "开始时间,格式 yyyy-MM-dd HH:mm") String startTime,
            @ToolParam(description = "结束时间,格式 yyyy-MM-dd HH:mm") String endTime,
            @ToolParam(description = "参会人列表,逗号分隔") String attendees) {
        return "已预订会议室...";
    }
}
java 复制代码
@Service
public class LeaveService {

    @Tool(description = "提交员工请假申请,需要员工姓名、请假天数、请假原因")
    public String submitLeave(
            @ToolParam(description = "员工姓名") String name,
            @ToolParam(description = "请假天数") int days,
            @ToolParam(description = "请假原因") String reason) {
        return "已为 " + name + " 提交请假申请...";
    }
}

2.3 配置 MCP Server

yaml 复制代码
# application.yml
spring:
  ai:
    mcp:
      server:
        enabled: true
        name: my-mcp-server
        version: 1.0.0
        type: SYNC
        stdio: false
        sse:
          sse-endpoint: /sse                    # SSE 端点,默认 /sse
          sse-message-endpoint: /mcp/message     # 消息端点,默认 /mcp/message

2.4 底层原理

spring-ai-starter-mcp-server 的自动配置类 McpServerAnnotationScannerAutoConfiguration 扫描容器中所有带 @Tool 注解的方法,构建 McpServerFeatures.SyncToolSpecification,注册到 McpSyncServer

HttpServletSseServerTransportProvider(继承 HttpServlet)处理两个 HTTP 端点:

  • GET /sse:客户端建立 SSE 长连接,服务端推送事件流
  • POST /mcp/message:客户端发送 JSON-RPC 消息
bash 复制代码
客户端 GET /sse
  ← 服务端推送 session_id 和 messageEndpoint
客户端通过 SSE 通道接收事件
客户端 POST /mcp/message { JSON-RPC }
  → 服务端处理,通过 SSE 返回结果

3. MCP Client:工具发现与调用

McpClientService 是核心客户端,支持双模式合并

  1. 本地模式 :扫描本进程的 @Tool 方法,反射调用
  2. 远程模式:通过 SSE 连接 MCP Server,走标准 MCP 协议

3.1 核心实现

java 复制代码
@Service
public class McpClientService {

    private final ApplicationContext applicationContext;

    // 远程模式
    private McpSyncClient mcpClient;
    private Set<String> remoteToolNames = Set.of();

    // 本地模式
    private Map<String, LocalTool> localToolMap = Map.of();

    // 合并后的工具列表(本地 + 远程)
    private List<McpSchema.Tool> tools = List.of();

    @EventListener(ApplicationReadyEvent.class)
    public void init() {
        // 第一步:扫描本地 @Tool 方法(始终成功)
        scanLocalTools();

        // 第二步:尝试 SSE 连接远程 MCP Server(失败不影响)
        try {
            connectViaSse();
        } catch (Exception e) {
            log.warn("远程连接失败,仅使用本地工具");
        }
    }
}

3.2 本地扫描

java 复制代码
private void scanLocalTools() {
    for (String beanName : applicationContext.getBeanDefinitionNames()) {
        Object bean = applicationContext.getBean(beanName);
        for (Method method : bean.getClass().getMethods()) {
            Tool toolAnn = method.getAnnotation(Tool.class);
            if (toolAnn == null) continue;

            // 解析方法签名,构建 JSON Schema
            String toolName = method.getName();
            Map<String, Object> properties = new LinkedHashMap<>();
            List<String> required = new ArrayList<>();

            for (Parameter param : method.getParameters()) {
                ToolParam tpAnn = param.getAnnotation(ToolParam.class);
                String paramName = param.getName();
                String paramDesc = (tpAnn != null) ? tpAnn.description() : "";
                // 构建属性定义...
            }

            // 构建标准 McpSchema.Tool(和远程返回的格式相同)
            McpSchema.JsonSchema schema = new McpSchema.JsonSchema(
                    "object", properties, required, null, null, null);
            McpSchema.Tool tool = new McpSchema.Tool(
                    toolName, null, desc, schema, null, null, null);
        }
    }
}

3.3 远程连接与合并

java 复制代码
private void connectViaSse() {
    String sseUrl = "http://localhost:" + serverPort + "/sse";
    HttpClientSseClientTransport transport =
            HttpClientSseClientTransport.builder(sseUrl).build();

    mcpClient = McpClient.sync(transport)
            .clientInfo(new McpSchema.Implementation("my-agent", "1.0.0"))
            .build();
    mcpClient.initialize();

    List<McpSchema.Tool> remoteTools = mcpClient.listTools().tools();

    // 记录远程工具名称
    Set<String> names = remoteTools.stream().map(McpSchema.Tool::name)
            .collect(Collectors.toSet());
    this.remoteToolNames = names;

    // 合并:本地不冲突的保留,远程全部加上
    List<McpSchema.Tool> merged = new ArrayList<>(this.tools);
    merged.removeIf(lt -> names.contains(lt.name()));
    merged.addAll(remoteTools);
    this.tools = merged;
}

3.4 调用路由

java 复制代码
public String callTool(String name, Map<String, Object> arguments) {
    // 远程优先
    if (mcpClient != null && remoteToolNames.contains(name)) {
        McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(name, arguments);
        return mcpClient.callTool(request).content().stream()
                .filter(c -> c instanceof McpSchema.TextContent)
                .map(c -> ((McpSchema.TextContent) c).text())
                .collect(Collectors.joining("\n"));
    }

    // 本地降级:反射调用 @Tool 方法
    return callLocal(name, arguments);
}

4. Agent:直调 LLM API

4.1 为什么不用 ChatModel

LangChain4j 的 ChatModel.chat(ChatRequest) 要求工具定义为 ToolSpecification + JsonObjectSchema,而 MCP 返回的是 McpSchema.Tool + JsonSchema。两者虽然都是 JSON Schema,但 Java 类型不同,需要手动转换:

scss 复制代码
MCP 返回               → 需要转换           → ChatModel 要求
McpSchema.Tool         → buildToolSpec()   → ToolSpecification
  .inputSchema()       → JsonObjectSchema    .parameters()
  (JsonSchema)                                  (JsonObjectSchema)

这个转换层没有技术价值,纯粹是 API 不兼容造成的。更干净的做法是绕过 ChatModel,直接用 RestClient 调用 OpenAI 兼容 API

4.2 核心原理

MCP 的 inputSchema 和 OpenAI 的 tools[].function.parameters 都是 JSON Schema,可以直接传递:

java 复制代码
for (McpSchema.Tool tool : mcpClientService.getTools()) {
    ObjectNode function = mapper.createObjectNode();
    function.put("name", tool.name());
    function.put("description", tool.description());

    // ★ 直接传,不需要转换
    McpSchema.JsonSchema schema = tool.inputSchema();
    params.set("properties", mapper.valueToTree(schema.properties()));
    params.set("required", mapper.valueToTree(schema.required()));

    function.set("parameters", params);

    ObjectNode toolNode = mapper.createObjectNode();
    toolNode.put("type", "function");
    toolNode.set("function", function);
    toolsArray.add(toolNode);
}

4.3 完整实现

java 复制代码
@Service
public class DirectAgentService {

    private final McpClientService mcpClientService;
    private final ObjectMapper mapper;
    private final RestClient restClient;

    public DirectAgentService(McpClientService mcpClientService,
                               LlmProperties llmProperties,
                               RestClient.Builder restClientBuilder) {
        this.mcpClientService = mcpClientService;
        this.mapper = new ObjectMapper();

        String chatUrl = llmProperties.getBaseUrl()
                .replaceAll("/+$", "") + "/v1/chat/completions";

        this.restClient = restClientBuilder
                .baseUrl(chatUrl)
                .defaultHeader("Authorization", "Bearer " + llmProperties.getApiKey())
                .defaultHeader("Content-Type", "application/json")
                .build();
    }

    public String chat(String userMessage) {
        List<McpSchema.Tool> tools = mcpClientService.getTools();

        // 构造消息
        List<ObjectNode> messages = new ArrayList<>();
        messages.add(createMessage("system", "你是一个企业智能助手..."));
        messages.add(createMessage("user", userMessage));

        // 构造工具定义(MCP 的 JsonSchema 直接作为 parameters)
        ArrayNode toolsArray = buildToolsArray(tools);

        // 调 LLM
        JsonNode response = callLlm(messages, toolsArray);

        // 处理工具调用(支持多轮)
        return handleResponse(messages, toolsArray, response, 0);
    }

    private JsonNode callLlm(List<ObjectNode> messages, ArrayNode tools) {
        ObjectNode body = mapper.createObjectNode();
        body.put("model", modelName);
        body.set("messages", messages.stream()
                .collect(Collectors.toCollection(ArrayNode::new)));
        if (tools != null && tools.size() > 0) {
            body.set("tools", tools);
            body.put("tool_choice", "auto");
        }

        return restClient.post()
                .body(body)
                .retrieve()
                .body(JsonNode.class);
    }
}

4.4 工具调用循环

java 复制代码
private String handleResponse(List<ObjectNode> messages, ArrayNode tools,
                               JsonNode response, int round) {
    if (round >= 5) return "已达到最大工具调用轮次";

    JsonNode choice = response.get("choices").get(0);
    JsonNode message = choice.get("message");
    messages.add((ObjectNode) message);

    // 有工具调用?
    if (message.has("tool_calls") && !message.get("tool_calls").isEmpty()) {

        for (JsonNode toolCall : message.get("tool_calls")) {
            String name = toolCall.get("function").get("name").asText();
            String argsJson = toolCall.get("function").get("arguments").asText();
            String id = toolCall.get("id").asText();

            // 通过 MCP Client 执行(远程优先,本地兜底)
            Map<String, Object> args = parseArguments(argsJson);
            String result = mcpClientService.callTool(name, args);

            // tool 结果加入消息历史
            ObjectNode tr = mapper.createObjectNode();
            tr.put("role", "tool");
            tr.put("tool_call_id", id);
            tr.put("content", result);
            messages.add(tr);
        }

        // 递归:把工具结果送回 LLM
        JsonNode nextResponse = callLlm(messages, tools);
        return handleResponse(messages, tools, nextResponse, round + 1);
    }

    return message.get("content").asText();
}

5. 配置

5.1 LLM 配置

yaml 复制代码
langchain4j:
  open-ai:
    chat-model:
      api-key: sk-xxx
      base-url: https://your-llm-api.com/api
      model-name: Qwen3-8B
      temperature: 0.7
      max-tokens: 2000
      timeout: 60s

5.2 配置类

java 复制代码
@Configuration
@ConfigurationProperties(prefix = "langchain4j.open-ai.chat-model")
public class LlmProperties {
    private String baseUrl;
    private String apiKey;
    private String modelName;
    private double temperature = 0.7;
    private int maxTokens = 2000;
    // getter / setter ...
}

5.3 Agent 端点

java 复制代码
@RestController
@RequestMapping("/agent")
public class AgentController {

    private final AgentAssistant agentAssistant;        // AiServices 纯聊天
    private final DirectAgentService directAgentService; // MCP 工具调用

    public AgentController(AgentAssistant agentAssistant,
                           DirectAgentService directAgentService) {
        this.agentAssistant = agentAssistant;
        this.directAgentService = directAgentService;
    }

    // 纯聊天(无工具)
    @GetMapping("/chat")
    public String chat(String message) {
        return agentAssistant.chat(message);
    }

    // MCP 工具调用
    @GetMapping("/mcp-chat")
    public String mcpChat(String message) {
        return directAgentService.chat(message);
    }
}

6. AgentAssistant(可选保留)

如果还需要 LangChain4j 的 AiServices 做纯聊天(无工具),可以保留:

java 复制代码
public interface AgentAssistant {
    @SystemMessage("你是一个乐于助人的AI助手,你的回答需要简洁明了。")
    String chat(@UserMessage String userMessage);
}

@Configuration
public class AgentConfig {
    @Bean
    public AgentAssistant agentAssistant(ChatModel chatModel) {
        return AiServices.builder(AgentAssistant.class)
                .chatModel(chatModel)
                .build();
    }
}

7. 实现要点与注意事项

7.1 Spring 初始化顺序

MCP Client 必须在 Tomcat 和 MCP Server 就绪后才能连接。使用 @EventListener(ApplicationReadyEvent.class) 确保在所有 Bean 初始化完成、Tomcat 已启动、MCP Server Servlet 已注册之后才执行:

java 复制代码
@EventListener(ApplicationReadyEvent.class)  // ← 应用完全就绪后触发
public void init() { ... }

7.2 Lombok 注解处理器

LangChain4j 1.15.x + Spring Boot 3.x 环境下,Lombok 的注解处理器可能没有自动配置。如果使用 @Slf4j 报错,改为手动 Logger:

java 复制代码
// 不依赖 Lombok
private static final Logger log = LoggerFactory.getLogger(MyService.class);

或在 pom.xml 中显式配置 Lombok 的 annotationProcessorPaths

xml 复制代码
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

7.3 LangChain4j 版本注意事项

LangChain4j 1.15.x 中的 API 有重大变化:

旧名称(0.36.x) 新名称(1.15.x)
ChatLanguageModel ChatModel
StreamingChatLanguageModel StreamingChatModel
model.generate(messages, tools) model.chat(ChatRequest)

配置属性也变了:

yaml 复制代码
# 旧(0.36.x)
langchain4j:
  chat-model:
    open-ai:
      api-key: ...

# 新(1.15.x)
langchain4j:
  open-ai:
    chat-model:
      api-key: ...

7.4 MCP SDK 客户端 API

io.modelcontextprotocol.sdk:mcp-core:0.17.0 的关键 API:

java 复制代码
// 创建传输层
HttpClientSseClientTransport transport = HttpClientSseClientTransport
        .builder("http://localhost:8090/sse")
        .build();

// 构建客户端
McpSyncClient client = McpClient.sync(transport)
        .clientInfo(new McpSchema.Implementation("name", "1.0.0"))
        .build();

// 初始化会话
client.initialize();

// 发现工具
ListToolsResult tools = client.listTools();
// tools.tools() → List<McpSchema.Tool>

// 调用工具
CallToolResult result = client.callTool(
        new McpSchema.CallToolRequest("toolName", arguments));
// result.content() → List<McpSchema.Content>

7.5 类型转换(本地反射调用)

本地模式通过反射调用 @Tool 方法时,需要正确的类型转换:

java 复制代码
private static Object convertValue(Object value, Class<?> targetType) {
    if (value == null) return null;
    if (targetType.isInstance(value)) return value;
    if (value instanceof Number num) {
        if (targetType == int.class || targetType == Integer.class) return num.intValue();
        if (targetType == long.class || targetType == Long.class) return num.longValue();
        if (targetType == double.class || targetType == Double.class) return num.doubleValue();
    }
    if (value instanceof Boolean bool) {
        if (targetType == boolean.class || targetType == Boolean.class) return bool;
    }
    return value.toString();
}

7.6 MCP 端点路径默认值

MCP SDK 的默认端点:

路径 MCP SDK 默认 Spring AI 默认
SSE 端点 /sse /sse
消息端点 --- /mcp/message

可以通过 application.yml 覆盖:

yaml 复制代码
spring:
  ai:
    mcp:
      server:
        sse:
          sse-endpoint: /custom/sse
          sse-message-endpoint: /custom/message

7.7 不要在同一进程里走完整的 HTTP 栈

当 MCP Client 和 MCP Server 在同一进程时,调用链是:

arduino 复制代码
McpClientService
  → HttpClientSseClientTransport
  → java.net.http.HttpClient
  → TCP localhost:8090
  → Tomcat
  → HttpServletSseServerTransportProvider (Servlet)
  → McpSyncServer
  → @Tool 方法

这个链路可以工作,但在同一 JVM 内是多余的。本方案的本地模式可以直接反射调用,避免了网络开销。生产部署时,MCP Server 和 Client 应作为独立微服务部署,通过 SSE 通信。


8. 完整项目结构

ruby 复制代码
langchain4j-agent/
├── pom.xml
└── src/main/java/org/example/agent/
    ├── LangChain4jAgentApplication.java
    ├── config/
    │   ├── AgentConfig.java           # AiServices 装配
    │   └── LlmProperties.java         # LLM API 配置
    ├── controller/
    │   └── AgentController.java       # HTTP 端点
    ├── mcp/
    │   ├── McpClientService.java      # MCP 客户端(本地+远程)
    │   └── DirectAgentService.java    # Agent(直调 LLM API)
    ├── service/
    │   └── AgentAssistant.java        # AiServices 接口
    └── tool/
        ├── AnnouncementService.java   # 公告查询 @Tool
        ├── MeetingService.java        # 会议室预订 @Tool
        └── LeaveService.java          # 请假申请 @Tool

9. 总结

关注点 方案
工具定义 Spring AI @Tool 注解
MCP Server spring-ai-starter-mcp-server 自动配置
MCP Client mcp-core SDK,本地+远程双模式合并
LLM 调用 RestClient 直调 OpenAI 兼容 API
工具格式转换 不需要,MCP 的 JSON Schema 直接传递
容错 远程连接失败自动降级本地反射调用
生产部署 Server 和 Client 可拆为独立微服务
相关推荐
Cloud_Shy6181 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第七章 Item 48 - 50)
开发语言·人工智能·笔记·python·microsoft·学习方法
麦哲思科技任甲林1 小时前
人类编程爱敏捷,AI编程爱CMMI
人工智能·ai编程·敏捷开发·cmmi
狗头大军之江苏分军1 小时前
前端路由是怎么来的
前端·javascript·后端
云恒要逆袭1 小时前
Java类型转换详解:小数字转大自动跑,大数字转小要小心
java·后端
刘明L1 小时前
SpringCloud整合skywalking实现链路追踪和日志采集
后端
sali-tec1 小时前
C# 基于OpenCv的视觉工作流-章84-包胶有无检测
图像处理·人工智能·opencv·算法·计算机视觉
哈哈,柳暗花明1 小时前
人工智能专业术语详解(P)
人工智能·专业术语
Web极客码1 小时前
从生成式AI到智能代理:AI正在进入“第二阶段”
服务器·人工智能·ai
万俟淋曦1 小时前
【论文速递】2026年第04周(Jan-18-24)(Robotics/Embodied AI/LLM)
人工智能·ai·机器人·大模型·llm·具身智能·vla