LlamaIndexのVectorStore(ChromaDB、Faiss、Qdrant)

LlamaIndexのVectorStore(ChromaDB、Faiss、Qdrant)

https://highreso.jp/edgehub/machinelearning/llamaindexvectorstore.html
の記事を読んでみる

スペックは
* GPU:NVIDIA A100 80GB
* GPUメモリ(VRAM):80GB
* OS :Ubuntu 22.04

Colabでなら再現できるかも

Docker部分はいらないので飛ばす

Llama-3-ELYZA-JP-8B-q4_k_m.gguf
を使用してる

LLMの実行にはOllamaを使用

日本語性能が高く、無料で使える埋め込みモデル「intfloat/multilingual-e5-large」を指定

from llama_index.embeddings.huggingface import HuggingFaceEmbedding

Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")

Wikipediaの『宮部みゆきの小説』に関するページを指定

from llama_index.readers.web import SimpleWebPageReader
documents = SimpleWebPageReader(html_to_text=True).load_data(
    ["https://ja.wikipedia.org/wiki/%E7%81%AB%E8%BB%8A_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E7%90%86%E7%94%B1_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E5%90%8D%E3%82%82%E3%81%AA%E3%81%8D%E6%AF%92",
    "https://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%AD%E3%83%A2%E3%83%B3%E3%81%AE%E5%81%BD%E8%A8%BC"]
)

SimpleWebPageReader(html_to_text=True).load_data()で
指定したWebページから取得したHTMLデータをテキスト形式で読み込み

テキストをチャンクに分割
読み込んだデータをチャンクに分割し、各チャンクをノードとして処理します。
ノードは効率的な検索やクエリ処理をサポートするため、メタデータや関係情報を含む単位

from llama_index.core.node_parser import TokenTextSplitter

splitter = TokenTextSplitter(
    chunk_size=1000,
    chunk_overlap=100,
    separator=" ",
)
nodes = splitter.get_nodes_from_documents(documents)
nodes[10:13]

コード解説

splitter = TokenTextSplitter(…

* chunk_size=1000: 1つのチャンクに含まれるトークンの最大数を1000トークンに設定しています。これにより、テキストが1000トークンずつ分割されます。
* chunk_overlap=100: 隣接するチャンク間で100トークンの重複が設定されています。これにより、文脈の連続性を保つことができ、モデルがより意味を理解しやすくなります。
* separator=” “: トークンを分割する際の区切り文字として、スペース(” “)を使用しています。これは、一般的に単語の区切りを意味します。

nodes = splitter.get_nodes_from_documents(documents)

指定したdocumentsをトークン単位で分割し、それぞれをノードに変換します。ノードはテキストの一部分を保持し、後でモデルに入力するためのチャンクとなります

LlamaIndexで用意されているシンプルなVectorStoreの構築について

pip install llama-index llama-index-core

でインストール

VectorStoreIndexクラスを使って、ベクトルストアのインデックスを作成

from llama_index.core import VectorStoreIndex

index = VectorStoreIndex(nodes, show_progress=True)

なお

show_progress=True

だと
インデックスの構築プロセス中に進捗バー(プログレスバー)が表示される
特に大量のノードを処理する場合、インデックスの構築には時間がかかることがあります。このオプションを True にすることで、現在の処理状況や残り時間を視覚的に確認できるようになり、ユーザーエクスペリエンスが向上
なおデフォルトはfalse

次に
インデックスをもとにクエリエンジンを作成

query_engine = index.as_query_engine(similarity_top_k=3)
response = query_engine.query("火車の概要を教えて下さい")
print(response)


index.as_query_engine():

* これは、VectorStoreIndex オブジェクトが持つメソッドで、このインデックスを基にしたクエリエンジンを生成

オプションの解説

similarity_top_k=3:

* これは、クエリエンジンに渡すオプションの引数です。
* similarity_top_k は、与えられたクエリに対して、ベクトルストア内で最も類似度が高い上位 k 個のノード(チャンク)を取得するように指定します。
* この場合、similarity_top_k=3 なので、クエリのベクトルと最も近い3つのノードがインデックスから検索され、それらのノードの内容が回答生成の文脈として使用されます。
* この値を調整することで、取得する情報の量と関連性のバランスを取ることができます。値を大きくするとより多くの情報が取得されますが、必ずしも関連性の高い情報ばかりとは限りません。

インデックスのIDを設定し、指定したディレクトリに保存

index.set_index_id("miyabe_miyuki")
index.storage_context.persist("./storage/simple_vs")

解説

index.set_index_id(“miyabe_miyuki”)

* indexに対して、IDとして “miyabe_miyuki” を設定しています。インデックスIDは、インデックスを一意に識別するためのもので、後でインデックスをロードしたり、クエリを実行したりする際に役立ちます。

index.storage_context.persist(“./storage/simple_vs”)

データの保存や管理を行う部分(storage_context)を指定されたディレクトリ(”./storage/simple_vs”)に保存しています。このディレクトリにインデックスのデータが保存され、次回以降の使用時に再インデックスすることなく、同じデータをすぐに利用できるようになります。

なお

./storage/simple_vs

がないと自動で作成される
また、
index.storage_context.persist(“./my_custom_index_data”)
というように
カレントディレクトリ直下の別の名前のフォルダに保存するのも可能

次に
保存されたインデックスをストレージからロードして再利用

from llama_index.core import StorageContext, load_index_from_storage

storage_context = StorageContext.from_defaults(persist_dir="./storage/simple_vs")

simple_vc_index = load_index_from_storage(storage_context, index_id="miyabe_miyuki")

解説

storage_context = StorageContext.from_defaults(persist_dir=”./storage/simple_vs”)

* ここでは、”./storage/simple_vs” というディレクトリにある保存済みデータを扱うための StorageContext を作成しています。

simple_vc_index = load_index_from_storage(storage_context, index_id=”miyabe_miyuki”)

* storage_context と index_id=”miyabe_miyuki” を使って、ストレージからインデックスをロードしています。
• index_id=”miyabe_miyuki” は、保存したインデックスに設定された一意のIDです。このIDを指定することで、その特定のインデックスをロードしています。これにより、以前に保存したインデックスを再構築することなく、再度使用できるようになります。

つまり
先の部分で

index.set_index_id("miyabe_miyuki")
index.storage_context.persist("./storage/simple_vs")

でIDとデータ保存先を指定しているので
これを読み込んでいるということ
データ先を指定してIDを指定して読み込んでいる

ここまでがllamaindexのデータ保存

ここからがChromaDBを使用してベクトルストアを保存および再利用する方法

pip install chromadb llama-index-vector-stores-chroma

でインストール

ChromaDBを使用してベクトルストアのインデックスを作成

import chromadb
from llama_index.core import VectorStoreIndex
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import StorageContext

chroma_client = chromadb.EphemeralClient()
chroma_collection = chroma_client.create_collection("miyabe_miyuki")

vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)

