Gemini無料枠で作る!長期記憶つき自分専用AIエージェント

Gemini無料枠で作る!長期記憶つき自分専用AIエージェント 

こんにちは!

「ChatGPT PlusやClaudeなどの有料プランは便利だけど、毎月の固定費がかさむ…」
「自分好みの設定や過去の会話をずっと覚えていてくれる、専用のアシスタントが欲しい…」

そんな悩みをお持ちではありませんか?

今回は、Google AI Studioの無料枠(Gemini API)を活用し、長期記憶・Google検索・ファイル直接操作・画像のコピペまでできる「自分専用のAIエージェント」を構築した全手順を公開します。

初心者の方が必ずつまずく「黒いターミナル画面が見づらい問題」の解決策や、開発中に直面したリアルなエラーの対処法まで、実体験をもとにわかりやすく解説します!


1. なぜ「公式基盤」ではなく「ローカル自作」を選んだのか?

Googleには公式に「Agent Platform Memory Bank」というエンタープライズ向けの記憶機能が用意されています。しかし、実際に調べてみると以下のことが分かりました。

  • 公式Memory Bank: Google Cloud(GCP)のインフラ設定が必要で、費用や管理の手間が個人開発にはやや大掛かりすぎる。
  • ローカル自作: Geminiは一度に読める文字数(コンテキスト)が非常に巨大なため、「ローカルのJSONファイルに記憶を書き込んでプロンプトに渡すだけ」で、無料枠の範囲内で同等以上の高精度な記憶エージェントが動く。

「自分専用のアシスタント」が目的であれば、余計なクラウド費用がかからず、記憶のルールを100%自分でコントロールできるローカル自作ルートが圧倒的に最適でした。


2. 第1の壁:「ターミナルの黒い画面」が見づらい問題をStreamlitで解決

最初はCursorのターミナル(CUI)で動かしていたのですが、文字の折り返しや過去ログが見づらく、普段使いにはストレスがありました。

VSCodeの自律エージェント拡張機能(Clineなど)も検討しましたが、Clineは裏で何十回もAPIを連打するため、Google AI Studioの無料枠(1分間に15回制限)が一瞬で枯渇してエラーになるリスクがあります。

そこで採用したのが、Pythonで簡単にChatGPT風のWebアプリが作れる「Streamlit(ストリームリット)」です。

  • 普段使っているブラウザ上で、見やすいチャット画面が開く
  • ユーザーがメッセージを送った時だけAPIを呼ぶため、無料枠制限に引っかからない
  • 画面左側のサイドバーに「AIが覚えた記憶」を常時リアルタイム表示できる

この組み合わせにより、操作性が劇的に向上しました。


3. 【実録トラブルシューティング】直面したエラーと完全対処法

実際に手を動かす中で遭遇した、開発者が必ず知っておくべきエラーと解決法をシェアします。

① モデル世代交代エラー(404 NOT_FOUND)

現象: 古いモデル名(gemini-2.5-flash など)を指定すると、404 NOT_FOUND が発生。
原因: GoogleのAPI側でモデルの世代交代が行われたため。
対処法: エラーメッセージの指示通り、最新の安定モデル gemini-3.5-flash-lite を指定することで一発で解決しました。

② Googleサーバーの混雑エラー(503 UNAVAILABLE)

現象: 503 UNAVAILABLE(This model is currently experiencing high demand)が発生して画面が止まる。
原因: ユーザー側のミスではなく、Googleのサーバーにアクセスが殺到した際の一時的な過負荷。
対処法: 送信処理を try-except で囲み、「混雑中ですので少し待って再送信してください」と画面に優しく表示させる設計にしてアプリのクラッシュを防止しました。

③ Google AI Studioのツールの排他仕様

Google AI Studioの画面を触る際、「Google検索」と「Function calling(自作関数)」を同時にONにしようとすると警告が出てブロックされます。外部検索ツールとプログラミングツールは画面上では排他関係にあるため、試す際は片方をOFFにする必要があります(※Pythonコードから直接呼び出す場合は併用可能です)。


4. 完成したエージェントの全コード(app.py)

