カトホームの技術構成 2025冬 - さくらのクラウド移行

はじめに

カトホーム は2021年1月にリリースし、もうすぐ5周年を迎えようとしています。

昨年の技術構成の紹介記事 では Akamai(Linode) の Linode Kubernetes Engine で立てた cluster 上での構成を紹介しましたが、2025年12月に さくらのクラウド を中心としたインフラ構成に変更しました。

Linode Kubernetes Engine をやめて さくらのクラウド に移行したこと以外は昨年と変わらないので、本記事では さくらのクラウド への移行の話のみ扱います。

2025年12月現在の構成

全体像

さくらのクラウド

サーバー側の機能は概ね さくらのクラウド 上に移行しました。

なぜ Kubernetes をやめたか

Kubernetes cluster の管理が辛くなってきたというのが理由です。

cluster の定期的なアップグレード対応など、個人開発で維持するには運用負荷が少し高かったです。

もっとサービス自体の開発に集中できるようにしたいと思っていました。

なぜ さくらのクラウド を選んだか

最初は Google Cloud の Cloud Run を中心としたインフラを検討していました。

移行を検討している中で、さくらのクラウド が2025年12月にコンテナ実行環境の AppRun やワークフローサービスの Workflows を正式リリースすることを知りました。

これにより Cloud Run と同等のことができそうだったこと、Google Cloud で組むより安そうだったこと、そしてどうせなら国内の企業を応援したいという思いから さくらのクラウド への移行を決めました。

AppRun - コンテナ実行環境

サーバー側のアプリケーションは AppRun 共用型 で動作しています。

AppRun は Google Cloud の Cloud Run や AWS の App Runner に相当するコンテナ実行環境です。

共用型専有型 があります。 共用型は費用やゼロスケールできる点で、専有型は安定性やセキュリティの点で優れています。

今回は費用が安い共用型を選択しました。

カトホームでは以下の3つのアプリケーションを運用しています。

  • api: クライアントから呼び出すための API
  • i-api: 定期バッチ処理などを実行する内部向け API(後述する Workflows から呼び出す用)
  • worker: SimpleMQ(さくらのクラウド のメッセージキューサービス) をポーリングして Workflows を発火する worker

EventBus + シンプルMQ + Workflows - バッチ処理

配信情報の取得やランキング集計などのバッチ処理EventBusシンプルMQWorkflows を組み合わせて実現しています。

EventBus はイベント検知やスケジュールでジョブを実行できるサービスです。 定期的に シンプルMQ にメッセージを投入することができます。

シンプルMQ はメッセージキューサービスです。 タスクキューとして使っています。

WorkflowsYAML で定義するワークフローエンジンです。 HTTP リクエストの発行や並列実行などができます。

これらを組み合わせ、EventBus が シンプルMQ にメッセージを投入し、worker がポーリングして Workflows を発火することで定期的なバッチ処理を実現しています。

カトホームでは複数の定期バッチを動かしており、それぞれに対応した EventBus スケジュールを設定しています。

Kubernetes で動かしていた頃は定期バッチは Argo Workflows を使い、タスクごとにコンテナを起動して実行していました。 Google Cloud であれば Cloud Run Jobs のようにコンテナでジョブを実行するサービスがありますが、さくらのクラウド にはまだ同等のサービスがありません。

そこで今回の移行では、バッチ処理のロジックを i-api(内部向け API)のエンドポイントとして実装し、Workflows から HTTP リクエストで呼び出す方式に変更しました。

ウェブアクセラレータ - 独自ドメイン設定

API の公開には ウェブアクセラレータ を使っています。

AppRun 共用型は自動生成される URL でしかアクセスできず、独自ドメインを設定する機能がありません。 そこでウェブアクセラレータを使って独自ドメインでアクセスできるようにしています。

コンテナレジストリ

コンテナイメージの管理には さくらのクラウドコンテナレジストリ を使っています。

以前は ECR を使っていましたが、AppRun 共用型では現状 さくらのクラウド のコンテナレジストリのイメージにしか対応していないので移行しました。

Kubernetes 時代との比較

費用面

Kubernetes cluster の維持費用が無くなったため少し安くなりました

元々使っていたのが AkamaiLinode Kubernetes Engine で他のサービスと比べて安かったことや、カトホームはゼロスケールの旨味を受けにくいサービスであることから大きく下がってはいません。

運用負荷

Kubernetes cluster の管理から解放されたのは大きいです。

AppRun はログやメトリクスを確認する設定も簡単にできるので、移行時にエラーがあっても調査しやすいです。

一方で AppRun や Workflows は新しいサービスのため、まだ機能が揃っていないところがあります。 今後拡充されていくことに期待しています。

例えば Workflows はリリース当時(2025年12月18日) Terraform に対応していなかったので Web UI か API を使って設定する必要がありました。 現在では 対応された ので、そちらに移行していきたいです。

開発体験

カトホームの場合 Kubernetes を使っていた頃から引き続き GitOps で運用しているのであまり変わらないです。

以下の点で Web UI が使いづらいのは気になりましたが、運用時は頻繁に触るわけではないので我慢しています。

  • 1時間ごとに再ログインを要求される
  • サービスごとにメニュー画面が別れていて移動しづらい
  • プロジェクトの切り替えがクラウドホームからしかできない

おわりに

カトホームの2025年現在のインフラ構成をまとめました。

Kubernetes から さくらのクラウド への移行は思い切った判断でしたが、運用負荷の軽減とコスト削減を実現できました。

この構成で安定した運用を続けていきたいと思います。

さくらのクラウドで「趣」を検出するアプリを作った

この記事は 趣 Advent Calendar 2025 の5日目の記事です。 4日目は 獏星(ばくすたー) さんの 「無職期間(5ヶ月)の振り返り」 でした。 無職、大変趣がありますね。


さて、趣アドベントカレンダーということですが、人によって何にどのような趣を感じるかは異なるものです。 そこで LLM に趣を見出してもらうことで新たな視点を獲得することを目指します。

以下のようなものを作りました。

趣ディテクター

テキストや画像を与えると LLM が趣を見出してくれます。

さて、このアプリを作るにあたり、せっかくなので趣のある国産のクラウドさくらのクラウド を使ってみることにしました。

本記事では LLM を使ったアプリケーションを作り、さくらのクラウドで動かす手順を紹介します。

注意

実装は以下にあります。

nananaman/omomuki

アプリケーション

アーキテクチャ

構成は以下のようなシンプルなものにしています。

flowchart LR
    User["ユーザー"]
    Hono["Hono サーバー"]
    AI["さくらの AI Engine<br/>(preview/Qwen3-VL-30B-A3B-Instruct)"]

    User -->|"テキスト/画像"| Hono
    Hono -->|"API リクエスト"| AI
    AI -->|"ストリーミング"| Hono
    Hono -->|"SSE"| User