chroma_index = VectorStoreIndex(nodes, storage_context=storage_context)

解説

chroma_client = chromadb.EphemeralClient()

ChromaDBのエフェメラル(短期間の)クライアントを作成します。これは、データを一時的に保存し、サーバーが終了すると削除されるメモリ内のデータベースを意味します。

chroma_collection = chroma_client.create_collection(“miyabe_miyuki”)

chroma_clientを使用して、新しいコレクション(データのコンテナ)を作成しています。ここではコレクション名を”miyabe_miyuki”として指定しています。

vector_store = ChromaVectorStore(chroma_collection=chroma_collection)

ChromaVectorStoreを使って、ChromaDB内のコレクション(chroma_collection)をベクトルストアとして扱うためのオブジェクトを作成しています。これにより、コレクション内のデータに対してベクトル検索を行えるようになります。

storage_context = StorageContext.from_defaults(vector_store=vector_store)

StorageContextは、データの保存や管理を行うためのオブジェクトです。ここでは、先に作成したvector_store(ChromaDBベースのベクトルストア)をStorageContextにセットして、データ管理を行えるようにしています。

そして
インデックスをもとにクエリエンジンを作成

query_engine = chroma_index.as_query_engine()
response = query_engine.query("『理由』で事件のあったマンション名を教えて下さい。")
print(response)

解説

query_engine = chroma_index.as_query_engine()

• as_query_engine:インデックスもとにユーザーの質問に対して回答を生成するクエリエンジンを構築します。

つまり

llamaindexの時にはquery_engine.query

だったけど
chromaなら

chroma_index.as_query_engine()

を使う

インデックスのIDを設定し、指定したディレクトリに保存

db = chromadb.PersistentClient(path="./storage/chroma")
chroma_collection = db.get_or_create_collection("miyabe_miyuki")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
chroma_index = VectorStoreIndex(nodes, storage_context=storage_context, show_progress=True)

解説

db = chromadb.PersistentClient(path=”./storage/chroma”)

* PersistentClientを作成し、path=”./storage/chroma”という指定されたディレクトリにデータを永続化します。

chroma_collection = db.get_or_create_collection(“miyabe_miyuki”)

* get_or_create_collectionメソッドは、”miyabe_miyuki”という名前のコレクションを取得するか、存在しない場合は新しく作成します。

vector_store = ChromaVectorStore(chroma_collection=chroma_collection)

* ChromaVectorStoreを作成し、先ほど作成したchroma_collectionをベクトルストアとして使用します。

storage_context = StorageContext.from_defaults(vector_store=vector_store)

* StorageContextを作成し、vector_storeをその一部として設定します。

chroma_index = VectorStoreIndex(nodes, storage_context=storage_context, show_progress=True)

* VectorStoreIndexを作成し、nodesとstorage_contextをもとにインデックスを構築します。
* VectorStoreIndexは、ベクトル検索用のインデックスを作成し、指定されたnodesをインデックス化します。
• show_progress=Trueは、インデックス作成中に進行状況を表示するオプションで、インデックスの構築が進んでいるかを確認できます。

以前のllamaindex単体の時には

index.set_index_id("miyabe_miyuki")
index.storage_context.persist("./storage/simple_vs")

chromaの時には

db = chromadb.PersistentClient(path="./storage/chroma")
chroma_collection = db.get_or_create_collection("miyabe_miyuki")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
chroma_index = VectorStoreIndex(nodes, storage_context=storage_context, show_progress=True)

というように最初に
DB保存先を指定
get_or_create_collectionでDB情報の取得、ないなら新規作成する
ChromaVectorStoreを作成してベクトルスコアを指定

from_defaults() によって設定されるデフォルトのインメモリ vector_store を、
ユーザーが事前に定義した vector_store オブジェクトで上書きしている

デフォルトでは、すべてのストア(docstore, vector_store, index_store)は、シンプルなインメモリ(RAM上)のストアとして初期化される

つまり

storage_context = StorageContext.from_defaults(vector_store=vector_store)

によって
このコード行の目的は、VectorStoreIndex を構築する際に、ベクトル埋め込みの保存先として、デフォルトのインメモリストアではなく、指定された外部のベクトルデータベース(vector_store 変数で指定されたもの)を使用するように設定

次に
保存されたインデックスをストレージからロードして再利用

db = chromadb.PersistentClient(path="./storage/chroma")
chroma_collection = db.get_or_create_collection("miyabe_miyuki")
chroma_vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
chroma_index = VectorStoreIndex.from_vector_store(vector_store=chroma_vector_store)

解説

chroma_collection = db.get_or_create_collection(“miyabe_miyuki”)

PersistentClientを使って、以前保存したChromaDBのデータベースを再利用するための接続を行います。
pathで指定したディレクトリ (./storage/chroma) に保存されたデータベースを読み込み、再利用します。

chroma_vector_store = ChromaVectorStore(chroma_collection=chroma_collection)

すでに保存されている “miyabe_miyuki” という名前のコレクションを取得します。g
et_or_create_collectionは、もしコレクションが存在しない場合に新たに作成しますが、
ここでは既存のコレクションを再利用しています。

chroma_index = VectorStoreIndex.from_vector_store(vector_store=chroma_vector_store)

取得したコレクションを元にChromaVectorStoreを読み込み、保存されたベクトルストアを再利用します。
これにより、新規にベクトルストアを構築することなく、保存済みのベクトルデータをそのまま利用することができます。

