本文へスキップ

個人開発のInterviewCat(エンジニア面接対策プラットフォーム)をAI駆動開発で3日でフルリニューアルした話

1. InterviewCat とは

InterviewCat は、エンジニア向けに技術面接対策のコンテンツを提供している個人開発のサービスです。システムデザイン面接やコーディング面接などの面接対策教材を提供しているほか、無料の技術面接対策のサポートも行っています。テック企業の求人掲載も提供しています。

この記事では、個人で開発・運営している InterviewCat を、AI 駆動開発で約 3 日のうちにフルリニューアルできた話を紹介します。Vercel + Next.js + Notion バックエンドの構成を、Cloudflare Workers 上のフルスタック構成へ作り直しました。

約 3 日でリニューアルを終えた時点のコミット数は合計 440 で、そのうち約 359 コミットは、人間が実装内容を逐一指示せずに AI エージェントが作りました(その後も改善を続けているので、現在のコミット数はこれより多くなっています)。

これを支えたのは、ハーネスを整えてループエンジニアリングを実践したことです。ハーネスとは、AI エージェントが自分で実装・検証・修正を回すための環境で、テスト、lint、計測、ログ・トレース、スキルなどを指します。これを整えたうえで、ヒューマン・イン・ザ・ループをなるべく挟まずに、長いタスク・長いセッションのまま AI が実装を完了させる。この進め方が、全体の 8 割ほどで成り立っていました。この記事では、そのハーネスの整え方を書きます。

なお参考情報として、InterviewCat の販売実績は執筆時点で累計の購入者が約 3,000 ユニークユーザー、コンテンツ販売数が累計約 5,800 です。趣味のプロトタイプではなく、実際に売上と購入者のいるサービスを作り直した話として読んでもらえたらと思います。

2. なぜリニューアルしたのか

初期バージョンは購入したソースコードだった

前提として、InterviewCat は完全なゼロベース開発ではありませんでした。初期バージョンは購入したソースコードをベースにしていて、その中に Next.js / Vercel / Stripe / Notion といった構成が含まれていました。教材を Notion で書き、Notion をバックエンド CMS として配信する構成です。

個人開発の立ち上げとしては合理的な選択でしたが、この構成を長く運用するうちに、次の課題が積み上がっていきました。

Notion バックエンドの運用リスク

Notion を CMS としてバックエンドに使う構成は、以前からやめたいと思っていました。非公式 API に依存していたため、

  • 教材内の画像が表示されなくなる
  • API の仕様変更で呼び出しが失敗する
  • そもそも、いつ使えなくなるか分からない

という運用リスクを抱えていたからです。

ただし、コンテンツの移行と独自レンダラーの開発は工数が大きく、個人開発ではずっと後回しになっていました。

状況が変わったのは、今年(2026 年)に入ってから AI コーディングエージェントが特に大きく進化したからです。コーディングの能力だけでなく、コンテンツの自動変換のような作業も含めて、現実的に高いクオリティでこなせることが分かったので、移行に踏み切りました。

Notion からのコンテンツ移行は、次の流れで進めました。

  1. Notion の API を呼び出して、全コンテンツをダウンロードする
  2. ダウンロードしたコンテンツを Markdown に変換する
  3. 変換した Markdown を表示する独自レンダラーを作る
  4. 独自レンダラーの表示を、いま Notion のレンダラー(react-notion-x)で表示しているページと比べ、差異がなくなるまで変換処理とレンダラーを直す

現在の表示という正解が手元にあるので、AI は表示を比べて差分を直すループを自分で回せます。その結果、コンテンツの移行と独自レンダラーの開発を、人間がほとんど手を加えずに進められることが分かりました。

こうして、Git 管理の Markdown を Single Source of Truth(SSOT)とする独自 CMSへ移行しました。教材がコードと同じリポジトリに入ったことで、lint・レビュー・PR という開発の規律をコンテンツにもそのまま適用できるようになります(詳しくは「7. コンテンツ配信の仕組み」で書きます)。

3. AI 駆動開発で重要なのは完成条件を作ること

この記事でいちばん伝えたいのは、実装前に完成条件を作ることです。

AI の精度が上がった今、How(どう実装するか)を人間が細かく知っていることの重要性は、以前よりずっと小さくなっています。How は AI が提案してくれるので、人間はその提案を判断して選べばいい。細かい流儀やコードの書き方は、ルールとして都度調整していけば十分です。

それよりも大事なのはWhat — どういう状態になりたいかを描けることです。

たとえば Observability という概念を知らなくても、「アプリケーションの状態を細かく分かるようになりたい」という What のイメージさえ描ければ、AI が計装の仕組みや OpenTelemetry のようなツールを提案してくれます。人間が知らない技術でも、あるべき姿を伝えれば選択肢は AI が持ってきてくれる。逆に、なりたい状態を描けなければ、どれだけ How に詳しくても AI に何を頼むかが決まりません。

