sci-genの備忘録

初心者エンジニアの備忘録

個人開発成果の数値化:2025年のリクエスト統計とリリースしたアプリについて (Github Copilot)

Github CopilotにはPremium request analyticsという機能があります。要するに、有料リクエストの統計値を出してくれます。

2025年のリクエストを見て、私の個人開発の頑張り?を振り返ってみようと思います。

まあ、厳密には頑張ったのはAIなんですけどね。

全体のリクエスト数

6,066回でした。

多いのか少ないのかはわかりませんが、Pro plusプランで3ヶ月間毎月でプレミアムリクエストの回数上限まで使っているくらいの計算ですね。

Billed premium requestsは$0です。

上限が来たら無料プランを使っていました。

モデル別のリクエスト数

どんなモデルを何回使ったかを確認してみます。

この続きを読むには
購入して全文を読む

YAMLから履歴書をPDF生成するCLIツールを作った

https://raw.githubusercontent.com/sci-gen/render-jpcv/refs/heads/main/user-guide/image.png

renderCVという履歴書を作るのに便利なパッケージがあります。

ただ、日本語の履歴書を作りこむのが面倒で、これなら専用のパッケージにしてしまおう、と思ってつくりました。

厚生労働省が出している履歴書様式例に近い履歴書をyamlからPDF出力できるCLIツールです。

↓今回作ったもの

github.com

使い方

1. 経歴データの作成

まず、自分の経歴情報を記述したyamlファイルを用意します。 フォーマットは決まっているので、それに沿って埋めるだけです。

yamlの例

config:
  font_size: 11pt
  year_style: seireki # or wareki
  render_date: 2025-12-29

profile:
  name: 山田 太郎
  name_kana: ヤマダ タロウ
  # 写真パスは環境に合わせて変更してください
  face_image_path: "../syoumeisyashin_man.png"
  birthday: 2004-05-15
  age: 21
  gender:address_zip: 160-0022
  address: 東京都新宿区新宿1-2-3 ハイツ新宿 201号室
  address_kana: トウキョウト シンジュクク シンジュク
  phone: 090-1234-5678
  email: taro.yamada@example.com

  # 帰省先などがない場合は連絡先欄は省略または同上でOK(ここでは省略)
  # contact_zip: 
  # contact_address: 
  # contact_address_kana: 
  # contact_phone: 
  # contact_email: 

education:
  - date: 2020-04
    event: 東京都立〇〇高等学校 入学
  - date: 2023-03
    event: 東京都立〇〇高等学校 卒業
  - date: 2023-04
    event: 〇〇大学 経済学部 経済学科 入学
  - date: 2027-03
    event: 〇〇大学 経済学部 経済学科 卒業見込み

work:
  - date: 2023-05
    event: コンビニエンスストア〇〇 新宿店 アルバイト入社
    description: レジ打ち、品出し、清掃業務を担当
  - date: 2024-03
    event: コンビニエンスストア〇〇 新宿店 退社
    description: 学業に専念するため
  - date: 2024-04
    event: カフェ・ド・〇〇 渋谷店 アルバイト入社
    description: ホール・キッチン補助。
  - date: 2024-12
    event: カフェ・ド・〇〇 渋谷店 退社
    description: 引越しに伴う通勤困難のため

qualifications:
  - date: 2022-08
    name: 普通自動車第一種運転免許(AT限定) 取得
  - date: 2023-01
    name: 実用英語技能検定 2級 合格

other:
  motivation: |
    以前客として利用した際、スタッフの方々の明るい接客とお店の雰囲気に惹かれ、
    私もその一員として働きたいと思い志望いたしました。
    前職のカフェではホールとキッチン補助を経験しており、
    ピークタイムの忙しい状況でも笑顔で丁寧な対応を心がけていました。
    大学の授業が終わった夕方以降や土日を中心に、長く勤務したいと考えております。

  hopes: |
    【勤務可能日】
    月・水・金:17:00 〜 23:00
    土・日・祝:10:00 〜 23:00
    ※大学の試験期間中(7月・1月)はシフト調整をご相談させていただけますと幸いです。
    【通勤時間】
    約 20 分(電車) 最寄駅:〇〇線 新宿駅

2. 実行(PDF生成)

npxを使ってすぐに実行できます。 以下のコマンドを叩くと、カレントディレクトリにある resume.yaml を読み込み、PDFを生成します。

npx render-jpcv build resume.yaml

これだけで、整ったレイアウトの resume.pdf(履歴書)が出力されます(冒頭画像)。