chroma_index = VectorStoreIndex.from_vector_store(vector_store=chroma_vector_store)

from_vector_storeメソッドを使って、保存されたベクトルストア (chroma_vector_store) を元にVectorStoreIndexを再利用します。

とりあえず解説はこれがわかりやすい

他の事例も調べる
ChromaDBのコレクションを作成またはアクセスする際にwatsonx.aiのEmbeddingモデルを使用する方法(watsonx.ai + LlamaIndex + Chroma DB)

なお

import ollama
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama

modelfile='''
FROM ./Llama-3-ELYZA-JP-8B-q4_k_m.gguf
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>

{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>

{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>

{{ .Response }}<|eot_id|>"""
PARAMETER stop "<|start_header_id|>"
PARAMETER stop "<|end_header_id|>"
PARAMETER stop "<|eot_id|>"
PARAMETER stop "<|reserved_special_token"
'''

ollama.create(model='elyza8b', modelfile=modelfile)
Settings.llm = Ollama(model="elyza8b", request_timeout=360.0)

でモデルを作成しているが既にあるなら

modelfile='''
FROM ./Llama-3-ELYZA-JP-8B-q4_k_m.gguf
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>

{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>

{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>

{{ .Response }}<|eot_id|>"""
PARAMETER stop "<|start_header_id|>"
PARAMETER stop "<|end_header_id|>"
PARAMETER stop "<|eot_id|>"
PARAMETER stop "<|reserved_special_token"
'''

ollama.create(model='elyza8b', modelfile=modelfile)

は不要

また

embed_model = HuggingFaceEmbedding(model_name="all-MiniLM-L6-v2")

よりも

Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")

の方が日本語取扱には適するらしい

 touch save_index_test.py

でテストする

import ollama
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama

Settings.llm = Ollama(model="elyza8b", request_timeout=360.0)

#日本語性能が高く、無料で使える埋め込みモデル「intfloat/multilingual-e5-large」を指定
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")

#Wikipediaの『宮部みゆきの小説』に関するページを指定
from llama_index.readers.web import SimpleWebPageReader
documents = SimpleWebPageReader(html_to_text=True).load_data(
    ["https://ja.wikipedia.org/wiki/%E7%81%AB%E8%BB%8A_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E7%90%86%E7%94%B1_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E5%90%8D%E3%82%82%E3%81%AA%E3%81%8D%E6%AF%92",
    "https://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%AD%E3%83%A2%E3%83%B3%E3%81%AE%E5%81%BD%E8%A8%BC"]
)

#読み込んだデータをチャンクに分割し、各チャンクをノードとして処理します
from llama_index.core.node_parser import TokenTextSplitter

splitter = TokenTextSplitter(
    chunk_size=1000,
    chunk_overlap=100,
    separator=" ",
)
nodes = splitter.get_nodes_from_documents(documents)
nodes[10:13]

#VectorStoreIndexクラスを使って、ベクトルストアのインデックスを作成
from llama_index.core import VectorStoreIndex

index = VectorStoreIndex(nodes, show_progress=True)

#インデックスをもとにクエリエンジンを作成
query_engine = index.as_query_engine(similarity_top_k=3)
response = query_engine.query("火車の概要を教えて下さい")
print(response)

#インデックスのIDを設定し、指定したディレクトリに保存
index.set_index_id("miyabe_miyuki")
index.storage_context.persist("./storage/simple_vs")

#保存されたインデックスをストレージからロードして再利用
from llama_index.core import StorageContext, load_index_from_storage

storage_context = StorageContext.from_defaults(persist_dir="./storage/simple_vs")

simple_vc_index = load_index_from_storage(storage_context, index_id="miyabe_miyuki")

というようにまとめたものを
インデックス作成のみにコード変更
作成は gemini を使用

import ollama
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.readers.web import SimpleWebPageReader
from llama_index.core.node_parser import TokenTextSplitter
from llama_index.core import VectorStoreIndex

# --- 1. LLMと埋め込みモデルの設定 ---
# OllamaのLLMを設定
# 'elyza8b'モデルが既にOllamaサーバーに存在していることを前提とします。
Settings.llm = Ollama(model="elyza8b", request_timeout=360.0)

# 日本語性能の高い埋め込みモデルを設定
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. データのロード ---
# Wikipediaの指定されたページからデータをロード
print("Wikipediaページからデータをロード中...")
urls = [
    "https://ja.wikipedia.org/wiki/%E7%81%AB%E8%BB%8A_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E7%90%86%E7%94%B1_(%E5%B0%8F%E8%AA%AC)",
    "https://ja.wikipedia.org/wiki/%E5%90%8D%E3%82%82%E3%81%AA%E3%81%8D%E6%AF%92",
    "https://ja.wikipedia.org/wiki/%E3%82%BD%E3%83%AD%E3%83%A2%E3%83%B3%E3%81%AE%E5%81%BD%E8%A8%BC"
]
documents = SimpleWebPageReader(html_to_text=True).load_data(urls)
print(f"合計 {len(documents)} ページのドキュメントをロードしました。")

# --- 3. ドキュメントのチャンク化とノード化 ---
# 読み込んだドキュメントをチャンクに分割し、ノードとして処理
print("ドキュメントをノードに分割中...")
splitter = TokenTextSplitter(
    chunk_size=1000,
    chunk_overlap=100,
    separator=" ",
)
nodes = splitter.get_nodes_from_documents(documents)
print(f"合計 {len(nodes)} 個のノードが生成されました。")
# 確認のため、一部のノードを表示(オプション)
# print("\n一部のノードの例:")
# for i, node in enumerate(nodes[10:13]):
#     print(f"Node {i+10}: {node.text[:100]}...") # 最初の100文字を表示
# print("---")

# --- 4. VectorStoreIndexの作成 ---
# ノードからVectorStoreIndexを作成
print("VectorStoreIndexを作成中...")
index = VectorStoreIndex(nodes, show_progress=True)
print("VectorStoreIndexの作成が完了しました。")

