摘要:微服务虽成熟,但调用链在编码阶段就固化了。本文给出一个可直接运行的方案:用 Spring AI 给微服务装上"大脑",ReAct 模式驱动智能编排。6 个模块,从零跑通。


目录


一、为什么微服务需要 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 设计原则

  1. Agent 即服务 — Orchestrator 是标准微服务,注册到 Eureka
  2. Tool 即 API — 业务接口通过 ToolDefinition 描述注册,运行时自动发现
  3. Gateway 智能分流 — 明确路由走静态规则,模糊意图交 Agent
  4. 全链路可观测 — 每步 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

常见坑

  1. 幻觉 — LLM 可能编造返回结果。对策:最终回答必须基于实际 Observation 数据。
  2. 延迟 — 每轮 ReAct = 1 次 LLM + 1 次 HTTP。对策:SSE 流式输出中间状态。
  3. Token 成本 — 长 prompt + 多轮推理。对策:缓存高频结果,语义去重。
  4. 安全 — 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

参考Spring AI · Spring Cloud Gateway · ReAct 论文

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