完成条件とは、この What をLLM が判断できる、もしくは機械的に判定できる形に固定したものです。

  • 何を完成とするか(What)
  • どうなれば成功なのか(Goal)
  • それを LLM や機械でどう検証できるか(Verification)

これが先に定義されていれば、AI は実装 → 検証 → 修正のループを人間の介在なしに回せます。完成条件が人間の頭の中にしかなければ、AI は一歩ごとに人間へ確認を取りに来ます。

リニューアルでは、この方針を次の形で実践しました。以降の章はすべてこの各論です。完成条件と 2 つのループ(1・3)は「4. InterviewCat の AI 駆動開発のワークフロー」、設計(4)は「6. 主な設計原則」、検証と観測(2・5・6)は「8. AI で性能改善の実験を高速に回す」から「10. 人間を検証ループから外す」で具体的に書きます。

  1. 最初に完成条件を作る
  2. AI 自身が検証できる環境を作る(テスト、lint、計測、ログ・トレース)
  3. 大きな自律ループと小さな PR ループを使い分ける
  4. 設計の一貫性とレイヤー分けによって AI を迷わせない
  5. E2E・Observability・性能メトリクスを AI から利用可能にする
  6. 人間が実装・検証ループへ介在する回数を減らす
  7. How ではなく、What / Goal / Verification を人間が設計する

4. InterviewCat の AI 駆動開発のワークフロー

開発フローは、大きく 2 つのフェーズに分けています。リニューアル初期の大きな自律ループと、大枠が完成した後の小さな PR 単位のループです。

Phase 1: 大きな自律ループ

リニューアル初期は、完成形に近いところまで AI に大きなループを回させました。人間が先に用意するのは、

  • 実データ入りの HTML
  • Markdown の仕様
  • テスト
  • 機械的に判定可能なゴール

という「完成状態を判断できる材料」です。AI はこの材料に向かって実装し、自分でビルド・テスト・検証を回し、ゴールとの差分を見て修正します。この流れを、完成条件を満たすまで繰り返します。冒頭に書いた「440 コミット中、約 359 コミットを AI エージェントが作った」という数字は、この長いループが実際に成立した結果です。人間が介在したのは、材料とゴールを作る入口と、完成条件を満たした後の最終確認が中心でした。

人間が完成状態を判断できる材料とゴールを用意し、AI が実装・検証・差分確認のループを完成条件まで回す

「実データ入りの HTML」は、静的 HTML だけで作ったデザインプロトタイプです。全 31 ページで、デザインシステム・用語集・配色比較のページも含み、教材カタログには実際の 14 商品(タイトル・説明・価格・ページ数)を流し込んであります。人間はこのプロトタイプを見て「これが完成形」と合意し、現在の InterviewCat の画面はこのデザインをベースに実装しています。サンプルデータの雰囲気合わせではなく実データで見た目を確定させたので、実装後に「実物を流したらレイアウトが崩れた」というやり直しがほぼ発生しませんでした。

完成条件として最初に作った静的 HTML プロトタイプの教材一覧。実際の 14 商品のタイトル・価格が入っている。現在の画面はこのデザインがベース

プロトタイプには画面だけでなく、デザインの判断材料になるページも含めました。デザイン原則・カラー・タイポグラフィ・コンポーネントをまとめたデザインシステム、各概念の正式な用語と避ける表現を決めた用語集(ユビキタス言語)、そして配色ラボです。配色ラボは、16 パレットをライブの UI プレビューで見比べられるページです。プライマリーから本文・リンク・罫線・教材カバーまで 14 トークンを 1 色ずつ入れ替えられ、本文やボタン文字のコントラスト比が基準(4.5:1)を満たしているかも自動で表示します。選んだ配色はプロトタイプ全ページへそのまま反映されるので、「この色でいくか」という主観的になりがちな判断も、実画面と数値で比較しながら決められました。

プロトタイプに含めた配色ラボ。16 パレットの比較、14 トークンの個別編集、コントラスト比の自動チェックができ、選んだ配色は全ページに反映される

こうした比較ページやデザインシステムのページも、AI があれば静的 HTML として低コストに作れます。成果物そのものだけでなく「合意と判断のための道具」を先に作るのが、完成条件づくりの実践です(後述の Local Viewer も同じ考え方です)。

Phase 2: 小さな PR 単位のループ

大枠が完成した後は、ループを小さく切り替えます。Phase 2 の小さな自律ループは、基本的には望む方向へ微調整するために回しています。

大きなループと小さなループは、扱う範囲で使い分けています。大きなループが向いているのは、複数の機能をまとめて実装するような、時間のかかる作業です。その代わり、1 回のループがカバーする範囲が広いぶん、問題が後から発覚することもあり、最後にできあがったものが人間の意図と微妙に異なる場合があります。