今回は簡単のため Hono を採用しました。 Hono を使うと簡単に Web API を作れますし、JSX もサポートしているので雑なアプリを作るのに便利です。

LLM には さくらの AI Engine で提供されている preview/Qwen3-VL-30B-A3B-Instruct を採用しています。

Qwen3-VL-30B-A3B-Instruct は2025年10月4日に登場した比較的新しい軽量なマルチモーダルモデルです。 入力として画像も扱いたいのでこのモデルにしました。

さくらの AI Engine では他にもマルチモーダルモデルとして preview/Phi-4-multimodal-instruct も提供されていますが、Qwen の方が新しくて精度が良いので採用しています。

ちなみに さくらの AI Engine は無料ではないのですが、今回採用したのは preview モデルなので激安で使えます。 ただしあくまで preview なので正常に動作しない場合がありましたし、そもそものモデルが軽量なものなので精度はほどほどです。 用途は考えたほうがいいでしょう。

安すぎる

実装

UI を返すエンドポイントと LLM で処理を行って結果を返すエンドポイントの2つを実装しました。

ここでは要点だけ紹介します。

LLM クライアントの設定

直接 さくらの AI Engine の Web API を呼んでもいいですが、フレームワークを介したほうが実装がシンプルになります。 今回は LangChain.js を用いました。 自分は普段 Python 版 を触っていて比較的馴染みがあるためです。

とはいえ LangChain.js は Python 版と比べ未成熟な印象があります。 真面目に開発するなら Python で Web API を立てて Python 版を使うか、mastra-ai/mastra などの別のフレームワークを検討するといいかもしれません。

さくらの AI Engine では OpenAI 互換 API を提供しているため LangChain の ChatOpenAI をそのまま使えます。

import { ChatOpenAI } from '@langchain/openai'

const llm = new ChatOpenAI({
  model: 'preview/Qwen3-VL-30B-A3B-Instruct',
  maxTokens: 1024,
  configuration: {
    baseURL: 'https://api.ai.sakura.ad.jp/v1/',
    apiKey: process.env.API_KEY
  },
  streaming: true
})

streaming: true を指定することでレスポンスをストリームとして受け取れます。

今回用いたプロンプトは以下です。 少し長いので一部省略しています。

あなたは日本の美意識に精通した日本人の鑑賞者です。
与えられた画像やテキストから「趣(おもむき)」を見出してください。

<INSTRUCTIONS>
1. 入力を解釈し、その情景や状況を情緒豊かに表現したサマリを書く
2. そのサマリにどのような要素や特徴があるか分析する
3. 各要素や特徴について、趣があるかどうかを判断する
4. 趣があると判断した要素や特徴について、どのような趣があるかをわかりやすく表現する
</INSTRUCTIONS>

<RULES>
- 文章は**日本語で**出力する
- 各「趣」は具体的な描写に基づいて、単独で完結したものにする
- 凝りすぎた表現は避け、自然でわかりやすい言葉を使う
- 後述する「出力形式」を厳守する
</RULES>

## 出力形式

1. **サマリ**:入力の情景や状況を2~3文の情緒豊かな自然な文章で表現
2. **趣**:見出した趣を1〜3個、XMLタグで表現
  - 各趣タグは targetと type, reasoning の3つの子要素を持つ
    - target: 趣を生み出している具体的な要素
    - type:趣の種類(以下の4つの中から選ぶ)
      - 侘び寂び:不完全さや経年の美しさ
      - 幽玄:言葉にできない奥深い余韻
      - もののあはれ:移ろいゆくものへの哀惜
      - 風雅:自然との調和、季節感
    - reasoning: 入力のどの要素がどう趣を生み出しているかを具体的に述べる

## 出力例
### 1. 障害報告メールを送ろうとしたらメールサーバーも落ちていた

<summary>障害報告のメールを送ろうとしたその瞬間、メールサーバーまでもが沈黙していた。伝えるべき言葉を抱えたまま、ただ画面を見つめるしかない。現代のインフラが見せる、皮肉な一幕です。</summary>
<omomuki-array>
<omomuki>
  <target>メールサーバーの障害</target>
  <type>もののあはれ</type>
  <reasoning>
    「障害を報告する」という行為が、その障害によって阻まれている。伝える手段が失われたとき、私たちは当たり前に動いていたものの儚さに気づく。
  </reasoning>
</omomuki>
<omomuki>
  <target>送信ボタンを押しても反応しない画面</target>
  <type>幽玄</type>
  <reasoning>
    送信ボタンを押しても何も起きない画面。エラーメッセージすら返ってこない沈黙に、システムの向こう側の深い闇を感じる。
  </reasoning>
</omomuki>
</omomuki-array>

...

それでは RULES を厳守し、以下の入力に基づいて趣のある要素を**日本語で**見出してください。

「趣」の定義は Claude Code に適当に書いてもらったものです。 真面目に作るならここが最も難しいポイントだと思います。

XML で出力するように指示しているのがポイントです。 これを UI 側でパースしてカード状の表示を行っています。

他のポイントとしては日本語で出力するように念入りに指示していることが挙げられます。 Gemini などの大きなモデルを使っている際はこのような現象はほとんど見かけませんが、今回選択したのが軽量なモデルだからか時折日本語以外を出力してしまうことがありました。 特にモデルとして Phi-4-multimodal-instruct を試していたときに顕著で、やはりこの辺りはモデルの品質に左右されるなという印象です。

ストリーミングエンドポイント

Hono の streamSSE を使って SSE(Server-Sent Events)でクライアントにストリームを返します。

import { streamSSE } from 'hono/streaming'

app.post('/api/stream', async (c) => {
  const { text, imageUrl } = await c.req.json()

  // 今回 text か imageUrl のどちらか一方しか受け付けない設計にしている
  const content = imageUrl
    ? { type: 'image_url' as const, image_url: { url: imageUrl } }
    : { type: 'text' as const, text: text! }

  const messages = [
    new SystemMessage(SYSTEM_PROMPT),
    new HumanMessage({ content: [content] })
  ]

  return streamSSE(c, async (stream) => {
    const response = await llm.stream(messages)
    for await (const chunk of response) {
      await stream.writeSSE({ data: chunk.content })
    }
  })
})

llm.stream() について for await...of でチャンクを逐次処理できます。 受け取ったチャンクは stream.writeSSE() でクライアントに送信します。

クライアント側ではこれを受け取りパースして表示しています。

また、今回 LLM に構造化出力を強いたり出力を validation したりすることはあえてしていません。 これによって LLM の部分的な出力を即座に表示できます。

このやり方の場合、代わりに LLM の品質や機嫌次第で構造が壊れることもあるので一長一短ですね。

UI

今回 UI はそれらしいものがあればいいので Claude Code を使って 100% vibe coding で書かせています。

上述した API を予め作ってから以下のような指示でベースを作りました。 その後 UI の動作などを見て細かく指示して調整しました。

