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絡みはフレーキーで遅く、継続運用はコストが上回る 本当に価値があるなら、使い回しても作り直してもいい