2026/04/13

Ollama + Open WebUI:ローカルLLM構築【2026年完全ガイド】

ローカルで LLM を動かすなら Ollama + Open WebUI が標準。2025年の古い記事では断片的だったが、2026年版は実装から運用まで全て網羅。


なぜローカル LLM なのか

【クラウド API(ChatGPT など)】

  • 毎回データが外に出る(セキュリティ問題)
  • API 代がかさむ(月数千~数万円)
  • 速度が遅い場合がある(ネットワーク遅延)

【ローカル LLM(Ollama)】

  • データが自分のサーバーだけ(セキュアー)
  • 無料(初期構築後、ランニング費用ほぼゼロ)
  • 速度が速い(ネットワーク遅延ゼロ)

必要な環境

ハードウェア

【最小構成】

  • CPU:4コア以上(Ryzen 5 相当)
  • メモリ:8GB(推奨 16GB)
  • ストレージ:30GB(モデルサイズ次第)

【GPU あると大幅に高速化】

  • Nvidia GPU:RTX 3060 以上推奨
  • AMD GPU:RX 6700 XT 以上推奨
  • Mac M1/M2:統合 GPU で十分

【GPU なし場合】

  • CPU のみでも動作(ただし遅い)
  • 推論速度:1トークン/秒程度
  • GPU あり場合:10~20倍高速

ソフトウェア

【必須】

  • Docker
  • Docker Compose

【推奨(GPU使用時)】

  • Nvidia GPU ドライバー
  • CUDA Toolkit 12.0+

インストール手順

ステップ 1:Docker & Docker Compose インストール

# Ubuntu/Debian
sudo apt-get update
sudo apt-get install docker.io docker-compose

# 権限設定(sudo なしで実行可能に)
sudo usermod -aG docker $USER
newgrp docker

ステップ 2:docker-compose.yml 作成

version: '3.8'

services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
    gpus:
      - driver: nvidia
        all: true  # GPU 全て使用
    restart: always

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    ports:
      - "8080:8080"
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
    depends_on:
      - ollama
    restart: always
    volumes:
      - webui_data:/app/backend/data

volumes:
  ollama_data:
  webui_data:

ステップ 3:起動

docker-compose up -d

# ログ確認
docker-compose logs -f

ステップ 4:モデルのダウンロード

# Ollama コンテナに入る
docker exec -it ollama bash

# モデルをダウンロード
ollama pull llama2:7b
# または
ollama pull mistral:7b
# または
ollama pull neural-chat:7b

ステップ 5:Open WebUI にアクセス

ブラウザで http://localhost:8080 を開く
→ ユーザー登録(初回のみ)
→ モデル選択して利用開始

GPU 設定(高速化のため)

Nvidia GPU の場合

# CUDA Toolkit インストール
sudo apt-get install cuda-toolkit-12-0

# docker-compose.yml で GPU を有効化(上記の例参照)
services:
  ollama:
    gpus:
      - driver: nvidia
        all: true

GPU が認識されているか確認

# コンテナ内で確認
docker exec -it ollama bash
ollama run llama2:7b

# プロンプトで GPU 使用状況を確認
# "GPU acceleration enabled" と表示されたら OK

推奨モデル一覧(2026年)

モデル サイズ 速度 品質 用途
Llama 2 7B 4GB 速い 標準 汎用・最初の1択
Mistral 7B 4GB 高速 高い 日本語・コード
Neural Chat 7B 4GB 速い 高い 会話型・日本語
Llama 2 13B 8GB 中程度 高い より精度重視
Code Llama 7B 4GB 速い コード特化 プログラミング
Mistral Large 34GB 遅い 非常に高い 重い処理用

初心者推奨:Mistral 7B(バランス型)


よくある落とし穴

1. GPU が認識されない

# 確認コマンド
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi

# 出力がなければ、nvidia-docker が必要
sudo apt-get install nvidia-docker2
sudo systemctl restart docker

2. メモリ不足エラー

"OOM Killed" または メモリ不足エラーが出た場合

対策 1:小さいモデルを使う

ollama pull llama2:7b  # 7B は 4GB
# ではなく
ollama pull phi:2.7b   # 2.7B は 2GB

対策 2:スワップメモリを増やす

sudo fallocate -l 16G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

3. Open WebUI が Ollama に接続できない

# docker-compose.yml で
environment:
  - OLLAMA_BASE_URL=http://ollama:11434

# これが正しくないと接続失敗
# ホスト名は「ollama」(service 名)
# ポートは 11434(Ollama のデフォルト)

4. 応答が遅い(GPU 未使用の場合)

# GPU が使われているか確認
docker exec -it ollama ollama ps

# "GPU acceleration disabled" が出たら GPU 未認識
→ 上記「GPU が認識されない」の対策を実施

実装時のベストプラクティス

1. モデルの永続化

volumes:
  ollama_data:/root/.ollama  # ← ここにモデルが保存される

# コンテナ削除後も モデルが残る
docker-compose down  # コンテナ削除
docker-compose up -d  # 再起動(モデルダウンロード不要)

2. Web UI のセキュリティ設定

open-webui:
  environment:
    - OLLAMA_BASE_URL=http://ollama:11434
    # ローカルホストのみアクセス許可
    - WEBUI_AUTH_REQUIRED=true  # ユーザー認証必須

3. 複数モデルの管理

# ダウンロード済みモデルを確認
docker exec -it ollama ollama list

# 不要なモデルを削除
docker exec -it ollama ollama rm llama2:7b

4. バックアップ

# モデルデータをバックアップ
docker cp ollama:/root/.ollama ./ollama_backup

# WebUI のデータ(会話履歴)をバックアップ
docker cp open-webui:/app/backend/data ./webui_backup

トラブルシューティング

ポート競合

"Address already in use" エラーが出た場合

docker ps  # 既存コンテナを確認
docker stop <コンテナ ID>
docker-compose up -d  # 再起動

メモリリーク

# コンテナのメモリ使用量を監視
docker stats

# メモリが増え続ける場合、モデルを再ロード
docker restart ollama

ネットワーク接続の問題

# コンテナ間の通信確認
docker exec -it open-webui ping ollama

# 失敗する場合、ネットワークドライバを確認
docker network ls
docker network inspect <network_name>

2025年版との変更点

項目 2025年 2026年
モデルサイズ Llama 2 のみ 複数選択肢
GPU 設定 簡潔 詳細なトラブルシューティング
セキュリティ 未記載 ユーザー認証設定
バックアップ 未記載 手順明記
トラブルシューティング なし 充実

次のステップ

【1日目】

  • Ollama + Open WebUI をセットアップ
  • モデル 1つをダウンロード・テスト

【1週間後】

  • GPU 最適化を試す
  • 複数モデルを試して相性を確認

【1ヶ月後】

  • 実運用に向けて、セキュリティ設定を強化
  • API 化(他アプリから Ollama を呼び出し)を検討

参考資料

Android Jetpack Compose LazyGrid で斜めスクロール実装【2026年版】

Android 斜めスクロールView実装 ~2026年Jetpack Compose版

📌 関連記事: この記事は 【android】ScrollViewで縦横斜めにスクロール(2015年)の2026年版アップデートです。当時のコンセプトが、今どう実装されているかをご紹介します。

かつて「Androidで斜めスクロールって実装できないのか?」という問い合わせがあった。2015年時点では標準APIになく、自作が必須だった。今は Jetpack Compose の登場で、状況が大きく変わった。

2026年なら、Compose + LazyGrid で 1行で解決できる。