stream のエンドポイントを呼ぶ画面を作成したいです。フロントエンドスキルを使ってください。

フロントエンドスキルとは以下の Claude Code 用のスキルのことです。 

claude-code/plugins/frontend-design at main · anthropics/claude-code

このスキルを使わせることでデザインの質が良くなるので、今回のように雑に画面を作りたいときはおすすめです。

以下を Claude Code に入力することで導入できます。

/plugin marketplace add anthropics/claude-code
/plugin install frontend-design@claude-code-plugins

インフラ

今回さくらのクラウドでインフラを固めました。 もうすぐさくらのクラウドの各種サービスの正式版が出るので、CR 版として無料で使える今のうちに遊んでおきたいという魂胆です。

アプリケーションは さくらの AppRun 上で動かしています。 コンテナをサーバーレスで実行できるサービスで、要は Google Cloud Run のようなものです。

AppRun で動かすためにはコンテナイメージをさくらのコンテナレジストリに push する必要があるので、以下のような手順が必要です。

  1. コンテナレジストリを作成
  2. コンテナレジストリを操作できるユーザーを作成
  3. コンテナイメージをビルドして push
  4. AppRun の設定

これを Web UI でやっている記事はインターネットに山程あるので、今回は Terraform での設定例を紹介します。

まずは provider です。

terraform {
  required_version = ">= 1.0"

  required_providers {
    sakuracloud = {
      source  = "sacloud/sakuracloud"
      version = "2.31.2"
    }
  }
}

provider "sakuracloud" {
  token  = var.sakuracloud_access_key_id
  secret = var.sakuracloud_access_key_secret
  zone   = "is1a"
}

token と secret には さくらのクラウド ホーム > API キー から API キーを発行して入れます。

アクセスレベルは「作成・削除」を指定、サービスへのアクセス権は AppRun CR にチェックを入れる

次にコンテナレジストリ

resource "sakuracloud_container_registry" "omomuki" {
  name            = var.container_registry_name
  subdomain_label = var.container_registry_name
  access_level    = "none" # 外部からアクセスされないように非公開にする

  description = "omomuki 用コンテナレジストリ"

  user { # AppRun から pull するための user を作成する
    name       = var.sakuracloud_registry_user
    password   = var.sakuracloud_registry_password
    permission = "all" # 厳密には pull だけで十分だが、今回は手元から push する際にも使ったので強めの権限
  }
}

output "sakuracloud_container_registry_fqdn" {
  description = "コンテナレジストリの FQDN"
  value       = sakuracloud_container_registry.omomuki.fqdn
}

本命の AppRun の設定は以下です。

resource "sakuracloud_apprun_application" "app" {
  name            = "omomuki"
  timeout_seconds = 300
  port            = 3000
  min_scale       = 0
  max_scale       = 1

  components {
    name       = "app"
    max_cpu    = "1"
    max_memory = "512Mi"

    deploy_source {
      container_registry {
        image    = "${sakuracloud_container_registry.omomuki.fqdn}/app:latest"
        server   = sakuracloud_container_registry.omomuki.fqdn
        username = var.sakuracloud_registry_user
        password = var.sakuracloud_registry_password
      }
    }

    env {
      key   = "API_KEY"
      value = var.sakura_ai_engine_account_token
    }
  }

  traffics {
    version_index = 0
    percent       = 100
  }
}

output "sakuracloud_apprun_api_url" {
  description = "AppRun APIの公開URL"
  value       = sakuracloud_apprun_application.app.public_url
}

さくらの AI Engine 用の API キーは環境変数 API_KEY 経由で渡しています。 さくらのクラウドにはシークレットマネージャがありますが、現時点では AppRun に直接渡す機能がないので環境変数にするのが楽です。

これらを terraform apply してからイメージを push すれば動きます。

AppRun CR 版のハマりどころ

AppRun CR 版にはデプロイ時のログやエラーメッセージがないので動かなかったときに理由がわかりにくいです。 自分が最近遊んでいてハマったところを備忘録的に書いておきます。

まとめ

LLM を使ったアプリケーションを作り、さくらのクラウドで動かす手順を紹介しました。

今回はシンプルに LLM を一度呼んでレスポンスをストリームで表示するだけの構成にしましたが、ワークフローにしたりエージェントにしたりすると出来ることや考えることが増えて面白くなります。 各々思い思いの趣あるアプリを作ってみてはいかがでしょうか。

また、さくらのクラウドから様々な製品が今月製品版としてリリースされるようです。 CR 版として無料で触ることが出来る期間は残りあと僅かなので遊ぶなら今のうちです。 趣ある国産のクラウドを応援していきたいですね。


明日 (6日目は空いていて7日目) は 慕狼ゆに@エンジニア集会 さんの「なんか書く」です。趣がありそうですね。

nvim-lspconfig を使っているとき uv のワークスペースの仮想環境を Pyright が正しく認識できない問題

結論

nvim-lspconfig で Pyright を使用している環境で uv のワークスペース機能を利用すると仮想環境が正しく認識されない問題があります。

これは nvim-lspconfig が pyproject.toml をルートマーカーとして使用しているため、各パッケージのディレクトリをルートとして認識してしまうことが原因です。

以下の設定ファイル(~/.config/nvim/after/lsp/pyright.lua)を作成することで解決できます。

return {
  root_markers = { "uv.lock", ".venv", ".git" },
  settings = {
    python = {
      pythonPath = "./.venv/bin/python",
    },
  },
}

これにより uv.lock.venv をルートマーカーとして使用し、ワークスペースのルートディレクトリを正しく認識できるようになります。

前提

uv の workspace 機能とは

uv のワークスペース機能とはコードベースを共通の依存関係を持つ複数のパッケージに分割する機能です。 ワークスペースに複数のパッケージを紐付けてまとめて管理できます。

各パッケージは独自の pyproject.toml を持ちますが、依存関係はワークスペースが単一のロックファイルで一元管理します。 そのため仮想環境もワークスペース側に一つだけ作られ、各パッケージ側には作られません。

ワークスペース機能を使った場合のディレクトリ構成は一般に以下のようになります。

albatross
├── packages
│   ├── bird-feeder
│   │   ├── pyproject.toml
│   │   └── src
│   │       └── bird_feeder
│   │           ├── __init__.py
│   │           └── foo.py
│   └── seeds
│       ├── pyproject.toml
│       └── src
│           └── seeds
│               ├── __init__.py
│               └── bar.py
├── pyproject.toml
├── README.md
├── uv.lock
└── src
    └── albatross
        └── main.py

(公式ドキュメントの例を引用)

nvim-lspconfig とは

nvim-lspconfig は Neovim で language server を利用する際の推奨設定をまとめたプラグインです。

2025年3月リリースの Neovim 0.11 から language server のセットアップは本体機能で行えるようになりましたが、language server ごとの設定を一から書くのは大変です。 各 language server について「どの拡張子のとき有効化するか」「どこをルートディレクトリとみなすか」など多数の項目を設定する必要があります。 nvim-lspconfig を導入すると、これらの設定が自動で行われるため便利です。

