第2268篇:旅游行业AI——行程规划和个性化推荐系统
第2268篇:旅游行业AI——行程规划和个性化推荐系统
适读人群:旅游平台工程师、Java后端开发者、OTA技术团队 | 阅读时长:约14分钟 | 核心价值:从旅游行业的真实挑战出发,实现智能行程规划、个性化目的地推荐和动态行程调整的工程方案
在某旅游平台做过一段时间,那时候有一个让我百思不得其解的数据:用户进入"行程规划"页面的转化率只有12%,但调研显示用户非常想要个性化行程规划这个功能。
为什么转化率这么低?深入分析后发现,用户在规划页面的平均停留时间只有47秒,然后就离开了。原因是什么?规划工具太复杂了——让用户自己选景点、排顺序、查交通、算时间,对大多数不是旅游达人的普通用户来说,这不是"帮助",这是"工作"。
用户要的是:告诉你我什么时候去、多少天、什么类型的玩法,你直接给我一个可以出发的行程单。
LLM出现后,这个需求才有了真正的解决方案。
旅游AI系统架构
智能行程规划
需求理解和行程生成
@Service
public class TravelPlanningService {
@Autowired
private OpenAIClient openAIClient;
@Autowired
private AttractionKnowledgeService attractionService;
@Autowired
private RouteOptimizationService routeService;
@Autowired
private UserTravelProfileService profileService;
@Autowired
private VectorStore attractionVectorStore;
/**
* 智能行程规划——多轮对话式
*/
public TravelPlanningSession startPlanning(TravelPlanningRequest request) {
String userId = request.getUserId();
String sessionId = UUID.randomUUID().toString();
// 1. 获取用户旅游偏好画像
UserTravelProfile profile = profileService.getProfile(userId);
// 2. 解析用户原始需求
TravelRequirement requirement = parseTravelRequirement(request.getUserInput(), profile);
// 3. 检索相关景点信息
List<Attraction> relevantAttractions = retrieveRelevantAttractions(requirement);
// 4. 生成初版行程
TravelItinerary itinerary = generateItinerary(requirement, relevantAttractions, profile);
// 5. 验证行程可行性
ItineraryValidationResult validation = validateItinerary(itinerary);
if (!validation.isValid()) {
itinerary = adjustItinerary(itinerary, validation.getIssues());
}
// 保存会话
TravelPlanningSession session = TravelPlanningSession.builder()
.sessionId(sessionId)
.userId(userId)
.requirement(requirement)
.currentItinerary(itinerary)
.status(PlanningStatus.DRAFT)
.build();
sessionRepository.save(session);
return session;
}
/**
* 解析用户原始输入,提取结构化需求
*/
private TravelRequirement parseTravelRequirement(String userInput,
UserTravelProfile profile) {
String profileContext = profile != null ? String.format(
"该用户历史偏好:%s,通常预算:%s元/天,偏爱%s类型旅游",
profile.getPreferencesSummary(), profile.getTypicalBudgetPerDay(),
profile.getTravelStyleSummary()
) : "新用户,无历史偏好数据";
String prompt = String.format("""
请从用户输入中提取旅游需求,返回JSON格式:
用户历史:%s
用户输入:%s
提取字段:
{
"destination": "目的地城市/地区",
"start_date": "出发日期(YYYY-MM-DD,若用户说'下周五'则计算具体日期)",
"end_date": "返回日期",
"days": 旅游天数,
"adults": 成人人数,
"children": 儿童人数(默认0),
"travel_style": ["文化历史/自然风景/美食/购物/亲子/休闲/探险"],
"budget_level": "经济/舒适/豪华",
"special_requirements": ["特殊要求,如无障碍/素食/宠物友好"],
"must_see": ["用户明确提到要去的地方"],
"avoid": ["用户明确说不想去的地方"],
"ambiguous_points": ["需要进一步确认的点"]
}
今天是%s,如果用户说"下周"请按此计算。
""",
profileContext,
userInput,
LocalDate.now()
);
String jsonResult = callLLMWithJson(prompt, "gpt-4o");
return JsonUtils.parseObject(jsonResult, TravelRequirement.class);
}
/**
* 检索相关景点(RAG)
*/
private List<Attraction> retrieveRelevantAttractions(TravelRequirement requirement) {
// 构建检索查询
String query = String.format("%s %s %s",
requirement.getDestination(),
String.join(" ", requirement.getTravelStyle()),
requirement.getMustSee() != null ? String.join(" ", requirement.getMustSee()) : ""
);
float[] queryVector = embeddingService.embed(query);
List<AttractionChunk> chunks = attractionVectorStore.search(
queryVector,
SearchParams.builder()
.topK(20)
.minSimilarity(0.75)
.filter("destination", requirement.getDestination())
.build()
);
// 去重并按评分排序
return chunks.stream()
.map(chunk -> attractionService.findById(chunk.getAttractionId()))
.filter(Objects::nonNull)
.distinct()
.sorted(Comparator.comparingDouble(Attraction::getCompositeScore).reversed())
.limit(15)
.collect(Collectors.toList());
}
/**
* 生成行程规划
*/
private TravelItinerary generateItinerary(TravelRequirement requirement,
List<Attraction> attractions,
UserTravelProfile profile) {
String attractionsInfo = attractions.stream()
.map(a -> String.format("- %s(评分%.1f,票价约%d元,建议游览%s小时,%s,地址:%s)",
a.getName(), a.getRating(), a.getTicketPrice(),
a.getRecommendedDurationHours(), a.getBriefDescription(), a.getAddress()))
.collect(Collectors.joining("\n"));
String prompt = String.format("""
请为用户制定详细的旅游行程。
旅游信息:
- 目的地:%s
- 日期:%s 至 %s(共%d天)
- 人数:%d人(含%d名儿童)
- 旅游风格:%s
- 预算级别:%s
- 必去景点:%s
- 特殊要求:%s
可选景点信息:
%s
规划要求:
1. 按地理位置合理安排,避免来回折腾
2. 每天安排合理,不要太累(一般2-3个主要景点+1个备选)
3. 考虑景点营业时间(大多数景点9:00-17:00)
4. 安排好餐厅(结合景点附近的特色美食)
5. 预留自由时间和休息时间
6. 第一天下午从酒店出发,最后一天下午预留退房时间
返回JSON:
{
"summary": "行程总体描述",
"days": [
{
"day": 1,
"date": "YYYY-MM-DD",
"theme": "当天主题",
"schedule": [
{"time": "09:00", "type": "attraction/meal/transport/hotel",
"name": "名称", "duration_hours": 2, "note": "小贴士",
"estimated_cost": 150}
],
"accommodation": "酒店建议",
"daily_estimated_cost": 800
}
],
"total_estimated_cost": 3000,
"tips": ["实用小贴士1", "实用小贴士2"],
"alternatives": ["备选方案建议"]
}
""",
requirement.getDestination(),
requirement.getStartDate(), requirement.getEndDate(), requirement.getDays(),
requirement.getAdults(), requirement.getChildren(),
String.join("、", requirement.getTravelStyle()),
requirement.getBudgetLevel(),
requirement.getMustSee() != null ? String.join("、", requirement.getMustSee()) : "无",
requirement.getSpecialRequirements() != null
? String.join("、", requirement.getSpecialRequirements()) : "无",
attractionsInfo
);
String jsonResult = callLLMWithJson(prompt, "gpt-4o");
return buildItinerary(JsonUtils.parseObject(jsonResult, ItineraryDraft.class), requirement);
}
}对话式行程调整
@Service
public class ItineraryAdjustmentService {
@Autowired
private TravelPlanningService planningService;
@Autowired
private OpenAIClient openAIClient;
/**
* 处理用户对行程的修改请求
* 例如:把第二天的故宫换成颐和园、第一天下午加一个购物安排
*/
public TravelPlanningSession adjustItinerary(String sessionId, String userMessage) {
TravelPlanningSession session = sessionRepository.findById(sessionId).orElseThrow();
TravelItinerary currentItinerary = session.getCurrentItinerary();
// 理解用户修改意图
AdjustmentIntent intent = parseAdjustmentIntent(userMessage, currentItinerary);
TravelItinerary adjustedItinerary;
switch (intent.getType()) {
case REPLACE_ATTRACTION:
adjustedItinerary = replaceAttraction(
currentItinerary, intent.getTargetAttractionId(), intent.getReplacementQuery()
);
break;
case ADD_ACTIVITY:
adjustedItinerary = addActivity(
currentItinerary, intent.getTargetDay(), intent.getActivityDescription()
);
break;
case CHANGE_PACE:
adjustedItinerary = adjustPace(
currentItinerary, intent.getPaceChange()
);
break;
default:
// 复杂修改,让LLM直接重新规划部分行程
adjustedItinerary = regeneratePartialItinerary(currentItinerary, userMessage);
}
// 验证调整后的行程
ItineraryValidationResult validation = planningService.validateItinerary(adjustedItinerary);
String responseMessage = buildResponseMessage(adjustedItinerary, validation, intent);
// 更新会话
session.setCurrentItinerary(adjustedItinerary);
session.addConversationTurn(ConversationTurn.assistant(responseMessage));
sessionRepository.save(session);
return session;
}
/**
* 行程可行性验证
*/
public ItineraryValidationResult validateItinerary(TravelItinerary itinerary) {
List<ItineraryIssue> issues = new ArrayList<>();
for (ItineraryDay day : itinerary.getDays()) {
// 检查时间安排是否合理
validateDaySchedule(day, issues);
// 检查景点营业时间
for (ScheduleItem item : day.getSchedule()) {
if (item.getType() == ScheduleItemType.ATTRACTION) {
Attraction attraction = attractionService.findById(item.getAttractionId());
if (attraction != null && !attraction.isOpenAt(day.getDate(), item.getStartTime())) {
issues.add(ItineraryIssue.builder()
.day(day.getDay())
.itemName(item.getName())
.issueType(IssueType.CLOSED_AT_TIME)
.description(String.format("%s在%s可能未开放,建议调整时间",
item.getName(), item.getStartTime()))
.build());
}
}
}
}
return ItineraryValidationResult.builder()
.valid(issues.isEmpty())
.issues(issues)
.build();
}
}旅游推荐和目的地发现
@Service
public class DestinationRecommendationService {
@Autowired
private OpenAIClient openAIClient;
@Autowired
private UserTravelProfileService profileService;
/**
* 个性化目的地推荐
*/
public DestinationRecommendation recommendDestinations(String userId,
TravelSearchContext context) {
UserTravelProfile profile = profileService.getProfile(userId);
String prompt = String.format("""
请根据用户偏好推荐5个旅游目的地。
用户偏好:
- 旅游风格:%s
- 去过的地方:%s
- 预算水平:%s
- 出发城市:%s
- 假期时长:%d天
推荐要求:
1. 根据季节推荐(现在是%s)
2. 考虑从出发城市的交通便利性
3. 避免推荐用户已去过的地方
4. 包含1-2个"意想不到"的小众目的地
返回JSON:
{
"recommendations": [
{
"destination": "目的地名称",
"province": "省份",
"highlight": "15字内核心吸引力",
"best_for": ["适合人群标签"],
"travel_days": 推荐天数,
"estimated_budget": 人均预算元,
"transport_from_origin": "从出发城市的交通方式和时间",
"best_season": "最佳季节",
"why_recommend": "推荐理由(结合用户偏好)"
}
]
}
""",
profile != null ? profile.getTravelStyleSummary() : "未知",
profile != null ? String.join("、", profile.getVisitedDestinations()) : "暂无记录",
context.getBudgetLevel() != null ? context.getBudgetLevel().getDescription() : "中等",
context.getDepartureCity(),
context.getAvailableDays(),
LocalDate.now().getMonth().getDisplayName(TextStyle.FULL, Locale.CHINESE)
);
String jsonResult = callLLMWithJson(prompt, "gpt-4o");
return JsonUtils.parseObject(jsonResult, DestinationRecommendation.class);
}
}旅游AI工程经验
1. 景点知识库的实时性很关键。景区票价、营业时间、临时关闭通知这些信息变化频繁。知识库要有自动更新机制,至少每周同步一次官网和OTA平台的最新信息,否则生成的行程会有错误。
2. 行程规划是个约束满足问题。景点之间的距离、游览时长、开放时间、预算上限——这些约束组合在一起,比看起来复杂得多。纯粹依赖LLM往往忽略这些约束,需要加规则引擎做验证和修正。
3. 冷启动和新用户处理。旅游不像外卖每天有数据,很多用户可能一年才旅游2-3次,历史数据稀疏。要充分利用注册信息、浏览行为和实时输入来做即时个性化。
4. 行程规划的商业闭环。规划功能要与预订功能打通,用户确认行程后,直接可以预订里面推荐的酒店和门票。规划到预订的转化率是核心业务指标,设计时要降低这个路径的摩擦。
5. 长行程的复杂度管理。7天以上的长线行程,如果全部靠LLM一次性生成,质量会下降。推荐分段生成:先确定大框架,再逐天细化。这样每次生成的文本量小,质量更可控。
