从SpringCloud到AI Agent:微服务架构与大模型的融合之道
摘要:微服务虽成熟,但调用链在编码阶段就固化了。本文给出一个可直接运行的方案:用 Spring AI 给微服务装上"大脑",ReAct 模式驱动智能编排。6 个模块,从零跑通。
目录
- 一、为什么微服务需要 AI Agent
- 二、地基:Spring Cloud 旧地图与新大陆
- 三、解构 AI Agent
- 四、融合架构
- 五、实战:六个模块,从零跑通
- 六、进阶:多 Agent 协作
- 七、落地避坑指南
- 八、总结
一、为什么微服务需要 AI Agent
传统微服务中,每一条调用路径都在编码阶段固化 — A 调 B 再调 C,顺序和逻辑不可动态调整。
但真实需求往往是模糊的。用户不会说"调用 order-service 的 /create 接口",而是说:
“帮我看看 P001 还有没有货,有的话帮我下单买 2 件。”
这句话背后需要 查库存 → 扣库存 → 创建订单,还附带一个条件判断。传统做法是写聚合接口硬编码这些步骤,换一种表达就得重写。
AI Agent 的介入改变了规则:LLM 充当推理引擎,理解自然语言意图,自主决策调用哪些微服务、以什么顺序调用、如何根据中间结果调整动作。
二、地基:Spring Cloud 旧地图与新大陆
| 组件 | 职责 | 本文使用 |
|---|---|---|
| Gateway | 统一入口、路由转发 | ✓ |
| Eureka | 服务注册与发现 | ✓ |
| LoadBalancer | 客户端负载均衡 | ✓ |
| OpenFeign | 声明式调用 | —(改用 RestTemplate) |
传统链路(编译时确定):
客户端 → Gateway → /api/orders → OrderService → InventoryService → DB
目标链路(运行时决策):
客户端 → Gateway → Agent Orchestrator → LLM 推理 → 动态选工具 → 业务服务
核心差异:决策发生的时机从编译时移到了运行时。
三、解构 AI Agent
AI Agent 是一种设计范式,由四个模块构成:
┌──────────────────────────────────────┐
│ AI Agent │
│ ┌─────────┐ ┌─────────┐ │
│ │ LLM │ │ Memory │ │
│ │ (大脑) │ │ (记忆) │ │
│ └────┬────┘ └────┬────┘ │
│ ┌────┴─────────────┴───┐ │
│ │ Planning (规划) │ │
│ └──────────┬────────────┘ │
│ ┌──────────┴────────────┐ │
│ │ Tool Use (工具调用) │ │
│ └───────────────────────┘ │
└──────────────────────────────────────┘
| 模块 | 职责 |
|---|---|
| LLM | 理解意图、推理、生成响应 |
| Memory | 存储对话上下文 |
| Planning | 复杂任务拆解为子步骤 |
| Tool Use | 实际执行:调用 API、操作数据库 |
映射到微服务:每个微服务的 API 就是一个 Tool。Agent 通过 Function Calling 自主选择工具、传参、汇总结果。
四、融合架构
4.1 总体架构
┌──────────────┐
│ 用户 / 前端 │
└──────┬───────┘
┌──────▼───────┐
│ AI Gateway │
│ (8080) │
└──────┬───────┘
┌────────────┼────────────┐
┌───────▼──┐ ┌──────▼──────┐ ┌──▼─────────┐
│ Eureka │ │ Agent │ │LLM Service │
│ (8761) │ │Orchestrator │ │ (8081) │
└──────────┘ │ (8082) │ └────────────┘
└──────┬──────┘
ReAct: Think→Act→Observe
┌────────────┼────────────┐
┌──────▼──┐ ┌────▼─────┐
│ Order │ │Inventory │
│ (8083) │ │ (8084) │
└─────────┘ └──────────┘
4.2 调用链路对比
传统: POST /api/orders/create → OrderService 内部调 InventoryService → 返回
问题: 库存不足需回滚,新增校验要改代码
Agent: POST /api/agent/execute "查P001库存,有货就下单2件,用户U1001"
→ Thought: 先查库存
→ Action: check_inventory(P001) → stock=100
→ Thought: 充足,扣库存
→ Action: deduct_inventory(P001,2) → success
→ Thought: 创建订单
→ Action: create_order(U1001,P001,2) → ORD-xxx
→ Final: "下单成功,订单号 ORD-xxx"
4.3 设计原则
- Agent 即服务 — Orchestrator 是标准微服务,注册到 Eureka
- Tool 即 API — 业务接口通过
ToolDefinition描述注册,运行时自动发现 - Gateway 智能分流 — 明确路由走静态规则,模糊意图交 Agent
- 全链路可观测 — 每步 Thought/Action/Observation 记录日志
五、实战:六个模块,从零跑通
技术栈:Spring Boot 3.2.5 · Spring Cloud 2023.0.2 · Spring AI 1.0.0-M4 · JDK 17
5.1 父 POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>ai-microservice-demo</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.2</spring-cloud.version>
<spring-ai.version>1.0.0-M4</spring-ai.version>
</properties>
<modules>
<module>eureka-server</module>
<module>llm-service</module>
<module>agent-orchestrator</module>
<module>order-service</module>
<module>inventory-service</module>
<module>ai-gateway</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
5.2 Eureka 注册中心
Agent Orchestrator 通过服务名(如 order-service)而非 IP 调用其他服务,依赖 Eureka 的服务发现。
<!-- eureka-server/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.demo</groupId>
<artifactId>ai-microservice-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>eureka-server</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
// eureka-server/src/main/java/com/demo/eureka/EurekaServerApplication.java
package com.demo.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
# eureka-server/src/main/resources/application.yml
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
client:
register-with-eureka: false
fetch-registry: false
server:
enable-self-preservation: false
5.3 LLM 调用服务
将大模型调用封装为独立微服务,好处:换模型只改配置、会话记忆集中管理、token 消耗可量化。
<!-- llm-service/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.demo</groupId>
<artifactId>ai-microservice-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>llm-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
</project>
// llm-service/src/main/java/com/demo/llm/LlmServiceApplication.java
package com.demo.llm;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LlmServiceApplication {
public static void main(String[] args) {
SpringApplication.run(LlmServiceApplication.class, args);
}
}
// llm-service/src/main/java/com/demo/llm/service/LlmAgentService.java
package com.demo.llm.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class LlmAgentService {
private final ChatClient chatClient;
private final Map<String, List<Message>> sessionMemory = new ConcurrentHashMap<>();
private static final String SYSTEM_PROMPT = """
你是智能微服务编排助手。职责:
1. 理解自然语言业务需求
2. 拆解为微服务调用步骤
3. 规划执行顺序
4. 汇总结果反馈用户
可用服务:order-service (创建/查询订单)、inventory-service (查询/扣减库存)
""";
public LlmAgentService(
@Value("${spring.ai.openai.api-key}") String apiKey,
@Value("${spring.ai.openai.base-url}") String baseUrl) {
OpenAiApi openAiApi = OpenAiApi.builder()
.apiKey(apiKey).baseUrl(baseUrl).build();
OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model("deepseek-chat")
.temperature(0.3).maxTokens(2048).build())
.build();
this.chatClient = ChatClient.create(chatModel);
}
public String chat(String sessionId, String userMessage) {
List<Message> history = sessionMemory
.computeIfAbsent(sessionId, k -> new ArrayList<>());
if (history.isEmpty()) {
history.add(new SystemMessage(SYSTEM_PROMPT));
}
history.add(new UserMessage(userMessage));
ChatResponse response = chatClient.call(new Prompt(history));
String reply = response.getResult().getOutput().getContent();
history.add(new AssistantMessage(reply));
if (history.size() > 20) {
List<Message> trimmed = new ArrayList<>();
trimmed.add(history.get(0));
trimmed.addAll(history.subList(history.size() - 18, history.size()));
sessionMemory.put(sessionId, trimmed);
}
return reply;
}
public String singleInference(String prompt) {
return chatClient.call(prompt);
}
public void clearSession(String sessionId) {
sessionMemory.remove(sessionId);
}
}
// llm-service/src/main/java/com/demo/llm/controller/ChatController.java
package com.demo.llm.controller;
import com.demo.llm.service.LlmAgentService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final LlmAgentService llmAgentService;
public ChatController(LlmAgentService llmAgentService) {
this.llmAgentService = llmAgentService;
}
@PostMapping("/{sessionId}")
public String chat(@PathVariable String sessionId,
@RequestBody String message) {
return llmAgentService.chat(sessionId, message);
}
@PostMapping("/inference")
public String inference(@RequestBody String prompt) {
return llmAgentService.singleInference(prompt);
}
@DeleteMapping("/session/{sessionId}")
public void clearSession(@PathVariable String sessionId) {
llmAgentService.clearSession(sessionId);
}
}
# llm-service/src/main/resources/application.yml
server:
port: 8081
spring:
application:
name: llm-service
ai:
openai:
api-key: ${OPENAI_API_KEY:sk-your-api-key-here}
base-url: ${OPENAI_BASE_URL:https://api.deepseek.com}
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
5.4 Agent 编排器(核心)
核心思想:ReAct 模式 — LLM 在「思考」与「行动」之间循环迭代,直到任务完成。
Thought → Action(调工具A) → Observation(拿结果) → Thought → Action(调工具B) → ... → Final
关键价值:Agent 可根据中间结果动态调整 — 库存不足就自动跳过下单步骤。
<!-- agent-orchestrator/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.demo</groupId>
<artifactId>ai-microservice-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>agent-orchestrator</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
</project>
// agent-orchestrator/src/main/java/com/demo/agent/AgentOrchestratorApplication.java
package com.demo.agent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AgentOrchestratorApplication {
public static void main(String[] args) {
SpringApplication.run(AgentOrchestratorApplication.class, args);
}
}
// agent-orchestrator/src/main/java/com/demo/agent/tools/ToolDefinition.java
package com.demo.agent.tools;
import java.util.Map;
import java.util.function.Function;
/**
* @param name 工具名称(LLM 引用)
* @param description 功能描述(LLM 据此决定是否调用)
* @param parameterSchema 参数说明(LLM 据此构造参数)
* @param executor 实际执行逻辑(通过 RestTemplate 调用下游服务)
*/
public record ToolDefinition(
String name,
String description,
Map<String, String> parameterSchema,
Function<Map<String, Object>, String> executor
) {}
// agent-orchestrator/src/main/java/com/demo/agent/ToolRegistry.java
package com.demo.agent;
import com.demo.agent.tools.ToolDefinition;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class ToolRegistry {
private final Map<String, ToolDefinition> tools = new ConcurrentHashMap<>();
public void register(ToolDefinition tool) {
tools.put(tool.name(), tool);
System.out.println("[ToolRegistry] ✓ " + tool.name());
}
public String getToolsDescription() {
StringBuilder sb = new StringBuilder("可用工具列表:\n");
for (ToolDefinition t : tools.values()) {
sb.append(String.format("- **%s**: %s\n 参数: %s\n",
t.name(), t.description(), t.parameterSchema()));
}
return sb.toString();
}
public String execute(String toolName, Map<String, Object> params) {
ToolDefinition tool = tools.get(toolName);
if (tool == null) return "{\"error\": \"工具不存在: " + toolName + "\"}";
try {
return tool.executor().apply(params);
} catch (Exception e) {
return "{\"error\": \"执行失败: " + e.getMessage() + "\"}";
}
}
public List<String> getToolNames() {
return List.copyOf(tools.keySet());
}
}
// agent-orchestrator/src/main/java/com/demo/agent/tools/OrderTool.java
package com.demo.agent.tools;
import com.demo.agent.ToolRegistry;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@Component
public class OrderTool {
private final ToolRegistry registry;
private final RestTemplate restTemplate;
private static final String BASE_URL = "http://order-service/api/orders";
public OrderTool(ToolRegistry registry, RestTemplate restTemplate) {
this.registry = registry;
this.restTemplate = restTemplate;
}
@PostConstruct
public void init() {
registry.register(new ToolDefinition(
"create_order",
"创建新订单。需要 userId、productId、quantity",
Map.of("userId", "String", "productId", "String", "quantity", "Integer"),
params -> restTemplate.postForObject(BASE_URL + "/create",
Map.of("userId", params.get("userId"),
"productId", params.get("productId"),
"quantity", params.get("quantity")),
String.class)
));
registry.register(new ToolDefinition(
"query_order",
"根据订单ID查询订单详情",
Map.of("orderId", "String"),
params -> restTemplate.getForObject(
BASE_URL + "/" + params.get("orderId"), String.class)
));
}
}
// agent-orchestrator/src/main/java/com/demo/agent/tools/InventoryTool.java
package com.demo.agent.tools;
import com.demo.agent.ToolRegistry;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@Component
public class InventoryTool {
private final ToolRegistry registry;
private final RestTemplate restTemplate;
private static final String BASE_URL = "http://inventory-service/api/inventory";
public InventoryTool(ToolRegistry registry, RestTemplate restTemplate) {
this.registry = registry;
this.restTemplate = restTemplate;
}
@PostConstruct
public void init() {
registry.register(new ToolDefinition(
"check_inventory", "查询商品库存数量",
Map.of("productId", "String"),
params -> restTemplate.getForObject(
BASE_URL + "/" + params.get("productId"), String.class)
));
registry.register(new ToolDefinition(
"deduct_inventory", "扣减商品库存,返回剩余数量",
Map.of("productId", "String", "quantity", "Integer"),
params -> restTemplate.postForObject(BASE_URL + "/deduct",
Map.of("productId", params.get("productId"),
"quantity", params.get("quantity")),
String.class)
));
}
}
// agent-orchestrator/src/main/java/com/demo/agent/AgentExecutor.java
package com.demo.agent;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* ReAct 引擎:Thought → Action → Observation → Thought → ... → Final
* 最多 MAX_ITERATIONS 轮,防死循环。
*/
@Service
public class AgentExecutor {
private final ToolRegistry toolRegistry;
private final ChatClient chatClient;
private final ObjectMapper objectMapper = new ObjectMapper();
private static final int MAX_ITERATIONS = 5;
public AgentExecutor(ToolRegistry toolRegistry,
@Value("${spring.ai.openai.api-key}") String apiKey,
@Value("${spring.ai.openai.base-url}") String baseUrl) {
this.toolRegistry = toolRegistry;
OpenAiApi openAiApi = OpenAiApi.builder()
.apiKey(apiKey).baseUrl(baseUrl).build();
OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model("deepseek-chat").temperature(0.1).maxTokens(4096).build())
.build();
this.chatClient = ChatClient.create(chatModel);
}
public String execute(String userRequest) {
StringBuilder log = new StringBuilder();
log.append("【用户请求】").append(userRequest).append("\n\n");
List<String> context = new ArrayList<>();
for (int i = 0; i < MAX_ITERATIONS; i++) {
log.append("━━━ 第 ").append(i + 1).append(" 轮 ━━━\n");
String thought = think(userRequest, context, i);
log.append("[思考] ").append(thought).append("\n");
Decision d = parseDecision(thought);
if (d == null || d.isFinal) {
log.append("\n【答案】").append(d != null ? d.content : thought);
return log.toString();
}
log.append("[行动] ").append(d.toolName)
.append(",参数 ").append(d.params).append("\n");
String result = toolRegistry.execute(d.toolName, d.params);
log.append("[观察] ").append(result).append("\n\n");
context.add(String.format("轮次%d: %s(%s) → %s",
i + 1, d.toolName, d.params, result));
if (result.contains("\"error\""))
context.add("⚠ 上一步出错,请考虑备选方案。");
}
log.append("⚠ 已达最大迭代次数。\n");
String finalAnswer = summarize(userRequest, context);
log.append("【答案】").append(finalAnswer);
return log.toString();
}
private String think(String userRequest, List<String> context, int iteration) {
return chatClient.call(String.format("""
你是智能微服务编排 Agent。决定下一步动作:
【用户请求】%s
【可用工具】%s
【历史】%s
【第 %d 轮】
严格返回 JSON,三选一:
{"action":"call_tool","tool_name":"工具名","params":{...},"reasoning":"原因"}
{"action":"finish","final_answer":"对用户的回复"}
{"action":"error","message":"失败原因"}
只返回 JSON。""",
userRequest,
toolRegistry.getToolsDescription(),
context.isEmpty() ? "无" : String.join("\n", context),
iteration + 1));
}
private Decision parseDecision(String llmOutput) {
try {
String json = extractJson(llmOutput);
JsonNode node = objectMapper.readTree(json);
String action = node.get("action").asText();
if ("finish".equals(action)) {
Decision d = new Decision();
d.isFinal = true;
d.content = node.get("final_answer").asText();
return d;
}
if ("call_tool".equals(action)) {
Decision d = new Decision();
d.toolName = node.get("tool_name").asText();
d.params = objectMapper.convertValue(node.get("params"), Map.class);
return d;
}
return null;
} catch (Exception e) {
Decision d = new Decision();
d.isFinal = true;
d.content = llmOutput;
return d;
}
}
private String extractJson(String text) {
int s = text.indexOf("{"), e = text.lastIndexOf("}") + 1;
return (s >= 0 && e > s) ? text.substring(s, e) : text;
}
private String summarize(String req, List<String> ctx) {
return chatClient.call(String.format(
"用户请求: %s\n\n执行历史:\n%s\n\n汇总,自然语言回复。",
req, String.join("\n", ctx)));
}
private static class Decision {
boolean isFinal;
String content, toolName;
Map<String, Object> params;
}
}
// agent-orchestrator/src/main/java/com/demo/agent/controller/AgentController.java
package com.demo.agent.controller;
import com.demo.agent.AgentExecutor;
import com.demo.agent.ToolRegistry;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/agent")
public class AgentController {
private final AgentExecutor agentExecutor;
private final ToolRegistry toolRegistry;
public AgentController(AgentExecutor agentExecutor, ToolRegistry toolRegistry) {
this.agentExecutor = agentExecutor;
this.toolRegistry = toolRegistry;
}
@PostMapping("/execute")
public String execute(@RequestBody String userRequest) {
return agentExecutor.execute(userRequest);
}
@GetMapping("/tools")
public List<String> listTools() {
return toolRegistry.getToolNames();
}
@PostMapping("/tool/{toolName}")
public String callTool(@PathVariable String toolName,
@RequestBody Map<String, Object> params) {
return toolRegistry.execute(toolName, params);
}
}
// agent-orchestrator/src/main/java/com/demo/agent/config/AppConfig.java
package com.demo.agent.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class AppConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
# agent-orchestrator/src/main/resources/application.yml
server:
port: 8082
spring:
application:
name: agent-orchestrator
ai:
openai:
api-key: ${OPENAI_API_KEY:sk-your-api-key-here}
base-url: ${OPENAI_BASE_URL:https://api.deepseek.com}
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
5.5 订单服务
<!-- order-service/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.demo</groupId>
<artifactId>ai-microservice-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>order-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
// order-service/src/main/java/com/demo/order/OrderServiceApplication.java
package com.demo.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
// order-service/src/main/java/com/demo/order/controller/OrderController.java
package com.demo.order.controller;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final Map<String, Map<String, Object>> store = new ConcurrentHashMap<>();
@PostMapping("/create")
public Map<String, Object> create(@RequestBody Map<String, Object> req) {
String orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8);
Map<String, Object> order = Map.of(
"orderId", orderId,
"userId", req.get("userId"),
"productId", req.get("productId"),
"quantity", req.get("quantity"),
"status", "CREATED",
"createdAt", System.currentTimeMillis());
store.put(orderId, order);
return order;
}
@GetMapping("/{orderId}")
public Map<String, Object> get(@PathVariable String orderId) {
return store.getOrDefault(orderId, Map.of("error", "订单不存在: " + orderId));
}
}
# order-service/src/main/resources/application.yml
server:
port: 8083
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
5.6 库存服务
<!-- inventory-service/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.demo</groupId>
<artifactId>ai-microservice-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>inventory-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
// inventory-service/src/main/java/com/demo/inventory/InventoryServiceApplication.java
package com.demo.inventory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class InventoryServiceApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryServiceApplication.class, args);
}
}
// inventory-service/src/main/java/com/demo/inventory/controller/InventoryController.java
package com.demo.inventory.controller;
import jakarta.annotation.PostConstruct;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RequestMapping("/api/inventory")
public class InventoryController {
private final Map<String, Integer> store = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
store.put("P001", 100);
store.put("P002", 50);
store.put("P003", 0); // 设为 0,用于测试库存不足
store.put("P004", 200);
}
@GetMapping("/{productId}")
public Map<String, Object> check(@PathVariable String productId) {
Integer stock = store.getOrDefault(productId, -1);
return stock == -1
? Map.of("error", "商品不存在: " + productId)
: Map.of("productId", productId, "stock", stock, "available", stock > 0);
}
@PostMapping("/deduct")
public Map<String, Object> deduct(@RequestBody Map<String, Object> req) {
String productId = (String) req.get("productId");
int quantity = ((Number) req.get("quantity")).intValue();
Integer current = store.get(productId);
if (current == null) return Map.of("success", false, "error", "商品不存在");
if (current < quantity) return Map.of("success", false, "error", "库存不足",
"currentStock", current, "requested", quantity);
int remaining = current - quantity;
store.put(productId, remaining);
return Map.of("success", true, "productId", productId,
"deducted", quantity, "remaining", remaining);
}
}
# inventory-service/src/main/resources/application.yml
server:
port: 8084
spring:
application:
name: inventory-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
5.7 智能网关
Gateway 统一入口 + AiIntentFilter:请求带 ?query= 参数时调用 LLM 意图分类,否则走静态路由。
<!-- ai-gateway/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.demo</groupId>
<artifactId>ai-microservice-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>ai-gateway</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
// ai-gateway/src/main/java/com/demo/gateway/AiGatewayApplication.java
package com.demo.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(AiGatewayApplication.class, args);
}
}
// ai-gateway/src/main/java/com/demo/gateway/filter/AiIntentFilter.java
package com.demo.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
@Component
public class AiIntentFilter
extends AbstractGatewayFilterFactory<AiIntentFilter.Config> {
private final WebClient llmClient;
public AiIntentFilter() {
super(Config.class);
this.llmClient = WebClient.builder()
.baseUrl("http://localhost:8081").build();
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String query = exchange.getRequest()
.getQueryParams().getFirst("query");
if (query == null || query.isEmpty()) {
return chain.filter(exchange);
}
return classifyIntent(query)
.flatMap(intent -> {
exchange.getAttributes().put("ai_intent", intent);
return chain.filter(exchange);
});
};
}
private reactor.core.publisher.Mono<String> classifyIntent(String query) {
return llmClient.post()
.uri("/api/chat/inference")
.bodyValue(String.format(
"分类以下请求,只回复一个词(order/inventory/chat): %s", query))
.retrieve()
.bodyToMono(String.class)
.map(String::trim)
.defaultIfEmpty("chat");
}
public static class Config {}
}
# ai-gateway/src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: ai-gateway
cloud:
gateway:
routes:
- id: order-route
uri: lb://order-service
predicates: [Path=/api/orders/**]
- id: inventory-route
uri: lb://inventory-service
predicates: [Path=/api/inventory/**]
- id: llm-route
uri: lb://llm-service
predicates: [Path=/api/chat/**]
- id: agent-route
uri: lb://agent-orchestrator
predicates: [Path=/api/agent/**]
default-filters: [AiIntentFilter]
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
5.8 启动与测试
# 终端1 — 注册中心
cd eureka-server && mvn spring-boot:run
# 终端2 — LLM 服务(替换 API Key)
cd llm-service
mvn spring-boot:run -Dspring-boot.run.jvmArguments="-DOPENAI_API_KEY=sk-xxx"
# 终端3 — 订单服务
cd order-service && mvn spring-boot:run
# 终端4 — 库存服务
cd inventory-service && mvn spring-boot:run
# 终端5 — Agent 编排器
cd agent-orchestrator
mvn spring-boot:run -Dspring-boot.run.jvmArguments="-DOPENAI_API_KEY=sk-xxx"
# 终端6 — 网关
cd ai-gateway && mvn spring-boot:run
访问 http://localhost:8761 确认 5 个服务均已注册。
场景一:库存充足
curl -X POST http://localhost:8080/api/agent/execute \
-H "Content-Type: text/plain" \
-d "帮我查P001库存,有货就下单2件,用户U1001"
预期输出:
【用户请求】帮我查P001库存,有货就下单2件,用户U1001
━━━ 第 1 轮 ━━━
[思考] {"action":"call_tool","tool_name":"check_inventory","params":{"productId":"P001"},...}
[行动] check_inventory,参数 {productId=P001}
[观察] {"productId":"P001","stock":100,"available":true}
━━━ 第 2 轮 ━━━
[思考] {"action":"call_tool","tool_name":"deduct_inventory","params":{"productId":"P001","quantity":2},...}
[行动] deduct_inventory,参数 {productId=P001, quantity=2}
[观察] {"success":true,"deducted":2,"remaining":98}
━━━ 第 3 轮 ━━━
[思考] {"action":"call_tool","tool_name":"create_order","params":{"userId":"U1001","productId":"P001","quantity":2},...}
[行动] create_order,参数 {userId=U1001, productId=P001, quantity=2}
[观察] {"orderId":"ORD-a3f8c2b1","status":"CREATED",...}
━━━ 第 4 轮 ━━━
[思考] {"action":"finish","final_answer":"下单成功!订单号 ORD-a3f8c2b1,剩余库存98。"}
【答案】下单成功!订单号 ORD-a3f8c2b1,剩余库存98。
场景二:库存不足(Agent 自动中断)
curl -X POST http://localhost:8080/api/agent/execute \
-H "Content-Type: text/plain" \
-d "帮我下单买3件P003,用户U1002"
━━━ 第 1 轮 ━━━
[观察] {"productId":"P003","stock":0,"available":false}
━━━ 第 2 轮 ━━━
[思考] {"action":"finish","final_answer":"很抱歉,P003库存为0,无法下单。"}
【答案】很抱歉,P003库存为0,无法下单。
关键:Agent 发现库存为 0 后,自主跳过扣库存和创建订单,直接告知用户。无需
if (stock <= 0) return error— Agent 自己判断。
六、进阶:多 Agent 协作
单一 Agent 适合 2-3 个领域的编排。当扩展到订单+推荐+风控+支付时,需要 Master Agent 模式:
┌──────────────────┐
│ Master Agent │
│ (任务分解+分发) │
└────────┬─────────┘
┌──────────────────┼──────────────────┐
┌──────▼─────┐ ┌───────▼──────┐ ┌───────▼──────┐
│Order Agent │ │Inventory Agt │ │Payment Agent │
└────────────┘ └──────────────┘ └──────────────┘
每个 Sub-Agent 拥有独立工具集,Master 拆分任务 → 并行分发 → 聚合结果:
public class MasterAgent {
private final Map<String, AgentExecutor> subAgents = new HashMap<>();
public String execute(String request) {
List<SubTask> tasks = decomposeTask(request); // LLM 拆分
List<CompletableFuture<String>> futures = tasks.stream()
.map(t -> CompletableFuture.supplyAsync(() ->
subAgents.get(t.domain).execute(t.query)))
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
return chatClient.call("汇总:\n" +
futures.stream().map(CompletableFuture::join)
.reduce("", (a, b) -> a + "\n---\n" + b));
}
}
七、落地避坑指南
| 实践 | 原因 | 实现 |
|---|---|---|
| 接口幂等 | Agent 可能重试同一调用 | 唯一键去重 |
| 超时设置 | LLM 推理可达数秒 | RestTemplate 设 connectTimeout |
| 熔断降级 | 下游不可用时快速失败 | Resilience4j @CircuitBreaker |
| Tool Schema 详细 | 描述模糊 = LLM 调用错误 | 注明类型、取值、示例 |
| 人工回退 | 高风险操作需确认 | 返回"待确认",MQ 通知审核 |
| 记忆裁剪 | 会话过长超出上下文 | 保留系统消息 + 最近 N 轮 |
| 模型降级 | 主模型故障自动切换 | 配置多 endpoint,异常 fallback |
常见坑:
- 幻觉 — LLM 可能编造返回结果。对策:最终回答必须基于实际 Observation 数据。
- 延迟 — 每轮 ReAct = 1 次 LLM + 1 次 HTTP。对策:SSE 流式输出中间状态。
- Token 成本 — 长 prompt + 多轮推理。对策:缓存高频结果,语义去重。
- 安全 — Agent 获得内部调用权。对策:Tool 层参数校验 + 审计日志 + 敏感操作二次确认。
八、总结
把每个微服务看作 Agent 工具箱里的一把工具,让 LLM 成为知道何时该用哪把工具的"老师傅"。
阶段一:传统微服务
Service-A → Feign → Service-B → 硬编码 → 静态路由
阶段二:AI 增强(本文)
Gateway → LLM 意图路由 → ReAct Agent → 动态工具选择
阶段三:自治 Agent 网格
Master Agent → Sub-Agent Mesh → 自主协作 + 故障自愈
基于本文代码可继续扩展:更多业务工具、本地模型(Ollama+Qwen)、RAG 知识库、SSE 流式输出。
运行环境:JDK 17+ · Maven 3.8+ · 任意 OpenAI 兼容协议的 API Key
更多推荐


所有评论(0)