問題

nvim-lspconfig を使っているとき、uv のワークスペースの仮想環境を Pyright が正しく認識できない問題があります。 これは Pyright が各パッケージのディレクトリをルートディレクトリとして認識してしまい、ワークスペース側の .venv ディレクトリを見つけられないためです。

原因は nvim-lspconfig の Pyright の設定 にあります。 ルートディレクトリとして pyproject.toml が存在するディレクトリを指定する設定になっています。 util.root_pattern() は親ディレクトリを遡って、指定されたファイルを持つ最初のディレクトリを返す関数です。

...

local root_files = {
  'pyproject.toml',
  'setup.py',
  'setup.cfg',
  'requirements.txt',
  'Pipfile',
  'pyrightconfig.json',
  '.git',
}

...

    root_dir = function(fname)
      return util.root_pattern(unpack(root_files))(fname)
    end,

...

通常の Python プロジェクトであれば pyproject.toml.venv が同じディレクトリにあるためこの設定で問題ありませんが、uv のワークスペース機能を使っている場合は異なります。

ワークスペース内のパッケージにある Python ファイルを開くと、親ディレクトリを遡って最初に見つかる pyproject.toml はパッケージ側のものでありワークスペース全体のものではありません。

前述のとおり仮想環境はワークスペース側に作られるため、ワークスペースのルートディレクトリを認識させる必要があります。

解決方法

以下のファイル(~/.config/nvim/after/lsp/pyright.lua)を作成することで解決できます。

return {
  root_markers = { "uv.lock", ".venv", ".git" },
  settings = {
    python = {
      pythonPath = "./.venv/bin/python",
    },
  },
}

~/.config/nvim/after/ に配置した Lua ファイルは読み込み順が後になるため、nvim-lspconfig の設定を上書きできます。

これによりルートディレクトリの探索に pyproject.toml ではなく uv.lock.venv を使用するようになります。

uv.lock.venvワークスペース側にあるため、各パッケージ内の Python ファイルを開いてもワークスペースのルートディレクトリを正しく認識できます。

GitHubActions から Claude Code を呼んでコードレビューしてもらう

はじめに

GitHubCopilot コードレビューgemini-code-assist を使うと PR のコードレビューを AI にさせることができます。

これはこれで便利なのですが、これらによるレビューはあくまでPR単位であり一般的な観点に留まるものです。 より広い観点からのレビューをしてもらいたい場合、AI にそれらの知識を与えてレビューさせる必要がありそうです。

しかし、先ほど挙げた2つのツールではカスタマイズが難しいです。 前者は enterprise プランであればできそう ですが個人では難しいですし、後者はそもそもできなそうです。

そこで CLI として使えるコーディングエージェントに PR のレビューをさせ、その際に知識を与えることを考えます。 本記事では、GitHubActions から Claude Code を使って、カスタマイズ性の高いコードレビューを実現する方法を記します。

なぜ Claude Code を使うのか

LLM を直接使う場合と比べ、Claude Code にはコーディングのためのシステムプロンプトや機能が組み込まれており1から設定するよりも良い結果が期待できます。

また、類似の Codex CLI と異なり Claude Code は MCP を使うことができます。 これにより多くの MCP の資産を活用でき、例えば社内ドキュメントを MCP で閲覧できるように整えてある組織であれば簡単に連携できます。 もっとも、Codex CLI にも MCP Client の機能が入りそう ではあるのでそのうち好みになりそうです。

設定方法

API キーの準備

GitHub Actions で Claude Code を利用できるように、 Anthropic の API キーを発行して GitHub Secrets に ANTHROPIC_API_KEY のような名前で登録しておいてください。

レビュールールファイルの準備

AI に前提知識として与えるレビューのルールファイルを準備しておきます。 レビューの実行を指示するプロンプト自体に含める手もありますが、別のファイルにしておく方が人間によるレビューの際にも参照しやすく、ローカル環境で AI にレビューして貰う際にも与えやすく便利です。

ルールファイルを MCP で与える方法もありそうですが、ここではリポジトリに commit しておくことにします。 以下のようなものを docs/ai/review-rules.md などで commit してください。

## レビュー基準

以下の観点からコードをレビューしてください:

1. **コード品質**
   - コードは明確で理解しやすいか
   - 適切な命名規則が守られているか
   - 不要なコードや重複はないか
   - エラー処理は適切か

2. **機能性**
   - PRの目的に沿った実装になっているか
   - エッジケースやバリデーションが考慮されているか
   - パフォーマンス上の問題はないか

3. **セキュリティ**
   - セキュリティ上の脆弱性はないか
   - 機密情報の扱いは適切か
   - ハードコードされた認証情報やAPIエンドポイントはないか

4. **テスト**
   - 適切なテストがあるか
   - テストカバレッジは十分か

5. **コンプライアンス**
   - プロジェクトの規約やスタイルガイドに準拠しているか
   - ライセンスや法的な問題はないか

ワークフローファイルの作成

以下のようなワークフローファイルを .github/workflows/claude-code-review.yml のようなパスで作成します。

name: Claude Code Review

on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]