長期記憶(JSON自動保存)、Googleリアルタイム検索、Claude Code風のファイル操作、画像のコピペ貼り付け機能をすべて盛り込んだコードがこちらです。

【必要なライブラリのインストール】

pip install streamlit google-genai streamlit-paste-button pillow

【app.py のコード】

import streamlit as st
import json
import os
import io
from PIL import Image
from streamlit_paste_button import paste_image_button
from google import genai
from google.genai import types

# ページ設定
st.set_page_config(page_title="My Personal AI", page_icon="🤖", layout="wide")

# APIクライアントの設定
client = genai.Client()
MEMORY_FILE = "my_memory.json"
BASE_DIR = os.path.abspath(".")

# --- 1. 記憶の処理関数 ---
def load_memories():
    if not os.path.exists(MEMORY_FILE):
        default_mem = {"プロフィール": [], "好み・ルール": [], "プロジェクト・仕事": [], "メモ・その他": []}
        with open(MEMORY_FILE, "w", encoding="utf-8") as f:
            json.dump(default_mem, f, ensure_ascii=False, indent=2)
        return default_mem
    with open(MEMORY_FILE, "r", encoding="utf-8") as f:
        return json.load(f)

def save_user_memory(category: str, detail: str) -> str:
    memories = load_memories()
    if category not in memories:
        memories[category] = []
    if detail not in memories[category]:
        memories[category].append(detail)
        with open(MEMORY_FILE, "w", encoding="utf-8") as f:
            json.dump(memories, f, ensure_ascii=False, indent=2)
        return f"記憶しました: [{category}] {detail}"
    return "すでに記憶されています。"

# --- 2. ファイル操作ツール(Claude Code風) ---
def get_safe_path(file_name: str) -> str:
    target_path = os.path.abspath(os.path.join(BASE_DIR, file_name))
    if not target_path.startswith(BASE_DIR):
        raise ValueError("作業フォルダ外の操作は許可されていません。")
    return target_path

def write_project_file(file_name: str, content: str) -> str:
    try:
        safe_path = get_safe_path(file_name)
        os.makedirs(os.path.dirname(safe_path), exist_ok=True)
        with open(safe_path, "w", encoding="utf-8") as f:
            f.write(content)
        return f"ファイル '{file_name}' を作成・保存しました。"
    except Exception as e:
        return f"エラー: {str(e)}"

def read_project_file(file_name: str) -> str:
    try:
        safe_path = get_safe_path(file_name)
        if not os.path.exists(safe_path):
            return "ファイルが見つかりません。"
        with open(safe_path, "r", encoding="utf-8") as f:
            return f.read()
    except Exception as e:
        return f"エラー: {str(e)}"

def list_project_files() -> str:
    try:
        files = []
        for root, _, filenames in os.walk(BASE_DIR):
            if any(part.startswith(".") or part == "__pycache__" for part in root.split(os.sep)):
                continue
            for f in filenames:
                files.append(os.path.relpath(os.path.join(root, f), BASE_DIR))
        return "フォルダ内ファイル一覧:\n" + "\n".join(files)
    except Exception as e:
        return f"エラー: {str(e)}"

# --- 3. UI構築 ---
with st.sidebar:
    st.header("⚙️ 設定 & 記憶")
    use_search = st.toggle("🌐 Google検索を利用する", value=False)
    
    st.divider()
    st.header("🧠 AIが記憶している情報")
    if st.button("🔄 記憶を再読み込み"):
        st.rerun()
    
    memories = load_memories()
    for cat, items in memories.items():
        st.subheader(cat)
        if items:
            for item in items:
                st.write(f"- {item}")
        else:
            st.caption("(なし)")

st.title("🤖 マイパーソナルアシスタント")

if "messages" not in st.session_state:
    st.session_state.messages = []
if "attached_parts" not in st.session_state:
    st.session_state.attached_parts = []

