创建一个基于Spring AI的智能旅行助手案例,展示Function Calling、LLM和AI Agent的综合应用。
项目概述
这个智能旅行助手可以:
- 查询天气信息
- 推荐旅行景点
- 计算两地距离
- 提供综合旅行建议
1. 项目依赖配置
xml
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0-M2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
2. 配置文件
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4
temperature: 0.7
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
3. Function定义和实现
天气查询Function
java
@Component
public class WeatherFunction implements Function<WeatherFunction.Request, WeatherFunction.Response> {
@JsonClassDescription("获取指定城市的天气信息")
public record Request(
@JsonProperty(required = true, value = "city")
@JsonPropertyDescription("城市名称")
String city
) {}
@JsonClassDescription("天气信息响应")
public record Response(
String city,
String temperature,
String condition,
String humidity,
String windSpeed
) {}
@Override
public Response apply(Request request) {
// 模拟天气API调用
return switch (request.city().toLowerCase()) {
case "北京" -> new Response("北京", "25°C", "晴朗", "45%", "15km/h");
case "上海" -> new Response("上海", "28°C", "多云", "60%", "12km/h");
case "广州" -> new Response("广州", "32°C", "雷阵雨", "80%", "8km/h");
default -> new Response(request.city(), "22°C", "未知", "50%", "10km/h");
};
}
}
景点推荐Function
java
@Component
public class AttractionFunction implements Function<AttractionFunction.Request, AttractionFunction.Response> {
@JsonClassDescription("获取指定城市的旅游景点推荐")
public record Request(
@JsonProperty(required = true, value = "city")
@JsonPropertyDescription("城市名称")
String city,
@JsonProperty(value = "category")
@JsonPropertyDescription("景点类型:历史文化、自然风光、现代娱乐")
String category
) {}
@JsonClassDescription("景点推荐响应")
public record Response(
String city,
List<Attraction> attractions
) {}
public record Attraction(
String name,
String description,
String rating,
String ticketPrice
) {}
@Override
public Response apply(Request request) {
List<Attraction> attractions = getAttractionsByCity(request.city(), request.category());
return new Response(request.city(), attractions);
}
private List<Attraction> getAttractionsByCity(String city, String category) {
Map<String, List<Attraction>> cityAttractions = Map.of(
"北京", List.of(
new Attraction("故宫博物院", "明清两代皇宫,世界文化遗产", "4.8", "60元"),
new Attraction("天安门广场", "世界最大城市广场之一", "4.7", "免费"),
new Attraction("长城", "世界七大奇迹之一", "4.9", "45元")
),
"上海", List.of(
new Attraction("外滩", "上海标志性景观带", "4.6", "免费"),
new Attraction("东方明珠", "上海地标性建筑", "4.5", "180元"),
new Attraction("豫园", "江南古典园林", "4.4", "40元")
)
);
return cityAttractions.getOrDefault(city,
List.of(new Attraction("当地特色景点", "待发现的美丽之地", "4.0", "未知")));
}
}
距离计算Function
java
@Component
public class DistanceFunction implements Function<DistanceFunction.Request, DistanceFunction.Response> {
@JsonClassDescription("计算两个城市之间的距离")
public record Request(
@JsonProperty(required = true, value = "from")
@JsonPropertyDescription("出发城市")
String from,
@JsonProperty(required = true, value = "to")
@JsonPropertyDescription("目标城市")
String to
) {}
@JsonClassDescription("距离计算响应")
public record Response(
String from,
String to,
String distance,
String travelTime
) {}
@Override
public Response apply(Request request) {
// 模拟距离计算
String key = request.from() + "-" + request.to();
Map<String, Response> distances = Map.of(
"北京-上海", new Response("北京", "上海", "1213公里", "高铁4.5小时"),
"上海-北京", new Response("上海", "北京", "1213公里", "高铁4.5小时"),
"北京-广州", new Response("北京", "广州", "2129公里", "高铁8小时"),
"广州-北京", new Response("广州", "北京", "2129公里", "高铁8小时")
);
return distances.getOrDefault(key,
new Response(request.from(), request.to(), "未知距离", "未知时间"));
}
}
4. AI Agent核心服务
java
@Service
public class TravelAgentService {
private final ChatClient chatClient;
public TravelAgentService(ChatClient.Builder chatClientBuilder,
WeatherFunction weatherFunction,
AttractionFunction attractionFunction,
DistanceFunction distanceFunction) {
this.chatClient = chatClientBuilder
.defaultSystem("""
你是一个专业的旅行助手,可以帮助用户规划旅行。
你有以下能力:
1. 查询城市天气信息
2. 推荐旅游景点
3. 计算城市间距离
请根据用户的需求,主动调用相关功能并提供综合性的旅行建议。
回答要友好、详细且实用。
""")
.defaultFunctions("weatherFunction", "attractionFunction", "distanceFunction")
.build();
}
public String planTravel(String userRequest) {
return chatClient.prompt()
.user(userRequest)
.call()
.content();
}
public Flux<String> planTravelStream(String userRequest) {
return chatClient.prompt()
.user(userRequest)
.stream()
.content();
}
}
5. 旅行记录实体和服务
java
@Entity
@Table(name = "travel_plans")
public class TravelPlan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String userId;
private String destination;
private String planContent;
private LocalDateTime createdAt;
// 构造函数、getter、setter省略
}
@Repository
public interface TravelPlanRepository extends JpaRepository<TravelPlan, Long> {
List<TravelPlan> findByUserId(String userId);
}
@Service
public class TravelPlanService {
private final TravelPlanRepository repository;
public TravelPlanService(TravelPlanRepository repository) {
this.repository = repository;
}
public TravelPlan savePlan(String userId, String destination, String content) {
TravelPlan plan = new TravelPlan();
plan.setUserId(userId);
plan.setDestination(destination);
plan.setPlanContent(content);
plan.setCreatedAt(LocalDateTime.now());
return repository.save(plan);
}
public List<TravelPlan> getUserPlans(String userId) {
return repository.findByUserId(userId);
}
}
6. REST API控制器
java
@RestController
@RequestMapping("/api/travel")
public class TravelController {
private final TravelAgentService travelAgentService;
private final TravelPlanService travelPlanService;
public TravelController(TravelAgentService travelAgentService,
TravelPlanService travelPlanService) {
this.travelAgentService = travelAgentService;
this.travelPlanService = travelPlanService;
}
@PostMapping("/plan")
public ResponseEntity<TravelPlanResponse> createTravelPlan(@RequestBody TravelPlanRequest request) {
String aiResponse = travelAgentService.planTravel(request.getUserRequest());
// 保存旅行计划
TravelPlan savedPlan = travelPlanService.savePlan(
request.getUserId(),
request.getDestination(),
aiResponse
);
return ResponseEntity.ok(new TravelPlanResponse(savedPlan.getId(), aiResponse));
}
@GetMapping("/plan/stream")
public ResponseEntity<StreamingResponseBody> createTravelPlanStream(
@RequestParam String userRequest) {
Flux<String> responseStream = travelAgentService.planTravelStream(userRequest);
StreamingResponseBody streamingBody = outputStream -> {
responseStream.doOnNext(chunk -> {
try {
outputStream.write(("data: " + chunk + "\n\n").getBytes());
outputStream.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}).blockLast();
};
return ResponseEntity.ok()
.header("Content-Type", "text/plain; charset=utf-8")
.body(streamingBody);
}
@GetMapping("/plans/{userId}")
public ResponseEntity<List<TravelPlan>> getUserPlans(@PathVariable String userId) {
return ResponseEntity.ok(travelPlanService.getUserPlans(userId));
}
}
// DTO类
@Data
class TravelPlanRequest {
private String userId;
private String destination;
private String userRequest;
}
@Data
@AllArgsConstructor
class TravelPlanResponse {
private Long planId;
private String content;
}
7. 使用示例
启动应用并测试
bash
# 1. 设置环境变量
export OPENAI_API_KEY=your_openai_api_key
# 2. 启动应用
mvn spring-boot:run
# 3. 测试API
curl -X POST http://localhost:8080/api/travel/plan \
-H "Content-Type: application/json" \
-d '{
"userId": "user123",
"destination": "北京",
"userRequest": "我想去北京旅游3天,请帮我制定详细的旅行计划,包括天气情况和景点推荐"
}'
预期AI响应示例
json
{
"planId": 1,
"content": "根据您的需求,我为您制定了北京3天旅行计划:\n\n**天气情况**:\n北京当前天气:25°C,晴朗,湿度45%,风速15km/h,非常适合旅游。\n\n**推荐景点**:\n1. 故宫博物院 - 明清两代皇宫,世界文化遗产(评分4.8,门票60元)\n2. 天安门广场 - 世界最大城市广场之一(评分4.7,免费参观)\n3. 长城 - 世界七大奇迹之一(评分4.9,门票45元)\n\n**3天行程安排**:\n第一天:上午游览天安门广场,下午参观故宫博物院\n第二天:长城一日游\n第三天:探索北京胡同文化,品尝地道美食\n\n建议携带舒适的步行鞋和防晒用品。祝您旅途愉快!"
}
8. 高级功能扩展
添加缓存支持
java
@Service
@EnableCaching
public class CachedTravelAgentService {
private final TravelAgentService travelAgentService;
@Cacheable(value = "travelPlans", key = "#userRequest")
public String getCachedTravelPlan(String userRequest) {
return travelAgentService.planTravel(userRequest);
}
}
添加异步处理
java
@Service
@Async
public class AsyncTravelService {
@Async
public CompletableFuture<String> generateTravelPlanAsync(String userRequest) {
// 异步处理旅行计划生成
return CompletableFuture.completedFuture("异步生成的旅行计划");
}
}
这个完整的案例展示了:
- Function Calling: 通过定义天气、景点、距离查询函数,让AI能够调用外部服务
- LLM集成: 使用Spring AI的ChatClient与OpenAI GPT模型交互
- AI Agent: 创建智能旅行助手,能够理解用户意图并主动调用相关功能
- 实际应用: 包含完整的Web API、数据持久化和流式响应
该系统可以根据用户需求自动规划旅行,体现了现代AI应用的实用价值。