jobs:
  claude-review:
    name: Claude Code Review
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write

    steps:
      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install GitHub MCP Server
        run: |
          claude mcp add-json github '{
            "command": "docker",
            "args": [
              "run",
              "-i",
              "--rm",
              "-e",
              "GITHUB_PERSONAL_ACCESS_TOKEN",
              "ghcr.io/github/github-mcp-server:sha-ff3036d"
            ],
            "env": {
              "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
            }
          }'

      - name: Create prompt file
        run: |
          mkdir -p /tmp/claude-prompts
          cat > /tmp/claude-prompts/claude-pr-review-prompt.txt << 'EOF'
          @docs/review-rules.md

          あなたはプロフェッショナルなコードレビュワーです。GitHub Pull Requestのコードを分析し、高品質なレビューコメントを提供してください。

          ## プルリクエスト情報
          - リポジトリ: ${{ github.repository }}
          - PR番号: ${{ github.event.pull_request.number }}
          - ベースブランチ: ${{ github.event.pull_request.base.ref }}
          - ヘッドブランチ: ${{ github.event.pull_request.head.ref }}

          ## レビュー対象

          1. まず、以下のツールを使用してPRの詳細情報を取得してください:
             - `mcp__github__get_pull_request`: PRのタイトル、説明、変更内容の概要を取得
             - `mcp__github__get_pull_request_files`: PRで変更されたファイル一覧を取得

          2. 次に、変更内容を詳細に分析してください:
             - 各変更ファイルの差分を確認(`git diff` または `mcp__github__get_pull_request_files`)
             - 関連するコンテキストを理解するために必要があれば、ローカルリポジトリの関連ファイルをで確認
             - 既存のPRコメントやレビューがあれば参照(`mcp__github__get_pull_request_comments`、`mcp__github__get_pull_request_reviews`)

          ## フィードバックの提供方法

          レビュー結果は以下の方法で提供してください:

          1. **インラインコメント**
             - 特定のコード行に対するコメントは`mcp__github__add_pull_request_review_comment`を使用
             - その際、以下の手順でPR内の有効なコミットIDを取得して使用してください:
               1. 先に`mcp__github__get_pull_request_files`を呼び出してPRの変更ファイル情報を取得
               2. 返された各ファイルオブジェクトに含まれる`sha`フィールドの値を抽出
               3. コメントしたいファイルのパスに一致する変更ファイルの`sha`値を`commit_id`パラメータとして使用する
               4. 同一ファイルに複数のコメントをする場合も、毎回同じ`sha`値を使用する
             - 具体的な問題点と改善提案を明確に記述
             - コメントは日本語で記述し、コードの意図を尊重する

          2. **総合レビュー**
             - PRの全体的な評価を`mcp__github__create_pull_request_review`で提供
             - 主要な改善点と良い点をまとめる
             - 以下のレビューステータスを使用:
               - COMMENT: 軽微な提案のみ
               - REQUEST_CHANGES: 修正が必要な重要な問題がある場合
               - APPROVE: 問題がなく、マージ可能な場合

          ## 重要なガイドライン

          - レビューコメントは敬意を持ち、建設的であること
          - コードの背景や制約を考慮したレビューを行うこと
          - 提案は具体的で、可能な限り例を示すこと
          - 問題点だけでなく、良い実装にも言及すること
          - レビュー中に質問がある場合は`mcp__github__add_pull_request_review_comment`を使用して質問すること

          ## プロセス

          1. PRの情報を取得して理解する
          2. 変更されたファイルを分析する
          3. `mcp__github__get_pull_request_comments`を使用して既存のコメントを取得・確認する
          4. 問題点や改善点を特定する
          5. コメントする前に必ず`mcp__github__get_pull_request_files`を呼び出してPRの変更ファイル情報を取得する
          6. 返された各ファイルオブジェクトの`sha`フィールドから有効なコミットIDを特定する
          7. 各ファイルのコメントには、そのファイル自身の`sha`値を`commit_id`パラメータとして使用する
          8. 総合的なレビューコメントを作成する
          9. レビュー全体をサブミットする

          では、レビューを開始してください。
          EOF


      - name: Run Claude Code
        run: |
          timeout_seconds=$((10 * 60))

          timeout $timeout_seconds claude \
            -p \
            --verbose \
            --output-format stream-json \
            "$(cat /tmp/claude-prompts/claude-pr-review-prompt.txt)" \
            --allowedTools "Bash,Bash(git diff),mcp__github__get_pull_request,mcp__github__get_pull_request_files,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_reviews,mcp__github__add_pull_request_review_comment,mcp__github__create_pull_request_review"
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

このワークフローでは以下を行なっています。

  • リポジトリの checkout
  • Claude Code のインストール
  • GitHub MCP のインストール
  • レビューを指示するプロンプトをファイルに書き出し
  • 書き出したプロンプトを Claude Code に与えて実行する

ワークフローの実装は Claude Code のリポジトリに設定されている GitHub Actions を参考にしています

実行

上述したワークフローを設定して PR を作成すると Claude Code によるレビューが行われます。

この辺に画像

Claude Code 実行時に --verbose オプションと --output-format stream-json オプションを指定しているので、実行時の Claude Code の動作を GitHub Actions の詳細画面から確認できます。

許可していないツールを呼んだり関係ないファイルを閲覧したりしていることがあるので、適宜確認してプロンプトを調整すると良いです。

カスタマイズ方法

上述した設定はあくまで一例であり、プロジェクトに応じて様々なカスタマイズが考えられます。

レビュールールの改善

レビュールールというよりはコーディングルールの話になりますが、アーキテクチャや業務内容などの視点を含めたルールを設定しておくとより適切なレビューを期待できるでしょう。

例えばテストを書く際の粒度はチームの状況や業務内容で変わってきそうなので、ルールに書く価値が高そうです。

## テスト記述規則
- API を実装する際は controller 層にテストを必ず追加し、正常系と異常系のテストケースを記述してください。
- controller よりも内側の層については複雑なロジックや業務上重要なロジックに対してのみテストを書いてください。
  - 例: 集計処理、金銭を取り扱う処理

レビュー指示のプロンプト改善

リポジトリで扱っている技術について、プロンプトで補強しておくと有効かもしれません。

あなたはReactとTypeScriptに精通したコードレビュアーです。

MCP による拡張

上述したように Claude Code は MCP を扱えるので、既に資産があれば活用できます。

例えば Jira の MCP を使ってコード中にコメントされたチケットの内容を確認させる、Slack の特定チャンネルの投稿内容をコンテキストに含ませるなど、様々な応用が考えられます。

まとめ

GitHub Actions から Claude Code を読んでコードレビューして貰う方法は出来合いのものと比べカスタマイズ性が高く便利です。

また、レビュールールを切り出しておくことでローカルでも Claude Code などから参照させてレビューして貰うことが可能です。 個人開発や高速なイテレーションが必要な場合は、push を介さないこちらの方が有利に働くでしょう。

参考文献

類似したことを行なっている記事

RuntimeContext を活用した Mastra エージェントの API 認証管理

はじめに

現代の Web アプリケーションでは、フロントエンドからバックエンド API を呼び出す構成が一般的です。
ユーザーは UI を操作して API リクエストを発行し、必要な情報を取得したり操作を行ったりします。
しかし、この方法にはいくつかの課題があります:

  • ユーザーは UI の使い方を覚える必要がある
  • 複雑な操作は手順が多く、ユーザーの負担となる
  • 特定のタスクを自動化することが困難

これらの課題を解決する一つの方法として、Mastra のようなチャットエージェントを活用することが考えられます。
この方法では、ユーザーは自然言語で指示を出すだけで、エージェントが適切な API を呼び出して操作を代行します。

本記事では、認証が必要なバックエンド API を Mastra Agent が Tool を使って呼び出せるようにする実装方法について解説します。

Mastra とは

Mastra は、TypeScript ベースのオープンソースエージェントフレームワークです。
AI アプリケーションや機能を素早く構築するための基本的な要素を提供しています。
主な要素として以下が挙げられます:

  • Agent: メモリを持ち、関数を実行できる AI エージェント
  • Tools: エージェントが利用できる機能(関数)
  • Workflow: 決定的な LLM 呼び出しのチェーン
  • RAG: アプリケーション固有の知識をエージェントに供給する仕組み
  • Playground: ローカルでエージェントと対話しながら開発できる環境

今回は Agent と Tools を扱います。

Agent

Mastra における Agent とは、LLM がタスクを実行するために必要な一連のアクションを自律的に決定できるシステムです。
Agent は Tool や Workflow などにアクセスでき、複雑なタスクを実行したり外部システムと連携したりすることができます。