# --- 5. インデックスの保存 ---
# インデックスにIDを設定し、指定したディレクトリに保存
# 保存先ディレクトリが存在しない場合は自動で作成されます。
index_id = "miyabe_miyuki"
persist_dir = "./storage/simple_vs"

print(f"インデックスを '{persist_dir}' に ID '{index_id}' で保存中...")
index.set_index_id(index_id)
index.storage_context.persist(persist_dir)
print("インデックスの保存が完了しました。")
print("これで、次回からはこのインデックスを再利用できます。")

実行したら

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/ollama/save_index_test.py", line 5, in <module>
    from llama_index.readers.web import SimpleWebPageReader
ModuleNotFoundError: No module named 'llama_index.readers.web'

となるので

pip install llama-index-readers-web

これを実行すると

ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
ollama 0.4.6 requires httpx<0.28.0,>=0.27.0, but you have httpx 0.28.1 which is incompatible.
Successfully installed chromedriver-autoinstaller-0.6.4 cssselect-1.3.0 feedfinder2-0.0.4 feedparser-6.0.11 httpx-0.28.1 jieba3k-0.35.1 llama-index-readers-web-0.4.1 lxml-5.4.0 lxml-html-clean-0.4.2 newspaper3k-0.2.8 oxylabs-2.0.0 requests-file-2.1.0 sgmllib3k-1.0.0 spider-client-0.0.27 tinysegmenter-0.3 tldextract-5.3.0

となる

なので

pip install httpx==0.28.0

これで再度

 python save_index_test.py     

を実行

modules.json: 100%|█████████████████████████████| 387/387 [00:00<00:00, 631kB/s]
README.md: 100%|██████████████████████████████| 160k/160k [00:00<00:00, 931kB/s]
sentence_bert_config.json: 100%|██████████████| 57.0/57.0 [00:00<00:00, 250kB/s]
config.json: 100%|█████████████████████████████| 690/690 [00:00<00:00, 2.76MB/s]
model.safetensors: 100%|███████████████████| 2.24G/2.24G [03:19<00:00, 11.2MB/s]
tokenizer_config.json: 100%|███████████████████| 418/418 [00:00<00:00, 1.71MB/s]
sentencepiece.bpe.model: 100%|█████████████| 5.07M/5.07M [00:00<00:00, 11.6MB/s]
tokenizer.json: 100%|██████████████████████| 17.1M/17.1M [00:01<00:00, 12.2MB/s]
special_tokens_map.json: 100%|█████████████████| 280/280 [00:00<00:00, 1.32MB/s]
config.json: 100%|██████████████████████████████| 201/201 [00:00<00:00, 994kB/s]
LLMと埋め込みモデルの設定が完了しました。
Wikipediaページからデータをロード中...
合計 4 ページのドキュメントをロードしました。
ドキュメントをノードに分割中...
合計 211 個のノードが生成されました。
VectorStoreIndexを作成中...
Generating embeddings: 100%|██████████████████| 211/211 [01:05<00:00,  3.20it/s]
VectorStoreIndexの作成が完了しました。
インデックスを './storage/simple_vs' に ID 'miyabe_miyuki' で保存中...
インデックスの保存が完了しました。
これで、次回からはこのインデックスを再利用できます。

次に
保存されたインデックスをロードし、キーボードからの質問に回答する

 ls storage/simple_vs/
default__vector_store.json	image__vector_store.json
docstore.json			index_store.json
graph_store.json


インデックスデータが保存されていることを確認

touch ask_index.py

import sys
from llama_index.core import StorageContext, load_index_from_storage
from llama_index.core import Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

# --- 1. LLMと埋め込みモデルの設定 ---
# インデックス作成時と同じモデルを設定する必要があります。
# これにより、クエリのベクトル化と回答生成が正しく行われます。
Settings.llm = Ollama(model="elyza8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    # 保存先のディレクトリとインデックスIDを指定
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    # StorageContext を設定し、保存先のディレクトリを指定
    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)

    # 指定されたストレージコンテキストとインデックスIDでインデックスをロード
    # これにより、ディスクからデータがメモリに読み込まれ、再利用可能になります。
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    # ロードしたインデックスからクエリエンジンを作成
    # similarity_top_k は、回答生成のために検索するノードの数を指定します。
    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    # --- 3. ユーザーからの質問を受け付けるループ ---
    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。")
            break

        if not query.strip(): # 空の入力の場合
            print("質問を入力してください。")
            continue

        print("回答を生成中...")
        try:
            # クエリエンジンで質問を処理し、回答を取得
            response = query_engine.query(query)
            print("\n--- 回答 ---")
            print(response)
            print("------------\n")
        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")
            print("もう一度お試しください。")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' ディレクトリが存在し、以前にインデックスがID '{index_id}' で保存されているか確認してください。")
    print("インデックスを保存するには、提供された「インデックス作成と保存コード」を実行する必要があります。")
    sys.exit(1) # プログラムを終了
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")
    sys.exit(1) # プログラムを終了

で保存し

python ask_index.py 
LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 火車の概要を教えて下さい 
回答を生成中...
質問処理中にエラーが発生しました: model "elyza8b" not found, try pulling it first
もう一度お試しください。
あなたの質問: exit
プログラムを終了します。

となるので、モデル名を修正
正しくは

Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)

これで実行すると

 python ask_index.py
LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 火車の概要を教えて下さい 
回答を生成中...

--- 回答 ---
『火車』は、宮部みゆきの推理小説である。社会問題としての消費者金融のありかたをテーマとしており、サラリーマン金融やカード破産などの借財と多重債務を題材にした作品となっている。
------------

というように問題なく動作することができる
ただし挙動が少し遅い

他の質問をすると

あなたの質問: 日本の総理大臣は?
回答を生成中...

--- 回答 ---
この情報には日本の総理大臣に関する記述がありません。
------------

あなたの質問: exit
プログラムを終了します。
となってしまう

この結果を Gemini で調べてみると