実際、リニューアル初期の長いループでも合意したデザインにできるだけ沿って作っていましたが、細かな部分では調整が必要なところが多々ありました。そうした部分を小さなループで 1 つずつ意図に近づけていく、というのが Phase 2 の役割です。

Phase 1 のゴールが「サービス全体の完成条件」だったのに対し、Phase 2 のゴールはタスクごとの完成条件です。基本単位は次の 3 つを 1 対 1 で揃えることです。

  • 1 worktree
  • 1 task
  • 1 PR

1 worktree / 1 task / 1 PR の中で実装から検証まで完結させ、人間は Description を審査してマージする

ループを小さくすることには、微調整以外の利点もあります。実際の購入者がいるサービスでは、1 回の変更が壊しうる範囲を小さく保ちたいこと。PR が小さければ、人間は Description を読むだけで判断しやすく、問題があっても戻しやすいこと。そしてタスクが独立していれば、複数のループを同時に走らせられることです。

Phase 2 の 1 周。人間はタスクの入口(What とゴール)と出口(Description の審査)だけを担当し、その間は AI が worktree の中で実装と検証を回す

1 周の流れは次のとおりです。

  1. 人間がタスクを渡す。How(どう作るか)が分かっているときは、それを書いて渡せば十分です。分からないときは、What(何をしたいか)とゴール(どうなれば完成か)を必ず書きます。性能改善なら「この指標をここまで」、画面の変更なら「この表示・操作になること」を伝えます。
  2. AI が worktree を切る。主チェックアウトの中の .agents/worktrees/<task-name>/ に専用の worktree とブランチを作ります。開発サーバーのポートも worktree ごとに環境変数で分けるので、他のタスクの作業と衝突しません。
  3. AI が読むべきスキルを選ぶ。 AGENTS.md のスキル選択表から、変更する責務(フロントエンド、API、DB、性能など)に合うスキルを自分で選んで読みます(詳しくは「一貫したレイヤー分けと Agent Skills」)。
  4. 実装と検証を繰り返す。変更に関係する E2E テストを索引から選んで実行します。コミットのたびに、pre-commit フックで lint・コンテンツ lint・アーキテクチャ検査・型検査・テストも走ります。性能改善では staging で実測もします。通らなければ原因を調べて直し、通るまでこのループを人間に確認を取らずに回します。
  5. AI が PR を作る。 Description には、何を変えたか、どんなリスクがあるか、何をどう検証したかを書きます。テストプランのチェックは実際に観測できた項目だけに付け、確認できなかった項目は理由と手順を添えて未チェックのまま残します。
  6. 人間が Description を読んで判断する。問題なければマージし、実物を見て新しい要件が出てきたら、それを次のタスクの完成条件として書き起こします。

ループの中で分かった検証の手順や落とし穴は、テストやスキルに書き戻します。たとえば性能改善の PR を続けて出した後には、その計測方法と判断基準を性能改善用のスキルとしてまとめました。次のタスクを担当する AI は、それを読んだ状態から始められます。

PR の中でどう検証を完結させるか(E2E、PR Description、AI レビュー)は「10. 人間を検証ループから外す」で詳しく書きます。

5. 技術スタックとシステム構成

実行基盤には Cloudflare Workers を選びました。理由はシンプルで、無料枠でもかなりの規模まで運用でき、有料プランにしても個人開発のレベルならほぼ月 5 ドル程度に収まるからです。Web も API も定期ジョブも、同じ基盤に Worker として並べられます。

技術スタック

領域技術
WebTanStack Start / Cloudflare Workers
APIHono / Cloudflare Workers
DatabaseCloudflare D1
StorageCloudflare R2
PaymentStripe
EmailAmazon SES
NotificationDiscord
ObservabilityOpenTelemetry / Cloudflare Workers Logs & Traces

Worker は 5 つ(Web / API / メディア配信 / 通知スケジューラー / 求人クローラー)、リポジトリは pnpm ワークスペースの 17 パッケージで構成しています。

システム構成

現在のシステム構成です。CI/CD で Markdown から Generated Content を生成して Web App に同梱し、実行時は 5 つの Worker が D1 / R2 と外部サービスへつながります。

黒が実行時、オレンジが定期ジョブ、グレーの破線がビルド時の組み込み。教材本文の配信(Browser → Web App)が Worker 同梱データだけで完結し、DB へ向かう線がないことがこの構成のポイント

Monorepo のディレクトリ構成

リポジトリは pnpm ワークスペースの Monorepo で、アプリ・共有パッケージ・教材・テストを 1 つのリポジトリに置いています。主要なフォルダは次のとおりです。