昔の問題(2015年)vs 現在(2026年)

2015年:框組みで解決しようとしていた時代

ScrollView(縦)+ HorizontalScrollView(横)
  ↓
マージして「斜めスクロール」を実現
  ↓
カスタムViewGroupを自作

当時は効果測定も「コンセプト段階」で、実装されていなかった。

2026年:Jetpack Compose なら標準

LazyVerticalGrid / LazyHorizontalGrid
  ↓
フリングスクロール、慣性スクロール完備
  ↓
nested scroll も自動対応

標準APIs の充実で、カスタム実装が不要になった。


2026年の実装:Jetpack Compose

ステップ 1:依存関係の追加

dependencies {
    implementation "androidx.compose.foundation:foundation:1.7.0"
    implementation "androidx.compose.material3:material3:1.3.0"
    implementation "androidx.activity:activity-compose:1.9.0"
}

ステップ 2:LazyGrid で斜めスクロール実現

@Composable
fun DiagonalScrollView() {
    LazyVerticalGrid(
        columns = GridCells.Fixed(3),
        modifier = Modifier
            .fillMaxSize()
            .padding(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(100) { index ->
            GridItem(index)
        }
    }
}

@Composable
fun GridItem(index: Int) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .height(120.dp),
        colors = CardDefaults.cardColors(
            containerColor = Color(0xFF6200EE)
        )
    ) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = "Item $index",
                color = Color.White,
                fontSize = 16.sp,
                fontWeight = FontWeight.Bold
            )
        }
    }
}

ステップ 3:複数方向スクロール(横 + 縦)

単純な縦スクロール + 横スクロール両対応が必要なら:

@Composable
fun BiDirectionalScroll() {
    LazyVerticalGrid(
        columns = GridCells.Fixed(4),
        modifier = Modifier
            .fillMaxSize()
            .horizontalScroll(rememberScrollState())
    ) {
        items(200) { index ->
            GridItem(index)
        }
    }
}
注意:
  • horizontalScroll()LazyVerticalGrid でも使用可能
  • フリングスクロール、慣性スクロールは自動対応
  • パフォーマンスは Compose の仮想化により最適化済み

実装時の落とし穴

1. NestedScrollConnection の設定を忘れずに

親スクロールと子スクロールが両方ある場合:

val nestedScrollDispatcher = remember { NestedScrollDispatcher() }

LazyVerticalGrid(
    columns = GridCells.Fixed(3),
    modifier = Modifier
        .nestedScroll(nestedScrollDispatcher.asNestedScrollConnection())
) {
    // ... items
}

2. パフォーマンス:アイテム数が大きい場合

❌ 悪い例:すべてのアイテムを描画
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
    repeat(10000) { index ->
        GridItem(index)  // 全項目メモリに残る
    }
}
✅ 良い例:仮想化による遅延描画
LazyVerticalGrid(columns = GridCells.Fixed(3)) {
    items(10000) { index ->
        GridItem(index)  // 見える範囲だけ描画
    }
}

3. スクロール位置の保存

ユーザーが別の画面から戻ってきた時、スクロール位置を復元:

val listState = rememberLazyGridState()

LazyVerticalGrid(
    columns = GridCells.Fixed(3),
    state = listState
) {
    items(100) { index ->
        GridItem(index)
    }
}

// スクロール位置を保存
LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .collect { index ->
            // ローカルDBに保存
            saveScrollPosition(index)
        }
}

2015年実装 vs 2026年実装:比較

項目 2015年 2026年
フレームワーク View(XML + Java) Jetpack Compose(Kotlin)
コード行数 100~200行 30~50行
パフォーマンス メモリ負荷大(全アイテム保持) 軽量(仮想化)
実装難度 中~高(カスタムViewGroup必須) 低(標準APIで解決)
フリングスクロール 手実装 自動対応
nested scroll対応 複雑 シンプル
テスト容易性 高(Composable テスト用ツール充実)

代替案:特殊な要件がある場合

要件 1:斜め45度のカスタムスクロール

@Composable
fun DiagonalCustomScroll(
    angle: Float = 45f  // 45度斜めスクロール
) {
    LazyVerticalGrid(
        columns = GridCells.Fixed(3),
        modifier = Modifier
            .fillMaxSize()
            .rotate(angle)  // 斜めに回転
    ) {
        items(100) { index ->
            GridItem(index)
        }
    }
}

要件 2:スクロール速度をカスタマイズ

@Composable
fun CustomSpeedScroll() {
    val scrollState = rememberScrollState()

    Column(
        modifier = Modifier
            .verticalScroll(
                scrollState,
                flingBehavior = ScrollableDefaults.flingBehavior()
            )
    ) {
        repeat(100) { index ->
            Text("Item $index", modifier = Modifier.padding(16.dp))
        }
    }
}

まとめ

2015年:「カスタムViewGroup自作が必須」

2026年:「Compose の LazyGrid で 30行で完成」

Android 開発は急速に進化している。かつての「困難な実装」が「標準機能」に変わるのは珍しくない。

古い記事の実装方法に固執するのではなく、2026年のプラットフォーム能力を活用する方が圧倒的に効率的。


参考資料

最近、Excel で選択したセルの内容を Ollama で要約・添削できる Add-in を作ってみた

最近、Excel で選択したセルの内容を Ollama で要約・添削できる Add-in を作ってみた。

正直なところ、想定外の落とし穴がいっぱいあった。特に「ローカルネットワーク別 PC の Ollama に、Excel Add-in から安全にアクセスする」という部分が、予想以上に複雑だった。その試行錯誤プロセスを記録しておく。


背景:なぜこんなもの作ったのか

仕事で長めの文章を扱うことが多い。メール、レポート、提案書とか。

最初は ChatGPT API で要約・添削をやってみたけど、3 つの理由でやめた:

  1. データを外に出したくない - 機密文書を API に送るのは避けたい
  2. API コストが地味に積もる - 毎日使うと月額が結構かかる
  3. レスポンス待ちが遅い - 社内 LAN 上なら圧倒的に速い
そこで目を付けたのが Ollama。ローカルネットワーク別 PC で Ollama を建てて、そこに直接アクセスできれば、データも保護できるし、レスポンスも速い。

「Excel から直接使えたら最高だな」と思いついて、Add-in を作り始めたのが始まり。


技術構成

実装してみた構成は以下:

【Excel Add-in】
  ↓ HTTPS でロード
https://localhost:8888(React UI の配信)
  ↓ HTTP リクエスト
【Go バックエンド】(localhost:8888)
  ↓ ローカルネットワーク経由
