メインコンテンツに移動

builders.flash

Kiro の 仕様駆動開発で作る AI ボードゲーム

2026-05-07 | Author : 清水 崇之

Missing alt text value

はじめに

こんにちは、しみずです。
Kiro仕様駆動開発 の機能を使って、Amazon Bedrock 上の 6 種類の LLM とボードゲーム (リバーシ) を楽しむプログラムを構築しました。

本記事では、ボードゲームのバックエンドにフォーカスして、要件定義から設計、実装、デプロイまで遭遇した問題とその解決までの全過程を紹介します。本記事では触れませんが、フロントエンドも Kiro を使ってサクっと作りましたので、私 (黒) が Anthropic Claude Opus 4.6 (白) に負けちゃう動画をお楽しみください。

Anthropic Claude Sonnet 4、Opus 4.6、Haiku 4.5、Amazon Nova Pro、Nova Micro、Meta Llama 3.3 70B の 6 モデルと対戦できるようにしています。一回ずつ対戦だけなので強さの参考にはなりませんが、以下の通りの結果となりました。あくまで体感ですが、Opus はレスポンスは遅いですがまぁまぁ強いです。Nova Micro や Haiku はレスポンスが高速なので流れに飲まれてしまう (負けてしまう) ことが何度かありました。どちらにせよ、どのモデルも以前 CNN で実装したものより強い印象です。

 

Missing alt text value

対戦結果

モデル
黒 (私)
白 (AI)
勝ったのは
Claude Sonnet 4

53

11

人間

Opus 4.6

23

41

AI

Haiku 4.5

42

22

人間

Nova Pro

35

29

人間

Nova Micro

37

27

人間

Llama 3.3 70B

50

14

人間

builders.flash メールメンバー登録

builders.flash メールメンバー登録で、毎月の最新アップデート情報とともに、AWS を無料でお試しいただけるクレジットコードを受け取ることができます。

今すぐ登録 »

なぜボードゲームを作ろうと思ったのか ?

Missing alt text value

JAWSDAYS 2018 (AWS コミュニティの旗艦イベント) のライトニングトーク用にボードゲームを制作して発表したことがあります。当時は機械学習やディープラーニングといったキーワードで表現されることが多く、現在の AI 関連技術のように汎用的に動作するものはほとんどありませんでした。

Missing alt text value

私が 2018 年 3 月に制作した際には、過去 40 年分の棋譜データを学習させ CNN 的手法を使って盤面情報から次の一手を推定する方法で実現しました。

2017 年 11 月にラスベガスで開催された AWS re:Invent 2017 にて Amazon SageMaker が発表された直後でしたが、当時は Deep Learning AMI (DL に関連するソフトウェア群が事前セットアップされた AMI)  がまだまだ主流でしたので Chainer on Deep Learning AMI で開発したことを今でも覚えています。実際に CNN でボードゲームが動作するかどうかなんて分かりませんでしたから、PoC しながら手探りでプログラムを組んでいきました。

結果的には、それほど強くはありませんが油断すると負けてしまう程度の強さのプログラムを作ることができました。ロボットアームやハードウェアの部分は開発経験がありましたので何とか適当に構築しています。ソフトウェアとハードウェアの一式を構築するために、だいたい 2 週間ほど必要だったと思います。

あれから早 9 年が経ち、AI 関連の技術革新が進みました。「今の技術を使えば、どれだけ早く、どれだけ簡単に、作れるのだろうか」と、ふと思ったんです。

そこで、ブログ記事のパート 1 として、ソフトウェア部分を Kiro で開発することにしました。ゆくゆくはハードウェア部分も作って誰でも対戦できるようにして re:Invent の展示にでも持って行けたらいいかなと考えています。


 