interviewcat/
├── AGENTS.md                  # エージェント共通の入口(CLAUDE.md はこのシンボリックリンク)
├── .agents/
│   ├── skills/                # Agent Skills(34 個)
│   └── worktrees/             # タスクごとの Git worktree(Git 管理外)
├── apps/                      # Cloudflare Workers(5 つ)
│   ├── interviewcat-web/      # TanStack Start の Web / BFF
│   ├── api/                   # Hono API(domain / application / infrastructure / presentation / composition)
│   ├── content-media/         # 教材メディアの配信
│   ├── notification-scheduler/# 求人アラートなどの定期通知
│   └── job-cralwer-worker/    # 求人クローラー
├── packages/                  # アプリ間で共有するパッケージ
│   ├── ui/                    # 共有 UI とデザイントークン
│   ├── web-shell/             # ヘッダー・フッターなどのページシェル
│   ├── content/               # 教材のスキーマ・ペイウォール・content lint
│   ├── content-ui/            # MarkdownRenderer と独自ディレクティブの描画
│   ├── auth/                  # 認証・セッション(JWT)
│   ├── persistence/           # D1 / Postgres のリポジトリ実装とマイグレーション
│   ├── integrations/          # Stripe・メール・通知・R2 などのアダプター
│   ├── observability/         # ロガーとトレーシング
│   └── shared/                # アプリ間で共有する API スキーマ・型・業務ルール
├── tools/
│   └── content-viewer/        # 教材の Local Viewer
├── content/
│   ├── products/              # 教材の Markdown(SSOT)
│   └── blog/                  # ブログ記事
├── e2e/                       # Playwright の E2E テスト(README が実行対象の索引)
└── scripts/                   # デプロイ・検証・移行などの補助スクリプト

6. 主な設計原則

設計原則は、AI 駆動開発でコードの品質を守るための大切な土台です。ここでは、InterviewCat で特に重要な 2 つの原則を紹介します。

外部サービスを交換できる設計: Functional Clean Architecture

個人開発では、コストやサービス事情によって利用する外部サービスが変わる可能性が常にあります。たとえば、

  • Cloudflare が大幅に値上げする
  • より良いプラットフォームが登場する
  • DB を別サービスへ移行する

といったことです。そのため、ビジネスロジックとサービス依存の実装を分離し、外部サービス部分だけを差し替えられる設計を重視しています。

バックエンド(API と通知スケジューラー)は、Functional Clean Architectureで作っています。クリーンアーキテクチャを、クラスではなく関数とファクトリー関数で実装するスタイルです。DI コンテナは使わず、依存はファクトリー関数の引数で明示的に渡します。

レイヤー役割
domain返金ルールのような、外部に依存しない純粋な業務ルール
applicationユースケースと、ユースケースが必要とするポート(リポジトリや外部サービスのインターフェース)の型定義
infrastructureポートを実装するアダプター(D1 / Postgres のリポジトリ、Stripe、SMTP、R2 など)
presentationHono のルート。HTTP のリクエストとレスポンスの変換だけを行う
compositionコンポジションルート。具体的なアダプターを選び、ユースケースに渡して組み立てる

ポイントは依存の向きです。application は自分が必要とするポートを型として定義するだけで、infrastructure がそれを実装します(依存性逆転の原則)。そのため application と domain は、D1 も Postgres も Hono も知りません。具体的な実装の名前が出てくるのは、コンポジションルートだけです。

DB に関するポートに注目して関係を描くと、次のようになります。

図を読み込み中…

矢印の向きに注目してください。ユースケースが依存しているのはポートの型だけで、アダプターのほうがポートに合わせて実装します。D1 から CockroachDB へ(あるいはその逆へ)切り替えても、変わるのは点線の「実装」の片側と、コンポジションルートでの選択だけです。

実際のフォーム受付の流れを簡略化すると、次のような形になります。

// application/forms: ユースケースが必要とする「ポート」を型で定義する
export type FormReceiptRepository = {
  accept(receipt: FormReceipt): Promise<FormReceipt>
}

// ユースケースは依存をファクトリー関数の引数で受け取る。D1 も Postgres も知らない
export const createReceiveForm =
  (deps: { repository: FormReceiptRepository; now: () => number }) =>
  async (input: unknown) => {
    const receipt = validateForm(input, deps.now())
    return deps.repository.accept(receipt)
  }

// packages/persistence: 同じポートを、D1 と Postgres の 2 つのアダプターが実装する
const d1FormReceiptRepository = (db: D1Database): FormReceiptRepository => ({
  accept: async (receipt) => { /* D1 の SQL で保存 */ },
})
const postgresFormReceiptRepository = (sql: Sql): FormReceiptRepository => ({
  accept: async (receipt) => { /* Postgres の SQL で保存 */ },
})

// どちらのアダプターを使うかは、設定値 DATABASE_BACKEND で選ぶ
export const createFormReceiptRepository = (database: Database) =>
  database.backend === 'd1'
    ? d1FormReceiptRepository(database.handle)
    : postgresFormReceiptRepository(database.handle)

// composition: コンポジションルートだけが具体的な実装を知り、組み立てる
const binding = databaseBindingOf(env) // 'd1' | 'postgres'。指定のバインディングがなければ起動しない
const receiveForm = createReceiveForm({
  repository: createFormReceiptRepository(openDatabase(binding)),
  now: Date.now,
})