【Ollama API】(http://172.19.10.10:11434)

バックエンド: Go(標準 net/http パッケージ)

  • シンプルに HTTP サーバーを立てるだけなので、フレームワークは不要だと判断
  • 自己署名証明書での HTTPS 対応

フロント: React

  • Office JavaScript API で Excel セルの読み書き
  • Go API への HTTP 通信

Ollama

  • ローカルネットワーク別 PC 上で実行
  • セルの内容を送信 → 要約・添削結果を取得

予想外の落とし穴:Excel Add-in の HTTPS 必須ポリシー

実装し始めてすぐにぶつかったのが、Excel Add-in は HTTP ではなく HTTPS が必須という制約。

「localhost で動かすんだから HTTP でいいか」と思ってた。甘かった。

さらに複雑だったのが、Ollama が ローカルネットワーク別 PC にあること。

構図:
  PC A(Excel を使ってる)
    ↓
  自分の PC(Go + React のサーバー)
    ↓ ローカルネットワーク
  PC B(172.19.10.10 に Ollama が動いてる)

やりたいこと:

  • Excel Add-in の UI(HTML/React)を自分の PC から HTTPS で配信
  • Add-in から Ollama(別 PC)に HTTP でアクセス

問題:

  • Excel Add-in は HTTPS 配信が必須だけど、自己署名証明書では「信頼されていない」と判定される
  • 別 PC の Ollama にはネットワーク経由でアクセスする必要がある

解決策 1:オレオレ証明書を localhost で動かす

まず自己署名証明書を生成した。

openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes \
  -subj "/CN=localhost"

この証明書と秘密鍵で、Go の net/http サーバーを立ち上げる。

package main

import (
    "net/http"
)

func main() {
    // ハンドラーを登録
    http.HandleFunc("/api/summarize", handleSummarize)
    http.HandleFunc("/api/proofread", handleProofread)

    // HTTPS で起動(自己署名証明書使用)
    http.ListenAndServeTLS(":8443", "server.crt", "server.key", nil)
}

これで https://localhost:8443 で HTTPS サーバーが起動。

ただし、ここまでではまだ不十分。Excel Add-in のコンテキストでは、この自己署名証明書が「信頼されていない」と判定される。


解決策:自己署名証明書 + ネットワークルーティング

ここが工夫の見せどころ。

2 つのポイントで解決した:

  1. localhost で自己署名証明書を使った HTTPS サーバー起動 - Excel Add-in の HTTPS 必須要件を満たす
  2. Go バックエンド - HTML/React を配信しつつ、Ollama への API リクエストを中継
【構図】
Excel Add-in
  ↓ HTTPS でロード
https://localhost:8888
  ↓ HTML/React の配信 + API エンドポイント
【Go サーバー】
  ├─ /(HTML/React UI を配信)
  └─ /api/summarize, /api/proofread(Ollama に転送)
    ↓ HTTP でローカルネットワーク経由
http://172.19.10.10:11434
  ↓
【Ollama API】

実装

まず自己署名証明書を生成。

openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes \
  -subj "/CN=localhost"

Go のサーバーコード:

package main

import (
    "net/http"
    "net/http/httputil"
    "net/url"
)

func main() {
    // Ollama API へのリバースプロキシを設定
    ollamaURL, _ := url.Parse("http://172.19.10.10:11434")
    reverseProxy := httputil.NewSingleHostReverseProxy(ollamaURL)

    // HTML/React UI を配信
    fs := http.FileServer(http.Dir("./public"))
    http.Handle("/", fs)

    // /api/generate は Ollama に中継
    http.HandleFunc("/api/generate", func(w http.ResponseWriter, r *http.Request) {
        reverseProxy.ServeHTTP(w, r)
    })

    // HTTPS で起動(自己署名証明書)
    http.ListenAndServeTLS(":8888", "server.crt", "server.key", nil)
}

React 側から Ollama API を呼び出すときは、相対パス /api/generate を使う。

// React コンポーネント
const handleSummarize = async (cellContent) => {
    const response = await fetch('/api/generate', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            model: 'llama2:7b',
            prompt: `要約してください:\n\n${cellContent}`,
            stream: false
        })
    });

    const result = await response.json();
    return result.response;
};

重要なポイント:

  • localhost:8888 で自己署名証明書を使った HTTPS サーバーを起動
  • Excel Add-in のセキュリティ要件(HTTPS 配信)を満たす
  • React は相対パス /api/generate で Ollama に通信(実際には Go バックエンドが仲介)
  • Go バックエンド内で、localhost から 172.19.10.10 へのネットワークルーティングを実現
  • シンプル:プロキシツール不要で、Go だけで実装完結

セルを選択→要約・添削:実装の流れ

実装が決まったら、次は実用機能。

Excel で文章が入ってるセルを選択して、「要約」「添削」ボタンを押すと、Ollama で処理してセルに結果を返す。

流れ:

  1. セル選択時のイベント取得
await Excel.run(async (context) => {
    const cell = context.application.getSelectedData();
    // セルの内容を取得
});
  1. Go API に送信
const summarized = await fetch('https://localhost:8888/api/summarize', {
    method: 'POST',
    body: JSON.stringify({ text: cellContent })
});
  1. Ollama から要約結果を取得
  • モデルはデフォルトで llama2:7b を使用
  • プロンプトエンジニアリングで「要約」「添削」を区別
  1. 結果をセルに書き込み
await Excel.run(async (context) => {
    context.worksheets.getActiveWorksheet()
        .getRange("A1")
        .values = [[result]];
});

実装時の工夫:

  • ローディング表示 - Ollama はレスポンスに数秒かかるので、「処理中...」を表示
  • エラーハンドリング - ネットワーク接続や Ollama が落ちてる場合の対応
  • タイムアウト設定 - 長すぎる文章は処理に時間がかかるので、上限を設ける
  • プロンプト最適化 - 同じモデルでも「要約」「添削」でプロンプトを変える
// 要約用プロンプト
func getSummarizePrompt(text string) string {
    return fmt.Sprintf(
        "以下のテキストを簡潔に要約してください。箇条書きで3点まで。\n\n%s",
        text,
    )
}

// 添削用プロンプト
func getProofreadPrompt(text string) string {
    return fmt.Sprintf(
        "以下のテキストの文法、表現、敬語を確認して、改善案を提示してください。\n\n%s",
        text,
    )
}

実際に使ってみて

完成して 1 ヶ月ほど使ってみた感想。

便利だったこと:

  1. データが外に出ない - 機密文書を扱うときは本当に安心。API に送ってる時間を気にしなくていい
  2. レスポンスが速い - ローカルネットワーク経由なので、ChatGPT API より圧倒的に高速。待ち時間がストレスにならない
  3. コストがゼロ - Ollama はローカル実行なので追加コストなし。PC のリソース(GPU)を使ってる分だけ

予想外だったこと:

  • 要約の質が思ったより良い - llama2:7b で十分実用的。わざわざ大型モデルに乗り換える必要がない
  • 添削より要約の方が使用頻度が高い - 最初は「添削機能メインで作ろう」と思ってたけど、実用では「要約」がメイン用途に
  • 複数人での利用が想定より難しい - 自分の PC でプロキシを立ててるので、他の PC からはアクセスできない。チーム展開するなら、プロキシを別サーバーに移す必要があった

今後の改善案:

  • Ollama の複数モデル対応 - llama2 以外の軽量モデル(mistral など)も選べるようにしたい
  • Web UI での設定画面 - 今は Go コード内でハードコードしてる設定を、UI で変更できるように
  • ローカルプロキシをサーバー化 - チーム利用を想定して、プロキシを別サーバーに移す

技術的な学び

このプロジェクトで学んだことが 3 つ。

1. Excel Add-in のセキュリティ要件は思ったより厳しい

Excel が HTTPS・自己署名証明書・CORS まで厳密にチェックする。SPA(Single Page Application)の世界と違って、企業向けアプリケーションの厳格さを感じた。

2. ローカル接続は「仲介役」として超有効

別ネットワークにある Ollama にアクセスする時、直接アクセスではなくローカルサーバー経由にすることで、セキュリティと利便性のバランスが取れた。

3. ローカル LLM の強さ

API の待ち時間がゼロに等しく、コストもかからない。確度は ChatGPT より落ちるかもしれないが、実務用途(要約・添削)には十分。「LLM の大型化よりローカル化の時代が来たのかも」と感じた。


まとめ

「Go + React + Ollama」という組み合わせで Excel Add-in を作った。