まずはボードゲームの要件をざっくり検討

  • 8×8 ボードゲームの盤面を JSON でリクエストすると、AI が最善手を返す
  • Claude Sonnet 4、Opus 4.6、Haiku 4.5、Nova Pro、Nova Micro、Llama 3.3 70B の 6 モデルをパラメータで切り替え可能
  • 合法手バリデーションを実装する — AI が不正な手を返した場合は自動フォールバック
  • Amazon API Gateway + AWS Lambda (Python 3.12) + AWS SAM によるサーバーレス構成
Missing alt text value

1. Kiro の Spec 駆動開発とは

Kiro の Spec 機能は、ラフなアイデアを「要件 → 設計 → タスク」の 3 ドキュメントに段階的に落とし込み、各フェーズでユーザーのレビューを挟みながら進める開発手法です。

 

.kiro/specs/bedrock-boardgame-api/
├── requirements.md # 要件定義 (ユーザーストーリー + 受け入れ基準) 
├── design.md # 設計 (アーキテクチャ、コンポーネント、データモデル、Correctness Properties) 
└── tasks.md # 実装タスクリスト (チェックボックス形式) 

 

特徴的なのは、設計ドキュメントに Correctness Properties (正しさの性質) を定義する点です。これは Property-Based Testing (PBT) で機械的に検証可能な形式仕様で、例えば:

-----------------------------------------------
*For any* 有効な Board データについて、パースして内部オブジェクトに変換し、それをシリアライズして再度パースした結果は、最初のパース結果と等価となる。
-----------------------------------------------

このような性質を Hypothesis (Python の PBT ライブラリ) でテストとして実装し、ランダムな入力で網羅的に検証します。

 

2. 要件定義

Kiro に「ボードゲームの盤面情報を送ると AI が次の手を返す API を作りたい」と伝えると、対話を通じて以下の 8 つの要件が整理されました。各要件にはユーザーストーリーと受け入れ基準が WHEN/SHALL 形式で記述されます。

# 要件
概要
1. 盤面送信と次の手の取得

POST /move で盤面を送り、white 側の最善手を取得

2. 盤面フォーマット

a-h 列 x 1-8 行、状態は black / white / empty

3. 盤面の検証

位置・状態・重複のバリデーション

4. エラーハンドリング

400 / 429 / 502 / 500 の適切なマッピング

5. ヘルスチェック

GET /health

6. セキュリティ

API キー認証、CORS

7. ログとモニタリング

リクエストログ (盤面データは含めない)

8. シリアライズ / デシリアライズ

ラウンドトリップ特性の保証

3. アーキテクチャ設計

アーキテクチャは以下のような方式を検討しました。API Gateway + Lambda で Amazon Bedrock をラップしてサーバーレスで実現しました。Bedrock からの応答などを処理する必要もでてくるだろうということで前段に Lambda を配置しています。この構成については、Kiro に指示することで要件に入れてもらいました。

Client → API Gateway (API Key 認証) → Lambda (Python 3.12) → Bedrock (Claude/Nova/Llama)

モジュール構成

プログラムのモジュールについては、Kiro が策定してくれたので私は「うんうん」していただけです。

bash
src/
├── handler.py # Lambda エントリーポイント、ルーティング
├── board.py # 盤面データモデル、バリデーション
├── bedrock_client.py # Bedrock 呼び出し (6モデル対応) 
├── move_validator.py # 合法手列挙・検証
└── response.py # レスポンスビルダー

盤面の座標系

API では、盤面の各マスを「列 (a-h) + 行 (1-8) 」の 2 文字で表現します。以下は駒の初期配置です。この初期配置を API に送る場合、4 つの石だけを指定すれば残りは自動的に empty として扱われます。

Missing alt text value

リクエスト/レスポンス例

API でやりとりするリクエスト/レスポンスの JSON は以下のとおりです。非常にシンプルで、対戦中のボードゲームの盤面情報をリクエストすると、コンピュータ側 (Bedrock) が打つべき次の一手をレスポンスします。