https://github.com/sci-gen/render-jpcv/blob/main/user-guide/sample-project/resume.pdf

これなら、AIに丸投げもできそうですね。

(個人情報には十分に気をつけるようにしてください)

おまけ

意外となかったのが履歴書の書式となるHTMLです。

wordを貼り付けてちまちま編集していったのですが、その記録として残しておきました。

こちらも是非お使いください。

github.com

SpeedCam プライバシーポリシー

プライバシーポリシー

本プライバシーポリシーは、SpeedCam(以下、「本アプリ」といいます)における利用者情報の取り扱いについて定めるものです。

1. 取得する情報と利用目的

本アプリは、以下の機能を提供するために、お客様のデバイス上のデータにアクセスします。これらのデータが開発者(sci-gen)のサーバーに送信されたり、第三者に提供されたりすることはありません。すべての処理はデバイス内(ローカル)で完結します。

1.1 カメラ(映像)

  • 利用目的: ビデオおよび写真の撮影、プレビュー表示、映像解析(プリロール撮影、ストロボ撮影、長時間露光など)のため。
  • データの取り扱い: 撮影された映像データは、お客様が保存操作を行った場合にのみ、デバイス内のフォトライブラリまたは一時フォルダに保存されます。

1.2 マイク(音声)

  • 利用目的: ビデオ撮影時の音声録音のため。
  • データの取り扱い: 録音された音声データはビデオファイルの一部としてのみ使用され、それ以外の目的で使用されることはありません。

1.3 フォトライブラリ(写真・動画)

  • 利用目的: 撮影した写真やビデオをデバイスの写真アプリ(カメラロール)に保存するため。
  • データの取り扱い: 本アプリは「追加(保存)」の権限のみを使用し、お客様の既存の写真やビデオを読み取ることはありません。

2. 情報の第三者提供

本アプリは、取得した個人情報や利用者データを第三者に提供することはありません。また、広告配信SDKや解析ツール(Google Analyticsなど)によるデータ収集も行っていません。

3. 免責事項

本アプリがユーザーの特定の目的に適合すること、期待する機能・商品的価値・正確性・有用性を有すること、および不具合が生じないことについて、何ら保証するものではありません。 本アプリの利用によって生じた損害について、開発者は一切の責任を負いません。

4. プライバシーポリシーの変更

本ポリシーの内容は、法令その他本ポリシーに別段の定めのある事項を除いて、ユーザーに通知することなく変更することができるものとします。 変更後のプライバシーポリシーは、本ページに掲載したときから効力を生じるものとします。


制定日: 2025年11月26日

Agnoチュートリアル Vol. 2:差分更新可能な「動的ナレッジベース」の構築

Vol. 1「Ollamaで動かす『フォルダ読みRAG』」では、ローカルPC上でRAGを簡単に体験することができました。

sci-gen.hatenablog.com

しかし、Vol. 1 のスクリプトには「起動するたびに、すべてのドキュメントをゼロから再処理する」という大きな課題がありました。ドキュメントが数十個になると、起動までに数分〜数十分かかってしまい、実用的ではありません。

このチュートリアル(Vol. 2)では、この課題を解決します。ベクトルDBを「永続化」させ、フォルダ内の変更(追加・削除)だけを検知してDBに反映させる「差分更新」が可能な、より高速で実用的なRAGエージェントを構築します。

達成目標:

  • ベクトルDBを永続化させ、2回目以降の起動を高速化する。
  • Agno の「Upsert(重複スキップ)」機能を利用した差分追加を実装する。
  • フォルダから削除されたファイルを検知し、ベクトルDBからも削除するロジックを実装する。
  • チームの共有フォルダでの運用にも耐えうる、効率的なDB更新プロセスを学ぶ。

想定読者:

  • Vol. 1 を完了し、次のステップに進みたい方。
  • RAG の起動時間を短縮したい方。
  • チームでナレッジベースを共有・運用する方法を検討している方。

1. 環境準備と Vol. 1 からの変更点

環境は Vol. 1 と同じです。Ollama と必要なライブラリ (agno, lancedb, pypdf, python-pptx) がインストールされていることを確認してください。

Vol. 1 からの主な変更点

  • DBを削除しない: shutil.rmtree(VECTOR_DB_PATH) を削除します。これによりDBが永続化されます。
  • 差分同期ロジックの追加: スクリプト起動時に、以下の3つを比較・実行するロジックを追加します。
    • 削除検知: フォルダから消されたファイルは、DBからも削除する。
    • 追加検知: フォルダに新しく増えたファイルは、DBに追加する。
    • 重複スキップ: 既にDBに存在するファイルは、処理をスキップする (Agnoが自動で対応)。

