「AIを使って稼ぎたい。でも何をやればいいのか分からない…」
そんな悩みを持つあなたに向けて、ローカルLLMを利用した“副業アイデア製造機”を作ってみました。
ローカルLLMには、Gemma3を採用しました。Gemma3は軽量なローカルLLMでありながら、Chatbot Arenaでo1-previewモデルより上位の評価を受けています。人間の評価において、最新のクラウド型モデルと遜色ない会話ができます。また、短いトークン数での文章理解や知識蒸留で工夫されており、メモリ効率が極めて高いことが特徴です。
本アプリは、あなたのパソコン上で、Gemma3モデル2機が、Streamlit上で自動対話させることで、【ひらめき → 深掘り → 要約】をループしながら、アイデアを連続で生み出します。
そんな悩みを持つあなたに向けて、ローカルLLMを利用した“副業アイデア製造機”を作ってみました。
ローカルLLMには、Gemma3を採用しました。Gemma3は軽量なローカルLLMでありながら、Chatbot Arenaでo1-previewモデルより上位の評価を受けています。人間の評価において、最新のクラウド型モデルと遜色ない会話ができます。また、短いトークン数での文章理解や知識蒸留で工夫されており、メモリ効率が極めて高いことが特徴です。
本アプリは、あなたのパソコン上で、Gemma3モデル2機が、Streamlit上で自動対話させることで、【ひらめき → 深掘り → 要約】をループしながら、アイデアを連続で生み出します。
-
ネット接続もAPI代も不要。
-
自分のPC上で、好きなだけ回せる。
-
“副業のタネ”を見つけたら、あとは行動するだけ。
この記事では、副業アイデア製造機の使い方を解説します。
※Gemma3のライセンスは、Gemma Terms of Useです。商用利用可能ですが、Gemma Prohibited Use Policyで禁止された用途(児童性搾取、暴力や犯罪の助長等)では利用できません。

2. あとはAI同士が勝手に会話を始めて、毎ターン新しいアイデアが生まれていく。
3. 10発話ごとに会話を要約&保存、次のセッションへ自動で切り替え。
→ これをループしていくと、“アイデア”がどんどんストックされていきます。
※Ollamaの詳細なインストール手順と利用方法は、以下の記事の[2. Ollamaのインストールと利用方法]をご参考ください。
参考記事:OllamaをPythonから操作:WindowsでローカルLLM入門
お持ちのパソコンのメモリが32GB程度ある場合、gemma3:12bをpullしてください。もし、メモリが8GB程度で少な目の場合、gemma3:1bもしくはgemma3:4bモデルをpullしてください。
自動で開かない場合は、ブラウザに

2 ターン目:LLM‑B がそれぞれを補完する 5 つの「種」を提案。
この流れで創発→深化が交互にループし、アイデアがどんどん多角化します。
会話履歴+要約を Markdown にまとめます。
ファイル名は
ローカルストレージのまま Git 管理すれば、ナレッジベースとして蓄積可能です。
無料で高速推論できるローカルLLMの強みは、量をたくさん生成できることです。アイデア発想などは、質より量が重要なので、利用ケースとしては有効だと考えられます。
テーマを変えながらガンガン回して、ピンと来るアイデアを見つけたら、o3と実現方法を相談してみてください。
関連記事:Ollama×Streamlit×VoiceVoxで作るローカルLLM音声対話アプリ【完全ハンズオン】
アプリの全体像

