Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AI Agent時代のリアーキテクチャ戦略と実践

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

AI Agent時代のリアーキテクチャ戦略と実践

Avatar for Kazuhito Hokamura

Kazuhito Hokamura

September 12, 2026

More Decks by Kazuhito Hokamura

Other Decks in Programming

Transcript

  1. 第1部 — AI Agentとリアーキテクチャ 大規模な開発作業をAIに任せると起こりがちなこと 賢いはずなのに、正しくゴールにたどり着かない 01 いつの間にか間違った方向に進む 02 指示していない機能を勝手に追加する

    03 今やらなくていい修正を無限にやる 04 「できました!」と言うが全然できていない 05 アドホックな実装でテストを通す 06 途中で力尽きてゴールに着かない 08
  2. 第1部 — AI Agentとリアーキテクチャ 09 課題と原因 SYMPTOMS — 課題 いつの間にか間違った方向に進む

    指示していない機能を勝手に追加する CAUSE — 原因 ゴールと道筋が曖昧 今何をどうやるべきかがわからない =「方向」の問題 今やらなくていい修正を無限にやる 「できました!」と言うが全然できていない アドホックな実装でテストを通す 受け入れ条件が不明瞭 何をもって完成かがわからない =「判定」の問題 途中で力尽きてゴールに着かない
  3. 第1部 — AI Agentとリアーキテクチャ 10 これらに対する方法論 人がAIを操作するのではなく、 AIが自走する仕組みと環境を設計する Human-on-the-Loop ループの上から監視・調整(中には入らない)

    Harness Engineering - AIが走る実行環境を整える 実装・修正 テストで検証 NG → 自分で直す サンドボックス ・ ツール ・ Linter / Test Loop Engineering - 受け入れ条件とやり直しのループを設計する 受け入れ条件を満たす 完了
  4. 第1部 — AI Agentとリアーキテクチャ 12 本トークにおけるリアーキテクチャの定義 外部から見た機能や動作を変えずに、 内部構造を抜本的に作り変えること BEFORE AFTER

    外から見た機能・動作 = 同じ リアーキテクチャ 内部構造 内部構造 内部構造 ≠ 別物 簡単に言うと大規模なリファクタリング
  5. 第1部 — AI Agentとリアーキテクチャ 13 AI Agentによる開発とリアーキテクチャ リアーキテクチャは大規模になりがちだが AIに任せやすい 分野

    「振る舞いを変えない」 CASE 明確な受け入れ条件が 最初からある = CASE bun Cloudflare vinext Zig → Rust に全面書き換え Next.js を Vite 上で再実装
  6. 第1部 — AI Agentとリアーキテクチャ 15 設計とテストに魂を込める AI Agent時代は、先に時間をかける ほうが結果的に速い アジャイル開発

    設計 実装 テスト 設計 実装 テスト 設計 実装 テスト ゴール AI Agent時代 設計 テスト AIの自律ループ 実装 → 検証 → 修正 ゴール 結果的に速い
  7. 第1部 — AI Agentとリアーキテクチャ 17 AI Agentが自走するための設計 目的地(Why / What)を決める判断は人が握る

    人が判断・魂を込める 初期分だけ人がレビュー AI に任せて自走 Design Doc 実行計画 実装 なぜ・何を作るか どう到達するか 検証しながら (Why / What) (How) 走り続ける 人の関与 大きい 小さい
  8. 第1部 — AI Agentとリアーキテクチャ 18 Design Doc 設計 = なぜ・何を作るか(Why

    / What) の言語化 design-doc.md # タイトル ## 概要 一言でいうと何をするのか ## 背景 なぜやるのか ## 目標と非目標 どこまでやるか・やらないか ## 提案手法 何をどう作り変えるか ## 代替案 他の選択肢と、選ばなかった理由 ## トレードオフ この選択で何を諦めるか ## 非機能要件 性能・コスト・運用・セキュリティ
  9. 第1部 — AI Agentとリアーキテクチャ 19 実行計画 マイルストーン = 検証可能な単位 で道筋を切る

    M1 M2 M3 DoD DoD DoD … Mn DoD ゴール 各マイルストーンに完了条件(DoD)= ループの Exit 条件 を書く 初期分を人がレビューしたら、あとは AI が走りながら更新してよい ただし 計画を超える変更は、計画を先に直す(実装が先行しない)
  10. 第1部 — AI Agentとリアーキテクチャ 20 AI Agentが自走するためのテスト 受け入れ条件の判定を 自動テスト に落とす

    変更 → テスト → 修正 のループを 自分で回しながら進む AI Agent テスト = 振る舞いを固定するレール ゴール
  11. 第1部 — AI Agentとリアーキテクチャ 21 テストの対象や粒度 テスト設計に唯一の正解はない Q1 Q2 Q3

    何を? どの方法で? どこまで? テストする対象の選び方 unit / integration / E2E テスト対象の網羅性 答えは 対象の規模や性質 で大きく変わる
  12. 第2部 — 実践事例:Adventar Adventarのリアーキテクチャ AWS → Cloudflare へ全面移行 実装はほぼ AI

    に任せ、約2週間 で完了 コスト 使われるのはほぼ12月だけなのに、 インフラは常時稼働 メンテナンス限界 Nuxt 2 / Vue 2 が EOL。 スタックが多岐で更新が重い 24
  13. 第2部 — 実践事例:Adventar 25 技術スタック BEFORE — AWS AFTER —

    CLOUDFLARE ホスティング AWS ECS Fargate / Lambda Cloudflare Workers データベース RDS MySQL Cloudflare D1 (SQLite) バックエンド Go / gRPC Web Hono フロントエンド Nuxt 2 / Vue 2 React 画像リサイズ Go / S3 TypeScript / R2
  14. 第2部 — 実践事例:Adventar 26 アーキテクチャの変化 Before After ブラウザ ブラウザ CloudFront

    Lambda ECS Lambda Cloudflare Worker Cloudflare Worker Nuxt 2 SSR Envoy + Go gRPC Go Hono API, React SSR TypeScript フロントエンド APIサーバー 画像リサイズサーバー フロントエンド、APIサーバー 画像リサイズサーバー RDS MySQL AWS による構成 S3 D1 (SQLite) Cloudflare による構成 R2 / KV
  15. 第2部 — 実践事例:Adventar リアーキテクチャの設計を開始 最初のプロンプト Why を簡潔に伝えて、調査から始める Adventar というサービスのリアーキテクチャをおこないたい。 動機は2つで、ほぼ12月しか使われないのに

    ECS や RDS が常時課金されていることと、 Nuxt 2 が EOL でメンテナンスが限界なこと。 まず現行の構成を調査して、コストとメンテナンス性の観点で移行先の選択肢と論点を一緒 に議論したい。 28
  16. 第2部 — 実践事例:Adventar 31 実際のマイルストーン マイルストーン DoD (完了条件) M1 リポジトリ初期化

    ローカル起動、pnpm check が green M2 認証 + 主要 API 全 API が curl で動く。OAuth uid の互換を実機検証 M3 SSR + 配信エンドポイント OGP プレビュー正常、Lighthouse 90+ M4 CSR + ハイドレーション E2E(作成 → 編集 → 削除 → ログアウト)がパス M5 画像サーバー + バッチ 旧 digest URL がそのまま動作 M6 production integration 本番リソース構築、Sentry、ヘルスチェック M7 フロントエンド視覚一致 旧 UI と screenshot 比較で一致 M8 ローカルでのアプリ完成 主要導線・表示が旧 Adventar 同等 M9 データ移行リハーサル dump → D1 投入を2回連続60分以内、検証全通過 M10 カットオーバー本番 全 Phase 完走、エラーレート許容内 M11 旧スタック停止 AWS 段階停止(RDS は最後)
  17. 第2部 — 実践事例:Adventar 32 最初にやること 本格的な実装より先に、 「変更 → 自分で検証」のループ を整える

    pnpm check lint typecheck test build 直列実行 — 最初に落ちた段で止めて、AI に速くフィードバック CI PR ごとに pnpm check を実行 AGENTS.md コマンド・規約・spec 運用を集約した、Agent が最初に読むドキュメント
  18. 第2部 — 実践事例:Adventar 34 リアーキテクチャとテスト 外から見た振る舞いを固定できれば、 内部はいくら変えても安全 外から見た振る舞い = 固定(テストで守る)

    内部実装 = 自由に作り変える Nuxt 2 → React / Go → Hono / MySQL → D1 枠からはみ出した(振る舞いが変わった)瞬間に、テストが落ちて教えてくれる
  19. 第2部 — 実践事例:Adventar 35 テストの粒度 固定すべきは内部ではなく 境界 境界 = API

    / E2E / ユーザーから見た出力 リアーキ後も変わらない 内部実装(ユニットテスト) 内部に密結合 → テスト自体も一緒に書き換わる
  20. 第2部 — Adventarのテスト設計 38 「サービスの振る舞いが変わらない」をどう証明するか 画面・API・RSS まで、全部を一致させたい 画面 API RSS・iCal・oEmbed

    = 外から見えるもの全部 人間の目視チェック 既存テスト AIのループの中に人間を入れたくない 全面書き換えなので、そのままは使えない
  21. 第2部 — Adventarのテスト設計 39 新旧の出力を比較して一致させる Snapshot Testing を採用 同じデータを投入 単一の

    fixture regression.json 旧システム 旧の出力 AWS / MySQL / Nuxt 2 HTML / JSON / スクショ 突合 同じ Playwright シナリオを実行 差分ゼロ? 新システム 新の出力 Cloudflare / D1 / Preact HTML / JSON / スクショ
  22. 第2部 — Adventarのテスト設計 40 同じデータを新旧に投入する 旧 MySQL へ INSERT fixtures/regression.json

    新旧で同じ fixture を使う seed スクリプト 新 D1 へ INSERT // fixtures/regression.json { "users": [{ "id": 101, "name": "Regression Alice", "authProvider": "google", ... }], "calendars": [{ "id": 201, "userId": 101, "year": 2026, "title": "Regression Public 2026", ... }], "entries": } [{ "id": 301, "calendarId": 201, "day": 1, "comment": "Hatena Blog URL entry", ... }]
  23. 第2部 — Adventarのテスト設計 41 同じシナリオを新旧両方に流して保存 REGRESSION_TARGET baseline 旧に流して保存 current 新は

    baseline と突合 同一の Playwright spec 環境変数でターゲットを切り替える // REGRESSION_TARGET=baseline | current を切り替えて同じテストを実行 const target = process.env.REGRESSION_TARGET; test("トップページ", async ({ page }) => { await page.goto("/"); await save(`${target}/home.json`, { text: await page.innerText("body") }); await page.screenshot({ path: `${target}/home.png`, fullPage: true }); }); 外から見える出力だけを保存 innerText・スクショ・ API レスポンス・RSS
  24. 第2部 — Adventarのテスト設計 42 UIの振る舞いのテスト Snapshotを取るときにUIの振る舞いも全部テストする test("logged-in calendar and entry

    CRUD works", async ({ page }, testInfo) => { await login(page); // カレンダーを作成して詳細ページへ // 25日のセルを開いてエントリーを登録 await openEntryEditor(page, 25); await expect(page.locator("body")).toContainText( "登録数 1/25人" ); await fillEntryComment(page, "Regression CRUD entry"); await fillEntryUrl(page, "https://example.com/regression-crud"); await expect(page.locator("body")).toContainText("Regression CRUD entry"); // 操作後の画面も artifact に保存 → 新旧で突合 await savePageArtifact(page, testInfo, "page-crud-calendar"); });
  25. 第2部 — Adventarのテスト設計 新旧の DOM 差をどう吸収するか セレクタはユーザーから見た意味やロールに寄せて書く // ロール +

    アクセシブルネーム await cell.getByRole("button", { name: "登録" }).click(); // name 属性や placeholder など、意味的な属性を優先 const comment = page .locator('input[name="comment"], input[placeholder*="記事"]') .first() .or(page.getByRole("textbox").nth(0)); // 最後は n 番目の textbox に await comment.fill(value); 43
  26. 第2部 — Adventarのテスト設計 44 保存した出力を突き合わせる // baseline と current の同名ファイルをペアにして比較する

    for (const name of listArtifacts()) { const baseline = load(`baseline/${name}`); const current = load(`current/${name}`); if (name.endsWith(".json")) { // テキスト系: 正規化してから完全一致を要求 assertEqual(normalize(baseline), normalize(current)); } if (name.endsWith(".png")) { // スクショ系: ピクセル差を不一致率にして、しきい値以下を要求 assert(diffRatio(baseline, current) <= threshold(name)); } } .json → 正規化して完全一致 .png → 不一致率 ≤ しきい値
  27. 第2部 — Adventarのテスト設計 正規化による非決定性の排除 「本質的でない差」を消してから突き合わせる export const normalizeText = (value:

    string) => value // localhost の可変ポートを固定 .replaceAll(/https?:\/\/(?:localhost|127\.0\.0\.1):\d+/gu, "http://REGRESSION_HOST") // iCal の UID / DTSTAMP は毎回変わる .replaceAll(/UID:[^ ]+/gu, "UID:REGRESSION_UID") .replaceAll(/DTSTAMP:\d{8}T\d{6}Z?/gu, "DTSTAMP:REGRESSION_DTSTAMP") // 自動採番 ID は新旧で必ずズレる .replaceAll(/\/calendars\/\d+(?=[\s/"'<>]|$)/gu, "/calendars/REGRESSION_CALENDAR_ID") // whitespace 差を吸収 .replaceAll(/\s+/gu, " ") .trim(); 45
  28. 第2部 — Adventarのテスト設計 46 下ほど合わせやすい 上ほど信頼性が高い Visual Regression Testing ページ単位のスクショ

    最終描画結果の保証 コンポーネント単位のスクショ 部品単位でスタイルを合わせる コンポーネントの computed style 最低限のスタイルを合わせる 作業難易度 信頼性 難しい 高い 普通 中間 簡単 低い ピクセル差も数値に落とすことで AI が見た目の差を読めるようにする
  29. 第2部 — Adventarのテスト設計 47 ピクセル差分の計算 新旧のスクショを canvas に描き、ピクセルごとに RGBA の距離を比較。なぜか自前実装

    ctx.drawImage(expectedImage, 0, 0); // 旧の PNG を canvas に描画 const expectedData = ctx.getImageData(0, 0, width, height).data; ctx.drawImage(actualImage, 0, 0); // 新も同様に const actualData = ctx.getImageData(0, 0, width, height).data; let mismatchedPixels = 0; for (let i = 0; i < expectedData.length; i += 4) { const distance = Math.abs(expectedData[i] - actualData[i]) + // R Math.abs(expectedData[i + 1] - actualData[i + 1]) + // G Math.abs(expectedData[i + 2] - actualData[i + 2]) + // B Math.abs(expectedData[i + 3] - actualData[i + 3]); // A if (distance > pixelDistanceThreshold) mismatchedPixels += 1; } return { ratio: mismatchedPixels / (width * height) };
  30. 第2部 — Adventarのテスト設計 48 見た目のわずかな差をどうするか 微差(アンチエイリアス等)は距離のしきい値で許容する 重なりのズレ = 不一致ピクセル 旧

    コンポーネント単位 ≤ 1% 厳しめ ページ全体 ≤ 3% ゆるめ・合成なので 新 // 不一致ピクセル比率の許容上限(実測して調整) export const defaultScreenshotRatioThreshold = (name: string) => { if (name.includes("/component-")) return 0.01; // 部品は厳しめに 1% return 0.03; // ページ全体は合成なので緩めに 3% };
  31. 第2部 — Adventarのテスト設計 49 人間による目視確認も併用する 最後の「この差は許容できるか」の判断は 人間が責任を持つ AIの自動突合ループ 残った差分 実装

    → 検証を自分で回す 許容できるか判断 最後の一手だけ人が入る AIに100%一致まで詰めさせるのは token コストが割に合わない
  32. 第2部 — Adventarのテスト設計 差分の可視化: 旧 / diff / 新 の3段

    赤 = ピクセル不一致。どこが・どれだけ違うかが一目で分かる 50
  33. 第2部 — Adventarのテスト設計 53 数値だけでは決めきれないケース ここだけは、あえて Human in the Loop

    に 差分の結果を一覧にして人間が見て、ページ・コンポーネントごとにしきい値を調整できるようにした 1 基本は Human on the Loop を前提に設計する 2 を考える どこに人間が入ると一番レバレッジが効くか
  34. 第2部 — Adventarのテスト設計 54 Adventar におけるリグレッションテスト 01 02 03 Snapshot

    Testing を用いて 判定を数値に落として 人間がやったほうが 同一性を保証する AI が自分で検証できるループ 効率的な箇所を見極めて にする Human in the Loop にする
  35. 第2部 — テスト設計事例 59 APIテストの置き場所 — 既存のテストは流用できないことが多い テストは別に分け、境界の外 から新旧を叩く 既存のテストは外部サービスの

    mock を inject するなど実装側に密結合になっていることが多い 旧 API サーバー 新旧共通のテスト 外部サービスの ダミーサーバー HTTP 別リポジトリなどに分離 同じテストを新旧両方に流す 新 API サーバー システムの境界 mock も inject せず境界で差し替え
  36. 第2部 — テスト設計事例 61 LLM as a Judge LLM の出力は毎回ゆらぐ

    — 完全一致でも正規化でも比較できない → 「意味的に同じか」の判定を、別の LLM に任せる const verdict = await llm(` 以下の2つは、ユーザーへの回答として意味的に等価か? YES / NO と理由を答えよ。 A: ${baseline} // 旧実装の出力 B: ${current} // 新実装の出力 `); expect(verdict).toBe("YES"); judge 自体もゆらぐので、対策する YES / NO ではなく 複数回実行して 判定理由を出力させて、 スコア+しきい値 で判定 多数決 を取る 怪しいものは 人間が確認
  37. 第2部 — テスト設計事例 63 Case: ユーザーの回答で画面フローが無数に分岐するサービス 痛みの強さは? お腹が痛い いつから? 熱はある?

    さらに質問が続く… 症状は? 頭が痛い 息切れ どこが痛む? 安静時にも? … 他の症状は? 結果 … 考えられる病気・ 受診の目安 咳は出る? …
  38. 第2部 — テスト設計事例 64 課題 回答の組み合わせでフローが爆発し、 全パターンの列挙が不可能 … 1 3

    9 27 Adventar のような Snapshot Testing は 主要フローだけ手で列挙しても、 「パターンが有限」であることが前提 網羅の保証にはならない ∞ 列挙不可能
  39. 第2部 — テスト設計事例 65 Random Testing ランダムな回答 で旧を動かし、 同じ回答列 を新に流して突き合わせる

    ランダムな回答を生成 旧システム RECORD 新システム 回答列と各画面の 出力を記録 回答列 + 各画面の出力 同じ回答列を再生 何度も繰り返す 全網羅は諦めて、繰り返し実行で確率的に信頼を積む 無人で回し続けられる 突合
  40. 68 横断パターンとまとめ 横断して見えるパターン 1: 非決定性の制御 そのままでは一致しないものを、どう比較可能にするか 右ほど、抽象的な「ゆらぎ」まで扱える 正規化 数値化 +

    しきい値 LLM as a Judge 時刻・ID・環境の差を 比較から外して決定的にする 見た目の微差を不一致率に 落として許容する 「意味的に等価か」の 判定ごと LLM に任せる ◦ 速い・安い・確実 △ しきい値の調整が必要 △ 遅い・高い・判定もゆらぐ まず左の手段で済むかを考える — 済むならそれが一番安くて確実
  41. 70 横断パターンとまとめ 横断して見えるパターン 3: 使い切りの割り切り リアーキテクチャのテストは 使い切り前提 で作る 01 02

    03 雑に・速く作る 移行を支える(使う) 終わったら捨てる 負債にならない LLM絡みはフレーキーで遅く、継続運用はコストが上回る 本当に価値があるなら、使い回しても作り直してもいい