2. 動的RAGエージェントの構築 (Pythonコード)

Vol. 1 で作成した rag_app.py をベースに、差分更新ロジックを追加した rag_app_v2.py を作成します。

# shutil はもう使いません
from pathlib import Path
from agno.agent import Agent
from agno.models.ollama import Ollama
from agno.knowledge.knowledge import Knowledge
from agno.knowledge.embedder.ollama import OllamaEmbedder
from agno.vectordb.lancedb import LanceDb

# --- 1. 設定 (Vol. 1 と同じ) ---
DOCS_FOLDER = "./my_docs"
VECTOR_DB_PATH = "./vector_db"
OLLAMA_CHAT_MODEL = "llama3.2"
OLLAMA_EMBED_MODEL = "mxbai-embed-large"
SUPPORTED_EXTENSIONS = [".pdf", ".pptx"]


# --- 2. 既存のベクトルDBを「読み込む」 ---
# Vol. 1 と異なり、DBを削除せず「読み込み」から試みます
try:
    print(f"ナレッジベース '{VECTOR_DB_PATH}' をセットアップします...")

    # 2-1. Embeddingモデルの初期化
    embedder = OllamaEmbedder(id=OLLAMA_EMBED_MODEL)

    # 2-2. ベクトルストアの初期化 (DBが存在すれば読み込む)
    vector_db = LanceDb(uri=VECTOR_DB_PATH, embedder=embedder)

    # 2-3. ナレッジベース本体の初期化
    knowledge_base = Knowledge(vector_db=vector_db)

    print("ナレッジベースのセットアップ完了。")

except Exception as e:
    print(f"\nエラーが発生しました: {e}")
    print("Ollamaサーバーが正しく起動しているか確認してください。")
    exit()


# --- 3. 差分同期ロジック (ここがVol. 2の核心) ---
print("フォルダとナレッジベースの同期を開始します...")
try:
    # 3-1. DBに登録済みのファイルパス一覧を取得
    # Agno の get_contents() でDB内の情報を取得
    db_contents = knowledge_base.get_contents()
    # パス情報だけをSet(集合)に格納 (高速な比較のため)
    db_file_paths = {content.path for content in db_contents}
    print(f"  [DB] {len(db_file_paths)} 件のドキュメントが登録済みです。")

    # 3-2. ローカルフォルダ内のファイルパス一覧を取得
    doc_path = Path(DOCS_FOLDER)
    if not doc_path.exists():
        print(f"エラー: ドキュメントフォルダ '{DOCS_FOLDER}' が見つかりません。")
        exit()

    local_file_paths = set()
    for ext in SUPPORTED_EXTENSIONS:
        # Pathオブジェクトを絶対パスの文字列に変換してSetに追加
        for file in doc_path.rglob(f"*{ext}"):
            local_file_paths.add(str(file.resolve())) # .resolve()で絶対パスに
    print(f"  [フォルダ] {len(local_file_paths)} 件のドキュメントが見つかりました。")

    # 3-3. 【削除検知】 DBにのみ存在するファイル(=フォルダから削除された)を特定
    files_to_delete = db_file_paths - local_file_paths
    if files_to_delete:
        print(f"\n--- 削除処理({len(files_to_delete)} 件)---")
        for file_path in files_to_delete:
            # Agno の delete_content() でDBから削除
            if knowledge_base.delete_content(path=file_path):
                print(f"  [削除] {os.path.basename(file_path)}")
            else:
                print(f"  [削除失敗] {os.path.basename(file_path)}")

    # 3-4. 【追加検知】 フォルダにのみ存在するファイル(=新しく追加された)を特定
    files_to_add = local_file_paths - db_file_paths
    if files_to_add:
        print(f"\n--- 追加処理({len(files_to_add)} 件)---")
        for file_path in files_to_add:
            print(f"  [追加] {os.path.basename(file_path)} を処理中...")
            # Vol. 1 と同じ add_content_sync を使用
            knowledge_base.add_content_sync(
                path=file_path,
                chunk=True,
                chunk_size=1000,
            )

    # 3-5. 【変更なし】
    if not files_to_delete and not files_to_add:
        print("  [同期] 変更はありません。ナレッジベースは最新です。")

    print("同期が完了しました。")