# 画像コピペ・添付エリア
with st.expander("📎 画像の貼り付け / ファイル添付", expanded=False):
    col1, col2 = st.columns([1, 2])
    with col1:
        paste_result = paste_image_button(
            label="📋 クリップボードの画像を貼る",
            background_color="#2b2d42",
            hover_background_color="#4a4e69",
            key="paste_btn"
        )
        if paste_result.image_data is not None:
            buf = io.BytesIO()
            paste_result.image_data.save(buf, format="PNG")
            part = types.Part.from_bytes(data=buf.getvalue(), mime_type="image/png")
            st.session_state.attached_parts = [part]
            st.image(paste_result.image_data, caption="貼り付けた画像", width=250)

    with col2:
        uploaded_file = st.file_uploader("またはドロップ", type=["png", "jpg", "jpeg", "pdf", "txt", "md"])
        if uploaded_file is not None:
            file_bytes = uploaded_file.read()
            mime_type = uploaded_file.type or "application/octet-stream"
            part = types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
            st.session_state.attached_parts = [part]
            st.success(f"添付完了: {uploaded_file.name}")

    if st.session_state.attached_parts:
        if st.button("❌ 添付をクリア"):
            st.session_state.attached_parts = []
            st.rerun()

# ログ描画
for msg in st.session_state.messages:
    with st.chat_message(msg["role"]):
        st.markdown(msg["content"])

# 入力受付
if user_input := st.chat_input("メッセージを入力してください..."):
    st.session_state.messages.append({"role": "user", "content": user_input})
    with st.chat_message("user"):
        st.markdown(user_input)

    current_mem_str = json.dumps(load_memories(), ensure_ascii=False, indent=2)
    system_instruction = f"""
あなたはユーザー専用の非常に優秀なパーソナルAIアシスタントです。
以下の【現在の記憶】を前提知識として踏まえた上で、親身で的確に応答してください。

【現在の記憶】
{current_mem_str}

【行動指針】
- ユーザーの好みや仕事の文脈、制約など「今後も覚えておくべき情報」が出た場合、自発的に save_user_memory ツールを呼び出して保存してください。
- コード作成時は write_project_file などのファイル操作ツールも適宜利用可能です。
"""

    tools_list = [save_user_memory, write_project_file, read_project_file, list_project_files]
    if use_search:
        tools_list.append(types.Tool(google_search=types.GoogleSearch()))

    with st.chat_message("assistant"):
        response_placeholder = st.empty()
        try:
            chat = client.chats.create(
                model="gemini-3.5-flash-lite",
                config=types.GenerateContentConfig(
                    system_instruction=system_instruction,
                    tools=tools_list,
                    temperature=0.7,
                )
            )
            content_to_send = []
            if st.session_state.attached_parts:
                content_to_send.extend(st.session_state.attached_parts)
            content_to_send.append(user_input)

            response = chat.send_message(content_to_send)
            response_placeholder.markdown(response.text)
            
            st.session_state.messages.append({"role": "assistant", "content": response.text})
            st.session_state.attached_parts = []
            st.rerun()

        except Exception as e:
            if "503" in str(e):
                response_placeholder.warning("⚠️ Googleサーバーが混雑しています。少し待って再送信してください。")
            else:
                response_placeholder.error(f"エラー: {e}")

5. 実際に動かしてみて分かった感動ポイント

  1. 話すだけで勝手に記憶してくれる:
    「コーヒーはブラック派」「回答は箇条書きで」と話しかけただけで、左サイドバーに自動的にカルテが蓄積されます。ブラウザをリロードしても記憶は消えません。
  2. PCファイルを直接いじってくれる:
    「現在日時を表示するコードを書いて hello.py で保存して」と頼むと、本当にPCの中に hello.py が自動生成されます。手動コピペから完全に解放されました。
  3. スクショを貼るだけで即解決:
    エラー画面を Win + Shift + S で切り取り、「📋 クリップボードの画像を貼る」を押すだけでAIが画像の内容を分析してくれます。

まとめ:無料枠でもここまでできる!

「有料AIツールを契約しないと本格的なエージェントは作れないのでは?」と思っていましたが、Google AI Studioの無料枠とPython(Streamlit)を組み合わせることで、想像以上に賢く、実用的なパートナーを作ることができました。

今後はさらに、「Googleカレンダーとの連携」や「タスク管理」、「レシート画像を貼るだけの経理・収支管理」なども拡張していく予定です。

気になった方は、ぜひコピペして自分専用のアシスタントを育ててみてください!

コメント