-- 引题:本文介绍了在 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注解,框架自动处理函数调用
这些做法在小规模场景够用,但在企业生产环境中面临几个问题:
- 工具和 AI 应用强耦合------工具改个参数类型,AI 应用要重新编译部署
- 难以跨语言、跨框架复用 ------Java 的
@Tool注解,Python 的 Agent 用不了 - 没有标准协议------每个项目的工具接入方式都不一样
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 是核心客户端,支持双模式合并:
- 本地模式 :扫描本进程的
@Tool方法,反射调用 - 远程模式:通过 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 可拆为独立微服务 |