コンテンツにスキップ

blog_generation Lambda 概要

SQS(blog-generation キュー)のメッセージをトリガーに、元記事なしでレースの情報から ブログ本文を書き下ろす Lambda 関数です。生成結果は SQS(blog-builder キュー)経由で blog_builder Lambda に渡され、HTML 化されて blog コレクションに保存されます。

過去ブログのリライト(blog_rewriter)と違い、元記事を持ちません。


生成の種類(kind)

kind 内容 入力 レース動画の解析
recap(既定) レース回顧(レース後) レース結果・レース動画・コース画像(競馬のみ) あり
prediction レース予想(レース前) 出走表と AI予想印(race.prediction) なし(レースがまだ行われていないため)

予想では、照合先が公式レース結果ではなく出走表になるため、 結果由来の値(着順・払戻・タイム)は確定事実ブロックへ渡しません。 ただし距離・レース名・発走時刻はレース前に分かる情報なので、結果データから補います。


トリガー

送信元 タイミング
App Lambda(odds_poc_app) ブログ生成のリクエスト時(POST /v1/blogs/generate)

SQS イベントペイロード(受信)

{
  "blog_id": "string",
  "race_id": "string",
  "racing_type": "horse_racing",
  "kind": "recap"
}
フィールド データ型 必須 備考
blog_id string ◯ blog テーブルの _id(generating 状態)
race_id string ◯ レースの _id
racing_type enum ◯ auto_racing / bicycle_racing / horse_racing
kind enum recap / prediction。省略時は recap(既存キューとの互換のため)

処理フロー(recap)

  1. SQS イベントを受信し、ペイロードをパースする
  2. DocumentDB へ接続する
  3. 対象の blog が status=generating か確認する。違えば Bedrock を呼ばずに終了する
  4. レース結果を取得する(存在しなければエラー)
  5. 着順テーブル(全着順)と関連情報レースカードを組み立てる(blog_parts.race_card)
  6. S3 からレース動画・コース画像(競馬のみ)を取得する
  7. 1段目: race_analysis_agent でレースを総合解析する
  8. 2段目: blog_generation_agent が確定事実ブロックを一次典拠に、本文(ブロック JSON)と10秒サマリーを執筆する
  9. 生成本文から埋め込みベクトルを作成して blog.embedding に保存する
  10. カテゴリ「レース結果・回顧」を添えて SQS(blog-builder キュー)へ送信する
flowchart TD
    AppLambda([App Lambda]) -->|SQS| Start
    Start[SQS トリガー受信] --> DB[DocumentDB へ接続]
    DB --> Guard[blog が generating か確認]
    Guard -->|generating でない| Skip[何もせず終了]
    Guard -->|OK| Race[レース結果を取得]
    Race -->|prediction| PredBranch[出走表と AI予想印から予想を執筆]
    Race -->|recap| Table[着順テーブル・レースカードを組み立て]
    Table --> Media[S3 からレース動画・コース画像を取得]
    Media --> Analyze[1段目: race_analysis_agent で総合解析]
    Analyze --> Write[2段目: blog_generation_agent で本文とサマリーを執筆]
    PredBranch --> Embed
    Write --> Embed[生成本文をベクトル化して blog へ保存]
    Embed --> SendSQS[blog-builder キューへ送信]
    SendSQS -.->|SQS| Builder([blog_builder Lambda])

処理フロー(prediction)

回顧との違いは次のとおりです。

  • レース動画の解析(1段目)を行わない
  • 確定事実の照合先が出走表(race.prediction)になる
  • 着順テーブル(race_result_table)はレース前には出せないため null で送る
  • カテゴリは「レース予想・検証」を添える

予想データ(race.prediction)が未登録の場合はエラーになります (PUT /v1/races/prediction で登録してください)。


blog-builder キューへの送信内容(出力)

{
  "blog_id": "string",
  "race_id": "string",
  "racing_type": "horse_racing",
  "category": "レース結果・回顧",
  "race_result_table": {},
  "race_info": {},
  "content": {
    "title": "string",
    "body_html": "",
    "body_blocks": [],
    "summary": ["string"]
  }
}
フィールド データ型 必須 備考
blog_id string ◯ 更新対象の blog の _id
race_id string ◯ レースの _id
racing_type enum ◯ 競技種別
category string ◯ 記事テンプレートの選択に使う(レース結果・回顧 / レース予想・検証)
race_result_table object | null 着順テーブル(全着順)。予想では null
race_info object 関連情報に表示するレースカードの内容
content object ◯ 本文(ブロック JSON)と10秒サマリー

書き下ろしでは content.body_html は常に空文字で、content.body_blocks を使います。


生成のガード

blog_generation_agent は生成後に次のガードを通し、引っかかると検出内容を差し戻して作り直します (最大2回)。それでも解消しない場合はエラーにして公開させません。

ガード 内容
ブロック JSON 出力がブロック JSON として解釈できるか(blog_parts.blocks)
プレースホルダー すべて解決できたか・概算払戻(「10万円超」等)が無いか
事実照合 公式レース結果との食い違いがないか(race_result_facts.find_fact_errors)
NG ワード 出走者名・騎手名に部分一致するものは除外したうえで検査

出走者の「番号+名前」・払戻の金額・組番・人気は LLM に書かせず、 プレースホルダーで公式データの値を差し込みます。


使用モデル

用途 モデル
レース動画・コース画像の解析(1段目) Amazon Nova Pro(動画入力対応)
本文執筆・サマリー生成(2段目) Claude Sonnet(BEDROCK_CLAUDE_SONNET_PROFILE_ARN 未設定時は Nova Pro)
埋め込みベクトル Titan Text Embeddings v2(amazon.titan-embed-text-v2:0)

出力トークン上限は、ブロック JSON が途中で切れると解釈エラーになるため、 Claude では 16384、Nova では 8192 にしています。


エラーハンドリング

状況 挙動
最終試行以外での失敗 例外を再送出し、SQS の再配信による自己復旧に委ねる
最終試行での失敗 status=generating の blog を物理削除し、例外を飲んで終了する

article_generation(status=failed として残す)とは方針が異なり、 ブログは失敗したレコードを物理削除します。例外を飲むため、原因調査用にスタックトレースをログへ残します。