DB を切り替えるときに変わるのは、アダプターの実装と、コンポジションルートで選ぶ設定値だけです。ユースケースのコードとテストはそのまま使えます。ユニットテストでも、ポートを満たす偽物(フェイク)を渡すだけで、DB なしにユースケースを検証できます。

この設計は、実際の移行で役に立ちました。当初 DB には CockroachDB を使っていましたが、後から Cloudflare D1 へ移行しています。移行先が D1 だったこと自体より、ビジネスロジックへの影響を最小限にして、サービス依存レイヤーを差し替えられたことに意味がありました。この移行の話は「11. CockroachDB から D1 への移行の開発自律ループ」で事例として書きます。

一貫したレイヤー分けと Agent Skills

個人的には、AI 駆動開発で最も重要なのは、

  • 設計の一貫性
  • レイヤー分け
  • どこに何を書くかが明確であること

だと思っています。AI エージェントは賢くなりましたが、「このリポジトリではどこに何を書くべきか」はリポジトリ固有の知識であり、推測に任せると一貫性が壊れていきます。

そこで InterviewCat では、AI エージェントが迷わないように、

  • このディレクトリを触る場合は、この設計ルール
  • このサービスを触る場合は、この Skill
  • この package では、この責務

という形で Agent Skills を用意しています(.agents/skills/ に 34 個)。リポジトリの入口となるエージェント指示には、「変更する領域 → 読むべきスキル」の対応表があります。エージェントは、関連するディレクトリやサービスを変更するときに、この表から対応する設計 Skill を選んで読みます。API を変えるならバックエンド設計と Hono の Skill、教材を書くなら執筆ルールと用語集の Skill、という具合です。

たとえば「フォーム受付 API に入力項目を 1 つ追加する」タスクでは、エージェントは次のようにスキルを選び、変更を置く場所を決めます。

図を読み込み中…

どのスキルを読むかも、読んだ結果どこに書くかも、リポジトリの規約として決まっています。そのため、エージェントが変わっても同じ場所に同じ形のコードが置かれます。

デザインの一貫性も同じ考え方で守っています。デザイントークンの正本を 1 ファイルに置き、raw CSS も Tailwind も同じ変数を参照する規約にしたうえで、直値の混入を拒否するテストを置いています。規約はスキルにも書いてあるので、実装・検証・レビューの三者が同じ基準を参照します。

AI の賢さに任せきりにせず、AI が迷わない構造を人間が設計する。これがこのリポジトリの設計思想の中心です。

7. コンテンツ配信の仕組み

コンテンツビルドパイプライン

教材コンテンツの配信では、ビルド時に Markdown を前処理し、リクエストを受けたら即座にレンダリングできる状態にしておきます。Git 管理された Markdown を SSOT とし、アプリケーションのビルド時にコンテンツもビルドして、Generated Content としてアプリへ組み込みます。リクエスト時に Markdown をパースする処理は発生しません。

図を読み込み中…

各ページの本文は「無料版」と「購入者版」の 2 バリアントとして事前生成し、目次や Product 情報などのページ本文データのメタ情報と一緒に出力します。Worker はこの Generated Content を同梱して配信するので、本文を返すために DB へ行く必要がありません。

Markdown レンダリングの仕組み

教材の Markdown は、次の 3 つの要素からできています。

  • frontmatter — ページのメタ情報(ID、種別、slug、無料/有料、ペイウォールのモードなど)
  • 通常の Markdown — 見出し、本文、表、コード、数式
  • 独自ディレクティブ — Markdown だけでは表現できない UI 部品

Markdown の標準記法だけでは、教材に必要な UI 表現をすべて表現できません。そこで callout や toggle、リンクカード、ページ間リンク、そして有料・無料の境界(ペイウォール)を独自ディレクティブとして追加しています。

---
contentId: sd-websocket
kind: chapter
slug: comm-websocket
access: paid
paywallMode: continuation
---

:::callout{icon="💡" title="考えるポイント"}
WebSocket はステートフルなプロトコルである。
:::

:::paid
ここから先が購入者向けの本文。
:::

実際の表示は次のようになります(共有の MarkdownRenderer を Storybook で描画したもの)。

独自ディレクティブの表示例。callout、toggle(開いた状態)、リンクカード、色付きテキスト。本番の教材ページと同じ MarkdownRenderer で描画している

ビルド時のレンダリングは次の流れです。

図を読み込み中…

Parser が Markdown を構文木にし、独自 Directive を解釈して、Custom Renderer が表示用のデータへ変換します。数式やコードの装飾もこの段階で解決するので、実行時のレンダリングコストは残りません。

DB レスでどう高速配信するか: 認可でも DB まで到達させない

