Spring Boot 深度实战:构建企业级智能订单助手 Agent
# Spring Boot 深度实战:构建企业级智能订单助手 Agent
# 引言
在企业级应用场景中,AI Agent 正在重新定义人机交互的方式。与简单的问答系统不同,真正的 Agent 需要具备自主规划、工具调用、多轮对话和状态管理能力。Spring AI 框架为 Java 开发者提供了一套完整的 Agent 构建基础设施,让构建生产级 AI 应用成为可能。
本文将带你从零构建一个智能订单助手 Agent。这个 Agent 能够理解用户的自然语言指令,自主调用多个后端服务完成订单查询、库存检查、物流计算等复杂任务。通过这个实战项目,我们将深入探讨 Agent 的核心架构、工具系统设计、记忆管理、错误处理和可观测性等关键技术点。
# 场景设计:智能订单助手
# 业务背景
我们设计的智能订单助手需要解决以下业务问题:
- 订单查询:用户可以用自然语言查询订单状态、历史订单、订单详情
- 库存检查:在用户询问商品时自动检查库存情况
- 物流计算:根据收货地址和商品计算运费和预计送达时间
- 订单操作:支持取消订单、修改配送地址等操作
- 客户画像:获取客户等级、历史购买记录等信息
# 技术挑战
这个场景涉及多个技术难点:
- 多工具编排:Agent 需要根据用户意图自主选择和组合调用多个工具
- 状态管理:多轮对话中需要维护上下文状态
- 参数提取:从自然语言中准确提取结构化参数(如日期、订单号、地址)
- 错误恢复:工具调用失败时的优雅降级和重试策略
- 可观测性:完整的调用链追踪和日志记录
# 架构设计
# 整体架构
智能订单助手的架构采用分层设计,清晰分离关注点:
┌─────────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ REST API │ │ WebSocket │ │ SSE (Server-Sent) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Agent Core Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Planner │ │ Executor │ │ Memory Manager │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Tool System Layer │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Order Tools│ │Stock Tools │ │Ship Tools │ │User Tools │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ External Services │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Order SVC │ │Inventory │ │Logistics │ │ Customer │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 核心组件设计
Agent 核心引擎:负责任务规划、工具调用编排和结果处理。采用 ReAct(Reasoning + Acting)模式,让 Agent 能够思考下一步行动。
工具注册中心:统一管理所有可用工具,支持运行时动态注册和发现。
记忆系统:分层记忆管理,包括会话级记忆(短期)、用户级记忆(中期)和知识级记忆(长期)。
对话上下文:维护当前对话的状态,包括已识别的意图、已获取的参数、已调用的工具历史。
# 项目初始化
# 技术栈
- Spring Boot 3.4.x
- Spring AI 1.0.0+
- Java 21
- MyBatis-Plus(数据访问)
- Redis(缓存和会话存储)
- PostgreSQL(持久化存储)
# Maven 依赖配置
<?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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.2</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>smart-order-agent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>Smart Order Agent</name>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencies>
<!-- Spring AI Core -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- OpenAI Chat Model -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring WebFlux (for async) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.9</version>
</dependency>
<!-- PostgreSQL -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# 配置文件
server:
port: 8080
spring:
application:
name: smart-order-agent
ai:
openai:
chat:
options:
model: gpt-4o-mini
temperature: 0.7
api-key: ${OPENAI_API_KEY}
data:
redis:
host: localhost
port: 6379
postgres:
url: jdbc:postgresql://localhost:5432/order_db
username: postgres
password: postgres
# Agent Configuration
agent:
max-iterations: 10
tool-timeout-seconds: 30
conversation-timeout-minutes: 30
enable-tracing: true
# Logging
logging:
level:
com.example.agent: DEBUG
org.springframework.ai: DEBUG
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 核心实现
# 领域模型定义
首先定义订单、用户、商品等核心领域模型:
package com.example.agent.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Order {
private String orderId;
private String customerId;
private String customerName;
private List<OrderItem> items;
private OrderStatus status;
private BigDecimal totalAmount;
private String shippingAddress;
private String receiverName;
private String receiverPhone;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime estimatedDelivery;
private String trackingNumber;
private String logisticsCompany;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderItem {
private String productId;
private String productName;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal subtotal;
}
public enum OrderStatus {
PENDING,
PAID,
PROCESSING,
SHIPPED,
DELIVERED,
CANCELLED,
REFUNDED
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.example.agent.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Product {
private String productId;
private String productName;
private String category;
private BigDecimal price;
private Integer stockQuantity;
private String warehouseLocation;
private Boolean available;
private String description;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.agent.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
private String customerId;
private String name;
private String phone;
private String email;
private CustomerLevel level;
private Integer totalOrders;
private BigDecimal totalSpent;
private String defaultAddress;
private LocalDateTime registeredAt;
}
public enum CustomerLevel {
BRONZE,
SILVER,
GOLD,
PLATINUM
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.agent.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ShippingQuote {
private String logisticsCompany;
private String shippingMethod;
private BigDecimal cost;
private Integer estimatedDays;
private String estimatedDeliveryDate;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 服务层实现
# 订单服务
package com.example.agent.service;
import com.example.agent.model.Order;
import com.example.agent.model.OrderStatus;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class OrderService {
// 模拟数据存储
private final Map<String, Order> orders = new HashMap<>();
public OrderService() {
initializeMockData();
}
private void initializeMockData() {
Order order1 = Order.builder()
.orderId("ORD20240315001")
.customerId("C001")
.customerName("张三")
.items(List.of(
com.example.agent.model.OrderItem.builder()
.productId("P001")
.productName("iPhone 15 Pro")
.quantity(1)
.unitPrice(new BigDecimal("8999.00"))
.subtotal(new BigDecimal("8999.00"))
.build()
))
.status(OrderStatus.SHIPPED)
.totalAmount(new BigDecimal("8999.00"))
.shippingAddress("北京市朝阳区建国路88号")
.receiverName("张三")
.receiverPhone("13800138000")
.createdAt(LocalDateTime.now().minusDays(3))
.updatedAt(LocalDateTime.now().minusDays(1))
.estimatedDelivery(LocalDateTime.now().plusDays(2))
.trackingNumber("SF1234567890")
.logisticsCompany("顺丰速运")
.build();
Order order2 = Order.builder()
.orderId("ORD20240320002")
.customerId("C001")
.customerName("张三")
.items(List.of(
com.example.agent.model.OrderItem.builder()
.productId("P002")
.productName("MacBook Pro 14寸")
.quantity(1)
.unitPrice(new BigDecimal("15999.00"))
.subtotal(new BigDecimal("15999.00"))
.build()
))
.status(OrderStatus.PROCESSING)
.totalAmount(new BigDecimal("15999.00"))
.shippingAddress("北京市朝阳区建国路88号")
.receiverName("张三")
.receiverPhone("13800138000")
.createdAt(LocalDateTime.now().minusDays(1))
.updatedAt(LocalDateTime.now())
.build();
orders.put(order1.getOrderId(), order1);
orders.put(order2.getOrderId(), order2);
}
/**
* 根据订单号查询订单
*/
public Optional<Order> getOrderById(String orderId) {
return Optional.ofNullable(orders.get(orderId));
}
/**
* 查询客户的所有订单
*/
public List<Order> getOrdersByCustomerId(String customerId) {
return orders.values().stream()
.filter(order -> order.getCustomerId().equals(customerId))
.sorted(Comparator.comparing(Order::getCreatedAt).reversed())
.collect(Collectors.toList());
}
/**
* 根据状态查询客户订单
*/
public List<Order> getOrdersByCustomerIdAndStatus(String customerId, OrderStatus status) {
return orders.values().stream()
.filter(order -> order.getCustomerId().equals(customerId))
.filter(order -> order.getStatus() == status)
.sorted(Comparator.comparing(Order::getCreatedAt).reversed())
.collect(Collectors.toList());
}
/**
* 取消订单
*/
public boolean cancelOrder(String orderId) {
Order order = orders.get(orderId);
if (order == null) {
return false;
}
if (order.getStatus() == OrderStatus.SHIPPED ||
order.getStatus() == OrderStatus.DELIVERED) {
throw new IllegalStateException("已发货或已送达的订单无法取消");
}
order.setStatus(OrderStatus.CANCELLED);
order.setUpdatedAt(LocalDateTime.now());
return true;
}
/**
* 修改配送地址
*/
public boolean updateShippingAddress(String orderId, String newAddress, String receiverName, String receiverPhone) {
Order order = orders.get(orderId);
if (order == null) {
return false;
}
if (order.getStatus() == OrderStatus.SHIPPED ||
order.getStatus() == OrderStatus.DELIVERED) {
throw new IllegalStateException("已发货的订单无法修改地址");
}
order.setShippingAddress(newAddress);
order.setReceiverName(receiverName);
order.setReceiverPhone(receiverPhone);
order.setUpdatedAt(LocalDateTime.now());
return true;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# 库存服务
package com.example.agent.service;
import com.example.agent.model.Product;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
public class InventoryService {
private final Map<String, Product> products = new HashMap<>();
public InventoryService() {
initializeMockData();
}
private void initializeMockData() {
products.put("P001", Product.builder()
.productId("P001")
.productName("iPhone 15 Pro")
.category("手机")
.price(new BigDecimal("8999.00"))
.stockQuantity(50)
.warehouseLocation("北京仓")
.available(true)
.description("Apple iPhone 15 Pro, 256GB, 钛金属")
.build());
products.put("P002", Product.builder()
.productId("P002")
.productName("MacBook Pro 14寸")
.category("电脑")
.price(new BigDecimal("15999.00"))
.stockQuantity(20)
.warehouseLocation("上海仓")
.available(true)
.description("Apple MacBook Pro 14寸, M3 Pro, 18GB, 512GB")
.build());
products.put("P003", Product.builder()
.productId("P003")
.productName("AirPods Pro 2")
.category("耳机")
.price(new BigDecimal("1899.00"))
.stockQuantity(0)
.warehouseLocation("北京仓")
.available(false)
.description("Apple AirPods Pro (第二代)")
.build());
products.put("P004", Product.builder()
.productId("P004")
.productName("iPad Pro 12.9寸")
.category("平板")
.price(new BigDecimal("9999.00"))
.stockQuantity(15)
.warehouseLocation("广州仓")
.available(true)
.description("Apple iPad Pro 12.9寸, M2, 256GB, Wi-Fi")
.build());
}
/**
* 根据商品ID查询商品
*/
public Optional<Product> getProductById(String productId) {
return Optional.ofNullable(products.get(productId));
}
/**
* 根据商品名称搜索商品
*/
public List<Product> searchProducts(String keyword) {
String lowerKeyword = keyword.toLowerCase();
return products.values().stream()
.filter(p -> p.getProductName().toLowerCase().contains(lowerKeyword) ||
p.getCategory().toLowerCase().contains(lowerKeyword))
.toList();
}
/**
* 检查商品库存
*/
public boolean checkStock(String productId, int quantity) {
Product product = products.get(productId);
if (product == null) {
return false;
}
return product.getStockQuantity() >= quantity && product.getAvailable();
}
/**
* 获取库存状态描述
*/
public String getStockStatus(String productId) {
Product product = products.get(productId);
if (product == null) {
return "商品不存在";
}
if (!product.getAvailable()) {
return "商品已下架";
}
if (product.getStockQuantity() == 0) {
return "商品已售罄";
} else if (product.getStockQuantity() < 10) {
return "商品库存紧张,仅剩" + product.getStockQuantity() + "件";
} else {
return "商品充足,有库存" + product.getStockQuantity() + "件";
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# 物流服务
package com.example.agent.service;
import com.example.agent.model.ShippingQuote;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
public class LogisticsService {
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy年MM月dd日");
/**
* 计算运费
*/
public List<ShippingQuote> calculateShipping(String destination) {
// 根据目的地省份计算运费
int baseCost = calculateBaseCost(destination);
int days = calculateDeliveryDays(destination);
return List.of(
ShippingQuote.builder()
.logisticsCompany("顺丰速运")
.shippingMethod("标快")
.cost(new BigDecimal(baseCost * 1.5))
.estimatedDays(days)
.estimatedDeliveryDate(LocalDate.now().plusDays(days).format(DATE_FORMATTER))
.build(),
ShippingQuote.builder()
.logisticsCompany("中通快递")
.shippingMethod("普通")
.cost(new BigDecimal(baseCost))
.estimatedDays(days + 2)
.estimatedDeliveryDate(LocalDate.now().plusDays(days + 2).format(DATE_FORMATTER))
.build(),
ShippingQuote.builder()
.logisticsCompany("京东物流")
.shippingMethod("次日达")
.cost(new BigDecimal(baseCost * 1.2))
.estimatedDays(Math.max(1, days - 1))
.estimatedDeliveryDate(LocalDate.now().plusDays(Math.max(1, days - 1)).format(DATE_FORMATTER))
.build()
);
}
/**
* 查询物流信息
*/
public String trackShipment(String trackingNumber) {
return """
物流追踪信息:
快递公司:顺丰速运
运单号:%s
2024-03-14 14:30 [北京] 已发出,正在发往北京
2024-03-14 18:20 [北京] 到达北京朝阳区建国路营业部
2024-03-15 08:00 [北京] 正在派送中,预计今日送达
""".formatted(trackingNumber);
}
private int calculateBaseCost(String destination) {
if (destination.contains("北京") || destination.contains("上海") ||
destination.contains("广东") || destination.contains("江苏") ||
destination.contains("浙江")) {
return 12;
} else if (destination.contains("四川") || destination.contains("湖北") ||
destination.contains("陕西") || destination.contains("河南")) {
return 15;
} else {
return 18;
}
}
private int calculateDeliveryDays(String destination) {
if (destination.contains("北京") || destination.contains("上海")) {
return 1;
} else if (destination.contains("广东") || destination.contains("江苏") ||
destination.contains("浙江") || destination.contains("四川")) {
return 2;
} else {
return 3;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# 客户服务
package com.example.agent.service;
import com.example.agent.model.Customer;
import com.example.agent.model.CustomerLevel;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Service
public class CustomerService {
private final Map<String, Customer> customers = new HashMap<>();
public CustomerService() {
initializeMockData();
}
private void initializeMockData() {
customers.put("C001", Customer.builder()
.customerId("C001")
.name("张三")
.phone("13800138000")
.email("zhangsan@example.com")
.level(CustomerLevel.GOLD)
.totalOrders(15)
.totalSpent(new BigDecimal("85600.00"))
.defaultAddress("北京市朝阳区建国路88号")
.registeredAt(LocalDateTime.now().minusYears(2))
.build());
customers.put("C002", Customer.builder()
.customerId("C002")
.name("李四")
.phone("13900139000")
.email("lisi@example.com")
.level(CustomerLevel.SILVER)
.totalOrders(8)
.totalSpent(new BigDecimal("32000.00"))
.defaultAddress("上海市浦东新区世纪大道100号")
.registeredAt(LocalDateTime.now().minusYears(1))
.build());
}
/**
* 根据客户ID查询客户信息
*/
public Optional<Customer> getCustomerById(String customerId) {
return Optional.ofNullable(customers.get(customerId));
}
/**
* 根据手机号查询客户信息
*/
public Optional<Customer> getCustomerByPhone(String phone) {
return customers.values().stream()
.filter(c -> c.getPhone().equals(phone))
.findFirst();
}
/**
* 根据邮箱查询客户信息
*/
public Optional<Customer> getCustomerByEmail(String email) {
return customers.values().stream()
.filter(c -> c.getEmail().equals(email))
.findFirst();
}
/**
* 客户登录验证
*/
public Optional<Customer> authenticate(String phone, String password) {
// 简化实现,实际应该验证密码
return getCustomerByPhone(phone);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 工具系统实现
工具系统是 Agent 的"手"和"脚",让 AI 能够执行实际的操作。Spring AI 提供了 @Tool 注解来简化工具定义。
# 订单工具
package com.example.agent.tools;
import com.example.agent.model.Order;
import com.example.agent.model.OrderStatus;
import com.example.agent.service.OrderService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class OrderTools {
private final OrderService orderService;
public OrderTools(OrderService orderService) {
this.orderService = orderService;
}
@Tool(name = "get_order_by_id",
description = "根据订单号查询订单详情。如果用户提供了订单号,使用这个工具查询。")
public String getOrderById(
@ToolParam(description = "订单号,格式如:ORD20240315001") String orderId) {
return orderService.getOrderById(orderId)
.map(this::formatOrderDetail)
.orElse("未找到订单号为 " + orderId + " 的订单");
}
@Tool(name = "get_customer_orders",
description = "查询客户的所有订单列表。可以指定订单状态过滤。")
public String getCustomerOrders(
@ToolParam(description = "客户ID") String customerId,
@ToolParam(description = "订单状态,可选值:PENDING, PAID, PROCESSING, SHIPPED, DELIVERED, CANCELLED, REFUNDED",
required = false) String status) {
List<Order> orders;
if (status != null && !status.isEmpty()) {
try {
OrderStatus orderStatus = OrderStatus.valueOf(status.toUpperCase());
orders = orderService.getOrdersByCustomerIdAndStatus(customerId, orderStatus);
} catch (IllegalArgumentException e) {
orders = orderService.getOrdersByCustomerId(customerId);
}
} else {
orders = orderService.getOrdersByCustomerId(customerId);
}
if (orders.isEmpty()) {
return "未找到相关订单";
}
return orders.stream()
.map(this::formatOrderSummary)
.collect(Collectors.joining("\n\n"));
}
@Tool(name = "cancel_order",
description = "取消订单。只有未发货的订单才能取消。")
public String cancelOrder(
@ToolParam(description = "要取消的订单号") String orderId) {
try {
boolean success = orderService.cancelOrder(orderId);
if (success) {
return "订单 " + orderId + " 已成功取消";
} else {
return "取消订单失败:订单不存在";
}
} catch (IllegalStateException e) {
return "取消订单失败:" + e.getMessage();
}
}
@Tool(name = "update_shipping_address",
description = "修改订单的配送地址。只有未发货的订单才能修改地址。")
public String updateShippingAddress(
@ToolParam(description = "订单号") String orderId,
@ToolParam(description = "新地址") String newAddress,
@ToolParam(description = "收货人姓名") String receiverName,
@ToolParam(description = "收货人电话") String receiverPhone) {
try {
boolean success = orderService.updateShippingAddress(orderId, newAddress, receiverName, receiverPhone);
if (success) {
return "订单 " + orderId + " 的配送地址已修改为:" + newAddress;
} else {
return "修改地址失败:订单不存在";
}
} catch (IllegalStateException e) {
return "修改地址失败:" + e.getMessage();
}
}
private String formatOrderDetail(Order order) {
StringBuilder sb = new StringBuilder();
sb.append("订单号:").append(order.getOrderId()).append("\n");
sb.append("订单状态:").append(getStatusText(order.getStatus())).append("\n");
sb.append("下单时间:").append(order.getCreatedAt()).append("\n");
sb.append("商品明细:\n");
order.getItems().forEach(item -> {
sb.append(" - ").append(item.getProductName())
.append(" x ").append(item.getQuantity())
.append(" = ¥").append(item.getSubtotal()).append("\n");
});
sb.append("订单总价:¥").append(order.getTotalAmount()).append("\n");
sb.append("收货地址:").append(order.getShippingAddress()).append("\n");
sb.append("收货人:").append(order.getReceiverName())
.append(" ").append(order.getReceiverPhone()).append("\n");
if (order.getTrackingNumber() != null) {
sb.append("物流公司:").append(order.getLogisticsCompany()).append("\n");
sb.append("运单号:").append(order.getTrackingNumber()).append("\n");
}
if (order.getEstimatedDelivery() != null) {
sb.append("预计送达:").append(order.getEstimatedDelivery()).append("\n");
}
return sb.toString();
}
private String formatOrderSummary(Order order) {
return String.format("订单号:%s | 状态:%s | 金额:¥%s | 时间:%s",
order.getOrderId(),
getStatusText(order.getStatus()),
order.getTotalAmount(),
order.getCreatedAt().toLocalDate());
}
private String getStatusText(OrderStatus status) {
return switch (status) {
case PENDING -> "待付款";
case PAID -> "已付款";
case PROCESSING -> "处理中";
case SHIPPED -> "已发货";
case DELIVERED -> "已送达";
case CANCELLED -> "已取消";
case REFUNDED -> "已退款";
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# 库存工具
package com.example.agent.tools;
import com.example.agent.model.Product;
import com.example.agent.service.InventoryService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class InventoryTools {
private final InventoryService inventoryService;
public InventoryTools(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
@Tool(name = "search_products",
description = "搜索商品信息。根据关键词搜索商品名称或类别。")
public String searchProducts(
@ToolParam(description = "搜索关键词,商品名称或类别") String keyword) {
List<Product> products = inventoryService.searchProducts(keyword);
if (products.isEmpty()) {
return "未找到匹配的商品";
}
return products.stream()
.map(this::formatProduct)
.collect(Collectors.joining("\n\n"));
}
@Tool(name = "get_product_stock",
description = "查询特定商品的库存状态。")
public String getProductStock(
@ToolParam(description = "商品ID") String productId) {
return inventoryService.getStockStatus(productId);
}
@Tool(name = "check_product_available",
description = "检查商品是否可购买,包括库存和上下架状态。")
public boolean checkProductAvailable(
@ToolParam(description = "商品ID") String productId,
@ToolParam(description = "购买数量", required = false) Integer quantity) {
int qty = quantity != null ? quantity : 1;
return inventoryService.checkStock(productId, qty);
}
private String formatProduct(Product product) {
StringBuilder sb = new StringBuilder();
sb.append("商品ID:").append(product.getProductId()).append("\n");
sb.append("商品名称:").append(product.getProductName()).append("\n");
sb.append("类别:").append(product.getCategory()).append("\n");
sb.append("价格:¥").append(product.getPrice()).append("\n");
sb.append("库存:").append(product.getStockQuantity()).append("件\n");
sb.append("仓库:").append(product.getWarehouseLocation()).append("\n");
sb.append("状态:").append(product.getAvailable() ? "在售" : "已下架");
if (!product.getAvailable() || product.getStockQuantity() == 0) {
sb.append(" ❌");
} else if (product.getStockQuantity() < 10) {
sb.append(" ⚠️ 库存紧张");
} else {
sb.append(" ✅");
}
return sb.toString();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 物流工具
package com.example.agent.tools;
import com.example.agent.model.ShippingQuote;
import com.example.agent.service.LogisticsService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class LogisticsTools {
private final LogisticsService logisticsService;
public LogisticsTools(LogisticsService logisticsService) {
this.logisticsService = logisticsService;
}
@Tool(name = "calculate_shipping",
description = "计算运费和预计送达时间。根据收货地址计算不同物流方案的报价。")
public String calculateShipping(
@ToolParam(description = "收货地址") String destination) {
List<ShippingQuote> quotes = logisticsService.calculateShipping(destination);
return quotes.stream()
.map(this::formatQuote)
.collect(Collectors.joining("\n\n"));
}
@Tool(name = "track_shipment",
description = "查询物流追踪信息。")
public String trackShipment(
@ToolParam(description = "运单号") String trackingNumber) {
return logisticsService.trackShipment(trackingNumber);
}
private String formatQuote(ShippingQuote quote) {
return String.format("【%s】%s\n 运费:¥%s\n 预计送达:%s(%d天)",
quote.getLogisticsCompany(),
quote.getShippingMethod(),
quote.getCost(),
quote.getEstimatedDeliveryDate(),
quote.getEstimatedDays());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 客户工具
package com.example.agent.tools;
import com.example.agent.model.Customer;
import com.example.agent.service.CustomerService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
@Component
public class CustomerTools {
private final CustomerService customerService;
public CustomerTools(CustomerService customerService) {
this.customerService = customerService;
}
@Tool(name = "get_customer_info",
description = "获取客户基本信息和会员等级。")
public String getCustomerInfo(
@ToolParam(description = "客户ID") String customerId) {
return customerService.getCustomerById(customerId)
.map(this::formatCustomerInfo)
.orElse("未找到客户信息");
}
@Tool(name = "get_customer_by_phone",
description = "根据手机号查询客户信息。")
public String getCustomerByPhone(
@ToolParam(description = "手机号") String phone) {
return customerService.getCustomerByPhone(phone)
.map(this::formatCustomerInfo)
.orElse("未找到该手机号对应的客户");
}
@Tool(name = "authenticate_customer",
description = "客户登录验证。")
public String authenticateCustomer(
@ToolParam(description = "手机号") String phone,
@ToolParam(description = "密码") String password) {
return customerService.authenticate(phone, password)
.map(c -> "登录成功!欢迎回来," + c.getName())
.orElse("手机号或密码错误");
}
private String formatCustomerInfo(Customer customer) {
StringBuilder sb = new StringBuilder();
sb.append("客户ID:").append(customer.getCustomerId()).append("\n");
sb.append("姓名:").append(customer.getName()).append("\n");
sb.append("手机:").append(customer.getPhone()).append("\n");
sb.append("邮箱:").append(customer.getEmail()).append("\n");
sb.append("会员等级:").append(getLevelText(customer.getLevel())).append("\n");
sb.append("累计订单:").append(customer.getTotalOrders()).append("单\n");
sb.append("累计消费:¥").append(customer.getTotalSpent()).append("\n");
sb.append("默认地址:").append(customer.getDefaultAddress());
if (customer.getLevel() == CustomerLevel.GOLD ||
customer.getLevel() == CustomerLevel.PLATINUM) {
sb.append("\n⭐ 您是尊贵的").append(getLevelText(customer.getLevel()))
.append("会员,享受优先配送服务");
}
return sb.toString();
}
private String getLevelText(CustomerLevel level) {
return switch (level) {
case BRONZE -> "青铜";
case SILVER -> "白银";
case GOLD -> "黄金";
case PLATINUM -> "铂金";
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# Agent 核心引擎
Agent 核心引擎负责任务规划、工具调用编排和结果处理。我们将实现一个支持 ReAct 模式的 Agent。
# 对话上下文
package com.example.agent.agent;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ConversationContext implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 会话ID
*/
private String sessionId;
/**
* 客户ID
*/
private String customerId;
/**
* 对话历史
*/
@Builder.Default
private List<Message> messages = new ArrayList<>();
/**
* 工具调用历史
*/
@Builder.Default
private List<ToolCallRecord> toolCallHistory = new ArrayList<>();
/**
* 提取的上下文参数
*/
@Builder.Default
private Map<String, Object> extractedParams = new HashMap<>();
/**
* 当前意图
*/
private String currentIntent;
/**
* 会话开始时间
*/
private LocalDateTime startTime;
/**
* 最后活跃时间
*/
private LocalDateTime lastActiveTime;
/**
* 对话状态
*/
@Builder.Default
private ConversationState state = ConversationState.START;
public enum ConversationState {
START,
PARAM_EXTRACTION,
TOOL_EXECUTING,
COMPLETED,
ERROR
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Message implements Serializable {
private static final long serialVersionUID = 1L;
private String role; // user, assistant, system
private String content;
private LocalDateTime timestamp;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ToolCallRecord implements Serializable {
private static final long serialVersionUID = 1L;
private String toolName;
private Map<String, Object> parameters;
private String result;
private boolean success;
private LocalDateTime callTime;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# Agent 服务实现
package com.example.agent.agent;
import com.example.agent.agent.ConversationContext.Message;
import com.example.agent.agent.ConversationContext.ToolCallRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.ToolCallTracker;
import org.springframework.ai.model.chat.standard.ChatModelChatMemory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Service
public class SmartOrderAgent {
private static final Logger log = LoggerFactory.getLogger(SmartOrderAgent.class);
private final ChatClient chatClient;
private final ConversationMemory memory;
// 系统提示词
private static final String SYSTEM_PROMPT = """
你是智能订单助手,专门帮助用户处理订单相关业务。
## 你的能力
1. 查询订单状态和详情
2. 查询客户的订单历史
3. 检查商品库存情况
4. 计算运费和送达时间
5. 追踪物流信息
6. 取消订单
7. 修改配送地址
8. 查询客户信息和会员等级
## 工作流程
1. 理解用户意图
2. 从对话中提取必要的参数(订单号、客户ID、商品信息等)
3. 调用相应的工具完成操作
4. 将结果以友好的方式呈现给用户
## 重要规则
- 如果缺少必要参数,主动询问用户
- 调用工具后,将结果转化为人类可读的格式
- 如果工具执行失败,明确告知用户原因
- 保持友好、专业的服务态度
## 客户ID说明
- 如果用户未提供客户ID,可以使用默认客户ID:C001
- 也可以通过手机号查询客户信息
现在,请根据用户的需求提供帮助。
""";
public SmartOrderAgent(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem(SYSTEM_PROMPT)
.build();
this.memory = new InMemoryConversationMemory();
}
/**
* 处理用户请求
*/
public Mono<String> processRequest(String sessionId, String userMessage) {
// 获取或创建会话上下文
ConversationContext context = memory.getOrCreateSession(sessionId);
// 添加用户消息
context.getMessages().add(Message.builder()
.role("user")
.content(userMessage)
.timestamp(LocalDateTime.now())
.build());
context.setLastActiveTime(LocalDateTime.now());
log.info("处理会话 {},用户消息: {}", sessionId, userMessage);
// 调用 AI 处理
return chatClient.prompt()
.user(userMessage)
.tools() // 自动注册所有 @Tool bean
.stream()
.content()
.single()
.doOnNext(response -> {
// 记录 AI 响应
context.getMessages().add(Message.builder()
.role("assistant")
.content(response)
.timestamp(LocalDateTime.now())
.build());
})
.onErrorResume(e -> {
log.error("处理请求失败", e);
context.setState(ConversationContext.ConversationState.ERROR);
return Mono.just("抱歉,处理您的请求时发生错误:" + e.getMessage());
});
}
/**
* 处理用户请求(流式响应)
*/
public Flux<String> processRequestStream(String sessionId, String userMessage) {
ConversationContext context = memory.getOrCreateSession(sessionId);
context.getMessages().add(Message.builder()
.role("user")
.content(userMessage)
.timestamp(LocalDateTime.now())
.build());
context.setLastActiveTime(LocalDateTime.now());
log.info("流式处理会话 {},用户消息: {}", sessionId, userMessage);
return chatClient.prompt()
.user(userMessage)
.tools()
.stream()
.content()
.doOnNext(response -> {
// 可以在这里记录完整的响应
})
.doOnComplete(() -> {
log.info("会话 {} 流式响应完成", sessionId);
});
}
/**
* 获取会话历史
*/
public List<Message> getConversationHistory(String sessionId) {
ConversationContext context = memory.getSession(sessionId);
if (context == null) {
return Collections.emptyList();
}
return new ArrayList<>(context.getMessages());
}
/**
* 获取工具调用历史
*/
public List<ToolCallRecord> getToolCallHistory(String sessionId) {
ConversationContext context = memory.getSession(sessionId);
if (context == null) {
return Collections.emptyList();
}
return new ArrayList<>(context.getToolCallHistory());
}
/**
* 清理会话
*/
public void clearSession(String sessionId) {
memory.removeSession(sessionId);
}
/**
* 简单内存会话存储
*/
static class InMemoryConversationMemory implements ConversationMemory {
private final Map<String, ConversationContext> sessions = new ConcurrentHashMap<>();
private static final int MAX_SESSIONS = 1000;
@Override
public ConversationContext getOrCreateSession(String sessionId) {
return sessions.computeIfAbsent(sessionId, id -> ConversationContext.builder()
.sessionId(id)
.customerId("C001") // 默认客户
.startTime(LocalDateTime.now())
.lastActiveTime(LocalDateTime.now())
.build());
}
@Override
public ConversationContext getSession(String sessionId) {
return sessions.get(sessionId);
}
@Override
public void removeSession(String sessionId) {
sessions.remove(sessionId);
}
@Override
public void cleanupExpiredSessions(Duration maxAge) {
LocalDateTime expireTime = LocalDateTime.now().minus(maxAge);
sessions.entrySet().removeIf(entry ->
entry.getValue().getLastActiveTime().isBefore(expireTime));
}
}
/**
* 会话内存接口
*/
interface ConversationMemory {
ConversationContext getOrCreateSession(String sessionId);
ConversationContext getSession(String sessionId);
void removeSession(String sessionId);
void cleanupExpiredSessions(Duration maxAge);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# REST API 控制器
package com.example.agent.controller;
import com.example.agent.agent.ConversationContext;
import com.example.agent.agent.ConversationContext.Message;
import com.example.agent.agent.ConversationContext.ToolCallRecord;
import com.example.agent.agent.SmartOrderAgent;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/agent")
public class AgentController {
private final SmartOrderAgent agent;
public AgentController(SmartOrderAgent agent) {
this.agent = agent;
}
/**
* 发送消息(非流式)
*/
@PostMapping("/chat")
public Mono<Map<String, String>> chat(@RequestBody ChatRequest request) {
String sessionId = request.sessionId() != null ?
request.sessionId() : UUID.randomUUID().toString();
return agent.processRequest(sessionId, request.message())
.map(response -> Map.of(
"sessionId", sessionId,
"response", response
));
}
/**
* 发送消息(流式响应)
*/
@PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Map<String, String>> chatStream(@RequestBody ChatRequest request) {
String sessionId = request.sessionId() != null ?
request.sessionId() : UUID.randomUUID().toString();
return agent.processRequestStream(sessionId, request.message())
.map(chunk -> Map.of(
"sessionId", sessionId,
"chunk", chunk,
"done", "false"
))
.concatWith(Flux.just(Map.of(
"sessionId", sessionId,
"chunk", "",
"done", "true"
)));
}
/**
* 获取会话历史
*/
@GetMapping("/history/{sessionId}")
public List<Message> getHistory(@PathVariable String sessionId) {
return agent.getConversationHistory(sessionId);
}
/**
* 获取工具调用历史
*/
@GetMapping("/tools/{sessionId}")
public List<ToolCallRecord> getToolHistory(@PathVariable String sessionId) {
return agent.getToolCallHistory(sessionId);
}
/**
* 清理会话
*/
@DeleteMapping("/session/{sessionId}")
public Map<String, String> clearSession(@PathVariable String sessionId) {
agent.clearSession(sessionId);
return Map.of("message", "会话已清理");
}
/**
* 健康检查
*/
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "UP");
}
public record ChatRequest(String sessionId, String message) {}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# 高级特性
# 工具调用拦截器
实现工具调用的增强功能,如日志记录、参数验证、错误处理:
package com.example.agent.agent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.model.ToolCallExecutionContext;
import org.springframework.ai.chat.model.ToolCallTracker;
import org.springframework.stereotype.Component;
@Component
public class ToolCallLoggingTracker implements ToolCallTracker {
private static final Logger log = LoggerFactory.getLogger(ToolCallLoggingTracker.class);
@Override
public void trackToolCall(ToolCallExecutionContext context) {
log.info("工具调用 - 名称: {}, 参数: {}",
context.toolName(),
context.arguments());
context.onResult(result -> {
log.info("工具返回 - 名称: {}, 结果: {}",
context.toolName(),
truncate(result, 500));
});
context.onError(error -> {
log.error("工具执行失败 - 名称: {}, 错误: {}",
context.toolName(),
error.getMessage());
});
}
private String truncate(String str, int maxLength) {
if (str == null) return "";
return str.length() <= maxLength ? str :
str.substring(0, maxLength) + "...";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 重试策略
为工具调用添加重试机制:
package com.example.agent.agent;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import java.util.function.Supplier;
@Component
public class ToolRetryHandler {
@Retryable(
retryFor = Exception.class,
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public <T> T executeWithRetry(Supplier<T> action, String toolName) {
try {
return action.get();
} catch (Exception e) {
throw new ToolExecutionException("工具 " + toolName + " 执行失败: " + e.getMessage(), e);
}
}
@Recover
public String recoverFromError(ToolExecutionException e, String toolName) {
return "服务暂时不可用,请稍后重试";
}
public static class ToolExecutionException extends RuntimeException {
public ToolExecutionException(String message, Throwable cause) {
super(message, cause);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 可观测性集成
集成 Micrometer 和 OpenTelemetry 实现可观测性:
package com.example.agent.agent;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Component
public class AgentMetrics {
private final MeterRegistry registry;
private final ConcurrentHashMap<String, Counter> toolCounters = new ConcurrentHashMap<>();
private final Timer requestTimer;
public AgentMetrics(MeterRegistry registry) {
this.registry = registry;
this.requestTimer = Timer.builder("agent.request.duration")
.description("Agent 请求处理时间")
.register(registry);
}
public void recordToolCall(String toolName, long durationMs) {
toolCounters.computeIfAbsent(toolName, name ->
Counter.builder("agent.tool.calls")
.tag("tool", name)
.description("工具调用次数")
.register(registry)
).increment();
Timer.builder("agent.tool.duration")
.tag("tool", toolName)
.register(registry)
.record(durationMs, TimeUnit.MILLISECONDS);
}
public Timer.Sample startTimer() {
return Timer.start(registry);
}
public void recordRequest(Timer.Sample sample) {
sample.stop(requestTimer);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 测试验证
# 单元测试
package com.example.agent;
import com.example.agent.agent.SmartOrderAgent;
import com.example.agent.tools.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import reactor.test.StepVerifier;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class SmartOrderAgentTest {
@Autowired
private SmartOrderAgent agent;
@Autowired
private OrderTools orderTools;
@Autowired
private InventoryTools inventoryTools;
@Autowired
private CustomerTools customerTools;
@Test
void testQueryOrder() {
String sessionId = "test-session-1";
StepVerifier.create(agent.processRequest(sessionId, "查询订单 ORD20240315001 的状态"))
.assertNext(response -> {
assertNotNull(response);
assertTrue(response.contains("订单") || response.contains("查询"));
})
.verifyComplete();
}
@Test
void testQueryCustomerOrders() {
String sessionId = "test-session-2";
StepVerifier.create(agent.processRequest(sessionId, "查看我的所有订单"))
.assertNext(response -> {
assertNotNull(response);
// 验证工具被调用
assertTrue(response.contains("订单") || response.contains("未找到"));
})
.verifyComplete();
}
@Test
void testCheckInventory() {
String sessionId = "test-session-3";
StepVerifier.create(agent.processRequest(sessionId, "iPhone 15 Pro 有货吗"))
.assertNext(response -> {
assertNotNull(response);
assertTrue(response.contains("库存") || response.contains("商品"));
})
.verifyComplete();
}
@Test
void testCalculateShipping() {
String sessionId = "test-session-4";
StepVerifier.create(agent.processRequest(sessionId, "寄到北京要多少钱"))
.assertNext(response -> {
assertNotNull(response);
assertTrue(response.contains("运费") || response.contains("物流"));
})
.verifyComplete();
}
@Test
void testMultiTurnConversation() {
String sessionId = "test-session-5";
// 第一轮:查询客户信息
StepVerifier.create(agent.processRequest(sessionId, "你好,我想查一下我的会员等级"))
.assertNext(response -> {
assertNotNull(response);
})
.verifyComplete();
// 第二轮:基于上一轮上下文
StepVerifier.create(agent.processRequest(sessionId, "那我的订单有哪些"))
.assertNext(response -> {
assertNotNull(response);
})
.verifyComplete();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# 集成测试
package com.example.agent;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class AgentControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void testChatEndpoint() throws Exception {
String requestBody = """
{
"sessionId": "test-integration",
"message": "查询订单状态"
}
""";
mockMvc.perform(post("/api/agent/chat")
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isOk())
.andExpect(jsonPath("$.sessionId").exists())
.andExpect(jsonPath("$.response").exists());
}
@Test
void testStreamEndpoint() throws Exception {
String requestBody = """
{
"sessionId": "test-stream",
"message": "你好"
}
""";
mockMvc.perform(post("/api/agent/chat/stream")
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM));
}
@Test
void testHealthEndpoint() throws Exception {
mockMvc.perform(get("/api/agent/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 效果演示
# 场景一:查询订单状态
用户输入:
查询订单 ORD20240315001 的状态
Agent 执行流程:
- 识别用户意图:查询订单状态
- 提取参数:订单号 ORD20240315001
- 调用工具:
get_order_by_id(orderId="ORD20240315001") - 返回结果
输出示例:
订单号:ORD20240315001
订单状态:已发货
下单时间:2024-03-15T10:30:00
商品明细:
- iPhone 15 Pro x 1 = ¥8999.00
订单总价:¥8999.00
收货地址:北京市朝阳区建国路88号
收货人:张三 13800138000
物流公司:顺丰速运
运单号:SF1234567890
预计送达:2024-03-17
2
3
4
5
6
7
8
9
10
11
# 场景二:多轮对话
第一轮:
- 用户:我想买一个 iPhone 15 Pro
- Agent:好的,让我为您查询 iPhone 15 Pro 的信息...
- 调用
search_products(keyword="iPhone 15 Pro") - 返回商品详情和库存状态
- 调用
第二轮:
- 用户:广州发货的话几天能到?
- Agent:根据您选择的广州仓库,让我计算一下物流时间...
- 调用
calculate_shipping(destination="广州") - 返回多个物流方案
- 调用
第三轮:
- 用户:帮我下单一台
- Agent:好的,请确认以下订单信息:
- 商品:iPhone 15 Pro
- 价格:¥8999.00
- 预计送达:3天后
- 请提供收货地址
# 总结与展望
本文详细介绍了如何使用 Spring Boot 和 Spring AI 构建一个企业级智能订单助手 Agent。通过这个实战项目,我们覆盖了以下核心技术点:
- Agent 架构设计:分层架构清晰分离了展示层、Agent 核心层、工具层和外部服务层
- 工具系统:通过
@Tool注解优雅地定义和管理工具,支持订单、库存、物流、客户服务等多个领域 - 对话管理:实现了会话上下文管理和多轮对话能力
- 可观测性:集成了日志、指标和追踪功能
- 错误处理:实现了重试机制和优雅降级
# 进阶方向
- RAG 集成:结合向量数据库,实现商品知识库的语义检索
- MCP 扩展:使用 Model Context Protocol 连接更多外部系统
- 多 Agent 协作:构建 Agent 网络,实现复杂任务的分工协作
- 语音交互:集成语音识别和合成,实现语音对话
- 个性化推荐:基于用户历史行为,提供个性化商品推荐
通过 Spring AI,Java 开发者可以快速构建生产级的 AI 应用,将企业级应用的深厚积累与前沿 AI 能力完美结合。