最大の工夫は「自己署名証明書 + ネットワークルーティング」という組み合わせで、ローカルネットワーク別 PC の Ollama に安全にアクセスする仕組み。

完成してみると、想定外の利便性があった。特に「データが絶対外に出ない」という安心感と「レスポンスの速さ」は、クラウド API では代替できない価値。

チーム展開や本番運用となると、プロキシのサーバー化やアーキテクチャの見直しが必要だけど、個人ツールレベルなら「自己署名証明書 + ネットワークルーティング」で十分実用的。

ローカル LLM の活用方法は、これからも増えていくんだろうなと思った。

2026/03/31

【緊急】axiosがサプライチェーン攻撃に遭遇:汚染バージョン(1.14.1 / 0.30.4)と今すぐ取るべき対策

2026年3月31日、npm パッケージ「axios」がサプライチェーン攻撃の被害に遭った。

単なる「脆弱性が見つかった」ではなく、axios のメンテナアカウントが侵害されて、悪意あるバージョンが npm に直接公開された。つまり、ダウンロードして npm install しただけで、マルウェアが仕込まれる可能性がある。

この記事は、「うちの環境で使ってるけど、どうしたらいい?」という判断フローを書いておく。


事件の概要

何が起きたのか

axios のメンテナアカウント(複数疑い)が侵害され、以下の 2 つのバージョンが悪意あるコードを含んで npm に公開された:

  • axios@1.14.1(最新安定版)
  • axios@0.30.4(レガシー版)

どちらも、同じ手口でマルウェアが混入していた。

どのくらい危険か

めっちゃ危険。理由:

  1. axios は超ポピュラー - JavaScript/Node.js 開発で最も使われている HTTP クライアントライブラリ
  2. 直接実行される - npm install 時に postinstall フック で自動実行
  3. 複数プラットフォーム対応 - Windows/Mac/Linux すべてに対応したマルウェア

実際に axios のダウンロード数は週 2000 万超。2~3 時間の公開期間でも、数千~数万の開発者が被害に遭った可能性がある。


技術的な詳細

マルウェアの動作

npm install 時に postinstall フック が実行され、以下の処理が起きる:

  1. プラットフォーム判定 - OS(Windows/Mac/Linux)を自動判定
  2. マルウェア配置 - 各 OS の隠蔽しやすい場所に RAT(リモートアクセストロージャン)を配置
  3. C2 サーバーへの接続 - sfrclak[.]com:8000 へ接続
  4. トレース消去 - node_modules から証拠を削除

各 OS でのマルウェア配置先

OS 配置先 特徴
macOS /Library/Caches/com.apple.act.mond キャッシュフォルダに偽装
Windows %PROGRAMDATA%\wt.exe, %TEMP%\6202033.vbs システムフォルダに混在
Linux /tmp/ld.py /tmp に配置(起動時消滅する可能性)

含まれていた悪意あるパッケージ

公式記述では「plain-crypto-js」という偽パッケージ。これが dependencies に追加されて、npm install 時に自動ダウンロード・実行される。


うちは大丈夫?:判断フロー

ステップ 1:package.json / package-lock.json を確認

# 正確な確認方法
grep -E "axios.*1\.14\.1|axios.*0\.30\.4" package-lock.json

もし上記コマンドで何か出たら要注意。

ステップ 2:実際にインストールされているバージョンを確認

npm list axios

出力例:

└── axios@1.14.0

1.14.0 以下、または 0.30.3 以下なら OK。1.14.1 または 0.30.4 なら危険。

ステップ 3:「うちのチームはいつこれをインストールしたのか?」を確認

npm install したのが 3月31日の 2~3 時間の間 だったかどうかで、被害の可能性が変わる。

  • 3月31日より前にインストール: 被害なし
  • ⚠️ 3月31日 12:00~15:00 GMT 付近でインストール: 被害の可能性あり
  • 3月31日 15:00 以降でインストール: 被害なし(既にパッチ済み)

リスク評価:うちは被害に遭ったのか?

ケース 1:インストールしたけど実行環境が本番ではない(社内 PC のみ)

リスク度: 中~高

対応:

  1. 該当の PC のマルウェアをスキャン(下記参照)
  2. API キー・パスワードをすべてローテーション
  3. git commit / push のログをチェック(悪意あるコードがコミットされていないか)

ケース 2:本番環境で実行中

リスク度: 超高

すぐに以下を実行:

  1. サーバー再起動
  2. 認証情報のすべてをローテーション(API キー、DB パスワード、SSH キーなど)
  3. アクセスログ確認sfrclak[.]com への外部通信がなかったか)
  4. セキュリティ監査(外部セキュリティ企業に委託検討)

ケース 3:Docker / CI/CD パイプラインで npm install を実行

リスク度: 超高

  • Docker イメージが作成されて、本番環境にデプロイされている場合
  • GitHub Actions などの CI パイプラインで実行してた場合
  • → 該当期間の全デプロイを疑う

対応:

  1. 該当期間の全デプロイを一覧化
  2. 本番環境から当該バージョン削除
  3. 全認証情報ローテーション

すぐにやるべきこと:修復手順

ステップ 1:axios をダウンロードグレード

npm install axios@1.14.0 --ignore-scripts

なぜ --ignore-scripts が必要か

  • もし 1.14.1 がまだキャッシュに残ってたり、中途半端な状態だと危険
  • グレードダウン時も postinstall フック を無視して安全にする

ステップ 2:node_modules の手動確認

# 疑わしいファイルを探す
find node_modules -name "plain-crypto-js" -type d
find node_modules -name "*.vbs" -o -name "*.exe" -o -name "*.py"

見つかったら、即座に削除:

rm -rf node_modules/plain-crypto-js
npm ci  # package-lock.json から再インストール

ステップ 3:マルウェアスキャン(PC にインストールされている場合)

Windows:

Start-MpScan -ScanType FullScan

macOS:

# マルウェアバイトスキャン推奨
# https://www.malwarebytes.com/mac から Malwarebytes ダウンロード・実行

Linux:

sudo freshclam  # パターンアップデート
sudo clamscan -r /tmp /home /root --remove

ステップ 4:全認証情報のローテーション

感染可能性がある環境では、以下をすべてローテーション:


本番環境への影響判定

チェックリスト

すべて「Yes」に近いなら、セキュリティ監査推奨。


実装側の判断:パッケージアップデートはいつする?

axios の対応タイムライン:

バージョン リリース 判定
0.30.3 以下 2026/3/31 18:00 ✅ 安全
1.14.0 以下 2026/3/31 18:00 ✅ 安全
1.14.1 2026/3/31 12:00-15:00 ❌ 危険
0.30.4 2026/3/31 12:00-15:00 ❌ 危険
1.14.2 以上 2026/3/31 15:00 ✅ パッチ済み
0.30.5 以上 2026/3/31 15:00 ✅ パッチ済み

推奨

  • 本番環境: 1.14.2 以上 または 0.30.5 以上 に即座にアップデート
  • 開発環境: 余裕があれば同じく最新版へ

今後のためのセキュリティ対策

npm パッケージ監視ツールの導入

# npm audit で定期チェック
npm audit

# dependabot で自動 PR 生成(GitHub)
# Settings → Code security and analysis → Enable Dependabot

CI パイプラインでのセキュリティスキャン

# npm audit を CI に組み込む
npm audit --audit-level=moderate

package-lock.json の厳密管理

# 本番環境では必ず --frozen-lockfile を使う
npm ci --frozen-lockfile
# 決して npm install を使わない

最後に:この事件が教えてくれること