InterviewCat は基本的に静的コンテンツが多いサービスです。高速配信を実現するうえで大事にしているのは、リクエストを DB まで到達させないことです。本文は前述のとおり Worker に同梱しているので、DB は要りません。もう 1 つ DB へ行きたくなるのが認可、つまり「この人は有料版を見てよいか」の確認です。

購入状態を毎回 DB に問い合わせると、ページを開くたびに DB までの往復が発生します。そこで InterviewCat では、購入情報をセッションの JWT にクレームとして入れています。ログイン時に、DB の購入情報からクレームを作って JWT に署名します(有効期限 30 分)。ページ表示のときは、Worker がその JWT の署名と期限を検証し、中の購入クレームだけで無料版と購入者版を選びます。

DB を見るのは、画面表示の後にバックグラウンドで走るセッションの同期(sync)だけです。表示中のタブでは 60 秒ごと(とフォーカスが戻ったとき)に、DB でセッションの失効と最新の購入情報を確認して JWT を再発行します。購入や返金の反映はこの sync に任せ、ページ表示のクリティカルパスからは DB を外しています。

教材ページへのリクエストは、おおまかに次の流れで処理されます。

図を読み込み中…
  1. JWT を Worker 側でローカル検証する(認証サーバーへの問い合わせをしない)
  2. JWT 内の購入 claim を確認する
  3. Public / Paid を判定する
  4. Worker に同梱した Generated Content から本文を返す
  5. 本文はできるだけエッジに近いところから高速配信する

認可(誰に有料版を見せるか)は毎リクエスト JWT で判定し、本文の読み出しは同梱データなので、このパスに DB アクセスは 1 回もありません。有料版の本文を共有キャッシュに置かないことだけは絶対のルールにしていて、共有キャッシュ(Workers Cache)を使うのは匿名向けの読み取りに限定しています。

一部の動的な情報(ログイン状態に依存する表示など)は、まず本文をストリーミングで返してから、残りの要素を API から取得する形にしています。動的な情報が、静的な本文の表示速度の足を引っ張らない構成です。

章ページ。未購入だと本文の途中に有料境界(ペイウォール)が出る

購入者としてログインすると、同じ URL で購入者版の本文が出る

コンテンツ開発用ツール

コンテンツ側にも、コードと同じように検証の仕組みを置いています。

  • Markdown lint — frontmatter の項目と値、ディレクティブ構文、ペイウォール構造、目次の網羅、メディア登録を機械検証する。コミット前フックでも実行されるので、教材の構造ミスはコミット前に機械が落とす
  • コンテンツの validation — ビルド時にも同じ検証が走り、壊れた教材はデプロイに到達しない
  • Local Viewer — 公開前に教材を確認するための内部ツール。本番と同じレンダラーで描画し、ペイウォール分割を「境界マーカー付き」「訪問者視点」「購入者視点」の 3 モードで確認できる

執筆ビューアー。ツリーに各ページの paid バッジと lint の警告数が出て、本文は無料・有料の分割表示で確認できる

Local Viewer のような内部ツールは、以前なら「あると便利だが作る時間がない」ものの代表でした。AI 時代のメリットには、本番プロダクトの実装に加えて、こうした小さな internal tool を低コストで作れることもあります。教材の確認作業が楽になるツールを、思いついた日のうちに用意できます。

8. AI で性能改善の実験を高速に回す

性能改善で一番重要なのは、実験と計測だと考えています。

どんな最適化手法も、実装する → 計測する → 数値を見る → 改善したか判断する、までやらなければ意味がありません。「速くなるはず」の最適化で速くならないことも、逆に悪化させることも普通にあります。そして、この実装 → 計測 → 判断のループは地味に手間がかかるので、人間だけでやると回数が稼げません。

AI の良いところは、このループを大量に、自律的に回せることです。私は最初にゴールと制約を与えますが、How はほとんど指示しません。ゴールは TTFB、Core Web Vitals、Workers Traces で見るサーバー処理時間のような、機械的に判定できる数値で与えます。

人間はゴール(性能メトリクス)を与え、AI が仮説 → 実装 → 計測 → 比較のループを回し、結果は実測値付きの PR として出てくる

最後に PR を作らせ、何を試したのか、どんな実験をしたのか、実測値がどう変わったのかを PR の Description に書かせます。これにより、マージ時にも「本当に改善したのか」を実測値ベースで判断できます。

一例を挙げると、章本文へのエッジキャッシュは、一度導入した後に計測して外しました。同梱データからの読み出しには I/O がなく、キャッシュを挟むほうがわずかに遅いことが A/B 計測で分かったからです。導入も撤去も計測が根拠で、その記録が PR に残っています。「キャッシュを足せば速くなるはず」で止まっていたら、逆効果の機構を抱え続けていたことになります。

9. AI が自分で観測できるようにする

