はじめに
この記事では以下を一から作ります。
PokeAPIからポケモンの種族値データを取得してCSVに保存
そのCSVをRAG(検索拡張生成)用のベクトルDBに変換
ローカルLLMに食わせてチャットで質問できるWebアプリを作る
完成するとこんな質問ができます
Q: 素早さが最も高いポケモンは?
Q: HPが100以上で合計種族値が高いポケモンを教えて
Q: リザードンとガブリアスの種族値を比較して
すべてローカルで動くため、APIキー不要・通信費ゼロ・データが外に出ないのが特徴です。
動作環境
項目 内容
マシン iMac M1
メモリ 16GB
ストレージ 1TB
OS macOS Sonoma
Python 3.14(Homebrew)
M2・M3・M4でも同様に動きます。Intelマックでも動きますが推論速度は遅くなります。
全体の仕組み
ユーザーの質問
│
▼
┌─────────────────────────────┐
│ Python RAGアプリ │
│ LangChain + Gradio │
└────────┬──────────┬─────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ChromaDB │ │ Ollama │
│ベクトル検索│ │ LLM推論 │
│1350件 │ │ Metal GPU │
└──────────┘ └──────────────┘
│ │
└────┬─────┘
▼
回答を返す
RAGとは?
RAG(Retrieval-Augmented Generation) とは、LLMが回答を生成する前に関連データを検索してコンテキストとして渡す仕組みです。
通常のLLMは学習データにないことは答えられませんが、RAGを使うと「自分で用意したデータ」をもとに回答させることができます。
通常のLLM: 質問 → LLM → 回答(学習データのみ)
RAG: 質問 → 検索DB → 関連データ取得 → LLM → 回答(自分のデータをもとに)
使用技術 役割 ツール 選んだ理由
LLM Qwen2.5:7b 7Bクラス最高水準の日本語性能・4.7GBでM1 16GBに余裕
埋め込みモデル nomic-embed-text 軽量275MB・高精度・Ollamaで一元管理できる
LLM実行環境 Ollama Apple Silicon Metalに完全対応・モデル管理が楽
ベクトルDB ChromaDB ローカル永続保存・Pythonだけで完結・無料
RAGフレームワーク LangChain(LCEL) 業界標準・拡張しやすい
チャットUI Gradio 数行で動くWebチャットUI
データ取得 PokeAPI 無料・認証不要・全ポケモン情報が取れる
Part 1: 環境構築
1-1. Homebrewのインストール
macOSのパッケージマネージャーです。すでに入っている場合はスキップしてください。
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
インストール後、M1の場合はPATHを通す必要があります。
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
確認:
brew --version
# Homebrew 4.x.x と表示されればOK
1-2. Pythonのインストール
brew install python
確認:
python3 --version
# Python 3.x.x と表示されればOK
1-3. Ollamaのインストール
OllamaはローカルでLLMを動かすためのツールです。モデルのダウンロードや管理、APIサーバーの起動をすべて担当します。
brew install ollama
または公式サイト(https://ollama.com)からdmgをダウンロードしてインストールすることもできます。
確認:
ollama --version
# ollama version 0.x.x と表示されればOK
1-4. Ollamaのモデルダウンロード
LLM本体と埋め込みモデルの2つをダウンロードします。
# チャット用LLM(約4.7GB・ダウンロードに5〜10分かかる)
ollama pull qwen2.5:7b
# 埋め込み用モデル(約275MB)
ollama pull nomic-embed-text
Qwen2.5:7bを選んだ理由
7Bクラスのモデルの中で日本語の理解・生成性能が最も高いモデルの一つです。M1 16GBのメモリで余裕をもって動作し、推論速度も実用的な速さです。
確認(モデル一覧):
ollama list
# NAME ID SIZE MODIFIED
# qwen2.5:7b ... 4.7 GB ...
# nomic-embed-text ... 274 MB ...
1-5. Ollamaサーバーの起動設定
Ollamaはサーバーとして動作するため、アプリを使う前に起動しておく必要があります。
方法A: Mac起動時に自動起動(推奨)
brew services start ollama
これでMacを再起動してもOllamaが自動で起動します。
方法B: 手動で起動(作業時だけ使う場合)
ollama serve
このターミナルは閉じずにそのままにしておく必要があります。アプリを使い終わったら Ctrl + C で停止できます。
動作確認(ブラウザでアクセス):
http://localhost:11434
Ollama is running と表示されればOKです。
1-6. プロジェクトフォルダの作成
mkdir ~/Desktop/myprogram/CSVDate
cd ~/Desktop/myprogram/CSVDate
1-7. Python仮想環境の作成
なぜ仮想環境が必要か?
macOS SonomaではHomebrewで管理されているPythonのシステム環境を保護するため、直接 pip install しようとすると以下のエラーが出ます。
error: externally-managed-environment
仮想環境を使うことでシステムのPythonを汚さずに済みます。プロジェクトごとにライブラリを管理できるため、将来的にも管理がしやすくなります。
# プロジェクトフォルダにいることを確認
pwd
# /Users/ユーザー名/Desktop/myprogram/CSVDate
# 仮想環境を作成(venvという名前のフォルダが作られる)
python3 -m venv venv
# 仮想環境を有効化
source venv/bin/activate
有効化すると、ターミナルの先頭に (venv) が表示されます。
(venv) wakuwaku@WakugawanoMac-mini CSVDate %
重要: ターミナルを新しく開くたびに source venv/bin/activate を実行する必要があります。
1-8. Pythonライブラリのインストール
仮想環境が有効な状態((venv) が表示されている状態)でインストールします。
pip install langchain langchain-ollama langchain-chroma chromadb gradio pandas requests
各ライブラリの役割:
ライブラリ 役割
langchain RAGパイプライン構築のコアフレームワーク
langchain-ollama LangChainからOllamaを使うためのアダプター
langchain-chroma LangChainからChromaDBを使うためのアダプター
chromadb ローカルベクトルデータベース本体
gradio WebチャットUIを数行で作れるライブラリ
pandas CSVの読み込み・加工
requests PokeAPIへのHTTPリクエスト
インストール確認:
pip list | grep -E "langchain|chroma|gradio|pandas"
Part 2: ポケモンCSVデータの取得
2-1. PokeAPIについて
PokeAPI(https://pokeapi.co)は認証不要で使えるポケモン情報APIです。
今回は以下の2種類のエンドポイントを使います。
https://pokeapi.co/api/v2/pokemon?limit=1500 — ポケモン一覧取得
https://pokeapi.co/api/v2/pokemon/{id} — 個別の種族値取得
https://pokeapi.co/api/v2/pokemon-species/{id} — 日本語名取得
2-2. スクリプトの解説
日本語名の取得
PokeAPIの日本語名には2種類あります。
言語コード 内容 例
ja-Hrkt カタカナ表記 リザードン
ja 漢字混じり表記 リザードン
すべてのポケモンに ja-Hrkt があるとは限らないため、ja-Hrkt → ja の順でフォールバックします。
for lang_code in ['ja-Hrkt', 'ja']:
for name_data in data['names']:
if name_data['language']['name'] == lang_code:
jp_name = name_data['name']
break
if jp_name:
break
フォルム名の日本語変換
メガシンカやリージョンフォームはAPIの英語名(例: charizard-mega-x)を日本語に変換します。
FORM_JP_SUFFIX = {
'mega-x': 'メガ{name}X',
'mega-y': 'メガ{name}Y',
'mega': 'メガ{name}',
'primal': 'ゲンシ{name}',
'alola': '{name}(アローラのすがた)',
'galar': '{name}(ガラルのすがた)',
'hisui': '{name}(ヒスイのすがた)',
'paldea': '{name}(パルデアのすがた)',
'gmax': '{name}(キョダイマックス)',
}
APIへの負荷対策
1350件のデータを取得するため、以下の対策をしています。
time.sleep(0.15) で各リクエスト間に待機時間を設ける
species_cache で同じURLへの重複リクエストをキャッシュ
リトライ処理(最大3回)でネットワークエラーに対応
HTTPステータス429(レート制限)を検知して5秒待機
2-3. スクリプト全文
all_pokemon_stats.py としてプロジェクトフォルダに保存します。
# -*- coding: utf-8 -*-
import requests
import csv
import time
SESSION = requests.Session()
SESSION.headers.update({'User-Agent': 'pokemon-stats-fetcher/1.0'})
def get_with_retry(url, retries=3, wait=1.0):
"""リトライ付きGETリクエスト"""
for attempt in range(retries):
try:
res = SESSION.get(url, timeout=10)
if res.status_code == 429: # レート制限
print(f" レート制限。{wait * 5}秒待機...")
time.sleep(wait * 5)
continue
res.raise_for_status()
return res.json()
except Exception as e:
if attempt < retries - 1:
time.sleep(wait * (attempt + 1))
else:
raise e
return None
def get_japanese_name(species_url, species_cache):
"""ポケモンの日本語名を取得する(キャッシュ利用)"""
if species_url in species_cache:
return species_cache[species_url]
try:
data = get_with_retry(species_url)
if data is None:
return None
jp_name = None
# 優先順位: ja-Hrkt(カタカナ) → ja(漢字混じり)
for lang_code in ['ja-Hrkt', 'ja']:
for name_data in data['names']:
if name_data['language']['name'] == lang_code:
jp_name = name_data['name']
break
if jp_name:
break
species_cache[species_url] = jp_name
time.sleep(0.15) # サーバー負荷軽減
return jp_name
except Exception as e:
print(f" 日本語名取得エラー ({species_url}): {e}")
species_cache[species_url] = None
return None
# フォルム名の日本語変換マップ
FORM_JP_SUFFIX = {
'mega-x': 'メガ{name}X',
'mega-y': 'メガ{name}Y',
'mega': 'メガ{name}',
'primal': 'ゲンシ{name}',
'alola': '{name}(アローラのすがた)',
'galar': '{name}(ガラルのすがた)',
'hisui': '{name}(ヒスイのすがた)',
'paldea': '{name}(パルデアのすがた)',
'gmax': '{name}(キョダイマックス)',
'eternamax':'{name}(エタマックス)',
'origin': '{name}(オリジンフォルム)',
'sky': '{name}(スカイフォルム)',
'therian': '{name}(霊獣フォルム)',
'ultra': '{name}(ウルトラ)',
}
def build_jp_display_name(form_name, base_jp_name):
"""フォルム名から日本語表示名を生成"""
if base_jp_name is None:
return form_name
parts = form_name.split('-', 1)
suffix = parts[1] if len(parts) > 1 else ''
if suffix in FORM_JP_SUFFIX:
return FORM_JP_SUFFIX[suffix].replace('{name}', base_jp_name)
for key, template in FORM_JP_SUFFIX.items():
if suffix.startswith(key):
return template.replace('{name}', base_jp_name)
return base_jp_name
def main():
output_filename = 'all_pokemon_stats.csv'
print("ポケモンのリストを取得中...")
response = get_with_retry("https://pokeapi.co/api/v2/pokemon?limit=1500")
pokemon_list = response['results']
print(f"合計 {len(pokemon_list)} 件のポケモンを取得しました。")
species_cache = {}
success_count = 0
error_count = 0
with open(output_filename, mode='w', encoding='utf-8-sig', newline='') as f:
# utf-8-sig: ExcelでBOM付きUTF-8として認識させるため文字化け防止
writer = csv.writer(f)
writer.writerow(['図鑑番号', '名前', 'HP', 'こうげき', 'ぼうぎょ',
'とくこう', 'とくぼう', 'すばやさ', '合計'])
for i, p in enumerate(pokemon_list):
try:
p_data = get_with_retry(p['url'])
if p_data is None:
raise ValueError("データ取得失敗")
stats = {s['stat']['name']: s['base_stat'] for s in p_data['stats']}
hp = stats.get('hp', 0)
atk = stats.get('attack', 0)
defense = stats.get('defense', 0)
sp_atk = stats.get('special-attack', 0)
sp_def = stats.get('special-defense', 0)
speed = stats.get('speed', 0)
total = hp + atk + defense + sp_atk + sp_def + speed
base_jp_name = get_japanese_name(p_data['species']['url'], species_cache)
jp_name = build_jp_display_name(p['name'], base_jp_name)
writer.writerow([p_data['id'], jp_name, hp, atk, defense,
sp_atk, sp_def, speed, total])
success_count += 1
if (i + 1) % 100 == 0:
print(f" ... {i+1}/{len(pokemon_list)} 件処理完了"
f"(成功: {success_count}, エラー: {error_count})")
except Exception as e:
print(f" [ERROR] {p['name']}: {e}")
error_count += 1
print(f"\n完了! '{output_filename}' を確認してください。")
print(f"成功: {success_count} 件 / エラー: {error_count} 件")
if __name__ == "__main__":
main()
2-4. 実行
python all_pokemon_stats.py
完了まで約10〜20分かかります(API制限のためウェイトを入れているため)。
出力例:
ポケモンのリストを取得中...
合計 1350 件のポケモンを取得しました。
... 100/1350 件処理完了(成功: 98, エラー: 2)
... 200/1350 件処理完了(成功: 197, エラー: 3)
...
完了! 'all_pokemon_stats.csv' を確認してください。
成功: 1348 件 / エラー: 2 件
生成されたCSVの中身(先頭3行):
図鑑番号,名前,HP,こうげき,ぼうぎょ,とくこう,とくぼう,すばやさ,合計
1,フシギダネ,45,49,49,65,65,45,318
2,フシギソウ,60,62,63,80,80,60,405
6,リザードン,78,84,78,109,85,100,534
Part 3: ベクトルDBへの取り込み(ingest.py)
3-1. 埋め込み(Embedding)とは?
テキストを数値のベクトル(多次元の座標)に変換する技術です。意味が近い文章は近い座標に変換されます。
「リザードン」 → [0.12, -0.34, 0.89, ...] ┐
「ファイヤー」 → [0.11, -0.31, 0.87, ...] ┘ 近い(どちらも炎タイプ)
「カビゴン」 → [-0.78, 0.56, -0.12, ...] ← 遠い
ChromaDBはこのベクトルを保存し、質問文を同じくベクトルに変換して近いものを検索します。
3-2. スクリプト全文
ingest.py としてプロジェクトフォルダに保存します。
# -*- coding: utf-8 -*-
import pandas as pd
from langchain_core.documents import Document # ※langchain.schema は旧パス(非推奨)
from langchain_ollama import OllamaEmbeddings
from langchain_chroma import Chroma
# CSVを読み込んでDocumentリストに変換
df = pd.read_csv("all_pokemon_stats.csv", encoding="utf-8-sig")
docs = []
for _, row in df.iterrows():
# LLMに渡しやすいテキスト形式でDocumentを作成
content = (
f"図鑑番号: {row['図鑑番号']} 名前: {row['名前']}\n"
f"HP:{row['HP']} 攻:{row['こうげき']} 防:{row['ぼうぎょ']} "
f"特攻:{row['とくこう']} 特防:{row['とくぼう']} 素早:{row['すばやさ']} "
f"合計:{row['合計']}"
)
docs.append(Document(
page_content=content,
metadata={"name": row["名前"]}
))
print(f"{len(docs)} 件のドキュメントを作成しました。埋め込み生成を開始します...")
# nomic-embed-textで埋め込みを生成してChromaDBに保存
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma.from_documents(
docs,
embeddings,
persist_directory="./chroma_db" # このフォルダにデータが永続保存される
)
print(f"✅ {len(docs)} 件のポケモンをインデックス化しました")
print("chroma_db/ フォルダを確認してください")
3-3. 実行前の確認事項
Ollamaが起動しているか確認してください。
curl http://localhost:11434
# "Ollama is running" と表示されればOK
起動していない場合:
# 自動起動を設定していない場合は手動で起動
ollama serve
3-4. 実行
python ingest.py
出力例:
1350 件のドキュメントを作成しました。埋め込み生成を開始します...
✅ 1350 件のポケモンをインデックス化しました
chroma_db/ フォルダを確認してください
完了後、chroma_db/ フォルダが作成されます。このフォルダがベクトルDBの実体です。
このスクリプトは初回のみ実行します。 chroma_db/ が残っている限り再実行は不要です。
Part 4: チャットアプリの作成(app.py)
4-1. LCELとは?
LangChain Expression Language(LCEL)は |(パイプ)演算子でコンポーネントをつなぐ記法です。
chain = retriever | format_docs | prompt | llm | output_parser
旧来の RetrievalQA.from_chain_type() は非推奨になっており、現在はLCELが推奨されています。
4-2. RAGの処理フロー
ユーザーの質問(例:「素早さが最も高いのは?」)
│
▼
retriever: ChromaDBで意味的に近いポケモンデータを5件取得
│
▼
format_docs: 5件のドキュメントを1つのテキストに結合
│
▼
prompt: 「以下のポケモンデータを参考に日本語で答えてください」
+ 取得した5件のデータ
+ ユーザーの質問
│
▼
llm(Qwen2.5:7b): コンテキストをもとに回答生成
│
▼
StrOutputParser: LLMの出力を文字列に変換
│
▼
Gradio: 回答をWebチャットに表示
4-3. スクリプト全文
app.py としてプロジェクトフォルダに保存します。
:::note warn
日本語文字列を直接テンプレートに書くと環境によっては SyntaxError: invalid character が発生するため、Unicodeエスケープを使用しています。
:::
# -*- coding: utf-8 -*-
import gradio as gr
from langchain_ollama import OllamaEmbeddings, ChatOllama
from langchain_chroma import Chroma
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# --- ChromaDB(ベクトルDB)の読み込み ---
embeddings = OllamaEmbeddings(model="nomic-embed-text")
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings
)
# k=5: 質問に近いポケモンデータを5件取得する
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# --- LLMの設定 ---
# temperature=0: 毎回安定した回答を生成(0に近いほど決定論的)
llm = ChatOllama(model="qwen2.5:7b", temperature=0)
# --- プロンプトテンプレート ---
# {context}: ChromaDBから取得したポケモンデータが入る
# {question}: ユーザーの質問が入る
# ※日本語文字列はUnicodeエスケープで記述(文字化け対策)
TEMPLATE = (
"\u4ee5\u4e0b\u306e\u30dd\u30b1\u30e2\u30f3\u30c7\u30fc\u30bf\u3092\u53c2\u8003\u306b\u3001"
"\u65e5\u672c\u8a9e\u3067\u7b54\u3048\u3066\u304f\u3060\u3055\u3044\u3002\n\n"
"[\u53c2\u8003\u30c7\u30fc\u30bf]\n{context}\n\n"
"[\u8cea\u554f]\n{question}\n\n"
"[\u56de\u7b54]"
)
# 上記は以下と同じ意味:
# "以下のポケモンデータを参考に、日本語で答えてください。\n\n
# [参考データ]\n{context}\n\n[質問]\n{question}\n\n[回答]"
prompt = PromptTemplate(
input_variables=["context", "question"],
template=TEMPLATE
)
def format_docs(docs):
"""取得した複数のDocumentを1つのテキストに結合する"""
return "\n\n".join(doc.page_content for doc in docs)
# --- LCELチェーンの構築 ---
# 旧: RetrievalQA.from_chain_type()(非推奨)
# 新: LCEL(| 演算子でつなぐ記法)
chain = (
{
"context": retriever | format_docs, # 検索結果をテキストに変換
"question": RunnablePassthrough() # 質問をそのまま渡す
}
| prompt # プロンプトテンプレートに埋め込む
| llm # LLMで回答生成
| StrOutputParser() # 出力を文字列に変換
)
def chat(message, history):
"""Gradioのチャット関数"""
return chain.invoke(message)
# --- GradioでWebUIを起動 ---
gr.ChatInterface(
chat,
title="Pokemon Stats AI Chat",
description="Ask about Pokemon base stats in Japanese",
examples=[
"\u7d20\u65e9\u3055\u304c\u6700\u3082\u9ad8\u3044\u30dd\u30b1\u30e2\u30f3\u306f\uff1f",
"HP\u304c100\u4ee5\u4e0b\u3067\u5408\u8a08\u304c\u9ad8\u3044\u30dd\u30b1\u30e2\u30f3\u3092\u6559\u3048\u3066",
"\u30ea\u30b6\u30fc\u30c9\u30f3\u3068\u30ac\u30d6\u30ea\u30a2\u30b9\u306e\u7a2e\u65cf\u5024\u3092\u6bd4\u8f03\u3057\u3066",
]
).launch()
4-4. 実行
python app.py
起動後に以下が表示されます:
Running on local URL: http://127.0.0.1:7860
ブラウザで http://localhost:7860 を開くとチャット画面が表示されます。
Part 5: ハマりポイントと解決策
実際に構築した中でつまずいた箇所をまとめます。
エラー1: ModuleNotFoundError: No module named 'langchain.schema'
原因: LangChainのバージョンアップでモジュールの場所が変わった。
# ❌ 旧パス(エラーになる)
from langchain.schema import Document
# ✅ 新パス
from langchain_core.documents import Document
エラー2: ModuleNotFoundError: No module named 'langchain.chains'
原因: RetrievalQA が旧APIで非推奨になった。
# ❌ 旧記法
from langchain.chains import RetrievalQA
qa = RetrievalQA.from_chain_type(llm=llm, ...)
# ✅ 新記法(LCEL)
from langchain_core.runnables import RunnablePassthrough
chain = ({"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt | llm | StrOutputParser())
エラー3: ConnectionError: Failed to connect to Ollama
原因: Ollamaサーバーが起動していない。
# 確認
curl http://localhost:11434
# 起動(別ターミナルで)
ollama serve
# または常駐設定
brew services start ollama
エラー4: error: externally-managed-environment
原因: HomebrewのPythonに直接pipしようとした。
# ✅ 仮想環境を作って使う
python3 -m venv venv
source venv/bin/activate
pip install ...
エラー5: SyntaxError: invalid character '、' (U+3001)
原因: ファイルの文字コードが正しく認識されず、全角文字がSyntaxErrorになった。
解決策: 日本語文字列をUnicodeエスケープで書く。
# ❌ 環境によってはSyntaxError
template = "以下のデータを参考に、日本語で答えてください。"
# ✅ Unicodeエスケープで記述
template = "\u4ee5\u4e0b\u306e\u30c7\u30fc\u30bf\u3092\u53c2\u8003\u306b\u3001\u65e5\u672c\u8a9e\u3067\u7b54\u3048\u3066\u304f\u3060\u3055\u3044\u3002"
CSVをExcelで開くと文字化けする
原因: UTF-8のBOMなしファイルをExcelが認識できない。
# ❌ 文字化けする
open(filename, encoding='utf-8')
# ✅ BOM付きUTF-8にする
open(filename, encoding='utf-8-sig')
Part 6: 完成後のフォルダ構成と起動手順
フォルダ構成
CSVDate/
├── venv/ # Python仮想環境(gitignoreに追加推奨)
├── chroma_db/ # ChromaDBのデータ(gitignoreに追加推奨)
│ ├── chroma.sqlite3
│ └── ...
├── all_pokemon_stats.csv # 取得したポケモンデータ(1350件)
├── all_pokemon_stats.py # Step2: CSV取得スクリプト
├── ingest.py # Step3: ベクトルDB構築スクリプト
└── app.py # Step4: チャットアプリ
2回目以降の起動手順
# 1. プロジェクトフォルダへ移動
cd ~/Desktop/myprogram/CSVDate
# 2. 仮想環境を有効化(毎回必要)
source venv/bin/activate
# 3. アプリを起動(ingest.pyは初回のみ)
python app.py
# → ブラウザで http://localhost:7860 を開く
メモリ使用量の目安
コンポーネント 消費メモリ
Qwen2.5:7b(LLM) 約 4.7 GB
nomic-embed-text(埋め込み) 約 0.3 GB
Python + ChromaDB 約 1.0 GB
合計 約 6.0 GB
16GBのうち6GB使用。Safari・VSCode・Terminal を同時に開いても十分余裕があります。
まとめ
今回構築したシステムの特徴:
完全ローカル動作 — インターネット接続不要、データが外に出ない
無料 — APIキー不要、課金なし
高速 — Apple Silicon Metalで最適化されたOllamaで快適な推論速度
拡張しやすい — CSVを差し替えるだけで別のドメインにも応用可能
PokeAPI以外のデータ(例: 自社の商品データ、社内ドキュメントなど)にも同じ構成で応用できます。ingest.py のCSV読み込み部分を変えるだけです。
参考リンク
PokeAPI公式
Ollama公式
LangChain LCEL ドキュメント
ChromaDB ドキュメント
Gradio ドキュメント