import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";

export const myAgent = new Agent({
  name: "My Agent",
  instructions: "You are a helpful assistant.",
  model: openai("gpt-4o-mini"),
});

Tool

Tool は Agent や Workflow によって実行できる型付き関数です。
実際にタスクを実行したり外部システムと連携したりする処理を行います。

import { createTool } from "@mastra/core/tools";
import { z } from "zod";

export const weatherTool = createTool({
  id: "getWeather",
  description: "Get the current weather for a location",
  inputSchema: z.object({
    location: z.string().describe("The location to get weather for"),
  }),
  execute: async ({ context }) => {
    // 実際の天気 API の呼び出し
    const response = await fetch(`${your_api}/weather?location=${context.location}`);
    return response.json();
  },
});

アーキテクチャ概要

今回扱うアーキテクチャは既存のフロントエンドとバックエンドの間に Mastra Server を配置する方式です。

従来の構成:フロントエンド > バックエンド API
新しい構成:フロントエンド > Mastra Server > バックエンド API

この構成の利点は以下の通りです:

  • 既存のバックエンド API をそのまま活用できる
  • フロントエンドの変更を最小限に抑えられる
  • 実装コストが比較的低い
  • 自然言語インターフェースを通じた操作が可能になる

ユーザーは従来の UI を操作する代わりにチャットインターフェースを通じて「〇〇の情報を取得して」「××を更新して」などと自然言語で指示するだけで、エージェントが適切な API を呼び出して操作を実行します。

認証の課題

このアーキテクチャを実装する際の主な課題は、バックエンド API の認証情報をどのように Agent/Tool から渡すかということです。

ここでは Authorization ヘッダーに JWT などのアクセストークンを含める認証方式を考えます。
多くの場合ユーザーがログインするとフロントエンドがアクセストークンを取得し、それをリクエストヘッダーに含めてバックエンド API を呼び出します。

Mastra Server を中継点として導入する場合、このアクセストークンを Mastra Server 越しにバックエンド API へのリクエストに含めて渡す必要があります。

解決方法: RuntimeContext を利用したトークンの受け渡し

Mastra には「RuntimeContext」という依存性注入の仕組みがあり、これを使うことで解決できます。
RuntimeContext は Agent や Tool に実行時の設定を提供するためのシステムで、以下のような特徴があります:

  • 型安全な runtimeContext を通じてエージェントに実行時の設定変数を渡せる
  • ツールの実行コンテキスト内でこれらの変数にアクセスできる
  • 基盤となるコードを変更せずにエージェントの動作を変更できる
  • 同じエージェント内の複数のツール間で設定を共有できる

先ほどの課題は以下のようにして解決できます。

  1. Mastra Server のミドルウェアでリクエストヘッダーからアクセストークンを取り出し、RuntimeContext に保存
  2. Mastra Agent の Tool で RuntimeContext からアクセストークンを取得し、バックエンド API を呼び出す

ステップ1: ミドルウェアでのトークン取得

Mastra Server に ミドルウェア を設定し、クライアントからのリクエストヘッダーからアクセストークンを抽出します。抽出したトークンは RuntimeContext に保存します。

// RuntimeContext の型定義
type MyRuntimeContext = {
  authToken: string;
};

export const mastra = new Mastra({
  // Agent の設定など
  server: {
    middleware: [
      {
        handler: async (c, next) => {
          const runtimeContext =
            c.get<MyRuntimeContext>("runtimeContext");

          const authHeader = c.req.header("Authorization");
          if (!authHeader) {
            return new Response("Unauthorized", { status: 401 });
          } else {
            const authToken = authHeader.split(" ")[1];
            runtimeContext.set("authToken", authToken);
          }

          await next();
        },
        path: "/api/agents/yourAgent/*",
      },
    ],
  },
});

このコードではクライアントからのリクエストから Authorization ヘッダーを取得し、"Bearer " の後ろの部分をアクセストークンとして RuntimeContext に保存しています。

ステップ2: Tool での API リクエス

次に、Mastra Agent の Tool を実装し、RuntimeContext からアクセストークンを取得してバックエンド API を呼び出します。

import { createTool } from "@mastra/core/tools";
import { z } from "zod";

export const getUserInfo = createTool({
  id: "getUserInfo",
  description: "ユーザー情報を取得するツール",
  inputSchema: z.object({
    userId: z.string().describe("情報を取得するユーザーID"),
  }),
  execute: async ({ context, runtimeContext }) => {
    var authToken = runtimeContext.get("authToken") as string | undefined;
    if (!authToken) {
      throw new Error("認証情報がありません");
    }

    // 認証情報を使って API リクエスト
    const response = await fetch(`${YOUR_API_ENDPOINT}/users/${context.userId}`, {
      method: "GET",
      headers: {
        Authorization: `Bearer ${authToken}`,
        "Content-Type": "application/json",
      },
    });

    if (!response.ok) {
      throw new Error(`API エラー: ${response.status}`);
    }

    const data = await response.json();
    return data;
  },
});

このツールでは RuntimeContext から取得したアクセストークンを Authorization ヘッダーに含めてバックエンド API を呼び出します。

エージェントの設定と複数ツールの連携

最後に作成したツールをエージェントに登録します。複数の API エンドポイントに対応するツールを連携させることで、ユーザーは自然言語で様々な操作を依頼できるようになります。

import { Agent } from "@mastra/core/agent";
import { openai } from "@ai-sdk/openai";
import { getUserInfo, updateUserInfo, getOrderHistory } from "../tools/userTools";

export const userManagementAgent = new Agent({
  name: "ユーザー管理アシスタント",
  instructions: `あなたはユーザーがアカウント情報を管理するのを支援するアシスタントです。
  ユーザー情報の取得、プロフィールの更新、注文履歴の確認などを手伝うことができます。`,
  model: openai("gpt-4o-mini"),
  tools: {
    getUserInfo,
    updateUserInfo,
    getOrderHistory,
  },
});

この設定により、ユーザーは「私のアカウント情報を教えて」「メールアドレスを変更して」「最近の注文履歴を見せて」などと自然に問いかけるだけで、エージェントが適切なツールを選択して API を呼び出し、対応することができます。各ツールは RuntimeContext から同じ認証トークンを利用するため、ユーザーセッション内で一貫した認証状態を維持できます。

開発時の課題と解決策

上述した実装の場合、Mastra Playground をローカルで実行してプロンプトを調整する際、Playground からの呼び出しにはアクセストークンが含まれないため バックエンド API を呼び出せないという課題があります。

これに対しては以下のような解決策が考えられます:

  1. 開発環境では認証をバイパスする
  2. 開発環境用のモックアクセストークンを使用する
  3. 開発環境専用のモックAPIを用意する

開発環境での認証バイパスの例

