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 | ◯ | blog テーブルの _id(generating 状態) |
| race_id | string | ◯ | レースの _id |
| racing_type | enum | ◯ | auto_racing / bicycle_racing / horse_racing |
| kind | enum | recap / prediction。省略時は recap(既存キューとの互換のため) |
処理フロー(recap)
- SQS イベントを受信し、ペイロードをパースする
- DocumentDB へ接続する
- 対象の
blogがstatus=generatingか確認する。違えば Bedrock を呼ばずに終了する - レース結果を取得する(存在しなければエラー)
- 着順テーブル(全着順)と関連情報レースカードを組み立てる(
blog_parts.race_card) - S3 からレース動画・コース画像(競馬のみ)を取得する
- 1段目:
race_analysis_agentでレースを総合解析する - 2段目:
blog_generation_agentが確定事実ブロックを一次典拠に、本文(ブロック JSON)と10秒サマリーを執筆する - 生成本文から埋め込みベクトルを作成して
blog.embeddingに保存する - カテゴリ「レース結果・回顧」を添えて 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 として残す)とは方針が異なり、
ブログは失敗したレコードを物理削除します。例外を飲むため、原因調査用にスタックトレースをログへ残します。