except Exception as e:
    print(f"\n同期中にエラーが発生しました: {e}")
    exit()


# --- 4. RAGエージェントの作成 (Vol. 1 と同じ) ---
llm = Ollama(id=OLLAMA_CHAT_MODEL)
agent = Agent(
    model=llm,
    knowledge=knowledge_base,
    search_knowledge=True,
    description="あなたは提供されたドキュメントに関する質問に答える専門家です。",
    instructions=[
        "回答は、必ずナレッジベースから検索した情報(コンテキスト)に基づいてください。",
        "情報が見つからない場合は、その旨を正直に伝えてください。"
    ]
)

# --- 5. エージェントの実行 (Vol. 1 と同じ) ---
print("\n--- RAGエージェント起動 ---")
print("ナレッジベースに基づいて回答します。")
print("質問を入力してください (終了は 'q' または 'exit'):")

try:
    while True:
        query = input("\n[ 質問 ]: ")
        if query.lower() in ["q", "exit"]:
            print("終了します。")
            break

        print("[ 回答 ]:")
        for chunk in agent.run(query, stream=True):
            print(chunk, end="", flush=True)
        print("\n")

except KeyboardInterrupt:
    print("\n終了します。")

3. コードの詳細解説

Vol. 2 の核心である「セクション 3: 差分同期ロジック」を詳しく見ていきます。

3-1. DB内のファイル一覧取得

db_contents = knowledge_base.get_contents()
db_file_paths = {content.path for content in db_contents}

knowledge_base.get_contents() を使うと、Agno のナレッジベースに登録されているコンテンツのメタデータ(パス、チャンク数など)を取得できます。ここでは、比較のためにファイルパス(content.path)だけを set(集合)に格納しています。

3-2. ローカルフォルダ内のファイル一覧取得

local_file_paths = set()
for file in doc_path.rglob(f"*{ext}"):
    local_file_paths.add(str(file.resolve()))

Vol. 1 と似ていますが、pathlib で取得したパスを .resolve()絶対パスに変換し、str() で文字列化しています。

db_file_pathslocal_file_paths の形式を確実に一致させるための重要な処理です。(相対パスのままだと、実行場所によってパスが変わり、比較が失敗するため)

3-3 & 3-4. 集合演算による差分検知

# 削除検知
files_to_delete = db_file_paths - local_file_paths
# 追加検知
files_to_add = local_file_paths - db_file_paths

ここが差分検知のキモです。Python の set(集合)の演算(引き算)を使っています。

  • db_file_paths - local_file_paths: DBセットに「あって」、ローカルセットに「ない」もの = 削除されたファイル
  • local_file_paths - db_file_paths: ローカルセットに「あって」、DBセットに「ない」もの = 新しく追加されたファイル

set を使うことで、数百・数千のファイルがあっても一瞬で差分を計算できます。

3-3. 削除処理

knowledge_base.delete_content(path=file_path)

Agno の delete_content() を呼び出し、ファイルパスを指定してDBから該当するベクトルデータを削除します。

3-4. 追加処理 (Upsert)

knowledge_base.add_content_sync(path=file_path)

新しく追加されたファイルは、Vol. 1 と同じ add_content_sync で処理します。

add_content_sync は Upsert (Update + Insert) として機能するため、もし万が一、重複したファイルを追加しようとしても、Agno が自動でスキップしてくれるため安全です。

4. 実行と動作確認

rag_app_v2.py を実行して、差分更新の動作を確認してみましょう。

1回目: 初回実行

./vector_db フォルダがない状態で実行します。

python rag_app_v2.py

実行ログ(例):

ナレッジベース './vector_db' をセットアップします...
Embeddingモデルを初期化中...
ナレッジベースのセットアップ完了。
フォルダとナレッジベースの同期を開始します...
  [DB] 0 件のドキュメントが登録済みです。
  [フォルダ] 2 件のドキュメントが見つかりました。

--- 追加処理(2 件)---
  [追加] 業務マニュアル.pdf を処理中...
  [追加] 新製品プレゼン資料.pptx を処理中...
同期が完了しました。

--- RAGエージェント起動 ---
...

初回はDBが空(0件)なので、フォルダ内の全ファイルが「追加処理」されます。この処理は時間がかかります。

2回目: 変更なしで再実行

my_docs フォルダを何も変更せず、もう一度スクリプトを実行します。

実行ログ(例):