bash
curl -X POST https://xxxxx.execute-api.us-east-1.amazonaws.com/Prod/move \
	-H "x-api-key: YOUR_API_KEY" \
	-H "Content-Type: application/json" \
	-d '{
		"board": [
			{"position": "d4", "state": "white"},
			{"position": "d5", "state": "black"},
			{"position": "e4", "state": "black"},
			{"position": "e5", "state": "white"}
		],
		"model": "sonnet4"
	}'

上記を実行したときに応答された JSON

json
{
	"move": "d3",
	"reason": "Flanks 3 black discs diagonally",
	"confidence": "high"
}

4. 実装のポイント

4-1. 盤面バリデーションとパース

board.py では、64 マスに満たない入力を `empty` で補完するパース処理と、位置・状態・重複を検証するバリデーションを実装しています。

python
def parse_board(payload: dict) -> Board:
	board_data = payload.get("board", [])
	cells: dict[str, str] = {}
	for cell_data in board_data:
		cells[cell_data["position"]] = cell_data["state"]
	# 未指定セルを empty で補完
	for pos in ALL_POSITIONS:
		if pos not in cells:
		cells[pos] = "empty"
	return Board(cells=cells)

4-2. 合法手バリデーション

隣接するマス、かつ、相手駒を裏返せるマスにしか駒を打つことができません。この打ち手を合法手と呼んでいます。move_validator.py では、ボードゲームの 8 方向探索アルゴリズムで合法手を列挙します。LLM はほとんどの場合において合法手を返しますが、たまに不正な手を返すこともあります。そのためバリデーションする機構が必要となりました。また、不正な手を返した場合は、合法手リストからランダムに 1 手を選んでフォールバックします。合法手が存在しない場合は "move": "0" でパスを返します。

python
DIRECTIONS = [
	(-1, -1), (-1, 0), (-1, 1),
	(0, -1), (0, 1),
	(1, -1), (1, 0), (1, 1),
]

def get_flippable_discs(board: Board, position: str, color: str) -> list[str]:
	if board.cells.get(position) != "empty":
		return []
	opponent = "black" if color == "white" else "white"
	col, row = _pos_to_coords(position)
	flippable = []
	for dc, dr in DIRECTIONS:
		candidates = []
		c, r = col + dc, row + dr
		while 0 <= c <= 7 and 0 <= r <= 7:
			pos = _coords_to_pos(c, r)
			cell_state = board.cells.get(pos)
			if cell_state == opponent:
				candidates.append(pos)
				c += dc
				r += dr
			elif cell_state == color and candidates:
				flippable.extend(candidates)
				break
			else:
				break
	return flippable

4-3. マルチモデル対応

どのモデルが強いのか興味がありましたので、6 つの LLM をリクエストパラメータ model で切り替えられるようにしました。プロバイダーごとにリクエスト/レスポンスの形式が異なるため、ビルダーとパーサーをディスパッチテーブルで管理しています。

python
MODEL_REGISTRY = {
	"sonnet4": {"model_id": "us.anthropic.claude-sonnet-4-20250514-v1:0", "provider": "anthropic"},
	"opus4.6": {"model_id": "us.anthropic.claude-opus-4-6-v1", "provider": "anthropic"},
	"haiku4.5": {"model_id": "us.anthropic.claude-haiku-4-5-20251001-v1:0", "provider": "anthropic"},
	"nova-pro": {"model_id": "us.amazon.nova-pro-v1:0", "provider": "nova"},
	"nova-micro": {"model_id": "us.amazon.nova-micro-v1:0", "provider": "nova"},
	"llama3.3-70b": {"model_id": "us.meta.llama3-3-70b-instruct-v1:0", "provider": "llama"},
}

_REQUEST_BUILDERS = {
	"anthropic": _build_anthropic_request,
	"nova": _build_nova_request,
	"llama": _build_llama_request,
}

_RESPONSE_PARSERS = {
	"anthropic": _parse_claude_response,
	"nova": _parse_nova_response,
	"llama": _parse_llama_response,
}

