OllamaのFunction CallingでRAGを実装する方法(Python+Chroma DB)

AIテキスト

前回までの記事で、OllamaのEmbeddingモデルとChromaDBを使い、PythonでRAGの検索基盤を作る方法を解説しました。

この記事ではその続きとして、Function Callingを使い、LLM側でRAGを利用する構成を実装します。

これでRAGの基本的な構成を一通り試すことができます。

OllamaにおけるFunction Callingの位置づけ

Function Callingとは、LLMに必要な処理(検索・取得など)を構造化された関数呼び出しとして指示させる仕組みです。

対応したLLMとChroma DBを組み合わせることで、RAG検索をLLMの判断に基づいて実行できる構成を作れます。

OllamaはOpenAI互換のChat API形式をサポートしており、Function Calling相当の機能を利用できます。 公式情報は以下を参照してください。
OpenAI compatibility · Ollama Blog

RAGにおける役割分担

この構成では、LLMが「検索が必要かどうか」を判断して「指示」を返答します。 実際の検索はPython関数が担当します。EmbeddingやChromaDBの処理は前回記事のまま流用できます。

検索処理は、ユーザー質問を受け取り、ChromaDBから関連文書を返す関数として定義します。

def retrieve_documents(query):
    results = collection.query(
        query_texts=[query],
        n_results=3
    )
    return results["documents"][0]

Function Callingを使った問い合わせ処理

ユーザーの質問をLLMに渡す際、検索用関数を「呼び出し可能な関数」として定義します。 LLMが検索を必要と判断した場合、関数名と引数を返します。

tools = [
    {
        "type": "function",
        "function": {
            "name": "retrieve_documents",
            "description": "質問に関連する文書を検索する",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"}
                },
                "required": ["query"]
            }
        }
    }
]

このレスポンスを受け取り、Python側で検索関数を実行し、その結果を再度LLMに渡すことでRAGが成立します。

この構成のメリット

Function Callingを使うことで、ユーザーの曖昧な質問をLLMが解釈し、検索に使えるクエリへ変換したうえでRAG検索を実行できます。 これにより、キーワードを厳密に指定しなくても、意図に沿った情報を取得しやすくなります。

例えば前回までの「おみくじ」の例では、「吉」「kichi」などのキーワードを明示的に含める必要がありましたが、Function Callingを使うことで「今日はどんな運勢?」といった自然な質問から関連情報を引き出せます。

コード全文

import ollama
import chromadb
import json
from pathlib import Path

# ChromaDB 初期化
client = chromadb.Client()
collection = client.get_or_create_collection(name="rag_demo")

# omikuji.txt 読み込み
text_path = Path(__file__).parent / "omikuji.txt"

with open(text_path, encoding="utf-8") as f:
    documents = [
        line.strip()
        for line in f.readlines()
        if line.strip()
    ]

# ドキュメント登録
for i, doc in enumerate(documents):
    emb = ollama.embed(
        model="nomic-embed-text",
        input=doc
    )["embeddings"][0]

    collection.add(
        ids=[str(i)],
        documents=[doc],
        embeddings=[emb]
    )

# 検索関数(RAG)
def retrieve_documents(query: str) -> str:
    query_emb = ollama.embed(
        model="nomic-embed-text",
        input=query
    )["embeddings"][0]

    results = collection.query(
        query_embeddings=[query_emb],
        n_results=3
    )

    return "\n".join(results["documents"][0])

# Function 定義(LLMに渡す)
tools = [
    {
        "type": "function",
        "function": {
            "name": "retrieve_documents",
            "description": "おみくじ文書から占い結果の参考情報を検索する",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "占い内容を判断するための検索クエリ"
                    }
                },
                "required": ["query"]
            }
        }
    }
]

# System プロンプト(占い師調・検索強制)
system_prompt = """
あなたは神社のおみくじを読み上げる占い師です。
口調は丁寧で落ち着いた和風の語り口にしてください。

占い結果は必ず
「大吉・中吉・小吉・吉・凶」
のいずれか一つを選んでください。

回答は以下の構成で出力してください。
・最初に【運勢】として結果を明示する
・次に【内容】として運勢の意味を説明する
・最後に【ひとこと助言】を添える

回答を作成する前に、必ず retrieve_documents 関数を使って
おみくじ文を検索してください。

検索結果に含まれる内容のみを根拠にして回答し、
推測や創作で内容を補ってはいけません。
"""

# ユーザー入力
user_question = "今日の運勢を占って"

# 1回目:LLMに検索判断させる
response = ollama.chat(
    model="llama3.1:8b",
    messages=[
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_question}
    ],
    tools=tools
)

message = response["message"]

# Function Calling 判定
if "tool_calls" not in message:
    raise RuntimeError("LLMが検索を実行しませんでした")

tool_call = message["tool_calls"][0]
args = tool_call["function"]["arguments"]

# 検索実行(アプリ側)
context = retrieve_documents(args["query"])

# 2回目:検索結果を渡して回答生成
final_response = ollama.chat(
    model="llama3.1:8b",
    messages=[
        {"role": "system", "content": system_prompt},
        {"role": "system", "content": f"以下は検索結果です:\n{context}"},
        {
            "role": "user",
            "content": (
                "上記の検索結果のみを根拠にして、"
                "神社のおみくじを読み上げる占い師として、"
                "今日の運勢を占ってください。"
            )
        }
    ]
)

print(final_response["message"]["content"])

実行例(毎回変わります)

【運勢】小吉

今日は無理をせず、できることを一つずつ片付けると安心感が生まれます。

【内容】これから物事に取り組む際には大きな目標や急進的な動きよりも、小さな実行可能なステップを優先しましょう。小さいご利益でも気持ちよく進めば、成功へとつながるかもしれません。

【ひとこと助言】気をつけるポイントは「無理をせず」です。気力や時間が足りないと感じたら、その場を引き返すことも大切な時です。

まとめ

この記事では、Ollama+ChromaDB構成を前提に、Function Callingを使ってRAG検索を組み込む最小構成を解説しました。

ユーザーの曖昧な質問をLLMが解釈し、検索に適したクエリに変換したうえでRAG検索を実行するため、キーワードを厳密に指定しなくても、意図に沿った情報を取得できるようになります。

次回は、この構成を対話ループとして発展させる実装例を解説します。

タイトルとURLをコピーしました