ナレッジベース './vector_db' をセットアップします...
Embeddingモデルを初期化中...
ナレッジベースのセットアップ完了。
フォルダとナレッジベースの同期を開始します...
  [DB] 2 件のドキュメントが登録済みです。
  [フォルダ] 2 件のドキュメントが見つかりました。
  [同期] 変更はありません。ナレッジベースは最新です。
同期が完了しました。

--- RAGエージェント起動 ---
...

DB(2件)とフォルダ(2件)に差分がないため、追加・削除処理がスキップされ、一瞬で起動します。

3回目: ファイルを追加して実行

my_docs フォルダに、新しいファイル 企画書.pdf を追加してから実行します。

実行ログ(例):

...
  [DB] 2 件のドキュメントが登録済みです。
  [フォルダ] 3 件のドキュメントが見つかりました。

--- 追加処理(1 件)---
  [追加] 企画書.pdf を処理中...
同期が完了しました。
...

企画書.pdf(1件)だけが「追加処理」され、既存の2件はスキップされます。

4回目: ファイルを削除して実行

my_docs フォルダから 業務マニュアル.pdf を削除してから実行します。

実行ログ(例):

...
  [DB] 3 件のドキュメントが登録済みです。
  [フォルダ] 2 件のドキュメントが見つかりました。

--- 削除処理(1 件)---
  [削除] 業務マニュアル.pdf
同期が完了しました。
...

業務マニュアル.pdf(1件)だけが「削除処理」されます。

5. 次のステップ

これで、起動が高速で、ドキュメントの変更にも自動で追従できる、実用的なRAGエージェントが完成しました。

このスクリプトは、チームの共有フォルダ(ネットワークドライブ)で運用することも可能です。

しかし、チームで使うにはまだ課題があります。

  • 「誰かがDB更新処理(スクリプト実行)をしないと、他のメンバーは古い情報を見てしまう」
  • 「もし2人が同時に更新処理を実行したらどうなる?」(=同時書き込み問題)

この問題を解決するには、

  • 「DB更新申請APIサーバー」を構築し、RAGエージェントを「読み取り専用」と「書き込み管理」に分離する
  • 常に、フォルダを巡回して新しいファイルを読み込む

ような実装が求められますが、それはまたの機会ということで。

Agnoチュートリアル Vol. 1:Ollama で動かす「フォルダ読みRAG」

このチュートリアルでは、Ollama で起動したローカルLLMと、AIエージェントフレームワーク Agno を組み合わせ、ローカルPC上で完結するシンプルなRAG(Retrieval-Augmented Generation)システムを構築します。

特定のフォルダ(例: ./my_docs)にPDFやPowerPointファイルを入れるだけで、その内容に基づいてAIが質問に答えてくれるエージェントを作成します。

達成目標:

  • Ollama で起動したモデル(チャット用・Embedding用)を Agno から利用する方法を学ぶ。
  • 指定したフォルダ内のドキュメント(PDF, PPTX)を自動で読み込むナレッジベースを構築する。
  • すべてローカル環境で完結するRAGエージェントを実行する。

想定読者:

  • ローカルLLM(Ollama)を使った開発に興味がある方。
  • Agno を使ってRAGを手早く試してみたい開発者。
  • Python の基本的な知識がある方。

1. 前提条件と環境準備

まず、Ollama と必要な Python ライブラリを準備します。

ステップ 1-1: Ollama モデルの準備

Ollama がインストールされ、起動している必要があります。 ターミナルで以下のコマンドを実行し、このチュートリアルで使う2種類のモデルをダウンロード(pull)してください。

# 1. チャット用モデル (例: Llama 3.2)
# 回答の生成を担当します。
ollama pull llama3.2

# 2. Embedding(ベクトル化)用モデル
# ドキュメントをAIが検索可能な形式に変換するために使います。
# 高性能で軽量な mxbai-embed-large を推奨します。
ollama pull mxbai-embed-large

Agno 本体と、ベクトルDB(LanceDb)、ファイル読み込み用のライブラリをインストールします。

# Agno本体、ベクトルDB、ファイルリーダー
pip install agno lancedb pypdf python-pptx
  • lancedb: サーバー不要のファイルベース・ベクトルDB
  • pypdf: PDF ファイルを読み込むため
  • python-pptx: PowerPoint (.pptx) ファイルを読み込むため

2. RAG対象フォルダの準備

次に、AIに読み込ませたいドキュメントを入れるフォルダを作成します。

  1. このチュートリアル用の作業フォルダを作成します。
  2. その中に my_docs という名前のフォルダを作成してください。
  3. my_docs の中に、RAGで質問したいPDFやPPTXファイルをいくつか入れてください。