うまく動作していますね! LlamaIndex を使った RAG (Retrieval-Augmented Generation) システムが意図通りに機能していることを示しています。
動作の分析
あなたの実行結果は、以下のように解釈できます。
1. 火車の概要を教えて下さい:
* 期待通りの動作: 『火車』に関する情報は、インデックスの作成時に指定したWikipediaのURL(https://ja.wikipedia.org/wiki/%E7%81%AB%E8%BB%8A_(%E5%B0%8F%E8%AA%AC))に含まれています。
* RAGの成功: LlamaIndex が質問をベクトル化し、関連するノードをインデックスから検索し、その情報に基づいて Ollama の elyza8b モデルが正確な概要を生成しました。

2. 陰陽五行について教えて:
* 「記述がありません。」: これは、インデックス作成時に与えたWikipediaのURL(宮部みゆきさんの小説に関するページ)に陰陽五行に関する情報が含まれていないため、LlamaIndex が関連するノードを見つけられなかったことを意味します。
* LLMの正直な回答: 大規模言語モデル(LLM)は、検索された情報に基づいて回答を生成します。関連情報が見つからない場合、このように「記述がありません」といった回答を返すのは、ハルシネーション(でっち上げ)を防ぐための正しい挙動です。

3. 日本の総理大臣は?:
* 「この情報には日本の総理大臣に関する記述がありません。」: 陰陽五行 の場合と同様に、インデックスのソースデータには日本の総理大臣に関する情報が含まれていません。
* LLMの正しい挙動: LLMは与えられた知識ベース(今回RAGで検索されたノード)の範囲内で回答を生成しようとします。その範囲外の知識については、このように情報がないことを伝えます。

まとめ
あなたの LlamaIndex 環境は、指定した知識ベース(今回の場合、宮部みゆきさんのWikipediaページ)に基づいて質問に回答できる状態になっています。これは、RAG システムが意図通りに機能している証拠です。
* インデックスに含まれる情報: 質問に正確に回答。
* インデックスに含まれない情報: 情報がないことを正直に回答。
この挙動は、限定された専門知識を持つAIシステムを構築する上で非常に重要です。もし汎用的な知識も必要とする場合は、インデックスに追加するドキュメントの範囲を広げたり、RAGと通常のLLMの推論を組み合わせたりするなどの工夫が必要になります。

つまり動作は正常だが
RAGと通常のLLMの推論を組み合わせが必要になる

import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            response = query_engine.query(query)
            
            # RAGの回答が空または不十分な場合
            if not response or "情報がありません" in response.response or len(response.response.strip()) == 0:
                print("RAGに該当情報がないため、直接LLMで回答します。");
                direct_response = direct_llm.complete(query)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")

        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

としたが

実行すると

LLMと埋め込みモデルの設定が完了しました。 インデックスをロード中... インデックスのロードが完了しました。 クエリエンジンの準備ができました。 質問を入力してください(終了するには 'exit' と入力)。 あなたの質問: 日本の総理大臣は 回答を生成中... --- 回答 --- 提供された文書には、日本の総理大臣に関する情報は含まれていません。 ------------ 

となる

elyza:jp8b単体では答えが出る

これは、現在のコードでRAGの回答が「情報がない」と認識できるように明示的な処理ができていないために起きています。
具体的には、LlamaIndexのquery_engine.query()メソッドは、情報がないときでも通常「情報がありません」というような明確なテキストを返さず、「提供された文書には〜情報は含まれていません」という形式のメッセージを返します。このため、現在の条件式では不十分

解決方法:
RAGの回答に「含まれていません」「情報は含まれていません」などの特定のフレーズが含まれているかで判定し、直接LLMを使って回答を得るよう調整

response = query_engine.query(query)

# RAGの回答が不十分な場合の判断を改善
insufficient_phrases = ["含まれていません", "情報は含まれていません", "情報がありません", "該当情報がありません"]

if any(phrase in response.response for phrase in insufficient_phrases) or not response.response.strip():
    print("RAGに該当情報がないため、直接LLMで回答します。")
    direct_response = direct_llm.complete(query)
    final_response = direct_response.text
else:
    final_response = response.response

print("\n--- 回答 ---")
print(final_response)
print("------------\n")

しかし

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 日本の総理大臣
回答を生成中...

--- 回答 ---
提供された文書には、特定のページに関する情報やリンクが含まれていますが、日本の総理大臣に関する記述はありません。
------------

となる

RAGの回答判定がうまく働いていないようです。この問題の原因は、返ってくる回答オブジェクトの型や内容が期待と異なるためです。
以下のように、明確に response.response の内容をチェックし、不十分な回答だと判断したらLLMへフォールバックするようコードを厳密に書き直す

import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            response = query_engine.query(query)
            
            # RAGの回答が不十分な場合の判断を改善
            insufficient_phrases = [
                "含まれていません",
                "情報は含まれていません",
                "情報がありません",
                "該当情報がありません",
                "記述はありません",
                "見つかりません"
            ]


            # 判定をresponse.responseのテキストで明示的に行う
            if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases):
                print("RAGに該当情報がないため、直接LLMで回答します。")
                direct_response = direct_llm.complete(query)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")

        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

これで
* insufficient_phrases に『記述はありません』や『見つかりません』といった、今回表示された具体的なフレーズを追加しました。
* これにより、今回のような回答が出た場合でも、確実にLLM(elyza:jp8b)にフォールバックします。
この修正版のコードを適用すると、質問『日本の総理大臣』のようにRAGに情報がない場合でも、elyza:jp8bを使用して正しく回答を生成できるようになります

実行すると


LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 日本の総理
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
日本の歴代総理大臣の一覧です。

