过去六个月,一位开发者构建了一个专注于赛马预测的AI系统。这并非简单的“历史结果→预测”模型,而是一个多阶段的流水线:从数据质量管理、特征工程,到Claude推理,最终生成排序推荐。
以下是他从中获得的经验。
架构概览
该AI预测系统的核心架构如下:
- 数据抓取:
netkeiba scrape - 数据存储:
PostgreSQL (horse_races / horse_entries) - 每日批处理:
fetch_horse_racing.py - AI推理接口:
Supabase Edge Function (ai-hub: horse.predict) - 大模型推理:
Claude haiku - 结果整合:
horse_race_predictions_ensemble - 准确性评估:
evaluate_accuracy.ts (每周评估)
数据质量分数 (DQS)
预测准确性很大程度上取决于数据质量。作者为15个字段计算了数据质量分数(DQS,范围0-100),部分计算逻辑示例如下:
( CASE WHEN weight IS NOT NULL THEN 10 ELSE 0 END + CASE WHEN weight_diff IS NOT NULL THEN 10 ELSE 0 END + CASE WHEN last_3f IS NOT NULL THEN 15 ELSE 0 END + CASE WHEN prev_last_3f IS NOT NULL THEN 10 ELSE 0 END + CASE WHEN jockey_id IS NOT NULL THEN 10 ELSE 0 END + CASE WHEN trainer_id IS NOT NULL THEN 10 ELSE 0 END + CASE WHEN odds IS NOT NULL THEN 15 ELSE 0 END -- + 8 more fields...) AS data_quality_score任何DQS低于60的条目都会被跳过。作者指出,这个简单的过滤条件对预测准确性的提升,甚至超过了任何模型本身的改变。
特征工程:排名分数
作者根据经验贡献度,对八个因素进行了加权处理,以生成排名分数。关键因素及其权重如下:
- 历史名次率:25% (最稳定的信号)
- 最后3F时间 (
last_3f):20% (后期速度是重要预测指标) - 赔率倒数:15% (市场智慧的体现)
- 骑师胜率:15% (骑师效应真实存在)
- 体重变化:10% (状态信号)
- 最后3F与前一场比赛的对比:10% (动量趋势)
- 最佳时间记录:5% (能力上限指标)
Claude推理提示词
作者使用Claude来生成解释,而不仅仅是分数。以下是其提示词的结构:
const prompt = `您是一位赛马预测专家。[RACE INFORMATION]<<>>${raceInfo}<<>>[HORSE DATA]<<>>${horseData}<<>>请综合考虑以下因素,推荐前3匹马:1. 优先考虑DQS >= 70的马匹。2. 强调最佳时间记录和最后3F表现。3. 将体重变化 ±10kg 标记为风险因素。4. 每条推荐解释不超过100个字符。输出格式:JSON`; 提示词中的<<块用于保护系统,防止从抓取到的赛事数据中注入提示词攻击(prompt injection)。
解决N+1查询问题
最初的实现存在N+1查询问题:每次评估运行时,针对50场比赛,会产生2次查询×50场=100次查询。
// 优化前:N+1查询for (const race of races) { // query for race details // query for horse entries}为了解决这个问题,作者利用PostgreSQL的WITH RECURSIVE CTE(公用表表达式)和JSONB_AGG函数,将多次查询优化为一次高效的查询:
-- 优化后:单次查询WITH race_data AS ( SELECT r.id AS race_id, JSONB_BUILD_OBJECT( 'race_details', r, 'horse_entries', ( SELECT JSONB_AGG(he) FROM horse_entries he WHERE he.race_id = r.id ) ) AS data FROM races r WHERE r.date = '2023-10-26')SELECT JSONB_AGG(data) FROM race_data;这种方法通过一次数据库往返,就能获取所有比赛及其参赛马匹的详细信息,显著提升了数据获取效率。