第2266篇:出行和地图AI——路径规划和实时交通预测
第2266篇:出行和地图AI——路径规划和实时交通预测
适读人群:出行平台工程师、Java后端开发者、LBS技术团队 | 阅读时长:约14分钟 | 核心价值:深入讲解出行场景的路径规划算法、实时交通预测和个性化推荐的工程实现
在某出行平台做了两年后端,做过一个特别有意思的A/B实验。
我们做了一个"最快路线" vs "最稳路线"的对比测试。最快路线就是纯ETA最短的路,最稳路线是综合考虑了路段稳定性、不确定性低的路。A组用最快,B组用最稳。
结果出乎很多人预料:B组用户的实际到达时间与预估偏差更小(意料之中),但更关键的是,B组用户的差评率低了12%、复购率高了3%。
用户要的不只是最快,他们要的是"准"——你告诉我15分钟到,我就想15分钟到,不想被堵在路上骂娘。这个发现改变了我们对路径规划目标函数的理解:不是最小化ETA,而是最小化ETA方差,同时ETA满足用户期望。
这个业务洞察背后,是一套复杂的实时交通预测和不确定性量化工程。
出行AI系统架构
实时路况处理
浮动车数据处理
@Service
public class FloatingCarDataProcessor {
@Autowired
private KafkaConsumer<String, GpsPoint> gpsConsumer;
@Autowired
private RoadSegmentMapper segmentMapper;
@Autowired
private RealTimeSpeedRepository speedRepo;
/**
* 处理GPS轨迹点,实时更新路段速度
*/
@EventListener(ApplicationReadyEvent.class)
public void startProcessing() {
gpsConsumer.subscribe(List.of("float-car-gps"));
executorService.submit(() -> {
while (running) {
ConsumerRecords<String, GpsPoint> records = gpsConsumer.poll(Duration.ofMillis(100));
// 批量处理GPS点
Map<String, List<GpsPoint>> bySegment = new HashMap<>();
records.forEach(record -> {
GpsPoint point = record.value();
String segmentId = segmentMapper.mapToSegment(point.getLng(), point.getLat(),
point.getHeading());
if (segmentId != null) {
bySegment.computeIfAbsent(segmentId, k -> new ArrayList<>()).add(point);
}
});
// 更新路段速度
bySegment.forEach(this::updateSegmentSpeed);
}
});
}
private void updateSegmentSpeed(String segmentId, List<GpsPoint> points) {
if (points.size() < 2) return;
// 计算这批GPS点的平均速度
double avgSpeedKmh = calculateAverageSpeed(points);
// 过滤异常速度(可能是GPS漂移)
if (avgSpeedKmh < 0 || avgSpeedKmh > 200) return;
// 指数加权平均更新路段速度(α=0.3,新数据权重)
double currentSpeed = speedRepo.getCurrentSpeed(segmentId);
double updatedSpeed = currentSpeed * 0.7 + avgSpeedKmh * 0.3;
speedRepo.update(RoadSegmentSpeed.builder()
.segmentId(segmentId)
.speedKmh(updatedSpeed)
.sampleCount(points.size())
.updateTime(LocalDateTime.now())
.congestionLevel(CongestionLevel.fromSpeed(updatedSpeed))
.build());
}
private double calculateAverageSpeed(List<GpsPoint> points) {
double totalDistance = 0;
double totalTime = 0;
for (int i = 1; i < points.size(); i++) {
GpsPoint p1 = points.get(i-1);
GpsPoint p2 = points.get(i);
double distanceM = GeoUtils.haversineDistance(
p1.getLat(), p1.getLng(), p2.getLat(), p2.getLng()
);
double timeSec = (p2.getTimestamp() - p1.getTimestamp()) / 1000.0;
if (timeSec > 0 && distanceM < 500) { // 过滤跳变点
totalDistance += distanceM;
totalTime += timeSec;
}
}
return totalTime > 0 ? (totalDistance / 1000.0) / (totalTime / 3600.0) : 0;
}
}ETA预测服务
路段ETA预测
@Service
public class EtaPredictionService {
@Autowired
private RealTimeSpeedRepository speedRepo;
@Autowired
private ModelInferenceClient inferenceClient;
@Autowired
private HistoricalSpeedRepository historicalSpeedRepo;
/**
* 预测路径总ETA,返回期望值和置信区间
*/
public EtaResult predictPathEta(List<String> segmentIds, LocalDateTime departureTime) {
double totalMeanEta = 0.0;
double totalVariance = 0.0;
for (int i = 0; i < segmentIds.size(); i++) {
String segmentId = segmentIds.get(i);
// 预计到达该路段的时间(基于前面路段的ETA累积)
LocalDateTime estimatedArrivalAtSegment = departureTime.plusSeconds((long) totalMeanEta);
// 预测该路段的通行时间
SegmentEtaResult segmentEta = predictSegmentEta(segmentId, estimatedArrivalAtSegment);
totalMeanEta += segmentEta.getMeanSeconds();
totalVariance += segmentEta.getVarianceSeconds(); // 假设路段独立,方差可加
}
double totalStdDev = Math.sqrt(totalVariance);
return EtaResult.builder()
.meanSeconds((int) totalMeanEta)
.stdDevSeconds((int) totalStdDev)
.p10Seconds((int) (totalMeanEta - 1.28 * totalStdDev)) // 10th percentile
.p90Seconds((int) (totalMeanEta + 1.28 * totalStdDev)) // 90th percentile
.confidence(calculateConfidence(totalStdDev, totalMeanEta))
.build();
}
/**
* 单路段ETA预测——融合实时路况和历史规律
*/
private SegmentEtaResult predictSegmentEta(String segmentId,
LocalDateTime estimatedArrivalTime) {
RoadSegment segment = segmentRepo.findById(segmentId).orElseThrow();
RoadSegmentSpeed currentSpeed = speedRepo.getCurrentSpeed(segmentId);
// 1. 基于当前实时速度的估算
double realTimeEta = segment.getLengthMeters() / (currentSpeed.getSpeedKmh() / 3.6);
// 2. 历史同类时段的ETA统计
HistoricalStats historical = historicalSpeedRepo.getStats(
segmentId,
estimatedArrivalTime.getDayOfWeek(),
estimatedArrivalTime.getHour()
);
// 3. 模型预测(融合更多特征)
SegmentForecastRequest request = SegmentForecastRequest.builder()
.segmentId(segmentId)
.currentSpeedKmh(currentSpeed.getSpeedKmh())
.hourOfDay(estimatedArrivalTime.getHour())
.dayOfWeek(estimatedArrivalTime.getDayOfWeek().getValue())
.isHoliday(isHoliday(estimatedArrivalTime.toLocalDate()))
.upstreamCongestion(getUpstreamCongestionLevel(segmentId))
.historicalMeanSpeed(historical.getMeanSpeedKmh())
.build();
ModelPredictionOutput prediction = inferenceClient.predict("segment-eta-model-v3", request);
// 4. 融合三个来源的预测
double fusedEta = realTimeEta * 0.4
+ historical.getMeanEtaSeconds() * 0.2
+ prediction.getEtaSeconds() * 0.4;
// 5. 不确定性估计
double variance = Math.pow(historical.getStdDevSeconds(), 2) * 0.5
+ Math.pow(prediction.getEtaStdDevSeconds(), 2) * 0.5;
return SegmentEtaResult.builder()
.meanSeconds(fusedEta)
.varianceSeconds(variance)
.build();
}
}路径规划服务
多目标路径优化
@Service
public class RouteOptimizationService {
@Autowired
private RoadNetworkGraph roadGraph;
@Autowired
private EtaPredictionService etaService;
@Autowired
private OpenAIClient openAIClient;
/**
* 生成多候选路径(快速/稳定/经济/绿色)
*/
public RouteRecommendation findRoutes(RouteRequest request) {
Coordinate origin = request.getOrigin();
Coordinate destination = request.getDestination();
LocalDateTime departureTime = request.getDepartureTime();
UserPreferences preferences = request.getUserPreferences();
// 1. 搜索候选路径(A*算法,不同权重函数)
List<Path> fastestPaths = roadGraph.findTopKPaths(
origin, destination, 3,
WeightFunction.minimizeEta(etaService, departureTime)
);
List<Path> stablePaths = roadGraph.findTopKPaths(
origin, destination, 3,
WeightFunction.minimizeEtaVariance(etaService, departureTime)
);
// 2. 对所有候选路径计算完整ETA和不确定性
List<RouteCandidate> candidates = Stream.concat(
fastestPaths.stream().map(p -> RouteCandidate.of(p, RouteType.FASTEST)),
stablePaths.stream().map(p -> RouteCandidate.of(p, RouteType.STABLE))
).distinct()
.map(c -> enrichWithEta(c, departureTime))
.collect(Collectors.toList());
// 3. 去重(两种策略可能找到相同路径)
candidates = deduplicatePaths(candidates);
// 4. 个性化排序
candidates = personalizeRanking(candidates, preferences);
// 5. 生成推荐
return buildRecommendation(candidates, request);
}
private RouteCandidate enrichWithEta(RouteCandidate candidate, LocalDateTime departureTime) {
EtaResult eta = etaService.predictPathEta(
candidate.getPath().getSegmentIds(), departureTime
);
return candidate.withEta(eta);
}
private List<RouteCandidate> personalizeRanking(List<RouteCandidate> candidates,
UserPreferences preferences) {
return candidates.stream()
.sorted(Comparator.comparingDouble(c -> -computeScore(c, preferences)))
.collect(Collectors.toList());
}
private double computeScore(RouteCandidate candidate, UserPreferences preferences) {
// 综合评分:期望ETA + 用户偏好权重
double etaScore = 1.0 / (1 + candidate.getEta().getMeanSeconds() / 600.0); // 归一化
double stabilityScore = 1.0 / (1 + candidate.getEta().getStdDevSeconds() / 120.0);
double distanceScore = 1.0 / (1 + candidate.getPath().getDistanceMeters() / 5000.0);
// 根据用户历史偏好调整权重
double etaWeight = preferences.getEtaWeight(); // 默认0.5
double stabilityWeight = preferences.getStabilityWeight(); // 默认0.3
double distanceWeight = 1 - etaWeight - stabilityWeight; // 剩余0.2
return etaWeight * etaScore + stabilityWeight * stabilityScore + distanceWeight * distanceScore;
}
/**
* LLM生成路径描述——帮助用户理解路径特点
*/
public String generateRouteDescription(RouteCandidate primary, RouteCandidate alternative) {
if (alternative == null) {
return String.format("推荐路线,预计%s到达(误差±%s)",
formatDuration(primary.getEta().getMeanSeconds()),
formatDuration((int) primary.getEta().getStdDevSeconds()));
}
int etaDiff = primary.getEta().getMeanSeconds() - alternative.getEta().getMeanSeconds();
int varianceDiff = (int)(primary.getEta().getStdDevSeconds() - alternative.getEta().getStdDevSeconds());
String prompt = String.format("""
请用一句话描述这两条路径的对比,帮助用户做选择。
路径A(推荐):
- 预计%s到达(±%s误差)
- 路况:%s
路径B(备选):
- 比路径A%s%s
- 误差%s路径A%s
语言要口语化、简洁,像导航app的描述风格。不超过30字。
""",
formatDuration(primary.getEta().getMeanSeconds()),
formatDuration((int) primary.getEta().getStdDevSeconds()),
primary.getPath().getConditionSummary(),
etaDiff > 0 ? "快" : "慢",
formatDuration(Math.abs(etaDiff)),
varianceDiff < 0 ? "小于" : "大于",
formatDuration(Math.abs(varianceDiff))
);
return callLLM(prompt, "gpt-4o-mini");
}
}用户偏好学习
@Service
public class UserPreferenceLearningService {
@Autowired
private TripHistoryRepository tripRepo;
/**
* 从历史行程中学习用户偏好
* 用户实际选择了哪条路,就是偏好的真实信号
*/
public UserPreferences learnPreferences(String userId) {
// 近90天的路径选择历史
List<TripDecision> decisions = tripRepo.findByUserIdAndDateRange(
userId, LocalDate.now().minusDays(90), LocalDate.now()
);
if (decisions.size() < 5) {
return UserPreferences.defaultPreferences(); // 数据不足,使用默认值
}
// 统计用户在有多路径选择时的偏好
int choosesFastest = 0;
int choosesStable = 0;
int totalChoices = 0;
for (TripDecision decision : decisions) {
if (decision.getAlternativeRoutesPresented() > 1) {
totalChoices++;
if (decision.getChosenRouteType() == RouteType.FASTEST) {
choosesFastest++;
} else if (decision.getChosenRouteType() == RouteType.STABLE) {
choosesStable++;
}
}
}
if (totalChoices == 0) return UserPreferences.defaultPreferences();
double fastestRate = (double) choosesFastest / totalChoices;
double stableRate = (double) choosesStable / totalChoices;
return UserPreferences.builder()
.etaWeight(0.3 + fastestRate * 0.4) // 越偏好快速,ETA权重越高
.stabilityWeight(0.2 + stableRate * 0.3) // 越偏好稳定,稳定性权重越高
.updatedAt(LocalDateTime.now())
.build();
}
}出行AI工程经验
1. ETA准确性是核心竞争力。用户对导航的容忍度很低,迟到5分钟会差评,但"预计20分钟,实际18分钟"反而会让用户感觉惊喜。不确定性量化(置信区间)可以帮助给出保守预估,提升用户满意度。
2. 实时数据延迟是隐藏问题。GPS数据从车辆发出到服务器处理再到路段速度更新,有不可避免的延迟(通常5-15秒)。在高速变化的路况下,延迟数据可能比历史均值还不准。要在模型里做延迟补偿。
3. 特殊场景需要特殊处理。大型活动散场、高考、春节高峰——这些场景的路况规律与日常完全不同,普通模型严重低估拥堵。必须建立事件感知机制,在这些场景下切换到专用模型或调整参数。
4. 边界情况处理决定用户体验。起点或终点在偏远地区、路网数据缺失、临时施工改道——这些边界情况比主流场景更考验系统健壮性。做出行AI,花在边界情况处理上的时间,往往比核心算法还多。