1. 伊藤博文 (1878年12月22日 - 1885年12月24日)
2. 黒田清隆 (1885年12月24日 - 1889年4月10日)
3. 山縣有朋 (1889年4月10日 - 1891年11月30日)
4. 松方正義 (1891年11月30日 - 1896年9月18日)
5. 伊藤博文 (1896年9月18日 - 1898年11月30日)
6. 小松原英太郎 (1898年11月30日 - 1900年10月31日)
7. 加藤高明 (1900年10月31日 - 1901年6月13日)
8. 松方正義 (1901年6月13日 - 1902年9月15日)
9. 尾崎行雄 (1902年9月15日 - 1903年7月30日)
10. 上原勇作 (1903年7月30日 - 1904年11月29日)
11. 福島県知事・大隈重信 (1905年8月31日 - 1906年12月25日)
12. 桂太郎 (1907年1月18日 - 1911年10月28日)
13. 小村寿太郎 (1912年4月30日 - 1913年12月21日)
14. 内閣総理大臣・清浦奎吾 (1914年5月15日 - 1916年8月10日)
15. 加藤友三郎 (1916年8...
------------

あなたの質問: 今の日本の総理大臣
回答を生成中...

--- 回答 ---
この情報には、特定の人物や役職の名前が多数記載されています。ただし、現在の日本の総理大臣は明記されていません。
------------

あなたの質問: 今年の日本の総理大臣
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
2023年現在の日本の内閣総理大臣は、岸田文雄です。
------------

あなたの質問: 日本の首都は?
回答を生成中...

--- 回答 ---
東京
------------

あなたの質問: 現在のアメリカの大統領
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
2021年現在、アメリカ合衆国の大統領はジョー・バイデンです。
------------

となる

この状況が起きている原因は、LLMの回答においてモデルが「現在の年(2023年や2021年)」を正しく認識しておらず、古い情報や曖昧な回答を出してしまっているためです。
これはモデルが持つ知識が訓練データ(2023年10月まで)に依存しているため、現在(2025年)の最新情報とずれが生じていることが原因

これは
推論時に「現在の日付」をプロンプトに含めることで、回答を現在の日付に基づいた正確なものに誘導することができます

from datetime import datetime

response = query_engine.query(query)

insufficient_phrases = [
    "含まれていません",
    "情報は含まれていません",
    "情報がありません",
    "該当情報がありません",
    "記述はありません",
    "見つかりません"
]

current_date = datetime.now().strftime("%Y年%m月%d日")

if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases):
    print("RAGに該当情報がないため、直接LLMで回答します。")
    
    prompt_with_date = f"今日は{current_date}です。この日付を基準として次の質問に答えてください。\n質問:{query}"
    direct_response = direct_llm.complete(prompt_with_date)
    final_response = direct_response.text
else:
    final_response = response.response

print("\n--- 回答 ---")
print(final_response)
print("------------\n")

というようにコード変更する

これで実験する

LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 現在のアメリカの大統領
回答を生成中...

--- 回答 ---
この質問は、ウィキペディアやウィキデータに該当する情報が含まれています。
------------

あなたの質問: 今の日本の総理大臣
回答を生成中...

--- 回答 ---
この情報は、Wikipediaのページ編集用のメニューであり、政治や人物に関する情報が含まれています。したがって、今の日本の総理大臣を特定することができません。
------------

という結果になる

このため
RAGの応答が新たなパターンで返ってきているため、現在の条件文では対応できていないからです。
今回返ってきた文言:
* 「ウィキペディアやウィキデータに該当する情報が含まれています。」
* 「Wikipediaのページ編集用のメニューであり〜特定することができません。」
を追加の判定条件として組み込み対処

            insufficient_phrases = [
                "含まれていません",
                "情報は含まれていません",
                "情報がありません",
                "該当情報がありません",
                "記述はありません",
                "見つかりません",
                "特定することができません",
                "Wikipediaのページ編集用",
                "ウィキペディアやウィキデータ"
            ]

というように対応ワードを追加

実行結果で気になるのが

import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from datetime import datetime

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            response = query_engine.query(query)

            insufficient_phrases = [
                "含まれていません",
                "情報は含まれていません",
                "情報がありません",
                "該当情報がありません",
                "記述はありません",
                "見つかりません",
                "特定することができません",
                "Wikipediaのページ編集用",
                "ウィキペディアやウィキデータ",
                "記載がありません",
                "MediaWiki",
                "閲覧したとしても"
            ]

            current_date = datetime.now().strftime("%Y年%m月%d日")

            if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases):
                print("RAGに該当情報がないため、直接LLMで回答します。")
                
                prompt_with_date = f"今日は{current_date}です。この日付を基準として次の質問に答えてください。\n質問:{query}"
                direct_response = direct_llm.complete(prompt_with_date)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")


        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

を実行すると

あなたの質問: 現在時刻を教えて
回答を生成中...

--- 回答 ---
2025年5月11日(日)10:52
------------

となる
現在の日付とあっていない
しかし

あなたの質問: 今日は何曜日?
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
2025年05月29日は、木曜日にあたります。
------------

では正確な答えになる

これは
質問に日付を含めてモデルに渡してはいるものの、「現在時刻を教えて」の質問に対して、モデルが自分の持つ内部情報(訓練時の情報)を参照して回答を返してしまっているから
具体的に言うと、質問に『今日は2025年05月29日です』と日付は含めていますが、『時刻』までは渡していません。そのため、LLMが「時刻」に関しては独自の推測(誤った日時)を返しています

日付だけでなく、現在時刻も明示的にプロンプトに含めることで正確な回答を得られるようになります
ということなので

            current_date = datetime.now().strftime("%Y年%m月%d日")

            current_datetime = datetime.now().strftime("%Y年%m月%d日 %H:%M")

            prompt_with_datetime = f"現在の日時は {current_datetime} です。この日時を基準として次の質問に答えてください。\n質問:{query}"

と変更

全体コードとしては

import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from datetime import datetime

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            response = query_engine.query(query)

            insufficient_phrases = [
                "含まれていません",
                "情報は含まれていません",
                "情報がありません",
                "該当情報がありません",
                "記述はありません",
                "見つかりません",
                "特定することができません",
                "Wikipediaのページ編集用",
                "ウィキペディアやウィキデータ",
                "記載がありません",
                "MediaWiki",
                "閲覧したとしても"
            ]
            current_datetime = datetime.now().strftime("%Y年%m月%d日 %H:%M")
            prompt_with_datetime = f"現在の日時は {current_datetime} です。この日時を基準として次の質問に答えてください。\n質問:{query}"

            if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases):
                print("RAGに該当情報がないため、直接LLMで回答します。")
                direct_response = direct_llm.complete(prompt_with_datetime)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")

        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

しかし結果は

LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 現在の時刻は?
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
現在の時刻は、00:46です。
------------

あなたの質問: 現在時刻を教えて
回答を生成中...