// middleware.ts
export const authMiddleware = async (c, next) => {
  const runtimeContext = c.get<MyRuntimeContext>("runtimeContext");
  
  const authHeader = c.req.header("Authorization");
  if (!authHeader) {
    // 開発環境では認証をバイパス
    if (process.env.NODE_ENV === "development") {
      console.log("開発環境: 認証をバイパスします");
      const devToken = await getDevAuthToken(); // 開発用トークン取得
      runtimeContext.set("authToken", devToken);
    } else {
      return new Response("Unauthorized", { status: 401 });
    }
  } else {
    const authToken = authHeader.split(" ")[1];
    runtimeContext.set("authToken", authToken);
  }

  await next();
};

// 開発用トークン取得関数
async function getDevAuthToken() {
  // 開発環境用の認証サーバーからトークンを取得するか、
  // 事前に用意したトークンを返す
  return process.env.DEV_AUTH_TOKEN || "dev-token-for-testing";
}

この方法では、開発環境では Authorization ヘッダーが無い場合でも開発用のトークンを自動的に設定するため、Playground からのテストが容易になります。

モック API を活用する方法

もう一つの方法として、開発環境ではモック API を使用することもできます。

// tools/userAPI.ts
export const getUserInfo = createTool({
  // ...
  execute: async ({ context, runtimeContext }) => {
    var authToken = runtimeContext.get("authToken") as string | undefined;
    if (!authToken) {
      throw new Error("認証情報がありません");
    }

    // 開発環境ではモック API を使用
    if (process.env.NODE_ENV === "development") {
      return getMockUserInfo(context.userId);
    }

    // 本番環境では実際の API を呼び出し
    const response = await fetch(`https://api.example.com/users/${context.userId}`, {
      // ...
    });
    // ...
  },
});

// モックデータを返す関数
function getMockUserInfo(userId: string) {
  return {
    id: userId,
    name: "テストユーザー",
    email: "test@example.com",
    preference: {
      theme: "light",
      language: "ja",
    },
  };
}

この方法では、開発環境でモックデータを返すことで、実際の API がなくてもエージェントの動作をテストできます。

まとめ

Mastra の RuntimeContext を活用することで、認証が必要なバックエンド API を Agent/Tool 越しに安全に呼び出すことができます。この方法は、以下のような利点があります:

  • セキュリティを確保しながら API アクセスを実現
  • ユーザーエクスペリエンスの向上(自然言語インターフェース)
  • 既存のバックエンドシステムとの連携が容易
  • 複雑な操作の自動化が可能

このアプローチにより、ユーザーは複雑な UI の操作方法を覚える必要がなく自然言語で指示を出すだけで様々な操作が行えるようになります。また認証情報はフロントエンドで取得したものを Middleware を通じて Agent に渡す方式なので、セキュリティリスクも最小限に抑えられます。

Mastra は気軽にエージェントの開発や検証が出来て楽しいので、ぜひ触ってみてください。特に、既存のシステムに自然言語インターフェースを追加したい場合は今回紹介した RuntimeContext を活用したアプローチが参考になるでしょう。

PLaMo-Embedding-1B でベクトル検索して MCP で話せるようにする

はじめに

オレオレ RAG をさくっと作る の記事を読み、簡単そうだったので MCP で話せるようにしてみました。

nananaman/DuckDB-RAG-MCP-Sample

Claude Desktop から使う例

仕組み

Markdown ファイルをローカルでベクトル化して DuckDB を使って Parquet ファイルで保存します。
ベクトル化には Plamo-Embedding-1B を採用しており、軽量なのでローカルの CPU で十分動作します。

生成した Parquet ファイルを MCP サーバーの起動時に読み込み、クエリに応じてベクトル検索が実行されます。
MCP 対応には MCP Python SDK を使っています。

RAG 周りの詳しい仕組みは参考にさせていただいた オレオレ RAG をさくっと作る の記事を参照してください。

リポジトリ構成

リポジトリは以下のような構成になっています:

- main.py          # CLIツール(ドキュメントのベクトル化)
- server.py        # MCPサーバー
- duckdb_rag/
  - database.py    # DuckDB操作
  - model.py       # ベクトル化モデル
  - utils.py       # ユーティリティ
- tests/           # テスト

main.pyMarkdown ファイルを読み込み、ベクトル化して Parquet ファイルに保存します。

server.pyMCP プロトコルを実装し、Claude からの検索リクエストに応答します。

使い方

ベクトルデータの生成

まず、検索対象の Markdown ファイルをベクトル化します:

uv run main.py --directory ~/path/to/markdown/files --parquet vectors.parquet

MCP サーバーのビルドと設定

エンジニア以外が使うことも考えて pyinstaller を使ってバイナリを生成できるようにしています。

uv run pyinstaller --clean --strip --noconfirm --onefile server.py

生成したバイナリをクライアント側から呼ぶようにしてください。
Claude Desktop の場合は ~/Library/Application Support/Claude/claude_desktop_config.json を開いて以下のように設定してください。

{
  "mcpServers": {
    "DuckDB-RAG-MCP-Sample": {
      "command": "/path/to/binary", # 生成したバイナリのパス
      "env": {
        "VECTOR_PARQUET": "/path/to/vectors.parquet" # 生成した parquet ファイルのパス
      }
    }
  }
}

設定が完了すると Claude Desktop から検索できるようになります。

まとめ

MCP で文書と話せるようにしてみました。

実際に運用する場合は何らかのスケジューラから記事をベクトル化して S3 などに配置し、DuckDB からそのファイルを読む構成になりそうです。

DuckDB を使った RAG システムは簡単に構築できるので試してみてください。

カトホームの開発におけるコーディングエージェント活用

はじめに

カトホームの開発では2月中旬ごろからコーディングエージェントを導入しています。 個人開発の効率化やコーディングエージェント活用の知見を貯めることを目的としています。

先日リリースしたゲームの配信時間表示機能はほぼコーディングエージェントを使って開発したものです。 本記事では機能開発を通して得られた知見について記します。

導入したコーディングエージェント

以下のコーディングエージェントを導入しました

  • Windsurf
  • Claude Code

当初は Windsurf を主に使っていました。 UI や機能が分かりやすいことや定額制で安いことが魅力でした。 しかし自分が Vimmer なのでターミナル内で完結するほうが嬉しいこと、2種類あるクレジット消費のバランスが悪く一方を使いきれない点などが気になっていました(詳細は後述)。

現在は CLI として機能する Claude Code をメインにし、Windsurf は補助的に用いています。

他にも Devin を $20 から使えるようになったのを機に試していますが、導入したばかりなので本記事では深く扱いません。 今後活用していきたいと思っています。

その他試したが現在使ってないもの

  • Cline
    • 個人的には従量課金なら Claude Code でいい
  • OpenHands
    • 簡単なタスクを任せるのには便利
    • WebUI でコミュニケーションする必要がある
      • 似たような Devin は Slack から扱えるのが嬉しい