フォルダ構成(例):

/your-project-folder/
    my\_docs/
        ├─ 業務マニュアル.pdf
        ├─ 新製品プレゼン資料.pptx
        └─ ...
    rag\_app.py  (← 次のステップで作成)

3. RAGエージェントの構築 (Pythonコード)

それでは、RAGエージェント本体の Python スクリプトを作成します。 作業フォルダに rag_app.py という名前で以下のファイルを作成してください。

import os
import shutil
from pathlib import Path
from agno.agent import Agent
from agno.models.ollama import Ollama
from agno.knowledge.knowledge import Knowledge
from agno.knowledge.embedder.ollama import OllamaEmbedder
from agno.vectordb.lancedb import LanceDb

# --- 1. 設定 ---

# ユーザーがPDF/PPTXを入れるフォルダ
DOCS_FOLDER = "./my_docs"
# ベクトルDBを保存する場所 (このフォルダが作成されます)
VECTOR_DB_PATH = "./vector_db"

# Ollamaで起動するモデル名
OLLAMA_CHAT_MODEL = "llama3.2"
OLLAMA_EMBED_MODEL = "mxbai-embed-large"

# --- 2. 既存のベクトルDBをクリア ---
# このチュートリアルでは、起動時に必ず最新のフォルダ内容を反映させるため、
# 毎回ベクトルDBを削除してゼロから作り直します。
if os.path.exists(VECTOR_DB_PATH):
    print(f"既存のベクトルDB '{VECTOR_DB_PATH}' をクリアします。")
    shutil.rmtree(VECTOR_DB_PATH)

# --- 3. ナレッジベースのセットアップ ---
# (Ollamaサーバーが起動していないと、ここでエラーになります)
try:
    # 3-1. Embeddingモデルの指定 (Ollama)
    # ドキュメントをベクトル化するモデルを指定します。
    print("Embeddingモデルを初期化中...")
    embedder = OllamaEmbedder(id=OLLAMA_EMBED_MODEL)

    # 3-2. ベクトルストアの指定 (LanceDb)
    # ベクトルデータを保存する場所を指定します。
    # `uri` にローカルのフォルダパスを指定すると、そこにDBが作成されます。
    vector_db = LanceDb(
        uri=VECTOR_DB_PATH,
        embedder=embedder
    )

    # 3-3. ナレッジベース本体の初期化
    knowledge_base = Knowledge(vector_db=vector_db)

    # 3-4. 対象フォルダ内のドキュメントをスキャン
    doc_path = Path(DOCS_FOLDER)
    if not doc_path.exists():
        print(f"エラー: ドキュメントフォルダ '{DOCS_FOLDER}' が見つかりません。")
        print("フォルダを作成し、PDFやPPTXファイルを入れてください。")
        exit()

    supported_extensions = [".pdf", ".pptx"]
    files_to_add = []
    for ext in supported_extensions:
        # .rglob() でサブフォルダ内も再帰的に検索します
        files_to_add.extend(doc_path.rglob(f"*{ext}"))

    if not files_to_add:
        print(f"'{DOCS_FOLDER}' 内に対象ファイル (PDF, PPTX) が見つかりません。")
        exit()

    print(f"{len(files_to_add)} 件のファイルをナレッジベースに追加します...")

    # 3-5. ファイルをナレッジベースに追加
    # ここでファイルの読み込み、チャンク分割、Embedding、DBへの保存が
    # 自動的に行われます。
    for file in files_to_add:
        print(f"  -> {file.name} を処理中...")
        knowledge_base.add_content_sync(
            path=str(file),
            chunk=True,      # ドキュメントを自動でチャンク分割
            chunk_size=1000, # チャンクのサイズ(文字数)
        )
    print("ナレッジベースの作成が完了しました。")

except Exception as e:
    print(f"\nエラーが発生しました: {e}")
    print("Ollamaサーバーが正しく起動しているか、")
    print(f"またはモデル名 ('{OLLAMA_CHAT_MODEL}', '{OLLAMA_EMBED_MODEL}') が正しいか確認してください。")
    exit()

# --- 4. RAGエージェントの作成 ---

# 4-1. チャットモデルの指定 (Ollama)
# ユーザーとの会話と、RAGの最終的な回答生成を担当します。
llm = Ollama(id=OLLAMA_CHAT_MODEL)