役者は 3 つ
- Creator A(LLM‑A) … 量産型の奇抜アイデアを出す発散担当(温度0.9に設定)
- Creator B(LLM‑B) … 直前の案を深掘り・補完する収束/拡張担当
- Evaluator C … 5 ターン10発話ごとに両者の対話を要約し Markdown で保存
使い方のイメージ
1. あなたが「副業の方向性(例:教育 × AI × 地方)」を1行だけ入力。2. あとはAI同士が勝手に会話を始めて、毎ターン新しいアイデアが生まれていく。
3. 10発話ごとに会話を要約&保存、次のセッションへ自動で切り替え。
→ これをループしていくと、“アイデア”がどんどんストックされていきます。
環境構築の手順
1. Ollamaのインストール
Ollamaとは、ローカルLLMを動作・管理するソフトウェアです。無料のOSSでMITライセンスです。
以下の公式のURL にアクセスし、Ollamaのインストーラーをダウンロードします。
以下の公式のURL にアクセスし、Ollamaのインストーラーをダウンロードします。
ダウンロードしたOllamaSetup.exeをクリックし、インストールしてください。
インストールが完了したら、「すべてのアプリ」をクリックしてOllamaを起動しましょう。Ollamaは画面を持たないコマンドラインツールです。右下のシステムトレイにollamaが常駐するので、それで起動しているかどうかを確認できます。
インストールが完了したら、「すべてのアプリ」をクリックしてOllamaを起動しましょう。Ollamaは画面を持たないコマンドラインツールです。右下のシステムトレイにollamaが常駐するので、それで起動しているかどうかを確認できます。
※Ollamaの詳細なインストール手順と利用方法は、以下の記事の[2. Ollamaのインストールと利用方法]をご参考ください。
参考記事:OllamaをPythonから操作:WindowsでローカルLLM入門
2. Pythonのインストール
ダウンロードサイトからpythonをインストールする。
3. 必要なライブラリのインストール
pip install streamlit==1.37.1 ollama requests
4. Gemma3モデルを Ollama に pull
Gemma3のパラメータは、1b、4b、12b、27bから選べます。お持ちのパソコンのメモリが32GB程度ある場合、gemma3:12bをpullしてください。もし、メモリが8GB程度で少な目の場合、gemma3:1bもしくはgemma3:4bモデルをpullしてください。
ollama pull gemma3:12b
5. コードの保存
記事の最後にあるコード全体をコピーしてapp.pyのファイル名で保存する。6. アプリを起動
streamlit run app.py蒸気を実行すると、ブラウザが自動で開き、以下のような画面が表示されます。
自動で開かない場合は、ブラウザに
http://localhost:8501
と打ち込んでアクセスしてください。
使い方の手順
Step 1 ― アイデアの方向性を入力して再生ボタン押下
- テキスト入力欄にアイデアの方向性を日本語で入力します。(例:副業×AI×教育)
- (空欄の場合、 Creator A が “無から” 発想を試みます)
- 再生を押すと LLM‑A → LLM‑B が自動で走り出します。
Step 2 ― 会話を眺める
1 ターン目:LLM‑A が 5 案を “タイトル+概要” 形式で列挙。2 ターン目:LLM‑B がそれぞれを補完する 5 つの「種」を提案。
この流れで創発→深化が交互にループし、アイデアがどんどん多角化します。
Step 3 ― 5 ターンごとの要約をチェック
ターン数が 5 の倍数になると Evaluator C が要約を生成し、会話履歴+要約を Markdown にまとめます。
ファイル名は
20250508_191530_f3e2d6.md
のように日付+時刻+短縮 UUID。ローカルストレージのまま Git 管理すれば、ナレッジベースとして蓄積可能です。
Step 4 ― 必要に応じて停止 / 新しい会話
- 停止 … 現在のセッションを残したまま一時停止。再度 再生 で続行可能。
- 新しい会話を開始 … 全カウンタと履歴がリセット。別テーマの種を入れ直せます。
個別カスタマイズしたい人向け
1. 要約するターン数を変更したい
# ===== 会話履歴 & ファイル保存 ===== TURNS_PER_SUMMARY = 3 # 5ターン (=A+B 10発話) ごとに要約
2. モデルを切り替えたい
MODEL_NAME_A = "gemma3:4b" # Creator A MODEL_NAME_B = "gemma3:4b" # Creator B SUMMARIZER_MODEL = "gemma3:4b" # Evaluator C (要約)処理のスピードが遅い、動かない場合、gemma3:1bもしくはgemma3:4bの小型モデルを利用ください。Gemma3は、パラメータ数を減らしても十分賢く、あまり性能劣化は起きない印象です。
まとめ
このアプリはGemma 3 モデル×2だけで、「ひらめき ➜ 深掘り ➜ 要約アーカイブ」まで完結できる手軽なアイデア発散環境です。無料で高速推論できるローカルLLMの強みは、量をたくさん生成できることです。アイデア発想などは、質より量が重要なので、利用ケースとしては有効だと考えられます。
テーマを変えながらガンガン回して、ピンと来るアイデアを見つけたら、o3と実現方法を相談してみてください。
関連記事:Ollama×Streamlit×VoiceVoxで作るローカルLLM音声対話アプリ【完全ハンズオン】
(参考)コード全体
コード全体は以下の通りです。後半のHTMLの部分で一部表示がおかしいですが、問題なく動作するはずです。# -*- coding: utf-8 -*- import streamlit as st import ollama import uuid import os import json from datetime import datetime import base64 import requests # ===== LLM 設定 ===== MODEL_NAME_A = "gemma3:12b" # Creator A MODEL_NAME_B = "gemma3:12b" # Creator B SUMMARIZER_MODEL = "gemma3:12b" # Evaluator C (要約) # ===== プロンプト ===== ROLE_PROMPT_A = """ あなたは **Creator A** 。目標は大量のアイデアを0→1発想で生み出すことです。 - 1ターン5案 (番号付き)、各案は【タイトル(15字以内) + 概要(40字以内)】の2行構成。 - 既出案と重複させず、新規性が高い順に並べる。 - ホワイトスペース・生成AIコスト0円・越境EC など多角的に検討する。 """ ROLE_PROMPT_B = """ あなたは **Creator B** 。直前に示されたアイデアを補完・拡張する “種” を 5 つ提案してください。 - 150字以内/アイデア。既出と重複しない視点を含めること。 """ # ===== 会話履歴 & ファイル保存 ===== TURNS_PER_SUMMARY = 5 # 5ターン (=A+B 10発話) ごとに要約 JSON_PATH = "session_data.json" # -- 共通 I/O ユーティリティ -------------------------------------------------- def load_sessions() -> dict: """セッションファイルを読み込み (存在しなければ空dict)""" if not os.path.exists(JSON_PATH): return {} with open(JSON_PATH, "r", encoding="utf-8") as f: return json.load(f) def save_sessions(data: dict) -> None: """セッションデータをファイルへ書き込み""" with open(JSON_PATH, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) # -- 会話履歴操作 ------------------------------------------------------------- def fetch_history(session_id: str): return load_sessions().get(session_id, []) def add_message(session_id: str, role: str, content: str): data = load_sessions() data.setdefault(session_id, []).append({"role": role, "content": content}) # ---- ここだけ変更 ---- keep = TURNS_PER_SUMMARY * 2 # 例: 5ターン×2発話 = 10 data[session_id] = data[session_id][-keep:] save_sessions(data) # -- 要約生成 ------------------------------------------------------------------ def summarize_and_save(session_id: str, history_block): """履歴を Evaluator C で要約し、Markdown を out/ に保存""" plain_history = "\n".join(f"{h['role']}: {h['content']}" for h in history_block) messages = [ {"role": "system", "content": "あなたは Evaluator C。以下の会話を統合評価し、日本語で Markdown で出力してください。"}, {"role": "user", "content": plain_history}, ] response = ollama.chat(model=SUMMARIZER_MODEL, messages=messages) summary_md = response["message"]["content"].strip() # ---- Markdown 生成 ---- history_md = "\n".join(f"- **{h['role']}**: {h['content']}" for h in history_block) combined_md = f"## 要約\n\n{summary_md}\n\n---\n\n## 会話履歴\n\n{history_md}\n" # ---- ファイル保存 (重複しないファイル名) ---- os.makedirs("out", exist_ok=True) ts = datetime.now().strftime("%Y%m%d_%H%M%S") fname = f"out/{ts}_{session_id.split('-')[0]}.md" suffix = 1 while os.path.exists(fname): fname = f"out/{ts}_{session_id.split('-')[0]}_{suffix}.md"; suffix += 1 with open(fname, "w", encoding="utf-8") as f: f.write(combined_md) return combined_md # -- 画像 (アイコン) ----------------------------------------------------------- def get_base64_remote_image(url: str) -> str: """Unsplash など外部 URL 画像 → Base64 文字列へ変換""" resp = requests.get(url, timeout=15) resp.raise_for_status() return base64.b64encode(resp.content).decode() ICON_URL_A = get_base64_remote_image( "https://images.unsplash.com/photo-1502685104226-ee32379fefbe" "?w=100&h=100&crop=faces&fit=crop" ) ICON_URL_B = get_base64_remote_image( "https://images.unsplash.com/photo-1529626455594-4ff0802cfb7e" "?w=100&h=100&crop=faces&fit=crop" ) # ===== LLM 応答生成 =========================================================== def generate_response(session_id: str, assistant_role: str): """Creator A / B の応答を生成し履歴へ追加""" history = fetch_history(session_id) system_p = ROLE_PROMPT_A if assistant_role == "assistant_A" else ROLE_PROMPT_B model_name = MODEL_NAME_A if assistant_role == "assistant_A" else MODEL_NAME_B # システム + これまでの会話 messages = [{"role": "system", "content": system_p}] for turn in history: role = "assistant" if turn["role"] == assistant_role else "user" messages.append({"role": role, "content": turn["content"]}) # Creator A は温度高めで発散 options = {"temperature": 0.9} if assistant_role == "assistant_A" else {} response = ollama.chat(model=model_name, messages=messages, options=options) content = response["message"]["content"].strip() add_message(session_id, assistant_role, content) st.session_state.total_turns += 1 # ===== UI ヘルパー =========================================================== def render_history(history): """履歴をチャット形式で表示""" for turn in history: if turn["role"] == "user_seed": continue icon = ICON_URL_A if turn["role"] == "assistant_A" else ICON_URL_B speaker = {"assistant_A": "LLM‑A", "assistant_B": "LLM‑B"}.get( turn["role"], "📝 User" ) bg = "#F7F7F7" if "assistant_A" in turn["role"] else "#E8E8E8" st.markdown( f""" <div style='display:flex;align-items:flex-start;margin-bottom:1rem;'> <img src='data:image/png;base64,{icon if "assistant" in turn["role"] else ""}' style='width:40px;height:40px;margin-right:0.5rem;border-radius:50%;'/> <div style='background-color:{bg if "assistant" in turn["role"] else "#FFF"};padding:0.75rem; border-radius:0.5rem;flex:1;'> <strong>{speaker}</strong> <div style='margin-top:0.5rem;'>{turn['content']}</div> </div> </div> """, unsafe_allow_html=True, ) # ===== メインアプリ =========================================================== def main(): st.title("Gemma3でアイデア創出") # -- Session state 初期化 -- if "session_id" not in st.session_state: st.session_state.session_id = str(uuid.uuid4()) if "is_running" not in st.session_state: st.session_state.is_running = False if "last_time" not in st.session_state: st.session_state.last_time = datetime.now() if "step" not in st.session_state: st.session_state.step = 0 # 0:A ➜ 1:B if "total_turns" not in st.session_state: st.session_state.total_turns = 0 if "summaries" not in st.session_state: st.session_state.summaries = [] # -- アイデア種入力 (初回の Creator A に渡す) -- idea_seed = st.text_input("アイデアの方向性・種を入力してください (例: 教育×AI×副業)") # -- サイドバー (操作) -- with st.sidebar: st.header("操作パネル") interval = st.slider("会話の更新間隔 (秒)", 0, 3, 1) col1, col2 = st.columns(2) if col1.button("再生", use_container_width=True): st.session_state.is_running = True st.session_state.step = 0 if col2.button("停止", use_container_width=True): st.session_state.is_running = False if st.button("新しい会話を開始", use_container_width=True): st.session_state.session_id = str(uuid.uuid4()) st.session_state.step = 0 st.session_state.total_turns = 0 st.session_state.summaries = [] st.session_state.is_running = False st.markdown(f"**現在の会話数:** {st.session_state.total_turns}") # -- 会話表示 -- st.subheader("現在の会話") render_history(fetch_history(st.session_state.session_id)) # -- SUMMARY_THRESHOLD ごとに要約 -- completed_turns = st.session_state.total_turns // 2 if ( st.session_state.step == 0 and completed_turns > 0 and completed_turns % TURNS_PER_SUMMARY == 0 ): history_block = fetch_history(st.session_state.session_id) # A+B 10 発話そろっているかを確認 if len(history_block) >= TURNS_PER_SUMMARY * 2: md = summarize_and_save(st.session_state.session_id, history_block) st.session_state.summaries.append(md) # ----- 新セッションへリセット ----- st.session_state.session_id = str(uuid.uuid4()) st.session_state.step = 0 st.session_state.total_turns = 0 # -- 過去の要約 -- if st.session_state.summaries: st.subheader("過去の要約") for idx, sm in enumerate(reversed(st.session_state.summaries), 1): with st.expander(f"要約 #{idx}"): st.markdown(sm, unsafe_allow_html=True) # -- 自動対話ループ -- if st.session_state.is_running: now = datetime.now() if interval == 0 or (now - st.session_state.last_time).total_seconds() >= interval: if st.session_state.step == 0: # Creator A に種を渡すのは最初の 1 回だけ if idea_seed: add_message(st.session_state.session_id, "user_seed", idea_seed) role = "assistant_A" if st.session_state.step == 0 else "assistant_B" generate_response(st.session_state.session_id, role) st.session_state.step = 1 - st.session_state.step # A ↔ B 切替え st.session_state.last_time = now st.rerun() # 画面更新 if __name__ == "__main__": main()