サプライチェーン攻撃は「あり得ない」ではなく「あり得る」。

axios は信頼性の高いパッケージだが、それでも被害に遭った。理由は、人気が高いがゆえに、攻撃者にとって「おいしいターゲット」だったから。

対策:

  1. すべてのパッケージを信用しない - npm audit を常態化
  2. 本番環境を隔離 - 必ず --frozen-lockfile を使う
  3. credentials の定期ローテーション - 3~6ヶ月ごと
  4. ネットワークログの監視 - 不審な外部接続を検出

2026 年も、サプライチェーン攻撃の脅威は続く。

2026/03/26

SSL証明書の有効期限短縮ショック…手動更新の限界と自動化へ

CA/Browser Forum の新ルール:2026年から始まる SSL 証明書の有効期限の段階的短縮化。

最終的には 2029年に 47 日 という極めて短い有効期間になる。これまで「年 1 回更新」で済んでたやつが、近い未来「月複数回更新」になる可能性がある。

正直、手動管理では確実に破綻する。この記事は、「うちのチーム、どう対応すればいい?」という実装判断を書いておく。


タイムラインの全貌

CA/Browser Forum が定めた段階的短縮化

時期 有効期限 管理頻度目安 影響度
現在(2026年3月まで) 398 日 年1回更新
2026年3月15日~ 200 日 年1~2回更新
2027年3月15日~ 100 日 年3~4回更新
2029年3月15日~ 47 日 月複数回更新 超高

2029年の恐怖:47日制限とは

47日という期間は、現在の管理方法では実質的に自動化が必須を意味する。

なぜなら:

  • 人間が「そろそろ更新しようか」と判断→実行 → 検証 → デプロイ
  • この一連が確実に完了するまでに、確実に 1~2週間かかる
  • 47日 - 2週間 = 残り約5週間の余裕
  • その間に他の作業が入ると、簡単に「あ、更新忘れた」になる

問題の本質:「手動管理の破綻」

ケース 1:小規模チーム(1~3人)

現状

  • 証明書が 1~3個
  • 年1回のリマインダーで更新

2029年以降

  • 証明書が同じ数でも、更新頻度が 約 9 倍
  • 月に複数回、毎回「更新→検証→デプロイ」の手作業
  • 誰か 1人が退職したら、知識が失われてヤバい

結果:確実に誰かが忘れる → SSL エラー → サービス停止 → 信用喪失

ケース 2:中規模チーム(複数サーバー、複数ドメイン)

現状

  • 証明書が 10~20 個
  • スプレッドシートで管理

2029年以降

  • 月に 10~20 個 × 複数回の更新作業
  • 手作業では追いつかない
  • ミスの確率が指数関数的に増加

結果:管理が混乱 → 本番環境で SSL エラー → 顧客影響

ケース 3:大規模企業(複数部門、複数国)

現状

  • 証明書が 100 個超
  • 既に何かしらの管理システムで対応

2029年以降

  • 手動ステップが多いと管理システム自体がボトルネック
  • 完全自動化への移行が必須

解決策:ACME プロトコルによる完全自動化

根本的な対応:自動更新の導入

2029年の 47 日制限に耐えられる唯一の方法は、ACME プロトコルを使った完全自動更新

ACME とは:

  • Automated Certificate Management Environment
  • Let's Encrypt が提唱した、証明書の自動更新プロトコル
  • OpenSSL コマンドで certbot を使うことが最も一般的

実装方法:Certbot を使った自動更新

ステップ 1:Certbot のインストール

# Ubuntu/Debian
sudo apt-get install certbot python3-certbot-nginx

# CentOS/RHEL
sudo yum install certbot python3-certbot-nginx

# macOS
brew install certbot

ステップ 2:証明書の自動更新設定

# 証明書を取得(初回)
sudo certbot certonly --nginx -d example.com -d www.example.com

# 自動更新の動作確認
sudo certbot renew --dry-run

ステップ 3:cron で定期実行

# root ユーザーで crontab を編集
sudo crontab -e

# 以下を追加(毎日 2:30 に更新試行)
30 2 * * * /usr/bin/certbot renew --quiet && systemctl reload nginx

この設定で:

  • 毎日自動で更新チェック
  • 更新が必要なら自動実行
  • Nginx をリロード
  • すべて無人で完結

メジャーな証明書発行者の ACME 対応

CA ACME サポート URL 推奨度
Let's Encrypt https://letsencrypt.org ⭐⭐⭐⭐⭐
GlobalSign ACME API あり ⭐⭐⭐⭐
DigiCert ACME API あり ⭐⭐⭐⭐
Sectigo ACME API あり ⭐⭐⭐⭐

推奨:Let's Encrypt でテストしてから、有料 CA に移行。


段階的な導入ロードマップ

Phase 1:現状把握(今月)

- ドメイン名 / 発行元 CA / 有効期限 / 更新予定日

- 誰が更新してるのか / どのサーバーが対象か / 更新後の検証手順は

Phase 2:Let's Encrypt で試験運用(1~2ヶ月)

- インストール / 自動更新の動作確認 / 運用手順の整理

- Let's Encrypt の証明書を取得 / 1ヶ月運用してみる / 特に問題ないか確認

Phase 3:段階的な本番環境への展開(2~4ヶ月)

- 更新スクリプトの改善 / 監視・アラート設定 / トラブル対応の経験値を貯める

- 100日制限への対応が必須になる前に

Phase 4:完全自動化(2027年まで)

- ACME ベースの統一管理 / 中央管理ダッシュボード / 異常時のアラート設定


実装時の注意点

1. Let's Encrypt のレート制限

Let's Encrypt は無料だが、制限がある:

  • 週あたり 50 個の新規証明書
  • 1 ドメインあたり 5 個/週

複数ドメインで同時に導入すると、レート制限に引っかかる可能性がある。段階的導入が大事。

2. DNS 検証 vs HTTP 検証

Certbot の検証方法は 2 種類:

方法 特徴 推奨度
HTTP 検証 .well-known フォルダで検証。シンプル ⭐⭐⭐⭐⭐
DNS 検証 DNS レコード追加で検証。ワイルドカード対応 ⭐⭐⭐⭐

通常は HTTP 検証で十分。DNS 検証が必要なのはワイルドカード証明書の場合。

3. 更新スクリプトのテスト

本番環境を止めないために:

sudo certbot renew --dry-run --non-interactive

# このコマンドを cron で定期実行して
# 「更新が失敗していないか」を事前に検知する

4. 監視・アラート設定

ACME 自動化しても「更新に失敗する」可能性はある。例:

  • DNS が一時的に応答しない
  • サーバーのディスク容量が満杯
  • スクリプトのバグ

対策:

30 2 * * * /usr/bin/certbot renew --quiet || \
  mail -s "SSL renewal failed" admin@example.com

コスト評価

導入前(手動管理)

項目 金額 説明
証明書購入費 月 0~10万円 CA による
人件費(更新作業) 月 2~5万円 年1回なら月割り
合計 月 2~15万円

導入後(ACME 自動化)