# 4-2. エージェントの初期化
agent = Agent(
    model=llm,
    knowledge=knowledge_base, # 作成したナレッジベースを接続
    search_knowledge=True,    # これがRAGを有効にする設定 (Trueがデフォルト)
    description="あなたは提供されたドキュメントに関する質問に答える専門家です。",
    instructions=[
        "回答は、必ずナレッジベースから検索した情報(コンテキスト)に基づいてください。",
        "情報が見つからない場合は、その旨を正直に伝えてください。"
    ]
)

# --- 5. エージェントの実行 (チャットループ) ---

print("\n--- RAGエージェント起動 ---")
print(f"'{DOCS_FOLDER}' の内容に基づいて回答します。")
print("質問を入力してください (終了は 'q' または 'exit'):")

try:
    while True:
        query = input("\n[ 質問 ]: ")
        if query.lower() in ["q", "exit"]:
            print("終了します。")
            break

        # エージェントを実行し、ストリーミングで回答を表示
        print("[ 回答 ]:")
        response = ""
        for chunk in agent.run(query, stream=True):
            print(chunk, end="", flush=True)
            response += chunk
        print("\n")

except KeyboardInterrupt:
    print("\n終了します。")

4. コードの詳細解説

rag_app.py で行っていることを見ていきましょう。

セクション 2: 既存のベクトルDBをクリア

if os.path.exists(VECTOR_DB_PATH):
    shutil.rmtree(VECTOR_DB_PATH)

このチュートリアルでは、シンプルさを最優先しています。my_docs フォルダの内容が変更されたか(ファイルが追加・削除されたか)を賢くチェックする代わりに、起動するたびにベクトルDB (./vector_db フォルダ) を丸ごと削除し、ゼロから作り直します。 これにより、常に my_docs の最新の状態がDBに反映されます。

  • メリット: 実装が非常に簡単。
  • デメリット: ファイルが多いと、起動のたびにナレッジベースの作成(Embedding処理)に時間がかかる。

セクション 3: ナレッジベースのセットアップ

RAGの「心臓部」です。

# 1. Embeddingモデル
embedder = OllamaEmbedder(id=OLLAMA_EMBED_MODEL)

「ドキュメントをベクトル化する」ために、Ollama の mxbai-embed-large モデルを使うよう指定しています。

# 2. ベクトルストア
vector_db = LanceDb(uri=VECTOR_DB_PATH, embedder=embedder)

Agno は様々なベクトルDBに対応していますが、ここでは LanceDb を使用しています。LanceDb はサーバーが不要で、指定したフォルダ(./vector_db)にデータを保存するファイルベースのDBです。ローカルPCでの開発やテストに最適です。

# 3. ナレッジベースへの追加
knowledge_base.add_content_sync(path=str(file), chunk=True)

add_content_sync は、Agno が提供する強力な機能です。 path にファイルのパスを渡すだけで、以下の処理が自動的に実行されます。

  1. ファイル読み込み: pypdf や python-pptx を使い、ファイル(PDF, PPTX)からテキストを抽出します。
  2. チャンク分割: chunk=True にすると、長いテキストを chunk_size で指定したサイズ(ここでは1000文字)の小さな段落(チャンク)に分割します。
  3. Embedding: 分割した各チャンクを、OllamaEmbedder(mxbai-embed-large)に送り、ベクトルデータに変換します。
  4. DB保存: 変換したベクトルデータを、LanceDb(./vector_db フォルダ)に保存します。

セクション 4: RAGエージェントの作成

agent = Agent(
    model=Ollama(id=OLLAMA_CHAT_MODEL),
    knowledge=knowledge_base,
    search_knowledge=True,
)

ここでエージェントを定義します。

  • model: ユーザーとの会話や最終的な回答生成に使うOllamaモデル(llama3.2)を指定します。
  • knowledge: 先ほど作成したナレッジベース(ベクトルDB)をエージェントに接続します。
  • search_knowledge=True: これがRAGを有効にするスイッチです。これが True だと、エージェントはユーザーの質問に答える前に、まず knowledge(ナレッジベース)を検索するようになります。

5. 実行と動作確認

  1. my_docs フォルダにPDFやPPTXファイルが入っていることを確認します。
  2. Ollama が起動していることを確認します。
  3. ターミナルで rag_app.py を実行します。
python rag_app.py

実行ログ(例)

初回実行時、以下のようにナレッジベースが作成されるログが表示されます。