性能改善のループを AI が回すには、AI が計測結果を自分で見られる必要があります。InterviewCat の Observability は、

  • OpenTelemetry — アプリ側の計装。リクエストやレンダリングをスパンとして記録する
  • Cloudflare Workers Logs — デプロイ済み Worker の構造化ログ
  • Cloudflare Workers Traces — リクエストの分散トレース

を中心にしています。

Workers Logs。本番 Worker のログをダッシュボードとクエリで確認できる

トレース詳細。1 リクエストが Worker 間の呼び出しと D1 のクエリまで分解される

狙いは、AI が実装に加えて、本番・検証環境の状態も自分で観測できるようにすることです。AI エージェントがログやトレースを自分で取得・検索できるよう、環境別・Worker 別のログの取り方とフィルターの手順も Skill として用意しています。「このエラーのログを取ってきて貼ってください」と人間に頼む代わりに、エージェントが自分でログを引き、リクエスト ID で相関を辿り、サーバー側の内訳とブラウザー側の計測を突き合わせます。

なお、これは「AI が本番を常時監視して勝手に変更する」体制ではありません。観測と切り分け実験を自律的に行える手段を用意しているだけで、デプロイや本番の変更は明示的な依頼と手順に従います。

10. 人間を検証ループから外す

「Phase 2: 小さな PR 単位のループ」で書いた「PR の中で実装から検証まで完結させる」を支えているのが、この章の仕組みです。目標は、人間を検証ループからできるだけ外すこと。人間の確認をゼロにするのではなく、機械が判定できる部分をすべて機械に寄せて、人間の目は重要な箇所にだけ使います。

E2E テストを AI の検証手段にする

検証の中心は E2E テストです(35 スペックファイル、デスクトップ+モバイルの全実行で 148 件)。E2E は CI だけで動かすものではなく、AI エージェントがローカルで実行することを想定しています。

E2E のインデックスには、どのテストケースがあり、それぞれ何を検証するのかを書いてあります。そのため、エージェントは今回の変更に関係する E2E を自分で探して実行し、必要なら更新もします。「テストがどこにあるか」を人間が教える必要はありません。

実際の e2e/README.md の索引は、機能領域ごとに「テスト / 検証対象 / 実行する変更」の 3 列で書いています。3 列目の「実行する変更」があるので、エージェントは自分の差分と照らし合わせて実行対象を選べます。次は教材まわりの一部です。

教材・ライブラリ(e2e/content/)

テスト検証対象実行する変更
content-components.spec.tscallout、toggle、キーボード操作Markdown ディレクティブ、教材コンポーネント、見出し階層、開閉操作
content-loading.spec.ts商品構造/本文の取得、非公開保護、ローディング、失敗、キャッシュ教材生成物、取得境界、本文遅延読込、販売情報キャッシュ、エラー表示
learning-layout.spec.tsコース/リーダーのデスクトップ・モバイル配置教材レイアウト、見出し、ナビゲーション、レスポンシブ表示
learning.spec.tsカテゴリ、章検索、閲覧ナビゲーション、有料境界ライブラリ検索、章一覧、教材ルート、ペイウォール、購入導線

索引の冒頭には選び方のルールも書いてあります。たとえば「ヘッダーやフォーム部品などの共有 UI を変えたら e2e/ui/ と影響する代表画面を実行する」「影響を絞れない横断的な変更なら全件を実行する」「API・DB・メール・決済の実接続の変更は、この mock の E2E だけで完了にしない」といったものです。

PR の Description には、実行した E2E とテスト結果を必ず書かせます。これによって、人間が毎回ブラウザーを立ち上げて手動確認する量を減らしています。UI 変更についてはスクリーンショットも PR に貼らせるので、軽微な変更なら人間がアプリを立ち上げなくても目視確認できます。重要な機能だけは人間が直接確認します。

実際の PR のテストプラン。実行して確認できたチェックだけにチェックが付き、未実施の項目は理由付きで未チェックのまま残る

PR の Description が人間のインターフェース

人間側の運用はシンプルで、エージェントの書いたコードはさらっと目を通す程度にして、基本的には PR の Description を読みます。そのために、PR には固定の 5 セクション(サマリ / 変更内容 / 変更リスク / テストプラン / NOTE)を必須にしています。

実際の PR。何を変えたか、なぜ変えたか、何を検証したかが Description で完結する

コードを読まない代わりに、主張と証拠の対応が取れているかを Description で審査するイメージです。証拠が怪しければ、そこだけコードと計測を見に行きます。E2E・lint・アーキテクチャ検査が機械側の網で、Description が人間側のインターフェース、という分担です。

AI レビューは opt-in

AI によるコードレビューも利用していますが、必須の Gate にはしていません。token の消費が大きく、時間もかかるためです。必要なときだけ opt-in で利用しています。

図を読み込み中…

セルフレビューでは、実装と同じ session でレビューすると bias が生まれるため、独立した session / agent でレビューすることを重視しています。実装中の文脈を知らないレビュワーが、コードと要件だけを見て判断する形です。