項目 金額 説明
証明書購入費 月 0~10万円 CA による(Let's Encrypt なら ¥0)
人件費(初期構築) 50~100万円 1 回限り
人件費(運用・監視) 月 1~3万円 異常時対応のみ
合計 初期 50~100万円 + 月 1~13万円

ROI

  • 初期投資が回収できるのは、約 3~6ヶ月
  • 5 年運用なら 年平均で 15~20万円削減

実装チェックリスト

今月やること

来月やること

3ヶ月以内にやること

2027年3月までにやること


最後に:なぜこんなことになるのか?

CA/Browser Forum が証明書有効期限を短縮化した背景:

  1. セキュリティ向上 - 秘密鍵の露出リスクを早期に低減
  2. 自動化の推進 - 手動管理では対応できないレベルに引き上げることで、業界全体の自動化を加速
  3. 脆弱性対応の高速化 - 有効期限が短ければ、脆弱性検出時に「全サーバーの置き換え」が現実的

つまり、業界全体を「手動管理」から「自動化」へ強制的にシフトさせるための施策。

2029年の 47日制限は、もはや「手動では対応不可」という業界の合意。

早めに自動化しておけば、慌てずに対応できる。逆に、2028年になって「あ、対応しなきゃ」って思っても、もう手遅れ。

今から準備しておこう。

2026/03/20

YouTube LiveのチャットだけをAndroidで別画面表示するアプリを作ってリリースした

YouTube LiveのチャットだけをAndroidで別画面表示するアプリを作ってリリースした

テレビやモニターでYouTube Liveを全画面再生していると、ライブチャットが見えなくなります。スマホでYouTubeアプリを開き直しても動画が再生されてしまう。「Live Chatだけをスマホで別画面表示したい」という需要、ライブ配信を見る人なら絶対あると思っていました。そのまま自分で作ってリリースしました。

作ろうと思ったきっかけ

大画面でライブ配信を見ているとき、Live Chatの流れをリアルタイムで追いたいことがよくあります。でもテレビのフルスクリーン表示ではチャット欄が隠れてしまう。スマホでYouTubeアプリを開き直すと動画が二重再生になって邪魔。

既存のアプリを探してみたのですが、ちょうどいいものが見つからなかったので、自分で作ることにしました。シンプルに「URLを登録したらLive Chatだけが表示される」、それだけのアプリです。

アプリでできること

  • YouTube LiveのURLを登録してLive Chatのみをリアルタイム表示する
  • YouTubeアプリのシェアボタンから直接URLを送って登録できる
  • 動画を再生せずチャット画面だけを表示する
  • テレビ・モニターでライブを見ながらスマホでチャットを追える
ポイント

YouTubeアプリのシェアボタン経由で登録できるのが地味に便利です。URLをコピペする手間がなく、見ているライブ配信をそのまま登録してチャット画面を開けます。

使い方

  1. テレビやモニターでYouTube Liveをフルスクリーン再生する
  2. スマホのYouTubeアプリで同じライブ配信を開き、シェアボタンをタップ
  3. 共有先から「ChatTube」を選択
  4. アプリにLive Chatがリアルタイムで表示される

もちろんURLを直接入力して登録することもできます。動画IDだけでも対応しています。

こんな場面で使える

  • ライブ配信をテレビで見ながら、スマホでLive Chatをリアルタイムに追う
  • PC作業中に別モニターでライブ再生しつつ、スマホでチャットだけ確認する
  • 家族と大画面でライブを見ながら、自分だけチャットを追いたいとき
  • 配信者がチャットの反応を別画面で確認しながら配信するとき
セカンドスクリーン活用

テレビのリモコンでYouTubeを操作しているとチャット欄へのアクセスが面倒です。このアプリを使えばスマホをLive Chat専用画面として使えます。

ダウンロード

Android向けに Google Play で公開しています。無料でダウンロードできます(広告あり)。

Google Play で「ChatTube」を見る

不具合・要望があればコメントかレビューで教えてもらえると助かります。使ってみた感想もぜひ。

2026/03/13

LangChainでAI駆動開発を自動化する:要件定義からテストまでフェーズ別エージェント実装ガイド

LangChainでAI駆動開発を自動化する:要件定義からテストまでフェーズ別エージェント実装ガイド

うちのチームに新しいメンバーが入ってきて、「LangChainを使えば開発全部自動化できますよね?」と言い出しまして。気持ちはわかるんですが、そこには大きな落とし穴があります。要件定義で使うべき思考と、テストケースを考えるときの思考は、根本的に違う。それを1つのエージェントに丸投げするとどうなるか——自分で試して痛い目を見ました。この記事では、フェーズごとに専門エージェントを分業させるアーキテクチャをLangChainで実装する方法を、実際に動くコードとともに解説します。

注目ポイント

エージェントはステートレスに設計し、フェーズ間の引き渡しは「ドキュメントオブジェクト」で行うのが安定運用の鍵。コンテキストを引き継ぐのではなく、前フェーズの成果物を次フェーズへの入力として渡す。

なぜ「1エージェント・全工程」は破綻するのか

LangChainのReActエージェントやLCELチェーンを使って「ユーザーの要望を入力したら設計書とコードとテストが出てくる」パイプラインを作ろうとした経験がある方は多いと思います。最初の数回は動く。でも少し複雑な要件を入れた途端、出力が崩れ始める。

原因はシンプルで、LLMのコンテキストウィンドウには限界があるからです。要件定義の議論をしながら、同時に実装コードの詳細まで考えさせると、どちらも中途半端になる。さらに問題なのは、エラーになるのではなく「それっぽい何か」が出力されてしまうこと。これが一番タチが悪い。

よくある失敗パターン

「1つの巨大プロンプトに全フェーズの指示を詰め込む」アプローチ。最初は動いて見えるが、要件が複雑になった瞬間に崩壊する。プロンプトエンジニアリングで解決しようとするより、設計を分けるほうが早い。

全体設計と前提環境

今回実装する構成はこうなります。4つの専門エージェントを順番に実行し、各エージェントの出力をPydanticモデルで型定義した「ドキュメントオブジェクト」として次のエージェントに渡します。

# 全体のフロー
ユーザー入力(要件メモ)
    ↓
RequirementsAgent  → RequirementsDoc(曖昧さリスト・確定仕様)
    ↓
DesignAgent        → DesignDoc(アーキテクチャ・API定義・DB設計)
    ↓
CodingAgent        → CodeDoc(実装コード・ファイル構成)
    ↓
TestAgent          → TestDoc(テストケース・テストコード)

前提環境

pip install langchain langchain-community langchain-core
pip install pydantic ollama

# Ollamaでモデルを起動しておく
ollama run gpt-oss:20b

OpenAI APIを使う場合は langchain-openai に差し替えるだけです。今回はローカルLLM(Ollama)ベースで書きますが、モデル部分は差し替え可能な設計にします。

要件定義エージェント:曖昧さを構造化する

要件定義エージェントの仕事は2つ。ひとつは入力テキストから曖昧な箇所を検出して質問リストを生成すること、もうひとつは確定した仕様を構造化されたオブジェクトに変換することです。

from langchain_community.llms import Ollama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
from typing import List

# 出力の型定義
class RequirementsDoc(BaseModel):
    ambiguities: List[str] = Field(description="曖昧な点・確認が必要な事項のリスト")
    confirmed_specs: List[str] = Field(description="確定した仕様のリスト")
    constraints: List[str] = Field(description="技術的・ビジネス的制約のリスト")
    summary: str = Field(description="要件の概要(2〜3文)")

class RequirementsAgent:
    def __init__(self, model_name: str = "gpt-oss:20b"):
        self.llm = Ollama(model=model_name, temperature=0.1)
        self.parser = JsonOutputParser(pydantic_object=RequirementsDoc)
        self.prompt = ChatPromptTemplate.from_messages([
            ("system", """あなたはシニアのシステムアナリストです。
入力された要件テキストを分析し、以下を必ずJSON形式で出力してください。
- 定義が曖昧で確認が必要な項目
- 確定した仕様
- 技術的・ビジネス的制約
- 要件の概要

{format_instructions}"""),
            ("human", "以下の要件を分析してください:\n\n{requirements_text}")
        ])

    def run(self, requirements_text: str) -> RequirementsDoc:
        chain = self.prompt | self.llm | self.parser
        result = chain.invoke({
            "requirements_text": requirements_text,
            "format_instructions": self.parser.get_format_instructions()
        })
        return RequirementsDoc(**result)
設計のポイント

temperature=0.1 に設定するのがミソ。要件定義は「創造性」より「網羅性・一貫性」が重要なので、低めのtemperatureで安定した出力を得る。

設計エージェント:仕様書からアーキテクチャを生成する

設計エージェントは RequirementsDoc を受け取り、アーキテクチャ案・API定義・DBスキーマを出力します。ここでのポイントは、前フェーズの「曖昧さリスト」も一緒に渡すこと。曖昧なままの仕様を無視した設計を出力させないためです。

class DesignDoc(BaseModel):
    architecture: str = Field(description="アーキテクチャの説明(テキスト)")
    api_endpoints: List[dict] = Field(description="APIエンドポイント定義リスト")
    db_schema: List[dict] = Field(description="DBテーブル定義リスト")
    tech_stack: List[str] = Field(description="使用技術スタック")
    assumptions: List[str] = Field(description="設計上の前提・仮定(曖昧仕様への対応)")

class DesignAgent:
    def __init__(self, model_name: str = "gpt-oss:20b"):
        self.llm = Ollama(model=model_name, temperature=0.2)
        self.parser = JsonOutputParser(pydantic_object=DesignDoc)
        self.prompt = ChatPromptTemplate.from_messages([
            ("system", """あなたはシニアのソフトウェアアーキテクトです。
確定仕様と制約に基づいてシステム設計を行い、JSON形式で出力してください。
曖昧な仕様については、設計上の前提として assumptions に明記してください。

{format_instructions}"""),
            ("human", """確定仕様: {confirmed_specs}
制約: {constraints}
曖昧な点(前提として扱う): {ambiguities}
要件概要: {summary}""")
        ])

    def run(self, req_doc: RequirementsDoc) -> DesignDoc:
        chain = self.prompt | self.llm | self.parser
        result = chain.invoke({
            "confirmed_specs": "\n".join(req_doc.confirmed_specs),
            "constraints": "\n".join(req_doc.constraints),
            "ambiguities": "\n".join(req_doc.ambiguities),
            "summary": req_doc.summary,
            "format_instructions": self.parser.get_format_instructions()
        })
        return DesignDoc(**result)

実装エージェント:設計書をコードに落とす

実装エージェントは DesignDoc のAPIエンドポイント定義とDBスキーマを受け取り、コードを生成します。全体を一度に生成しようとするとコンテキスト超過するので、エンドポイントごとに分割して呼び出す設計にしています。

class CodeDoc(BaseModel):
    files: List[dict] = Field(description="生成したファイルのリスト(filename, content)")
    setup_instructions: str = Field(description="セットアップ手順")

class CodingAgent:
    def __init__(self, model_name: str = "gpt-oss:20b"):
        self.llm = Ollama(model=model_name, temperature=0.15)
        self.prompt = ChatPromptTemplate.from_messages([
            ("system", """あなたはシニアのバックエンドエンジニアです。
設計書に基づいてPythonコードを生成してください。
- FastAPIを使用
- 型ヒントを必ず付ける
- docstringは英語で記述
- エラーハンドリングを適切に実装"""),
            ("human", """以下のAPIエンドポイントを実装してください:
{endpoint}

DBスキーマ: {db_schema}
技術スタック: {tech_stack}""")
        ])

    def run(self, design_doc: DesignDoc) -> CodeDoc:
        all_files = []
        # エンドポイントごとに分割生成
        for endpoint in design_doc.api_endpoints:
            chain = self.prompt | self.llm
            code = chain.invoke({
                "endpoint": str(endpoint),
                "db_schema": str(design_doc.db_schema),
                "tech_stack": ", ".join(design_doc.tech_stack)
            })
            all_files.append({
                "filename": f"api_{endpoint.get('path','').strip('/').replace('/','_')}.py",
                "content": code
            })
        return CodeDoc(
            files=all_files,
            setup_instructions=f"Tech stack: {', '.join(design_doc.tech_stack)}"
        )

テストエージェント:コードからテストを生成する

テストエージェントは生成されたコードファイルを受け取り、pytestベースのテストコードを生成します。ここでもファイルごとに分割呼び出しです。一度に全コードを渡すとテストの質が落ちる。

class TestDoc(BaseModel):
    test_files: List[dict] = Field(description="テストファイルのリスト(filename, content)")
    coverage_notes: List[str] = Field(description="カバレッジ上の注意点・未テスト箇所")

class TestAgent:
    def __init__(self, model_name: str = "gpt-oss:20b"):
        self.llm = Ollama(model=model_name, temperature=0.1)
        self.prompt = ChatPromptTemplate.from_messages([
            ("system", """あなたはQAエンジニアです。pytestを使ったテストコードを生成してください。
- 正常系・異常系・境界値を網羅
- モックは unittest.mock を使用
- テスト関数名は test_[動詞]_[対象]_[条件] の形式で"""),
            ("human", "以下のコードのテストを生成してください:\n\n{source_code}")
        ])

    def run(self, code_doc: CodeDoc) -> TestDoc:
        test_files = []
        for file in code_doc.files:
            chain = self.prompt | self.llm
            test_code = chain.invoke({"source_code": file["content"]})
            test_files.append({
                "filename": f"test_{file['filename']}",
                "content": test_code
            })
        return TestDoc(
            test_files=test_files,
            coverage_notes=["境界値テストは手動での確認を推奨"]
        )

オーケストレーション:4エージェントをパイプラインで繋ぐ

最後に、4つのエージェントを順番に実行するオーケストレーターを実装します。各フェーズの結果をログに残しておくと、どこで品質が落ちたか後から追跡できます。

import json
from datetime import datetime
from pathlib import Path

class DevPipeline:
    def __init__(self, model_name: str = "gpt-oss:20b", output_dir: str = "output"):
        self.req_agent = RequirementsAgent(model_name)
        self.design_agent = DesignAgent(model_name)
        self.coding_agent = CodingAgent(model_name)
        self.test_agent = TestAgent(model_name)
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(exist_ok=True)

    def run(self, requirements_text: str) -> dict:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        results = {}

        print("▶ Phase 1: 要件定義エージェント")
        req_doc = self.req_agent.run(requirements_text)
        results["requirements"] = req_doc.model_dump()
        self._save(f"{timestamp}_requirements.json", results["requirements"])
        print(f"  曖昧な点: {len(req_doc.ambiguities)}件 / 確定仕様: {len(req_doc.confirmed_specs)}件")

        print("▶ Phase 2: 設計エージェント")
        design_doc = self.design_agent.run(req_doc)
        results["design"] = design_doc.model_dump()
        self._save(f"{timestamp}_design.json", results["design"])
        print(f"  APIエンドポイント: {len(design_doc.api_endpoints)}件")

        print("▶ Phase 3: 実装エージェント")
        code_doc = self.coding_agent.run(design_doc)
        results["code"] = code_doc.model_dump()
        self._save(f"{timestamp}_code.json", results["code"])
        print(f"  生成ファイル: {len(code_doc.files)}件")

        print("▶ Phase 4: テストエージェント")
        test_doc = self.test_agent.run(code_doc)
        results["tests"] = test_doc.model_dump()
        self._save(f"{timestamp}_tests.json", results["tests"])
        print(f"  テストファイル: {len(test_doc.test_files)}件")

        return results

    def _save(self, filename: str, data: dict):
        with open(self.output_dir / filename, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

# 実行例
if __name__ == "__main__":
    pipeline = DevPipeline(model_name="gpt-oss:20b")
    requirements = """
    ユーザー管理APIを作りたい。
    ユーザーの登録・ログイン・プロフィール更新ができること。
    認証はJWTを使う。DBはPostgreSQLを想定。
    """
    results = pipeline.run(requirements)
運用Tips

各フェーズの出力をJSONファイルとして保存しておくと、途中のフェーズからやり直せます。要件定義だけ人間がレビューして修正し、設計以降を再実行する、という使い方が現実的です。

実運用での限界と人間の介在ポイント

このパイプラインを実際に使ってみて気づいた限界をいくつか挙げておきます。まず、要件が曖昧なまま進むと雪だるま式に品質が落ちる。RequirementsAgentが出した曖昧さリストを無視してDesignAgentに進むと、設計の前提が崩れ、コードもテストも連鎖的に歪みます。

もうひとつは、テストエージェントのエッジケース認識が甘いこと。正常系は網羅してくれますが、「このAPIに不正なJWTを渡したらどうなるか」といった攻撃的なテストケースは自分で足す必要があります。コストの話をすると、ローカルLLMなのでAPI費用はゼロですが、20Bモデルで全フェーズ回すと5〜10分かかります。小規模プロジェクトなら十分実用的な速度です。

人間が介在すべきポイントは明確で、Phase 1終了後の曖昧さレビューPhase 3終了後のコードレビューの2点。それ以外はエージェントに任せて、人間は判断に集中する——これがこのアーキテクチャの狙いです。

Google Antigravityで変わるAI駆動開発:フェーズ別エージェント分業の実践

Google Antigravityで変わるAI駆動開発:フェーズ別エージェント分業の実践

先月、お客さんから「AI使えばもう設計書いらないですよね?」と言われまして。思わず苦笑いしてしまいました。確かに、Copilotやら何やらで「コードを書く速度」は上がった。でも要件定義でAIに曖昧な指示を投げると、自信満々で的外れなものを返してくる。「1つのAIに全部やらせる」という発想は、もう少し慎重に考えたほうがいいんじゃないかと思っていたんですよね。そこに出てきたのが、GoogleのAntigravityです。

注目ポイント

Antigravityの本質は「コード補完の強化」ではなく、「フェーズごとに専門エージェントを分業させ、人間はレビューに集中する」という設計思想の転換にある。

なぜ「1つのAI」では限界が来るのか

CursorやGitHub Copilotを使っている方なら実感があると思いますが、あのツールは基本的に「今書いているコードの文脈」しか見ていません。要件定義の話をしながら同時に実装コードを提案させると、途端にコンテキストが混乱する。結果、中途半端な出力になる。

これは設計の問題でもあって、要件定義・設計・実装・テストはそれぞれ「考え方の粒度」が全然違います。要件定義は「何を作るか」の議論、設計は「どう作るか」の整理、実装は「書く」作業、テストは「壊す」作業。これを1つのLLMに同時にやらせようとするから歪みが出る。Antigravityはここに目を向けた、という点で面白い。

Google Antigravityとは何者か

2025年11月にGoogleが発表したエージェント・ファーストのIDEです。VS Codeフォークなので、今使っている拡張機能や設定はそのまま使えます。この点はチームへの展開コストが低くて助かりますね。

特徴的な機能は3つです。エージェント・マネージャーでバックグラウンドの複数エージェントを並行稼働させられること、ブラウザ・エージェントがAI自身でドキュメントを調査してスクリーンショットまで撮れること、そしてArtifactsでAIの処理結果をタスクリスト・プラン・動画として可視化できること。この3つが噛み合うと、開発フローが変わります。

要件定義フェーズ:曖昧さを炙り出すエージェント

要件定義で一番つらいのは「曖昧なまま進んでしまうこと」です。お客さんの言う「使いやすいUI」って何なのか、「リアルタイム」って何ミリ秒を指しているのか。ここを詰め切れないまま設計に入ると、後で痛い目を見ます。

Antigravityでは、要件ヒアリングのメモや議事録を投げると、エージェントが「この要件は定義が曖昧です。以下を確認してください」という質問リストをArtifactsとして出力します。人間がやると遠慮が入るこの「ツッコミ」を、AIに任せるのは案外合理的です。

注意

エージェントが出した質問リストをそのままお客さんに送るのは危険です。技術的すぎる、あるいは失礼な表現が混じることがある。あくまで「人間がレビューする素材」として使うのが正解です。

設計フェーズ:Artifactsで叩き台を作らせる

設計フェーズでのAntigravityの使い方は「叩き台の高速生成」です。確定した要件をまとめたドキュメントを渡すと、エージェントがアーキテクチャ案・テーブル設計・APIエンドポイント一覧をArtifactsとして出力します。

これが地味に効いてくる。設計レビューって、ゼロから書いたものを見るより「これでいいか?」と叩き台を渡された方が指摘しやすいんですよね。エンジニア全員が設計を書ける訳じゃないし、特にジュニアメンバーは「何から書けばいいかわからない」という状態になりやすい。Artifactsが最初のたたき台を作ってくれるだけで、議論の密度が上がります。

実装フェーズ:ブラウザ・エージェントに調査を丸投げする

実装中に一番時間を取られるのって、コードを書くことより「ドキュメントを読む時間」じゃないかと思っています。新しいライブラリを使うとき、公式ドキュメントを読んで、Stack Overflowを漁って、GitHubのIssueを確認して…。

Antigravityのブラウザ・エージェントは、AIが自分でブラウザを操作してドキュメントを調査し、「このライブラリでやりたいことを実現するコード例」を持ってきます。自分が読む必要がなくなる、とは言いませんが、調査の起点として使うと明らかに速い。うちのチームでは「まずAntigravityに調べさせて、人間が裏取りする」という流れに変わりつつあります。

テストフェーズ:バックグラウンドで回し続ける

テストエージェントをバックグラウンドで動かしながら実装を続ける、というのがAntigravityの真骨頂です。実装中にエージェントが並行して「このコードに対するテストケース案」を生成し続けてくれる。

正直、テストを書く習慣がないチームには少し敷居が高いですが、「テストの素材をAIに作らせて、人間が選ぶ」というプロセスなら受け入れやすい。全部信用するのは危ないですが(エッジケースの認識が甘いことがある)、カバレッジの穴を埋める用途には十分使えます。

実践メモ

バックグラウンドエージェントはリソースを食います。ローカルマシンのスペックが低いと逆に開発体験が悪化します。チーム導入前にスペック要件を確認しておくのが無難です。

人間がやるべき仕事は何か

ここまで書いてきて思うのは、Antigravityを使いこなすには「何をエージェントに任せ、何を自分で判断するか」のタスク設計センスが問われるということです。ツールが変わっても、要件の本質を掴む力や、設計の良し悪しを判断する経験値は、引き続き人間の仕事です。

コストの話をすると、現時点でAntigravityのエンタープライズプランは月額が安くない。ただ、要件定義のやり直しが1回減るだけで、十分に元が取れる規模感の案件が多いのも事実です。まずは小規模プロジェクトで試して、上司への稟議のネタを作るところから始めるのが現実的かなと思っています。