コーディングエージェント活用のための取り組み

モノレポ化し、横断して知識を扱えるようにする

カトホームはもともと mobile、server、manifest の3つのリポジトリに分かれていました。AI がこれらの知識を横断的に活用できるようモノレポ構成に移行したところ、以下のような効果が得られました。

  • コード理解の向上: インフラで定期バッチの実行設定をする際、AIがサーバー側の実装知識を活用できた
  • ドキュメント生成の効率化: Devinとの連携において、インフラ知識も踏まえた包括的なwikiドキュメントを自動生成できた

モノレポ化は単にコードを一箇所にまとめるだけでなく、AI がプロジェクト全体の文脈を理解する重要な基盤となりました。

AI 用ドキュメント

最初はdocs/ai/hogehogeのようなディレクトリを作って、AI用のドキュメントを集約しました。 mobile や server におけるドメイン知識やタスクの進行状況などをAIが扱いやすくする目的がありました。

Windsurf には.windsurfrulesを使って必要に応じて参照するよう指示していました。 また AI に自分でこれらのファイルを管理させ、必要に応じて編集させていました。

docs/ai
├── 00_index.md
├── memory
│   ├── infra
│   │   ├── active_context.md
│   │   ├── github_actions_deploy.md
│   │   ├── kubernetes_structure.md
│   │   ├── product_context.md
│   │   ├── progress.md
│   │   ├── project_brief.md
│   │   ├── system_patterns.md
│   │   ├── tech_context.md
│   │   └── workflow_templates.md
│   ├── mobile
│   ├── server
│   │   ├── active_context.md
│   │   ├── db_migration_workflow.md
│   │   ├── product_context.md
│   │   ├── progress.md
│   │   ├── project_brief.md
│   │   ├── system_patterns.md
│   │   ├── tech_context.md
│   │   └── testing_strategy.md
│   └── task
│       └── video_game_time.md
└── rules
    ├── 00_basic.md
    ├── 01_memory.md
    ├── 02_git.md
    ├── 03_coding.md
    ├── 04_maintenance.md
    └── 05_task.md

しかし、この方法には次のような問題点がありました。

  • コンテキスト溢れ: セッション開始時にたくさんのドキュメントを読ませても、作業中にコンテキストから溢れて忘れてしまう
  • ファイルの肥大化: AI自身にメンテナンスさせると肥大化していき上手く扱えなくなります
  • 無関係な情報の混入: AIに自分で知識を探索させようとすると、関連性の低いファイルまで読み込んでコンテキストを圧迫して更に忘れやすくなる

現在ではこのドキュメントは使わなくなりました。 理由は3つあります。

1つはAIに与えるタスクを細かくして必要な知識量を絞ることで、そのようなドキュメントがなくてもセッション内で参考になるURLやコードのパスを与えるだけで十分になったためです。 AIに雑な指示を出してよしなにやってもらいたいのなら必要かもしれませんが、AI が明後日の方向に向かって実装していってしまうリスクや現在の AI のコンテキストに載せられる知識量を考えるとタスクは細かくしたほうがいいと考えます。

2つ目は Windsurf から Claude Code に主軸を移したためです。 2025年3月時点では Claude Code の方が既存の実装を把握して沿ったコードを書く力に長けている印象があり、コーディング規約的な AI 向けドキュメントがなくてもそれなりのコードを書いてくれました。

3つ目はタスク管理を GitHub の issue を使う方法に切り替えたためです。 そちらのほうが人間のタスク管理と統一でき、「GitHub の issue #n をやってください」と AI に指示するだけで済むのでチャット欄に全部書くより簡単です。

技術構成やサービスの機能などをまとめた膨大なテキストを丸ごと AI に与えても、コンテキストから溢れて効果的に活用できないというのが自分の感想です。 タスクを小さく分割して必要な知識量を減らし短いセッションで完結させることで、より効率的な運用が可能になりました。

その他の効果的だった取り組み

TDD

AI にテスト駆動開発(TDD)をさせると進行が安定します。 指示する自分と AI の間でタスクのゴールについて認識を合わせてから進められるためです。 テストをパスするためにテストケースの入力を if 文で見て期待値を返すような実装をすることがあるのが難点です。

DB スキーマのダンプ

DB のスキーマを mysqldump などで出力させておくと DB 関連タスクの際に AI に与えやすくて便利でした。

OpenAPI

OpenAPI の YAML はクライアントの実装時にAIに与える知識として便利です。

方針の合意を取らせる

Claude Code にはモードの概念がないので、タスクを与えると勝手に実装を始めてしまいます。例えば「~をやっていきましょう。まず方針を考えて私と合意を取ってください。」のように指示出しすることでプランニングに集中させられます。 この段階で知識が足りてなさそうなら追加で与えて考え直させると良いです。

また、合意したら Markdown ファイルに書かせたり issue を建てさせたりすることでセッションを変更できてコンテキストに余計な知識が乗らないように出来て便利です。 実装に失敗した時に戻るためのチェックポイントとしても機能します。

使用したコーディングエージェントの比較

Windsurf

良かった点:

  • UI がシンプルで見やすい
  • モードが Write と Chat の2つだけで分かりやすい
  • 安い(月額$15〜

悪かった点:

  • Flow Action 用のクレジット消費が激しい
    • Flow Action とはエージェントによる編集やコマンド実行などを指す
    • 2種類のクレジットが毎月付与されるが、Flow Action 用のクレジットの消費が早くもう一方のチャット用クレジットを使いきれない
    • Claude 3.7 Sonnetを使用しているため消費が早い可能性がある

Claude Code

良かった点:

  • 文脈理解能力が高い
    • 周辺実装を参照して適切なコードを提案してくれる
  • CLI ベースなのでターミナル作業に慣れた開発者に適している

悪かった点:

  • 個人で使うには高い
    • 今回の開発では $100 くらい使いました
  • IDE と統合されていないため、Language Server の恩恵を受けられない
    • linter の使用が不可欠
  • Anthropic 以外のモデルが使えない

まとめ

コーディングエージェントを開発プロセスに導入することで、個人開発の効率化と品質向上の両面でメリットがありました。特にモノレポ化との組み合わせは、AI が横断的な知識を活用する上で効果的でした。

最も重要な学びは AI への情報提供方法です。大量の情報をすべて与えるのではなく、タスクに関連する情報を厳選して提供することが成功の鍵でした。またタスクを小さく分割することで不確実性が下がって安定しました。

今後は Devin なども試しながら、さらに効率的な開発フローを構築していきたいと考えています。

この経験から、私自身が今後も心がけていきたいポイントは次の3つです:

  1. タスクを小さく分割すること
  2. 必要な情報を厳選して考えることを減らしコンテキストを圧迫しないこと
  3. タスクのゴールについて認識を合わせること

これらは AI との協業だけでなく、人間同士の開発においても重要なことだと実感しています。