各プロバイダーのリクエスト形式の違い

  • Anthropic Claude : anthropic_version + messages 形式
  • Amazon Nova : schemaVersion: "messages-v1" + inferenceConfig
  • Meta Llama : 特殊トークン (<|begin_of_text|> 等) を含むプロンプト文字列 + max_gen_len

4-4. IaC (AWS SAM)

xml
# template.yaml (抜粋) 
Resources:
	GameApi:
		Type: AWS::Serverless::Api
		Properties:
			StageName: Prod
			Auth:
				ApiKeyRequired: true
GameFunction:
	Type: AWS::Serverless::Function
	Properties:
		CodeUri: src/
		Handler: handler.lambda_handler
		Runtime: python3.12
		Timeout: 30
		Policies:
			- Version: '2012-10-17'
			Statement:
				- Effect: Allow
				Action: bedrock:InvokeModel
				Resource: '*'

5. 本番運用で遭遇した問題と解決 (Kiro が執筆してくれました)

この章は Kiro 自身に開発を振り返ってもらって執筆してもらいました。人間が躓きそうな問題に当たっていることに驚きつつも学びがあります。実際にデプロイしないとわからない問題に複数遭遇しましたが、いずれも Kiro との対話を通じて迅速に解決できました。

5-1. Lambda のインポートエラー

症状

デプロイ後、全エンドポイントで Internal Server Error が発生した

原因

SAM の CodeUri: src/ により、Lambda 実行環境では src/ がルートになります。ローカルでは from src.board import ... で動きますが、Lambda 上では from board import ... でないと見つかりません。

解決策

try/except でデュアルインポートに対応。

修正コード

python
try:
	from board import parse_board, validate_board
except ImportError:
	from src.board import parse_board, validate_board

5-2. Bedrock の推論プロファイル ID

症状

/move エンドポイントで ValidationException

原因 / 解決策

Bedrock のモデル ID にはリージョンプレフィックス (us.) が必要でした。anthropic.claude-sonnet-4-20250514-v1:0 ではなく us.anthropic.claude-sonnet-4-20250514-v1:0 を使う必要があります。

5-3. Claude のレスポンスパースエラー

症状

Bedrock 呼び出しは成功するが、レスポンスの JSON パースに失敗。

原因

Claude が JSON の前後に説明テキストを付けて返すことがある。また max_tokens が小さいと JSON が途中で切れる。

解決策

  • max_tokens を 4096 に増加
  • プロンプトに「JSON のみを返せ」と強調
  • 3 段階のパース戦略 (直接パース → マークダウンコードブロック抽出 → 正規表現 `{...}` 抽出)

修正コード

python
def _extract_json_from_text(text: str) -> dict:
	# 1. 直接パース
	try:
		return json.loads(text)
	except json.JSONDecodeError:
		pass
	# 2. ```json ... ``` ブロック
	match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
	if match:
		return json.loads(match.group(1))
	# 3. テキスト内の最初の {...}
	match = re.search(r"\{[^{}]*\}", text, re.DOTALL)
	if match:
		return json.loads(match.group(0))
	raise ValueError("No JSON found in model response")

5-4. Opus 4.6 のタイムアウトと API Gateway の 29 秒制限

症状

Opus 4.6 のみ 504 Gateway Timeout。

原因

Opus 4.6 は 1 回の推論に約 15 秒かかります。当初のリトライロジック (最大 3 回) では、不正な手が返された場合に最大 4 回の Bedrock 呼び出しが発生し、API Gateway の 29 秒ハードリミットを超過していました。

解決策

リトライロジックを廃止し、不正な手が返された場合は即座に合法手からランダム選択するフォールバック方式に変更。Bedrock 呼び出しは常に 1 回のみ。

修正コード

python
# 修正前: 最大3回リトライ
for attempt in range(MAX_RETRY_COUNT):
	result = invoke_model(board, model=model)
	if is_legal_move(board, result["move"], "white"):
		return success_response(result)

