2026 世界杯:一个通过与 CoCo 对话构建的实时应用 | 技术实践
2026 年,智能体将在企业级应用中取得哪些实质性突破?点击下载《2026 年 AI 与数据发展预测》白皮书,获悉专家一手前瞻,抢先拥抱新的工作方式!
我构建了一个 FIFA World Cup 2026 应用,它包含实时比分、淘汰赛对阵生成器、交锋统计、场馆地图、比赛预测等功能,而且整个过程几乎完全是通过与 Cortex Code Desktop(CoCo)的一系列自然语言对话完成的。
之所以花了几周时间,是因为一开始我完全没有任何样板代码;同时随着赛事推进,我也不断想把新的想法、更好的交互和更多实时统计数据迭代进来。
首页就是一个典型例子:它最初只是一个静态倒计时卡片,后来通过纯对话式开发,逐渐长成一个实时 dashboard。
一开始它只有四个指标卡片(距离开赛天数、剩余比赛场次、48 支球队、16 个场馆)和一个赛程 dataframe;
后来又加上了实时比赛卡片,以及带动画的统计 pills(控球率、射门、角球)和进球/黄牌/红牌事件标记;
接着加入了下一场比赛的 JS 倒计时,以及当多场比赛同时开踢时的多卡片堆叠显示;
然后加入了纵向淘汰赛 bracket,并叠加了以金色 badge 呈现的 AI 预测;
再后来加入了比赛进行中的实时胜率条,以及带赛事统计摘要的球队卡片;
最后,又加上了冠军庆祝卡片和 confetti 动效,用在 World Cup 2026 决赛结束之后。
其他页面也经历了类似过程,只是迭代次数可能略少,例如:
技术栈
GitHub → Streamlit Community Cloud (auto-deploy on push)
├── Snowflake
│ ├── TEAMS (48 teams, flags, FIFA rankings)
│ ├── VENUES (16 stadiums, lat/lng, capacity)
│ └── MATCHES (72 group stage fixtures)
├── ESPN Free API (live scores, no auth)
└── Snowflake Cortex AI_COMPLETE (match predictions)复制代码这个应用运行在 Streamlit Community Cloud 上,静态赛事数据存储在 Snowflake 中,实时比分和比赛统计来自 ESPN 的公开但未文档化的 API,而 AI 驱动的比赛预测则通过 Snowflake Cortex AI Functions 生成。
开发过程:AI 辅助迭代
从最初脚手架到最后的冠军庆祝卡片,每一个功能几乎都是通过和 CoCo 对话构建出来的。我只需要描述想要什么,然后在生成结果基础上继续迭代。
一个很典型的例子是 predictions.py 模块。这个模块有 200 多行代码,里面包含统计聚合、综合评分计算,以及对 Cortex AI Functions 的集成,而这些内容几乎是在一次对话中完成的。
ESPN 未文档化 API:踩过的一些坑
在使用这个 API 的过程中,我们遇到了几个很典型的问题。
100 条赛事截断
问题:当你查询 ?dates=20260611–20260719 这种覆盖整个赛事周期的时间范围时,ESPN 会悄悄把结果截断在大约 100 场比赛左右。我们是在赛事进行到中段时才发现这个问题,因为半决赛结果竟然“消失”了。
解决办法:把查询拆成两个日期区间。
@st.cache_data(ttl=60)
def get_all_results():
results = []
# Split to avoid ESPN's ~100 event cap
for date_range in ["20260611–20260627", "20260628-" + _today_str()]:
resp = requests.get(ESPN_API, params={"dates": date_range}, timeout=10)
# … normalize and collect
return results复制代码跨午夜边界丢比赛
问题:如果不传 dates 参数,ESPN 默认只返回当前美东时间自然日的数据。这意味着,一场在美东时间晚上 11:55 开始、并在午夜后仍处于补时阶段的比赛,会在跨天后直接从结果里消失。
解决办法:始终查询一个 3 天窗口。
et = pytz.timezone("US/Eastern")
now_et = datetime.now(et)
yesterday = (now_et - timedelta(days=1)).strftime("%Y%m%d")
tomorrow = (now_et + timedelta(days=1)).strftime("%Y%m%d")复制代码状态识别
ESPN 使用 6 种不同的进行中状态:STATUS_FIRST_HALF、STATUS_SECOND_HALF、STATUS_HALFTIME、STATUS_EXTRA_TIME、STATUS_PENALTY_SHOOTOUT、STATUS_IN_PROGRESS;还使用 3 种结束状态:STATUS_FULL_TIME、STATUS_FINAL_PEN、STATUS_FINAL_AET。
只要漏掉其中任何一个,淘汰赛 bracket 就会出错。
不用 WebSockets 也能做到实时更新
我们使用了 Streamlit 的 @st.fragment(run_every=N) 装饰器,它可以按固定间隔只重跑被装饰的函数,而无需整页刷新。
@st.fragment(run_every=10)
def _live_section():
live = get_live_matches()
if live:
for i, match in enumerate(live):
_render_live_match(match, i)复制代码再结合 API 调用上的 @st.cache_data(ttl=15),就能以极低的 ESPN API 负载实现接近实时的更新。实时比赛区域每 10 秒刷新一次,赛程/结果区域每 60 秒刷新一次,页面顶部 banner 也是每 60 秒刷新一次。
为了防止页面闪烁,我们还处理了一个额外问题:ESPN 偶尔会因为 CDN 抖动返回空结果。为了解决这个问题,我们把最近一次可用状态缓存在 st.session_state 里:
if live:
st.session_state["_last_live"] = live
elif "_last_live" in st.session_state:
live = st.session_state["_last_live"]复制代码使用 Snowflake Cortex AI 做比赛预测
预测系统分成两层。
第一层:统计评分
基于真实赛事表现数据计算加权综合评分:
rating = (
win_rate * 35 + # Points per game normalized
gd_norm * 20 + # Goal difference per game
poss_norm * 15 + # Average possession
sot_norm * 15 + # Shots on target per game
cs_rate * 15 # Clean sheet rate
)复制代码通过比较两支球队的评分,计算出对应的胜率百分比。这一过程完全基于确定性计算,速度很快,也不需要调用任何 API。
第二层:Cortex AI 推理
至于“为什么会得出这个预测”,我们会将两支球队汇总后的统计数据传入 Snowflake Cortex 的 AI_COMPLETE:
query = f"""
SELECT SNOWFLAKE.CORTEX.AI_COMPLETE(
'claude-sonnet-7',
'{prompt_escaped}'
) AS prediction复制代码Prompt 会输入两支球队完整的赛事统计数据,包括胜/平/负、进球、控球率、射正次数以及击败过的对手,并要求模型用不超过 15 个单词的一句话给出预测。
Based on these FIFA World Cup 2026 tournament stats, predict the winner
of France vs Spain in exactly one short sentence (max 15 words). Focus
on the key differentiator.
France: 4 played, 3W 1D 0L, 9 goals scored, 2 conceded, avg possession
57%, 4.8 shots on target/game, 2 clean sheets. Beat: Australia, Nigeria,
Colombia.
Spain: 4 played, 3W 0D 1L, 8 goals scored, 4 conceded, avg possession
62%, 5.2 shots on target/game, 1 clean sheets. Beat: Croatia, Japan,
Brazil.复制代码此外,这部分结果还设置了 5 分钟的 TTL 缓存,因此不会在每次页面加载时都重新触发模型调用。
实时胜率预测
比赛进行期间,另一套独立的 live_win_probability() 函数会根据实时赛况计算动态胜率,主要考虑以下因素:
比分: 每个进球大约会带来 18 个百分点的胜率变化。
比赛时间: 越接近比赛结束,当前比分的影响越大,预测结果也会越来越向实际赛果收敛。
控球率和射正次数: 作为较小幅度的修正因素,通常会带来约 ±5~8 个百分点的调整。
红牌: 会造成非常明显的胜率变化,每张红牌约带来 -15 个百分点的影响。
goal_diff = s1 - s2
if goal_diff == 0:
base1, base2 = 35, 35 # leaves 30% draw
elif goal_diff > 0:
shift = min(40, goal_diff * 18)
base1 = 50 + shift
base2 = max(5, 50 - shift - 15)复制代码这套胜率会与实时比分一起,每 10 秒更新一次。同时,系统还会生成一句由 Cortex 提供的推理说明,用来解释当前胜率为什么处于这一水平。这条推理结果会缓存 60 秒。
纵向淘汰赛对阵图:在 Streamlit 中使用 JavaScript
淘汰赛对阵图完全使用 HTML/JavaScript 实现,并通过 components.html() 进行渲染。这样既能支持点击选择球队的交互,也能实现自定义页面布局。
其中几个关键设计包括:
按轮次维护并行数组: 比赛日期按轮次分别存储,而不是使用以球队为 Key 的字典。因为如果同时存在多场“TBD vs TBD(待定球队 vs 待定球队)”,使用相同 Key 会发生冲突。
自动隐藏已经结束的轮次: 对阵图会自动聚焦接下来即将进行的比赛,避免页面被已经完成的赛事占满。
金色预测标签: 对于尚未进行的比赛,会显示国旗 Emoji 和对应的预测胜率百分比。
localStorage: 用于保存用户自己填写的淘汰赛对阵预测。
对阵图会自动读取 ESPN 的比赛结果,已经确定赛果的比赛会被锁定,无法再修改。在独立的 Bracket 对阵图页面中,可以通过 show_all_rounds 参数关闭自动隐藏,从而一次性查看完整的淘汰赛赛程。
部署流程:Git Push 即上线
整个部署流程实际上只有一句:
git push origin main
Streamlit Community Cloud 会持续监听这个代码仓库,一旦检测到更新,就会在几秒内自动重新部署。
就这么简单……
不需要 Docker,不需要 CI/CD,也不需要自己管理任何基础设施。
如果重新做一次,我会改进什么
我认为这个应用已经发展得相当完善,目前的状态也不错,不过永远都有继续优化的空间。以下是一些我想到的改进方向。
API 调用:相比直接依赖未公开文档的 API,我会考虑通过 Snowflake External Function 进行代理,并增加降级和重试机制。
复杂交互:如果使用支持双向消息传递的自定义 Streamlit Component,整体实现会更加干净。因为目前 iframe 中的 JavaScript 无法直接把信息传回 Streamlit,只能通过一些比较取巧的 window.parent DOM 操作来实现。
预测缓存:目前设置了 5 分钟 TTL,这意味着一场比赛刚刚结束后,下一轮比赛的预测不会立即更新。采用更智能的缓存失效机制,例如基于比赛结果数量生成 Hash,可以有所改善,但依然做不到完全实时。
代码重构:一个应用永远都还有继续重构的空间 :)
应用地址
可以体验这个在线应用,也欢迎分享给其他人。
可以在 GitHub 获取源代码,并在此基础上进行自己的定制开发。
还可以在 macOS 和 Windows 上开始使用 Cortex Code Desktop,构建属于你自己的应用。
保持联系
感谢你花时间读到这里。希望这篇博客既能让你有所收获,也能为你带来一些启发。
欢迎在 LinkedIn 上与我联系,获取更多 Demo、技术内容和产品更新。
一起学习,共同成长!

点击链接立即报名注册:Ascent-Snowflake Platform Training-China,更多 Snowflake 精彩活动请关注专区