書籍コードで農家AIVTuberを作るvol1:STEP1で学んだPythonアーキテクチャ設計の本質
書籍コードで農家AIVTuberを作る:STEP1で学んだPythonアーキテクチャ設計の本質
起:クレソン農家がAIVTuberを作ろうとした理由
私はナナカファームという屋号でクレソンを栽培している農家兼大学院生です。Neo4jで農業記録システムを作ったり、クレソンのレシピを提案するチャットAI(クレソンAI)を本番稼働させたりと、農業×テクノロジーの取り組みを続けてきました。
次のチャレンジとして取り組んでいるのが、農家AIVTuber「ナナカ」の開発です。
「農業のことを話せるVTuberがいたら面白い」という単純な動機でしたが、実装を始めると想像以上に学びが深かった。特にSTEP1として取り組んだ書籍コードの読み解きと移植作業では、Pythonのモジュール設計・非同期処理・API連携の基本が凝縮されていました。
今回は
「AITuberを作ってみたら生成AIプログラミングがよくわかった件」(日経BP)
を使ったSTEP1の実装過程と、書籍コードから学んだアーキテクチャ設計の本質をまとめます。
承:書籍コードを読んで気づいた「設計の意図」
アダプターパターンが全体を支えている
書籍のコードを core/ ディレクトリに移植してみると、全体がアダプターパターンで設計されていることに気づきます。
コメント取得(YouTubeAdapter)
↓
LLM応答生成(OpenAIAdapter)
↓
音声合成(VoicevoxAdapter)
↓
音声再生(PlaySound)
↓
配信出力(OBSAdapter)
各アダプターは独立していて、aituber_system.py が全体を束ねる構成です。これによりYouTube連携だけをモック化して開発を進める(= run_dummy.py)ことができる。STEP1では run_dummy.py でダミーコメントを5件流すだけですが、後のステップでLangGraphやNeo4jに差し替える際も、アダプターを差し替えるだけで対応できる設計になっています。
VOICEVOXの「2段POST」という設計の妙
VOICEVOXとの連携で最も印象的だったのが、音声生成が2回のHTTPリクエストに分かれている点です。
# 1回目: テキスト → クエリJSON(発音・アクセント・抑揚)
response = requests.post(f"{self.url}audio_query", ...)
# 2回目: クエリJSON → WAVバイナリ
response = requests.post(f"{self.url}synthesis", ...)
/audio_query で中間表現(クエリJSON)を生成し、/synthesis でそのクエリから音声波形を生成する。この2段構成の意図は**「中間で話速・ピッチ・抑揚を調整できる余地を残す」**ことです。
将来的に感情状態管理(STEP3)を実装するとき、例えば「怒った状態のときは話速を上げる」という実装が、audio_query のパラメータ変更だけで実現できる。単純にAPIを叩くだけではなく、なぜ2段にするのかという設計思想が理解できると、後のステップの実装イメージが鮮明になりました。
会話履歴管理の「5往復FIFO」
OpenAIAdapter の中に SAVE_PREVIOUS_CHAT_NUM = 5 というクラス定数があります。
def _update_messages(self, question, answer):
self.chat_log.append({"question": question, "answer": answer})
if len(self.chat_log) > self.SAVE_PREVIOUS_CHAT_NUM:
self.chat_log.pop(0) # 最古のものを削除
最大5往復を保持し、超過したら古いものから pop(0) で削除するFIFO(先入れ先出し)構造です。なぜ5往復かというと、当時のgpt-3.5-turboのコンテキストウィンドウ(4096トークン)に収めるためのバランス設計。
これをSTEP4の視聴者記憶(Neo4j連携)と対比すると面白いです。chat_log はセッション内のメモリ(短期記憶)であり、Neo4jに保存する視聴者ごとの過去情報は長期記憶に相当します。人間の記憶モデルと同じ構造が、コードレベルで実現されていることに気づきます。
転:理解が不十分だった部分と、気づいた改善ポイント
書籍を読んだ後でKilcode(AIコーディングエージェント)に5問のクイズを作成してもらいました。結果は5問中正解1問・部分正解2問・不正解2問。
特に重要な不正解が2点ありました。
__name__ == "__main__" の本質
「グローバルではなくローカルで実行されるからエラー処理として優位」という曖昧な答えを出してしまいましたが、正確には:
| 実行方法 |
__name__ の値 |
ブロック内の実行 |
|---|---|---|
python openai_adapter.py |
"__main__" |
される |
from openai_adapter import OpenAIAdapter |
"openai_adapter" |
されない |
このイディオムの本質は**「直接実行 vs import時の切り分け」**です。アダプタークラスのファイルに動作確認コードを書いても、他のモジュールからimportしたときに意図しない実行が起きない。
AIVTuber開発ではアダプタークラスが6〜8ファイルに増えていく予定なので、全ファイルでこのパターンを一貫して使うことが設計品質に直結します。
run_dummy.py の戻り値無視という「穴」
while True:
try:
aituber_system.talk_with_comment() # 戻り値を無視
time.sleep(5)
except Exception as e:
...
talk_with_comment() はコメントがないとき False を返しますが、呼び出し側はその戻り値を無視して5秒待機→再試行するだけです。
書籍のシンプルなデモ用コードとしては問題ありませんが、STEP3で感情状態管理を実装するときには「コメントなし→感情変化なし→前の感情状態を維持」という処理が必要になります。戻り値を活かした分岐処理はそのタイミングで追加する改善ポイントとして記録しておきます。
結:STEP1で得た「土台」とSTEP2以降への展望
ナナカ設定への書き換え
書籍サンプルのキャラクター「蛍」(16歳女性学生)を、農家AIVTuber「ナナカ」に書き換えました。system_prompt.txt の中身は:
あなたは熊本でクレソンを栽培している農家AIVTuberの「ナナカ」です。
農業の知識を持ち、視聴者と農業・食・料理について語り合います。
明るく親しみやすいキャラクターで、熊本弁を少し交えながら話します。
書籍の構造をそのまま活かしながら、プロンプトの差し替えだけでキャラクターが変わるというアダプターパターンの恩恵を体感できました。
今後のSTEPと書籍の対応
STEP1(完了): 最小構成 → 書籍1(AITuber本)
STEP2: プロンプト管理 → LangChain Ch4〜5
STEP3: 感情状態管理 → LangGraph Ch9
STEP4: 視聴者記憶 → Neo4j + LangChain Ch6
STEP5: マルチペルソナ → LangChain Ch12
特に楽しみにしているのがSTEP3とSTEP4の組み合わせです。VOICEVOX の audio_query パラメータを感情状態で変化させる × Neo4jで「前回この視聴者が聞いたこと」を記憶する、という実装は農業現場のコミュニティ感と相性が良いと感じています。
「先週クレソンの収穫について質問してくれた○○さんが来た!今日の出荷量を報告しよう」というような、記憶に基づく文脈のある対話が農家AIVTuberならではの強みになるはずです。
まとめ
| 学んだこと | 今後への活用 |
|---|---|
| 2段POST(/audio_query→/synthesis) | STEP3の感情パラメータ変更 |
| FIFOの会話履歴(5往復) | STEP4の短期・長期記憶設計 |
| アダプターパターン | STEP5のマルチペルソナ差し替え |
__name__ == "__main__" |
全アダプターファイルで一貫使用 |
書籍のコードは「シンプルすぎる」と感じる部分もありましたが、それが**「どこを拡張すべきか」**を明示してくれていると気づきました。STEP2以降ではLangChainとLangGraphを加えながら、農家AIVTuber「ナナカ」を育てていきます。
ナナカファームのAIVTuber開発シリーズ・前回記事:[クレソンAI Phase4bの記録]
次回:STEP2 LangChain Ch4〜5でプロンプト管理を整理する
Discussion