# 修正後: 1回呼び出し + 即フォールバック
result = invoke_model(board, model=model)
if is_legal_move(board, result["move"], "white"):
	return success_response(result)
chosen = random.choice(legal_moves)
return success_response({
	"move": chosen,
	"reason": "Fallback: selected from legal moves",
	"confidence": "low",
})

Kiro との衝突

実は、この問題の解決方法をめぐって、私と Kiro で一悶着ありました。

Kiroの言い分:API Gateway を外したりポーリングしたりで解決できる

API Gateway REST API の統合タイムアウトは 29 秒のハードリミットです。Lambda のタイムアウトをいくら伸ばしても、API Gateway が先に 504 を返します。Opus 4.6 のような応答の遅いモデルを安定して使うには、以下のような構成変更が必要です:

  • Lambda Function URL : API Gateway を経由せず直接 Lambda を呼ぶ (タイムアウト制限なし)
  • 非同期パターン : リクエスト受付 → ポーリングで結果取得
  • WebSocket API : 処理完了後にプッシュ通知

私の言い分 : 30秒 以上も応答がなかったらゲームのヤル気が失せる

ということで、LLM が応答しなかったとしても最大 3 0秒でタイムアウトさせてランダムな合法手を返却するという方針にしました。このあたりは、人間ならではの判断だなと自負しております。

6. テスト

今回は自動で全 135 テストが作成されており全て通過しています。Bedrock 呼び出しは unittest.mock でモックし、ユニットテストとして高速に実行できるようにしています。 (Kiro が全部やってくれたんですけどね)

bash
tests/test_board.py ... 盤面バリデーション、パース、シリアライズ
tests/test_bedrock_client.py ... プロンプト構築、レスポンスパース、モデル解決、各プロバイダー
tests/test_handler.py ... ルーティング、エラーハンドリング、合法手検証、フォールバック、モデル選択
tests/test_move_validator.py ... 座標変換、合法手列挙、合法性判定
tests/test_response.py ... レスポンスビルダー

よかった点

  • 要件の抜け漏れが減る : 対話形式で要件を整理するため、エラーハンドリングやログ要件など見落としがちな部分も自然にカバーされます
  • 設計判断が記録される : なぜその技術選定をしたのかが設計ドキュメントに残るのは便利です
  • タスク分割が適切 : 実装タスクが適度な粒度に分割され、1 タスクずつ進められます
  • Correctness Properties : 形式的な正しさの性質を定義することで、テストの網羅性が向上します

注意点

  • Spec はあくまで出発点。本番運用で遭遇する問題 (インポートパス、モデル ID の形式、タイムアウト制限など) は、実際にデプロイして初めてわかることも多いです
  • マルチモデル対応のような後から追加する機能は、既存の Spec を拡張するか新しい Spec を作るかの判断が必要です
  • 「やれ」と言ったら実現してくれるが、それが本当に必要かどうかは人間が判断すべきです

まとめ

Kiro の 仕様駆動開発を使って、要件定義から設計、実装、デプロイまでを一貫して進めることができました。最終的に、6 つの LLM を切り替え可能なボードゲーム AI API が完成し、合法手バリデーションによる堅牢なレスポンスを実現しています。9 年前に苦労して作ったものが、ほぼ自動で出来上がってしまいました。しかも、ゲーム対戦の強さもマシマシです。

筆者プロフィール

清水 崇之 (@shimy_net)
アマゾン ウェブ サービス ジャパン合同会社
技術統括本部 西日本ソリューション部 ソリューションアーキテクト / 部長

最先端技術を追いながら世界を爆笑の渦に巻き込みたい、そんな AWS 芸人になりたいと思って AWS に入社して早 11 年。サウンドプロデューサー としても活動中。

 

A person wearing a blue winter coat, gray scarf, and knit hat poses with a peace sign in front of Cloud Gate (The Bean) in Chicago.