Files
SuperBizAgent-java/knowledge_base/domain/spring-ai-tool-best-practices.md

260 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Spring AI 工具定义最佳实践
keywords: [Spring AI, Tool, 工具定义, Agent, 函数调用]
summary: 如何为 Spring AI Agent 定义高质量的工具(Tool),包括命名、描述、参数设计和错误处理
category: domain
---
# Spring AI 工具定义最佳实践
## 工具定义基础
### 基本注解
```java
@Component
public class MyTools {
@Tool(description = "查询用户信息。参数 userId: 用户ID(必填)")
public UserInfo getUserInfo(String userId) {
// 实现
}
}
```
### 关键要素
1. **@Component** - 让 Spring 扫描到
2. **@Tool** - 标记为 Agent 可调用的工具
3. **description** - 告诉 Agent 这个工具做什么
## 描述(Description)编写规范
### 好的描述
```java
@Tool(description = "查询知识库文档。优先精确匹配关键词,未命中或多个匹配时自动补充语义相关片段。" +
"参数 query: 查询关键词,例如 'ERR_TIMEOUT'、'支付网关超时'")
public LookupResult lookupKnowledge(String query) { ... }
```
**要点**:
- ✅ 说明工具用途(查询知识库)
- ✅ 说明工作机制(精确匹配 → 语义补充)
- ✅ 说明参数含义和示例
### 差的描述
```java
@Tool(description = "查询文档") // ❌ 太简略
public LookupResult lookup(String q) { ... }
```
## 参数设计
### 参数命名
```java
// ✅ 好的命名 - 语义清晰
public Result search(String query, int maxResults, String category)
// ❌ 差的命名 - 缩写难懂
public Result search(String q, int max, String cat)
```
### 参数类型
```java
// ✅ 使用明确的类型
public UserInfo getUser(String userId)
public List<Order> getOrders(LocalDate startDate, LocalDate endDate)
// ❌ 使用 Object 或 Map
public Object getUser(Map<String, Object> params) // Agent 不知道传什么
```
### 可选参数处理
```java
@Tool(description = "查询订单。参数 status: 订单状态(可选,不传则查所有)")
public List<Order> getOrders(
@Nullable String status // 使用 @Nullable 标注
) {
if (status == null) {
return orderRepository.findAll();
}
return orderRepository.findByStatus(status);
}
```
## 返回值设计
### 使用明确的返回类型
```java
// ✅ 好的返回类型
public class LookupResult {
private boolean found;
private PrimaryResult primary;
private SupplementResult supplement;
}
// ❌ 返回 String - Agent 难以解析
public String lookup(String query) {
return "找到文档: xxx"; // 非结构化
}
```
### 返回错误信息
```java
public LookupResult lookup(String query) {
if (query == null || query.isEmpty()) {
return LookupResult.builder()
.found(false)
.error("查询关键词不能为空")
.build();
}
// 正常逻辑
}
```
## 错误处理
### 优雅降级
```java
@Tool(description = "查询用户信息")
public UserInfo getUser(String userId) {
try {
return userService.findById(userId);
} catch (UserNotFoundException e) {
log.warn("用户不存在: userId={}", userId);
return UserInfo.notFound(userId); // 返回特殊对象,不抛异常
} catch (Exception e) {
log.error("查询用户失败: userId={}", userId, e);
return UserInfo.error("系统错误,请稍后重试");
}
}
```
### 不要抛出未捕获的异常
```java
// ❌ 不要这样做
@Tool(description = "查询用户")
public UserInfo getUser(String userId) {
return userService.findById(userId); // 可能抛出异常,Agent 无法处理
}
```
## 可观测性
### 日志规范
```java
@Tool(description = "查询订单")
public List<Order> getOrders(String userId) {
String requestId = UUID.randomUUID().toString().substring(0, 8);
long startTime = System.currentTimeMillis();
log.info("[{}] 收到订单查询请求: userId={}", requestId, userId);
try {
List<Order> orders = orderService.findByUserId(userId);
long elapsed = System.currentTimeMillis() - startTime;
log.info("[{}] 查询完成: count={}, time={}ms", requestId, orders.size(), elapsed);
return orders;
} catch (Exception e) {
log.error("[{}] 查询失败: userId={}", requestId, userId, e);
throw e;
}
}
```
## 性能优化
### 设置合理的超时
```java
@Tool(description = "查询大数据集")
public DataResult queryBigData(String query) {
// 设置超时保护
return CompletableFuture
.supplyAsync(() -> heavyQuery(query))
.orTimeout(5, TimeUnit.SECONDS)
.exceptionally(ex -> DataResult.timeout())
.join();
}
```
### 避免返回超大数据
```java
// ✅ 分页或限制数量
@Tool(description = "查询用户列表(最多返回 100 条)")
public List<User> listUsers(int page, int size) {
size = Math.min(size, 100); // 强制上限
return userService.findAll(PageRequest.of(page, size));
}
// ❌ 返回全量数据
public List<User> listAllUsers() {
return userService.findAll(); // 可能几万条
}
```
## 工具组合示例
### 查询 + 操作的组合
```java
@Component
public class OrderTools {
@Tool(description = "查询订单详情")
public OrderDetail getOrder(String orderId) { ... }
@Tool(description = "取消订单")
public CancelResult cancelOrder(String orderId, String reason) { ... }
@Tool(description = "申请退款")
public RefundResult refund(String orderId, Double amount) { ... }
}
```
**Agent 使用场景**:
1. 用户:"帮我查一下订单 12345"
2. Agent 调用 `getOrder("12345")`
3. 用户:"帮我取消这个订单"
4. Agent 调用 `cancelOrder("12345", "用户主动取消")`
## 常见陷阱
### ❌ 工具做太多事
```java
// 不要把整个业务流程塞进一个工具
@Tool(description = "处理订单")
public void processOrder(String orderId) {
// 查询订单
// 验证库存
// 扣减库存
// 创建物流单
// 发送通知
// ... 太多步骤,Agent 无法介入
}
```
### ✅ 拆分成多个工具
```java
@Tool(description = "查询订单")
public Order getOrder(String orderId) { ... }
@Tool(description = "验证库存")
public StockResult checkStock(String productId, int quantity) { ... }
@Tool(description = "创建物流单")
public ShipmentResult createShipment(String orderId) { ... }
```
### ❌ 描述不准确
```java
@Tool(description = "查询用户")
public UserInfo getUser(String query) {
// 实际上支持按 userId、email、手机号查询
// 但描述没说清楚,Agent 不知道
}
```
### ✅ 描述完整
```java
@Tool(description = "查询用户信息。支持按 userId、email 或手机号查询。" +
"参数 query: 用户ID、邮箱或手机号")
public UserInfo getUser(String query) { ... }
```