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

役者は 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()
【PR】電子書籍を出版しました
ローカルLLMの環境構築から音声対話までを最短で通したい方へ。
拙著[ゼロから始めるローカルLLM Pythonで動かすOllamaとVOICEVOX]では、ローカルLLMの入門書として、Ollama導入、VOICEVOXでの読み上げ、チャットアプリ完成までを、つまずきやすい点まで具体的に解説しました。
拙著[ゼロから始めるローカルLLM Pythonで動かすOllamaとVOICEVOX]では、ローカルLLMの入門書として、Ollama導入、VOICEVOXでの読み上げ、チャットアプリ完成までを、つまずきやすい点まで具体的に解説しました。