前回、Claude Codeと1日でリアルタイム音声AIを作った。今回はその続き。開発を続けるうちに、ある割り切りに行き着いた——人間はコードを読まない。読むのはAIで、人間は図と意図を持つ。この分業を成立させるために2本のPRを重ねた記録である。
図をdraw.ioではなくMermaidで書く理由
「仕組みをUMLで描いてほしい、コードは読まないから」と頼んだところ、返ってきた提案はdraw.ioではなくMermaid(テキストで書く作図記法)だった。理由は3つ。
- GitHubがREADMEやMarkdown内で自動レンダリングする
- テキストなのでPRで差分レビューできる
- AIが生成・更新できる=実装を変えるPRに図の更新を同梱できる
図はコードと同じで、現実とズレた瞬間に価値がマイナスになる。draw.ioのXMLは差分が読めず、更新が億劫になり、確実に腐る。Mermaidならコードと同じレビューの土俵に乗る。
こうして docs/architecture.md に6枚の図が入った。全体像、中継サーバーの2ループ構造(「両耳に受話器を持つ通訳者」)、Push-to-Talkの1ターン、function callingの伝票フロー、認証の門番ページ、切断と再接続の状態遷移。「実装を変えるPRでは該当する図も更新する」という運用ルールもREADMEに明記した。
実際、この後のPRで図に赤が入った。「Responses APIもOpenAIなんだから、OpenAIと分かるように囲んだほうがいい」——コードを読まない人間が図を見て設計の曖昧さを指摘し、AIが図とコードを直す。分業が回り始めた瞬間だった。
522行のmain.pyを7つに割る
図が揃ったところで、522行に育ったmain.pyを責務単位に分割した。
backend/
├── main.py # FastAPIの組み立てとルーティングだけ(90行)
├── config.py # 環境変数
├── auth.py # Cognito検証
├── relay.py # WebSocket中継の本体
├── personas.py / history.py / search.py
面白いのは、この分割を最初からやらなかったこと。当初「分割の必要ある?」と聞いたら、AIの答えは「今はない。トリガー(2個目のツール追加・800行超え・共同開発者の出現)を決めて、それまで触らない」だった。リファクタリングは「読みにくくて困った実績」が出てからやるのが一番安い。今回は次の機能(WebRTC実装)が控えていたので、その前準備として発動した。
挙動は1ミリも変えない純リファクタリングだが、検証は全経路E2E(認証・WebSocket・実際の音声往復)で行った。純リファクタリングこそ回帰が怖い。
余談: 直コミットにキレる人の話
docsだけの修正をPRなしでmasterに直コミットする運用にしたとき、昔の記憶が蘇った。以前、似たことをして30行のSlackで詰められたことがある。
いま思えばあれはプロセスの欠陥だった。masterへの直コミットがそんなに重大なら、ブランチ保護(GitHubの設定1つ)で物理的に不可能にできる。それをせず人間の自制心に任せ、破られたら感情で詰めるのは「機械が防ぐべきことを人間を責めて解決しようとする」行為だ。ポカヨケの思想——通れる扉を開けておいて、通った人を怒鳴るのは、扉の設計者の敗北である。
というわけで、我が家の運用はこうなった: 個人リポジトリは「挙動が変わればPR、docsは直コミット」のハイブリッド。共有リポジトリを持つ日が来たら、初日にブランチ保護を入れる。ルールは感情ではなく設定で守る。
コードは yoshiharu-ishii/realtime_voice で公開している。次回はWebRTC実装編。
リアルタイム音声AI 開発シリーズ
- Claude Codeと1日で作るリアルタイム音声AI ― $5から始めて、RAG・ペルソナ・Cognito認証・Terraformまで
- 「コードは読まない」開発スタイル ― Mermaid図解とモジュール分割で人間とAIの分業を作る(この記事)
- AIが空耳する日 — 音声通話アプリの誤検知対策、文字起こし崩壊の犯人探し、そしてコンテナ化
- terraform applyで音声AIサービスが立つまで — User Pool全消し事故と、死なないAPIキーの謎
- User Poolはインフラではなくデータである — Terraformスタック分割の2軸
- WebSocket中継 vs WebRTC直結 ― 両方実装して固有名詞で殴り合わせたら、精度の勝敗とバグが2匹出てきた
- 無線機から電話へ ― サーバーVADでハンズフリー通話を実装する