ただし個人開発では費用対効果を考え、「AI レビューを通さないと merge できない」という仕組みにはしていません。Verification は重視するが、すべてを Gate にはしない。これが現実的な落としどころだと考えています。

11. CockroachDB から D1 への移行の開発自律ループ

最後に、ここまでの方法論を DB 移行に当てはめた事例を紹介します。

移行のモチベーションはデータベースのリージョン

当初使っていた CockroachDB は、利用リージョンが Singapore でした。ユーザーの大半は日本からアクセスするため、リクエストのたびに DB までの往復で latency が発生し、ログイン後のページ表示が遅くなります。そこで、DB を日本に近いリージョンに置きたいという理由から、東京に配置できる Cloudflare D1 への移行を決めました。

DB をまるごと変更できたのは、「外部サービスを交換できる設計」で書いたとおり最初から外部サービス依存を分離する設計を重視していたからです。ビジネスロジックは DB の種類を知らず、変更はリポジトリ層の実装の差し替えに収まりました。

コードの移植より、挙動の検証

AI はコードをかなり正確に移植できます。しかし CockroachDB と Cloudflare D1 は異なる Database です。CockroachDB は PostgreSQL 互換、D1 は SQLite ベースなので、同じ意図の Query が完全に同じ挙動になるとは限りません。つまりこの移行では、単純なコード変換ではなく、実際に動作を検証することが何より大切です。

そこで検証の中心に置いたのが、同じ操作をしたときに、移行前後で同じ結果になることを確認する Integration / E2E テストです。

図を読み込み中…

同じ seed データと同じ操作を旧 DB と新 DB の両方に流し、戻り値・エラー・終了時のテーブル状態を比較します。個々のテストのアサーションでは引っかからない「静かな差」を拾うためです。

この比較テストが、移行の完成条件になりました。「両方の DB で同じ結果になること」が機械的に判定できるので、AI は「リポジトリを D1 向けに移植する → 比較テストを流す → 差分の原因を調べて直す」という自律ループを、人間に確認を取らずに回せます。比較した項目は、カタログ、管理画面、プロフィール、通知、配信停止、終了時のテーブル状態など 53 項目です。途中で見つかった差を直したうえで、最終的にすべて一致しました。決済まわりも、注文から返金までの 11 シナリオを両方の DB で同じアサーションのまま実行し、終了時の決済テーブルの状態(20 項目)まで一致を確認しています。

実際に、この比較で見つかった問題があります。決済ジョブが想定した順番で処理されていませんでした。期限順に取り出しているつもりの Query が、D1(SQLite)では格納順で行を返していたのです。処理は成功し、テストも通るのに、順序だけが旧 DB と違う。終了時の状態を比較して初めて見えた差でした。

この問題は、「AI がコードを正しく移植したか」をレビューしても見つからない可能性があります。しかし、実際に実行すれば簡単に分かる問題です。DB 移行は、完成条件と検証を人間が設計し、実装と検証の実行を AI に任せる、というこの記事の方法論の縮図でした。

12. まとめ

持ち帰ってほしいのは、個別の技術 Tips より次の開発方法論です。

  1. 最初に完成条件を作る — 実データ入り HTML、Markdown の仕様、機械判定できるゴール
  2. AI 自身が検証できる環境を作る — テスト、lint、計測、ログ・トレース
  3. 大きな自律ループと小さな PR ループを使い分ける — 立ち上げは長いループで完成形に近づけ、その後は 1 worktree / 1 task / 1 PR の小さなループで意図に合わせて微調整する
  4. 設計の一貫性とレイヤー分けによって AI を迷わせない — ポートとアダプターで外部サービスを差し替えられるようにし、ディレクトリ・サービス・package ごとに設計ルールと Skill を対応させる
  5. E2E・Observability・メトリクスを AI から利用可能にする — 検証と観測の手段をエージェントに開く
  6. 人間が実装・検証ループへ介在する回数を減らす — 人間の目は重要な箇所にだけ使う
  7. How ではなく、What / Goal / Verification を人間が設計する

実売のあるサービスを約 3 日でフルリニューアルでき、リニューアルの 440 コミットのうち約 359 コミットをほぼ自動で進められました。これは AI の性能だけによるものではなく、この 7 つを先に整えたからだと考えています。個人開発でも、完成条件・検証・観測を AI から使える形に整えれば、実装と検証のループの大部分を任せられる。その記録として、どこかの個人開発者・小規模チームの参考になれば嬉しいです。


計測値・テスト件数は当時の PR 報告値であり、記事執筆時の再実行結果ではありません。

著者について

サカモト。某外資ITで働くシニアソフトウェアエンジニア。InterviewCat を個人で開発・運営し、ビッグテック・メガベンなどのトップのテック企業へ入社するためのキャリア論と面接対策情報を発信。共著書に『プロフェッショナルAI駆動開発』。

ほかの記事を読む

ブログ一覧へ ↗