既存のベクトルDB './vector_db' をクリアします。
Embeddingモデルを初期化中...
2 件のファイルをナレッジベースに追加します...
  -> 業務マニュアル.pdf を処理中...
  -> 新製品プレゼン資料.pptx を処理中...
ナレッジベースの作成が完了しました。

--- RAGエージェント起動 ---
'./my_docs' の内容に基づいて回答します。
質問を入力してください (終了は 'q' または 'exit'):

動作確認(Q&A例)

my_docs に入れたファイルの内容について質問してみましょう。

[ 質問 ]: 業務マニュアルによると、経費申請の締め日はいつですか?

[ 回答 ]:
ナレッジベースの情報によると、経費申請の締め日は毎月25日です。

[ 質問 ]: 新製品プレゼン資料に記載されている、ターゲット層は誰ですか?

[ 回答 ]:
新製品プレゼン資料によれば、ターゲット層は「リモートワークを行う30代から40代のビジネスパーソン」と記載されています。

ドキュメントの内容に基づいた回答が返ってくれば成功です。

6. 次のステップ

このチュートリアルでは、Agno と Ollama を使って、ローカルPCでRAGを体験する最もシンプルな方法を学びました。

しかし、実用化を考えると「起動のたびに全ドキュメントを再処理するのは非効率」という課題が残ります。

次のチュートリアル(Vol. 2)では、この「毎回作り直す」問題を解決し、チームの共有フォルダで運用することも見据えた「差分更新可能なナレッジベース」の構築に挑戦します。

sci-gen.hatenablog.com

献立メモ

📅 4週間夜ごはん献立表

夜ごはんなにするか考えなくて済むようにメモ。

なにも考えずこれを食って生きていく。 (ちゃんと好きな食べ物は入ってます🍙)

献立画像

🍚夜ごはん献立

【1週目】

曜日 献立

月 鶏もも照り焼き/千切りキャベツ(惣菜)/味噌汁

火 豚こま生姜焼き/冷凍ブロッコリー/味噌汁

水 鮭の塩焼き/大根おろし/味噌汁

木 カレーライス(玉ねぎ・にんじん・じゃがいも)

金 カレーライス(2日目)+カットサラダ

土 チキン南蛮(手作り)/キャベツ/味噌汁

日 海鮮丼(刺身パック・タコ含む)/味噌汁

【2週目】

曜日 献立

月 豚肉とナスのみそ炒め/冷凍ほうれん草ソテー/味噌汁

火 鮭ムニエル/カットサラダ/コンソメスープ

水 麻婆豆腐(豆腐・ひき肉)/白米/味噌汁

木 カレーライス(再登場)

金 カレーライス(2日目)+温野菜

土 和風ハンバーグ(惣菜可)/冷凍ブロッコリー/味噌汁

日 冷凍唐揚げ/キャベツ/味噌汁

【3週目】

曜日 献立

月 親子丼(鶏もも・卵・玉ねぎ)/味噌汁

火 豚バラもやし炒め/味噌汁

水 鮭のバターしょうゆ焼き/ほうれん草のごま和え(冷凍)/味噌汁

木 肉じゃが

金 肉じゃが(2日目)/味噌汁

ビーフシチュー(ルー使用)/白米/サラダ

日 冷凍チャーハン/卵スープ(インスタント可)

【4週目】

曜日 献立

豚キムチ炒め/味噌汁

火 焼き鳥丼(冷凍)/味噌汁

水 鮭のホイル焼き(玉ねぎ・しめじ)/味噌汁

木 オムライス(ケチャップライス+卵)/コンソメスープ

金 麻婆豆腐(再登場)/味噌汁

土 ロールキャベツ(冷凍惣菜)/コンソメスープ

日 豚しゃぶサラダ(カット野菜+ポン酢)/味噌汁

🥗 調理なしで常備しておくと良い付け合わせ

種類 内容 ポイント

サラダ系 カットキャベツ・カットサラダ・ミニパックブロッコリー 洗わずそのまま出せる。冷蔵3日。

発酵食品 納豆・キムチ・ヨーグルト たんぱく質+腸内環境ケア。塩分注意。

汁物補助 即席味噌汁・カップスープ 忙しい日に置き換え可能。

果物 バナナ・りんご・みかん(冷蔵可) 食後のビタミン補給用。

冷凍野菜 ほうれん草・ブロッコリー・コーン・ミックスベジタブル 電子レンジだけで栄養・彩り追加。

たんぱく質補助 サラダチキン・ゆで卵・ちくわ 調理なしでボリューム調整可能。