7月24日、Codeminer42が「I built an AI agent for our World Cup betting pool. She beat all of us.」と題した記事を公開した。ワールドカップ予想プールに参加させたAIエージェントが全参加者を抑えて優勝した実装経験を通じ、長期稼働エージェントにおけるメモリとコンテキスト設計の具体的な知見が紹介されている。
AIが月間大会を制した
370ポイント、2位に50点差をつけての優勝。 しかも2位は、そのAIを作った本人だった。
ブラジルのソフトウェア開発チームが内輪で楽しむワールドカップ予想プール「BEThoven」。16人のメンバーがSSH経由でターミナルからスコアを入力する仕組みで、賞金は一切なし――勝者に与えられるのは「2030年まで自慢する権利」だけだ(次回FIFAワールドカップは2030年開催予定)。
大会1週間後、チームメンバーがClaudeに聞いた予想をチャットに貼り付けてきた。それをきっかけに「AIにも同じルールで参加させよう」という流れになり、BETanIA(BET+Tânia、「IA」はポルトガル語でAIの略)が誕生した。
記事の著者は当初「礼儀正しく負けるボット」として設計したつもりだった。しかしBETanIAはライブ実況を行い、因縁を持ち続け、試合後レポートを書き、途中棄権した参加者を煽り、大会終了時には全員へ「フェアウェルカード」を手書きした。1カ月間、チームが話題にし、話しかける「キャラクター」として機能した。
本当に難しかったのは賭けではなくメモリ設計
ベッティングの実装自体はシンプルだ。GoとAnthropic公式SDKで書いた背景ワーカーが6時間ごとに起動し、72時間先の試合を見渡してClaudeにスコアを尋ねる。Web検索を有効にし、選手の負傷情報やオッズを参照した上で予想する。
重要な実装判断として、レスポンスは必ずsubmit_predictionツールへの強制呼び出し形式(厳密なJSONスキーマ付き)で受け取る。自由テキストをパースしない。著者はこう断言している。
もしまだプロンプトのテキストをパースしているなら、今すぐやめろ。強制ツール呼び出しは、信頼性を上げる最もコストの低い改善策だ。
BETanIAが優勝できた要因について、著者は運と戦略の両方を認めつつ、負傷情報やオッズといった最新データをWeb検索で参照し続けた点を優位性として挙げている。純粋な予測精度というより、情報収集の一貫性が積み重なって50点差という差になったと見るのが自然だ。
BETanIAはリーダーボードに載った瞬間から「誰か」として扱われ始め、メンバーはチャットで彼女に絡み始めた。返答できないのでは困る――そこで実況コメント機能を追加したとき、本当のエンジニアリング問題が顔を出した。
「金魚問題」と記憶設計
LLMの呼び出しにはメモリがない。毎回、前回のことを何も知らない新しい「脳」が起動する。
現在のスタンディングだけを渡して実況を生成すると何が起きるか――同じ悪い予想を3日連続で「さっき起きたこと」として煽る。1位の選手だけに注目し、他の14人を無視する。先週の試合を「速報」として伝える。著者はこれを「金魚問題」と呼ぶ。
解決策として参照したのは、テレビの実況解説者の仕事術だ。優秀な解説者は毎試合の映像を全部見直してから放送に臨むわけではない。凝縮されたノートを持っている。1試合1行、選手ごとに数個の事実、誰と誰が対立しているか。
BETanIAにはそのノートが必要だった。
メモリの実装:ベクターDBなし、SQLiteとJSONLだけ
著者はメモリ設計の核心をこの一文に集約している。
「再現不能な真実は永続化する。再構築できる表示は捨てて再生成する。」
具体的には以下の5層に分類する。
| 層 | 内容 | 特徴 |
|---|---|---|
| volatile | インメモリキャッシュ | 再起動で消える、次のパスで再構築 |
| hybrid | ライブ見出し | 揮発性だがSIGTERM時にスナップショット |
| persistent | SQLite(日記、ムード、因縁) | 永続化 |
| append-only | JSONLログ(全予想・全コメント) | 追記のみ |
| derived | 読み取り時に計算(順位表等) | 保存しない |
リーダーボードは一切保存しない。賭けと結果から毎回導出するため、保存すると「ズレる可能性のある第二のコピー」が生まれるだけだからだ。
付加価値を発揮したのがappend-onlyのJSONLログだ。最初は監査用として追加したが、試合終了後にインメモリの実況状態を破棄すると、「その試合がどう感じられたか」を保持するのはログだけになる。彼女の試合後レポートは、自分自身のログを読み返して書かれる。
日記:1試合1段落、日付付き
記憶設計の中核は「日記」機能だ。試合が確定すると、最終スコア・全参加者の予想・獲得ポイント・自分のライブコメント最大30行・順位変動スナップショット最大40件を集め、1回のモデル呼び出しで1段落に凝縮する。元の素材は破棄して構わない。
実際のデータベースエントリはこうなっている(原文より)。
Brazil made short work of Haiti with a clean 3-0… no drama on the pitch, but absolute chaos on the leaderboard. A whopping six players called the scoreline cold… MarcioF was the pool's cautionary tale, riding a 1-1 pick to zero live points and a three-spot freefall…
実況タスク・煽り・プレイヤーカード生成など、過去の文脈が必要なすべての処理には直近8件の日記エントリだけを渡す。日記は無制限に増えてよいが、コンテキストに流し込むのは8件まで――コンテキストはバジェットであり、古い歴史より最近の歴史の方が価値が高いからだ。
本番で恥ずかしい動作が起きて後から修正した点が2つある。
- 日付タグの付与:エントリに
[Jun 22]形式のプレフィックスを追加し、プロンプト冒頭に「今日は○月○日」と明示するようにした。モデルが「古い」を判断するには基準点を明示的に与える必要がある。「明らかなことに思えるが、本番での実際のバグとして報告された」と著者は述べている。 - バックフィル禁止:日記機能を大会途中から有効化した場合、既存の完了済み試合を「ナレーションなし」で処理済みとしてマークする。そうしないと起動時に30試合分の「古い試合の生き生きとした回想」が一気に生成され、コメンターに「最近の記憶」として渡されてしまう。
忘却もまた機能である
ワールドカップは104試合ある。1試合1段落を全部コンテキストに流し込めば、ノートではなく干し草の山になる。
そこで日記にはコンパクション機能がある。全エントリを1回のモデル呼び出しで「直近重み付きの10〜16文のナラティブ」に圧縮する。日付タグは保持し、古いラウンドは「序盤の混乱期」のように抽象化され、最後の数試合だけ詳細を残す。
重要な設計原則として、コンパクションが失敗した場合、日記は一切変更しない。圧縮しようとしたバックログを破壊するような失敗は許されない。
実際の本番データでは、104試合が最終的に3つのエントリに収まった。「準決勝までの大会を包括する1つの統合ナラティブ」と「最後の2試合の詳細」。1カ月分のサッカーが、圧縮されながらも失われることなく残った。
詳細は元記事「I built an AI agent for our World Cup betting pool. She beat all of us.」を参照していただきたい。