--- 回答 ---
2025年5月11日 (日) 10:52
------------

あなたの質問: 今の時間は?
回答を生成中...
RAGに該当情報がないため、直接LLMで回答します。

--- 回答 ---
現在の時間は00:47です。
------------

あなたの質問: 今日は何日?
回答を生成中...

--- 回答 ---
この情報には、日付に関する記述は見当たりません。
------------

あなたの質問: 

というようにプロンプトにより結果が変わってしまう

前提条件を変更するため
Gemini で原因を聞いてみる

ご提示の実行結果を見ると、同じ「現在の時刻」に関する質問でも、RAG(Retrieval-Augmented Generation)の有無やLLMの回答内容に違いが生じていますね。この現象の主な原因は、プロンプトがLLMの回答に与える影響、特にRAGが機能しなかった場合のフォールバックメカニズムにあります。

プロンプトによる回答の正確さの変動要因
1. RAGの回答の有無と品質
RAGは、事前に用意された知識ベース(この場合はmiyabe_miyukiインデックス)から関連情報を検索し、その情報を基にLLMが回答を生成する仕組みです。
* RAGが機能した場合:
* あなたの質問「現在時刻を教えて」に対しては、RAGが何らかの情報を取得し、その情報をLLMが処理して「2025年5月11日 (日) 10:52」という回答を生成しています。これは、RAGが参照した情報源に、たまたま古い日付と時刻が含まれていた可能性を示唆しています。RAGは、その情報源に「現在時刻」に関する情報が含まれていない限り、リアルタイムの時刻を知ることはできません。 今回のインデックスが「宮部みゆき」氏に関するものであれば、時刻情報が含まれている可能性は極めて低いでしょう。
* RAGが機能しなかった場合(フォールバック):
* 「現在の時刻は?」や「今の時間は?」のように、RAGの検索結果に「不十分なフレーズ」が含まれていると判断された場合、スクリプトは直接LLMに質問を投げ直すフォールバック処理を行っています。この時、プロンプトには”現在の日時は {current_datetime} です。この日時を基準として次の質問に答えてください。\n質問:{query}”という形で、スクリプト実行時のリアルタイムの日時情報が付与されています。これにより、LLMは自身の持つ一般的な知識に加えて、明示的に与えられた最新の日時情報を参照して回答を生成します。そのため、「00:46」や「00:47」といった、スクリプト実行時に近い時刻を回答できています。
2. LLMの持つ一般的な知識とバイアス
LLMは学習データに基づいた一般的な知識を持っています。しかし、その知識はリアルタイムの情報ではありません。
* 「今日は何日?」への回答:
* この質問に対しては、RAGが「この情報には、日付に関する記述は見当たりません。」と回答しています。これはRAGの情報源に日付情報がなかったためと考えられます。そして、フォールバックが行われず、LLMに現在の日時情報を含むプロンプトが渡されなかったため、LLMはRAGの回答をそのまま出力してしまっています。もし、この質問もフォールバックの対象となっていれば、現在の正確な日付を回答できたはずです。
3. 「不十分なフレーズ」によるRAGの判断基準
スクリプト内で定義されているinsufficient_phrasesのリストは、RAGが生成した回答が「役に立たない」と判断するためのキーワードです。
* RAGの回答にこれらのフレーズが含まれていた場合、スクリプトはRAGの回答を破棄し、直接LLMに最新の日時情報を含むプロンプトを渡しています。これにより、LLMはより正確なリアルタイムの時刻情報を提供できています。

まとめ
プロンプトによって回答の正確さが変わる主な原因は、以下の2点に集約されます。
1. RAGが参照する情報源の特性: RAGは事前にインデックス化された情報しか利用できないため、リアルタイムの情報(現在の時刻や日付など)は提供できません。
2. フォールバック時のプロンプトの変化: RAGが役に立たないと判断された場合、スクリプトがLLMに渡すプロンプトにリアルタイムの日時情報が明示的に追加されるため、LLMはその情報に基づいてより正確な回答を生成できます。
したがって、今回のケースでは、スクリプトがフォールバック時に動的にプロンプトを調整していることが、時刻の回答の正確さに大きく寄与していると言えます。RAGが適切にリアルタイム情報を扱えない場合、このようなフォールバック戦略は非常に有効です。

この問題を解決するためにコードを変更

RAGの検索結果に関わらず、日付と時刻に関する質問に対しては、常に最新の日時情報をLLMに伝えるようにコードを変更

変更の概要
主な変更点は以下の通りです。
1. 日付・時刻関連のキーワードの検出: 質問内容が日付や時刻に関連するかどうかを判断するためのキーワードリストを定義します。
2. プロンプトへの日時情報追加の条件変更: RAGの回答が不十分な場合だけでなく、質問が日付や時刻に関連すると判断された場合にも、現在のリアルタイムの日時情報をLLMへのプロンプトに含めるようにします。
これにより、ユーザーが「現在の時刻は?」や「今日の日付は?」と質問した場合、RAGの検索結果に関わらず、スクリプトが取得した最新の日時情報がLLMに渡され、LLMはその情報に基づいて正確な回答を生成できるようになります

import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from datetime import datetime

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    # 日時関連の質問を検出するためのキーワード
    datetime_keywords = ["時刻", "時間", "日付", "何日", "今日", "今"]

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            # RAGで情報を検索
            response = query_engine.query(query)

            insufficient_phrases = [
                "含まれていません", "情報は含まれていません", "情報がありません",
                "該当情報がありません", "記述はありません", "見つかりません",
                "特定することができません", "Wikipediaのページ編集用", "ウィキペディアやウィキデータ",
                "記載がありません", "MediaWiki", "閲覧したとしても"
            ]

            # 日時関連の質問かどうかを判定
            is_datetime_query = any(keyword in query for keyword in datetime_keywords)

            # 現在の日時情報を取得
            current_datetime = datetime.now().strftime("%Y年%m月%d日 %H:%M")
            prompt_with_datetime = f"現在の日時は {current_datetime} です。この日時を基準として次の質問に答えてください。\n質問:{query}"

            final_response = ""

            # RAGの回答が不十分な場合、または日時関連の質問の場合に直接LLMで回答
            if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases) or is_datetime_query:
                print("RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。")
                direct_response = direct_llm.complete(prompt_with_datetime)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")

        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

とコード変更

現在の課題は、RAGがリアルタイム情報(特に日付と時刻)を提供できないために、LLMが常に正確な「現在」の情報を答えることができない点ですね。これを解決するために、RAGの検索結果に関わらず、日付と時刻に関する質問に対しては、常に最新の日時情報をLLMに伝えるようにコードを変更します。

変更の概要
主な変更点は以下の通りです。
1. 日付・時刻関連のキーワードの検出: 質問内容が日付や時刻に関連するかどうかを判断するためのキーワードリストを定義します。
2. プロンプトへの日時情報追加の条件変更: RAGの回答が不十分な場合だけでなく、質問が日付や時刻に関連すると判断された場合にも、現在のリアルタイムの日時情報をLLMへのプロンプトに含めるようにします。
これにより、ユーザーが「現在の時刻は?」や「今日の日付は?」と質問した場合、RAGの検索結果に関わらず、スクリプトが取得した最新の日時情報がLLMに渡され、LLMはその情報に基づいて正確な回答を生成できるようになります。

変更後のコード


import sys
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from datetime import datetime

# --- 1. LLMと埋め込みモデルの設定 ---
Settings.llm = Ollama(model="elyza:jp8b", request_timeout=360.0)
Settings.embed_model = HuggingFaceEmbedding(model_name="intfloat/multilingual-e5-large")
print("LLMと埋め込みモデルの設定が完了しました。")

# --- 2. インデックスのロード ---
print("インデックスをロード中...")
try:
    persist_dir = "./storage/simple_vs"
    index_id = "miyabe_miyuki"

    storage_context = StorageContext.from_defaults(persist_dir=persist_dir)
    loaded_index = load_index_from_storage(storage_context, index_id=index_id)
    print("インデックスのロードが完了しました。")

    query_engine = loaded_index.as_query_engine(similarity_top_k=3)
    print("クエリエンジンの準備ができました。")

    direct_llm = Settings.llm  # 直接LLM回答用のLLM

    # 日時関連の質問を検出するためのキーワード
    datetime_keywords = ["時刻", "時間", "日付", "何日", "今日", "今"]

    print("\n質問を入力してください(終了するには 'exit' と入力)。")
    while True:
        query = input("あなたの質問: ")

        if query.lower() == 'exit':
            print("プログラムを終了します。"); break

        if not query.strip():
            print("質問を入力してください。"); continue

        print("回答を生成中...")
        try:
            # RAGで情報を検索
            response = query_engine.query(query)

            insufficient_phrases = [
                "含まれていません", "情報は含まれていません", "情報がありません",
                "該当情報がありません", "記述はありません", "見つかりません",
                "特定することができません", "Wikipediaのページ編集用", "ウィキペディアやウィキデータ",
                "記載がありません", "MediaWiki", "閲覧したとしても"
            ]

            # 日時関連の質問かどうかを判定
            is_datetime_query = any(keyword in query for keyword in datetime_keywords)

            # 現在の日時情報を取得
            current_datetime = datetime.now().strftime("%Y年%m月%d日 %H:%M")
            prompt_with_datetime = f"現在の日時は {current_datetime} です。この日時を基準として次の質問に答えてください。\n質問:{query}"

            final_response = ""

            # RAGの回答が不十分な場合、または日時関連の質問の場合に直接LLMで回答
            if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases) or is_datetime_query:
                print("RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。")
                direct_response = direct_llm.complete(prompt_with_datetime)
                final_response = direct_response.text
            else:
                final_response = response.response

            print("\n--- 回答 ---")
            print(final_response)
            print("------------\n")

        except Exception as e:
            print(f"質問処理中にエラーが発生しました: {e}")

except FileNotFoundError:
    print(f"エラー: インデックスデータが見つかりません。'{persist_dir}' を確認してください。");
    sys.exit(1)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}");
    sys.exit(1)

変更点の解説
1. datetime_keywordsリストの追加:

datetime_keywords = ["時刻", "時間", "日付", "何日", "今日", "今"]

このリストは、ユーザーの質問にこれらのキーワードが含まれている場合に、その質問を日時関連の質問として識別するために使用

is_datetime_query変数の追加:

is_datetime_query = any(keyword in query for keyword in datetime_keywords)

ユーザーのqueryの中にdatetime_keywordsリスト内のいずれかのキーワードが含まれているかをチェックし、結果をブール値で保持

直接LLMで回答する条件の変更:

if (not response.response.strip()) or any(phrase in response.response for phrase in insufficient_phrases) or is_datetime_query:
    print("RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。")
    direct_response = direct_llm.complete(prompt_with_datetime)
    final_response = direct_response.text
else:
    final_response = response.response

ここが最も重要な変更点です。これまでの条件 (RAGの回答が空、または不十分なフレーズを含む) に加えて、or is_datetime_queryを追加しました。これにより、質問が日時関連であると判断された場合も、RAGの回答内容に関わらず、直接LLMに最新の日時情報付きのプロンプトが送られます

実行すると

LLMと埋め込みモデルの設定が完了しました。
インデックスをロード中...
インデックスのロードが完了しました。
クエリエンジンの準備ができました。

質問を入力してください(終了するには 'exit' と入力)。
あなたの質問: 今日は何日?
回答を生成中...
RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。

--- 回答 ---
現在の日付は2025年05月29日なので、今日は29日です。
------------

あなたの質問: 今 今の時間は?
回答を生成中...
RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。

--- 回答 ---
現在の日時は2025年05月29日00:59なので、00:59です。
------------

あなたの質問: 今のアメリカの大統領は?
回答を生成中...
RAGに該当情報がないか、日時関連の質問のため、直接LLMで回答します。

--- 回答 ---
2025年05月29日現在、米国の大統領はジョー・バイデン氏です。
------------

というように大統領の名前が違う

これは学習済みデータにもRAGにもないのが原因なので正常動作になる
次は取説と約款のRAG作成

その前に gemma3 4b + faster-whusoer のコードを公開する