LangGraphでCRAGエージェントを構築!『ヒロアカ』について聞く

【LangGraph】CRAG(Corrective-RAG)を構築!『ヒロアカ』を聞く

CRAG(Corrective-RAG)とは、RAGで取得したドキュメントが、質問に対して正しいかを評価する手法です。

この記事では、LangGraphを使ってCRAGエージェントを構築する方法を紹介します。

ざっくり言うと

  • CRAGはRAGの検索品質を向上するための手法
  • RAGエージェント構築にはLangGraphを使用する
  • LLMには無料の日本語モデルのLlama-3-ELYZA-JP-8Bを使用する

12/25開催の無料ウェビナー!

目次

LangGraphでCRAG(Corrective-RAG)を構築する

見出し画像

CRAG(Corrective-RAG)とは、RAGで取得したドキュメントが、質問に対して正しいかを評価する手法です。

この記事では、RAGで取得したドキュメントが、質問に対して関連性が不十分である場合に、WEB検索で回答を補完するCRAGを構築します。

LangGraphは、LangChainやLLMを使ってAIエージェントを構築するライブラリです。

LangGraphのワークフローは次のとおりです。

workflow
ワークフローの説明
  • Retriver:質問をもとにベクトルストアからドキュメントを取得する
  • ドキュメントの関連性評価:ベクトルストアから取得したドキュメントが質問に関連しているか評価する
  • Web検索の判定:関連性評価に応じて、回答を生成するか、Web検索をするかを判定します。
  • WEB検索:質問と取得したドキュメントとの関連性がないときにWeb検索を行う
  • 回答の生成:質問と取得したドキュメントとの関連性がある場合は、RAGで回答を生成する

LangGraphの実行環境

見出し画像

この記事で用意した実行環境は以下のとおりです。

  • GPU:NVIDIA A100 80GB
  • GPUメモリ(VRAM):80GB
  • OS :Ubuntu 22.04
  • Docker

Dockerで環境構築

Dockerを使用してLangGraphの環境構築をします

Dockerの使い方は以下の記事をご覧ください。

STEP
Dockerfileの作成

Ubuntuのコマンドラインで、Dockerfileを作成します。

mkdir crag
cd crag
nano Dockerfile

Dockerfileに以下の記述を貼り付けます。

# ベースイメージ(CUDA)の指定
FROM nvidia/cuda:12.1.0-cudnn8-devel-ubuntu22.04

# 必要なパッケージをインストール
RUN apt-get update && apt-get install -y python3-pip python3-venv git nano curl pciutils lshw python3-dev graphviz libgraphviz-dev pkg-config

RUN curl -fsSL https://ollama.com/install.sh | sh

# 作業ディレクトリを設定
WORKDIR /app

# アプリケーションコードをコピー
COPY . /app

# Python仮想環境の作成
RUN python3 -m venv /app/.venv

# 仮想環境をアクティベートするコマンドを.bashrcに追加
RUN echo "source /app/.venv/bin/activate" >> /root/.bashrc

# JupyterLabのインストール
RUN /app/.venv/bin/pip install Jupyter jupyterlab

# LangChain関連のインストール
RUN /app/.venv/bin/pip install ollama langchain-ollama langchain langsmith langgraph langchain-chroma faiss-gpu langchain-community langchain_huggingface langchain_core tiktoken pygraphviz

# コンテナの起動時にbashを実行
CMD ["/bin/bash"]
コマンドの説明

FROM nvidia/cuda:12.1.0-cudnn8-devel-ubuntu22.04

CUDA12.1のベースイメージを指定しています。

RUN apt-get update && apt-get install -y python3-pip python3-venv git nano curl pciutils lshw python3-dev graphviz libgraphviz-dev pkg-config

必要なパッケージをインストールしています。

RUN curl -fsSL https://ollama.com/install.sh | sh

Linux版のOllamaをインストールしています。PythonでOllamaを動かす際にもLinux版Ollamaのインストールが必要になりますのでご注意ください。

RUN /app/.venv/bin/pip install Jupyter jupyterlab

JupyterLabをインストールしています。

RUN /app/.venv/bin/pip install ollama langchain-ollama langchain langsmith langgraph langchain-chroma faiss-gpu langchain-community langchain_huggingface langchain_core tiktoken pygraphviz

LangChainとOllama関連のパッケージをインストールしています。

LLMはOllamaのライブラリを使って動かしますので、PyTorchやTransformerは別途インストール不要です。

[Ctrl + S]キーで変更内容を保存し、[Ctrl + X]キーで編集モードから抜けます。

STEP
docker-compose.ymlファイルの作成

docker-compose.ymlでDockerコンテナの設定をします。

docker-compose.ymlのYAMLファイルを作成して開きます。

nano docker-compose.yml

以下のコードをコピーして、YAMLファイルに貼り付けます。

services:
  crag:
    build:
      context: .
      dockerfile: Dockerfile
    image: crag
    runtime: nvidia
    container_name: crag
    ports:
      - "8888:8888"
    volumes:
      - .:/app/crag
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    command: >
      bash -c '/usr/local/bin/ollama serve & /app/.venv/bin/jupyter lab --ip="*" --port=8888 --NotebookApp.token="" --NotebookApp.password="" --no-browser --allow-root'

[Ctrl + S]キーで変更内容を保存し、[Ctrl + X]キーで編集モードから抜けます。

コマンドの説明

bash -c ‘/usr/local/bin/ollama serve

Ollama Serverを起動しています。PythonのOllamaを使用する際に、Ollama Serverを起動しておく必要がありますので、ご注意ください。

& /app/.venv/bin/jupyter lab –ip=”*” –port=8888 –NotebookApp.token=”” –NotebookApp.password=”” –no-browser –allow-root’

JupyterLabを8888番ポートで起動しています。

STEP
Dockerコンテナを起動

Dockerfileからビルドしてコンテナを起動します。   

docker compose up

 

Dockerの起動後にブラウザの検索窓に”localhost:8888″を入力すると、Jupyter Labをブラウザで表示できます。

localhost:8888

環境変数・LLM・Retrieverの設定

見出し画像

Dockerコンテナで起動したJupyter Lab上でLangChainを使ったRAGの実装をします。

STEP
環境変数の設定

LangChainとTavilyのAPIに関する環境変数を設定します。

import os
from uuid import uuid4

unique_id = uuid4().hex[0:8]

os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_PROJECT"] = f"crag - {unique_id}"
os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com"
os.environ["LANGCHAIN_API_KEY"] = "***************"
os.environ["TAVILY_API_KEY"] = "***************"
コードの説明

unique_id = uuid4().hex[0:8]

8桁のランダムな一意の識別子unique_idを生成しています。

os.environ[“LANGCHAIN_TRACING_V2”] = “true”

この設定により、LangChainのトレースが可能になります。

os.environ[“LANGCHAIN_PROJECT”] = f”crag – {unique_id}”

angChainプロジェクトの名前を設定しています。ここでは、生成したunique_idを使用してプロジェクト名を「crag – {unique_id}」の形式で一意にしています。

os.environ[“LANGCHAIN_ENDPOINT”] = “https://api.smith.langchain.com”

LangChainのAPIエンドポイントを指定しています。

os.environ[“LANGCHAIN_API_KEY”] = “***************”

LangChain APIを利用するためのAPIキーを設定しています。

os.environ[“TAVILY_API_KEY”] = “***************”

Tavily APIを利用するためのAPIキーを設定しています。

STEP
LLMの設定

日本語LLMモデル「Llama-3-ELYZA-JP-8B-q4_k_m.gguf」をダウンロードします。

!curl -L -o Llama-3-ELYZA-JP-8B-q4_k_m.gguf "https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B-GGUF/resolve/main/Llama-3-ELYZA-JP-8B-q4_k_m.gguf?download=true"

Llama-3-ELYZA-JPについては、別記事で詳しく解説しています。

LLMの実行にはOllamaを使用します。

LLMのモデルがOllama使えるようにプロンプトテンプレートを指定して、モデルを作成します。

import ollama
from langchain_ollama.chat_models import ChatOllama

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)
llm = ChatOllama(model="elyza8b", temperature=0)
llm.invoke(("human","ヒロアカを知っていますか?")).content
コードの説明

FROM ./Llama-3-ELYZA-JP-8B-q4_k_m.gguf

ダウンロードしたモデルのパスが入ります。

TEMPLATE “””{{ if .System }}<|start_header_id|>system<|end_header_id|>…

モデルで使用するプロンプトテンプレートが入ります。

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

モデルとプロンプトテンプレートを使ってOllama用のモデルを作成します。modelにはOllamaで呼び出す際に使用する名前をつけられます。

ChatOllama(model=”elyza8b”, temperature=0)

ChatOllamaをインスタンス化してLLMモデルを実行できる状態にしています。

確認のため、LLMからテキストを生成しています。

'「ヒロアカ」こと「僕のヒーローアカデミア」は、非常に人気のある日本の漫画作品です。作者は堀越耕平先生で、週刊少年ジャンプで2014年から連載されています。\n\n物語は、超常能力「個性」を持つ人々が当たり前のように存在する世界を舞台に、主人公の緑谷出久(みどりや いずく)がヒーローになるために通う雄英高校での学園生活と、様々な事件や敵との戦いを描いています。\n\n個性豊かなキャラクターたちが繰り広げるアクション、友情、成長の物語は多くのファンに支持され、TVアニメ化もされています。'

ollama.createのエラー「invalid digest format」の解消方法

STEP
Wikipediaからデータを取得

ベクトルストアに格納するデータをWikipediaから取得します。

Wikipediaは、『僕のヒーローアカデミア』のページを指定しています。

from langchain_community.document_loaders import WebBaseLoader
from langchain.text_splitter import CharacterTextSplitter

urls = [
    "https://ja.wikipedia.org/wiki/%E5%83%95%E3%81%AE%E3%83%92%E3%83%BC%E3%83%AD%E3%83%BC%E3%82%A2%E3%82%AB%E3%83%87%E3%83%9F%E3%82%A2",
]

docs = [WebBaseLoader(url).load() for url in urls]
docs_list = [item for sublist in docs for item in sublist]

text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    separator = "\n",
    chunk_size= 500,
    chunk_overlap=50,
)

doc_splits = text_splitter.split_documents(docs_list)
doc_splits[10]
コードの説明

WebBaseLoader(url).load()

指定したWebページのHTMLコンテンツを取得し、HTMLコンテンツを解析し、使いやすいデータ構造に変換し、そのデータを読み込みます。

CharacterTextSplitter.from_tiktoken_encoder…

テキストを指定された条件でチャンクに分割します。

separator = “\n”

チャンクを分割する際の区切り文字を「\n」として指定します。

chunk_size

各チャンクの最大トークン数を設定します。

chunk_overlap

各チャンク間で重複するトークン数を設定します。

確認のため、分割したチャンクを1つ表示しています。

Document(metadata={'source': 'https://ja.wikipedia.org/wiki/%E5%83%95%E3%81%AE%E3%83%92%E3%83%BC%E3%83%AD%E3%83%BC%E3%82%A2%E3%82%AB%E3%83%87%E3%83%9F%E3%82%A2', 'title': '僕のヒーローアカデミア - Wikipedia', 'language': 'ja'}, page_content='また、出久のキャラクターは『戦星のバルジ』2巻に描下ろされたあとがき漫画『獄宴編』の主人公が元になっている。それを見た堀越の担当編集者は「君は悟空やルフィのような主人公は描けなかったが、こういうオタクっぽい奴なら描ける」と評価していた[12]。\n連載当初は公式な略称は定まっておらず[13]『ヒロアカ[14]』『僕アカ[15]』などいくつか存在していたが、アニメのテレビシリーズ開始以降は公式な場での略称は『ヒロアカ』で統一されている[4]。\nテーマ・作風')
STEP
Retrieverの構築

Wikipediaのデータをもとにベクトル検索が可能なRetrieverを構築します。

from langchain.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings

embedding = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large")
vectorstore = FAISS.from_documents(doc_splits, embedding)
retriever = vectorstore.as_retriever(k=4)
question = "デクとは"
retriever.invoke(question)
コードの説明

HuggingFaceEmbeddings(model_name=”intfloat/multilingual-e5-large”)

テキストをベクトル表現に変換する埋め込みモデルを読み込みます。

intfloat/multilingual-e5-largeは日本語性能が高く、無料で使える埋め込みモデルです。

FAISS.from_documents(splits, embedding)

ベクトル検索用のインデックスを作成しています。インデックスはDBのテーブルのような概念です。

vectorstore.as_retriever()

インデックスからRetriverを作成しています。

retriever.invoke(question)

質問の内容に類似したドキュメントをRetriverで検索します。

Retriverから取得したデータを確認しています。

[Document(metadata={'source': 'https://ja.wikipedia.org/wiki/%E5%83%95%E3%81%AE%E3%83%92%E3%83%BC%E3%83%AD%E3%83%BC%E3%82%A2%E3%82%AB%E3%83%87%E3%83%9F%E3%82%A2', 'title': '僕のヒーローアカデミア - Wikipedia', 'language': 'ja'}, page_content='幼馴染の爆豪からは蔑称として「デク」と呼ばれるが、麗日が好意的に受け取ったことで「頑張れって感じ」という意味に転回し、後に自身のヒーロー名として定める。\n私服は無地の物に「○○シャツ」(○○内には「T」や「Y」、「ポロ」、「ドレス」などが入る)と書かれた服をよく着る。深く考えるときは手を口にあて呟く癖があり、その際の台詞が入ったふきだしや背景は、「ブツブツ……」という文字の繰り返しで構成される。'),
以下省略

各種Chainの構築

見出し画像

LangGraphのワークフロー内の関数で使用するChainを構築していきます。

  • ドキュメントの関連性を評価するChain
  • 回答を生成するChain
  • ハルシネーションを評価するChain
  • 回答の有用性を評価するChain
  • Web検索をするTool
STEP
ドキュメントの関連性を評価するChain

ユーザーの質問と取得したドキュメントが関連しているかを評価するChainを構築しています。

評価はバイナリースコア(yes または no)で表され、その結果はJSON形式で返されます。

# retrieval_chain
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate(
    template="""<|begin_of_text|><|start_header_id|>system<|end_header_id|> 
    あなたは採点者です。以下の内容が与えられます:
    ・質問
    ・事実
    
    あなたは「関連性の再現」を評価しています:
    スコアが1の場合、事実が質問に関連していることを意味します。
    スコアが0の場合、事実が質問に関連していないことを意味します。
    1が最高(最良)のスコアです。0は付けられる最低のスコアです。
    
    理由付けをステップバイステップで説明してください。理由付けと結論が正しいことを確認してください。
    最初に正しい答えを単に述べるのは避けてください。
    
    ドキュメントが質問に関連しているかどうかを示すバイナリスコア「yes」または「いいえ」を提供してください。 \n
    バイナリスコアを単一のキー「score」を持つJSON形式で提供し、前置きや説明は不要です。
     <|eot_id|><|start_header_id|>user<|end_header_id|>
    質問: {question} \n
    事実: \n\n {documents} \n\n <|eot_id|><|start_header_id|>assistant<|end_header_id|>
    """,
    input_variables=["question", "documents"],
)

llm = ChatOllama(model="elyza8b",format="json",temperature=0)
retrieval_chain = prompt | llm | JsonOutputParser()
question = "デクとは?"
docs = retriever.invoke(question)
doc_txt = docs[0].page_content
print(retrieval_chain.invoke({"question": question, "documents": doc_txt}))
print("\nユーザーの質問:\n" + question)
print("\n取得したドキュメント:\n" + docs[0].page_content)
コードの説明

PromptTemplate()

プロンプトのテンプレートを定義するために使用されるクラスです。テンプレート内で、指定された変数に動的に値を埋め込み、AIに指示を与えるプロンプトを生成します。

template には、AIに対してどのような指示を与えるかを記述します。

<|begin_of_text|><|start_header_id|>system<|end_header_id|> ...<|eot_id|>には、システムプロンプトが入ります。

<|start_header_id|>user<|end_header_id|>...<|eot_id|>には、ユーザーの質問が入ります。

<|start_header_id|>assistant<|end_header_id|>...には、LLMの回答が入ります。

input_variables では、テンプレート内で動的に置き換えられる変数名をリストで指定します。

この場合、questionの変数が指定されています。

prompt | llm | JsonOutputParser()

プロンプトの作成から、LLMによる応答の生成、JSON形式の出力までを一連の処理として組み合わせたChainを構築しています。

retrieval_chain.invoke({“question”: question})

questionをプロンプトに渡して、定義したChainを実行しています。

出力結果の確認

{'score': 'yes'}

ユーザーの質問:
デクとは?

取得したドキュメント:
幼馴染の爆豪からは蔑称として「デク」と呼ばれるが、麗日が好意的に受け取ったことで「頑張れって感じ」という意味に転回し、後に自身のヒーロー名として定める。
私服は無地の物に「○○シャツ」(○○内には「T」や「Y」、「ポロ」、「ドレス」などが入る)と書かれた服をよく着る。深く考えるときは手を口にあて呟く癖があり、その際の台詞が入ったふきだしや背景は、「ブツブツ……」という文字の繰り返しで構成される。

質問とドキュメントが関連する場合、{'score': 'yes'}とJSON形式で出力されます。

STEP
回答を生成するChain

回答を生成するChainを構築します。

# generate_chain
from langchain_core.output_parsers import StrOutputParser

prompt = PromptTemplate(
    template="""<|begin_of_text|><|start_header_id|>system<|end_header_id|> 
    あなたは質問応答のためのアシスタントです。
    次のドキュメントを使って質問に答えてください。 
    答えがわからない場合は、正直に「わかりません」と言ってください。
    回答は最大で3文にまとめ、簡潔にしてください。
    <|eot_id|><|start_header_id|>user<|end_header_id|>
    質問: {question} 
    コンテキスト: {documents} 
    回答: <|eot_id|><|start_header_id|>assistant<|end_header_id|>""",
    input_variables=["question", "documents"],
)

llm = ChatOllama(model="elyza8b",temperature=0)
generate_chain = prompt | llm | StrOutputParser()

question = "デクとは?"
docs = retriever.invoke(question)
generation = generate_chain.invoke({"documents": docs, "question": question})
print("\nユーザーの質問:\n" + question)
print("\n生成した回答:\n" + generation)
コードの説明

prompt | llm | StrOutputParser()

プロンプトの作成から、LLMによる応答の生成、文字列形式の出力までを一連の処理として組み合わせたChainを構築しています。

出力結果の確認

ユーザーの質問:
デクとは?

生成した回答:
デクとは、緑谷出久のヒーロー名です。
STEP
Web検索Toolの設定

Tavily APIを利用してWEB検索を行うToolの設定をします。

# web_search_tool
from langchain_community.tools.tavily_search import TavilySearchResults

web_search_tool = TavilySearchResults(max_results=3)
question = "デクとは?"
web_search_tool.invoke({"query": question})
コードの説明

TavilySearchResults(max_results=5)

Tavily の検索 API を利用して、指定された検索クエリに対して上位5件の検索結果を取得します。

デクについてWeb検索した結果

[{'url': 'https://dic.pixiv.net/a/デク',
  'content': 'デクがイラスト付きでわかる! デクには複数の意味があるが、その殆どは緑谷出久のヒーロー名として使われる事が多い。 曖昧さ回避 +『僕のヒーローアカデミア』の主人公。ピクシブ上では彼を描いた作品に付けられることがほとんど→ 緑谷出久 +『僕のヒーローアカデミア』の構想段階で ...'},
(以下省略)

LangGraphワークフローで使用する関数の定義

見出し画像

LangGraphのワークフローで使用する各種関数の定義をしていきます。

  • GraphStateの定義
  • Retriver関数
  • 回答を生成する関数
  • ドキュメントの関連性を評価する関数
  • Web検索の関数
  • WEB検索を判断する関数
STEP
ライブラリのインポート

必要なライブラリをインポートします。

from pprint import pprint
from typing import List
from langchain_core.documents import Document
from typing_extensions import TypedDict
STEP
Graphの状態を定義

Graphを初期化したときのノードやエッジの状態を定義します。

class GraphState(TypedDict):
    question: str
    generation: str
    search: str
    documents: List[str]
    steps: List[str]
STEP
Retriver関数

Retriverを使用してベクトルストアからユーザーの質問に関連するドキュメントを取得します。

「question(質問)」と「documents(ドキュメント)」、「steps」を次のステップで使用するため、状態 (state) を更新します。

# Retriver関数
def retrieve(state):
    print("---RETRIEVE---")
    question = state["question"]
    documents = retriever.invoke(question)
    steps = state["steps"]
    steps.append("retrieve_documents")
    return {"documents": documents, "question": question, "steps": steps}
STEP
回答を生成する関数

ユーザーの質問にもとづいた回答を生成します。

「documents(ドキュメント)」と「question(質問)」、「generation(回答)」、「steps」を次のステップで使用するため、状態 (state) を更新します。

# 回答を生成する関数
def generate(state):
    print("---GENERATE---")
    question = state["question"]
    documents = state["documents"]
    generation = generate_chain.invoke({"documents": documents, "question": question})
    steps = state["steps"]
    steps.append("generate_answer")
    return {
        "documents": documents,
        "question": question,
        "generation": generation,
        "steps": steps,
    }
STEP
ドキュメントの関連性を評価する関数

取得したドキュメントが質問に関連しているかどうかを評価します。

関連性の高いドキュメントを選別し、リストに格納します。

関連性が低いドキュメントが存在する場合は、Web検索に進むフラグを立てます。

「filtered_docs(関連性のあるドキュメント)」と「question(質問)」、「search(検索)」、「steps」を次のステップで使用するため、状態(sate)を更新します。

# ドキュメントの関連性を評価する関数
def grade_documents(state):
    print("---CHECK DOCUMENT RELEVANCE TO QUESTION---")
    question = state["question"]
    documents = state["documents"]
    steps = state["steps"]
    steps.append("grade_document_retrieval")
    filtered_docs = []
    search = "No"
    for d in documents:
        score = retrieval_chain.invoke(
            {"question": question, "documents": d.page_content}
        )
        grade = score["score"]
        if grade == "yes":
            print("---GRADE: DOCUMENT RELEVANT---")
            filtered_docs.append(d)
        else:
            print("---GRADE: DOCUMENT NOT RELEVANT---")
            search = "Yes"
            continue
    return {
        "documents": filtered_docs,
        "question": question,
        "search": search,
        "steps": steps,
    }
STEP
Web検索の関数

質問に基づいてウェブ検索を実行します。検索結果を取得して、それをドキュメントに追加します。

「question(質問)」と「documents(ドキュメント)」、「steps」を次のステップで使用するため、状態(state)を更新します。

# Web検索の関数
def web_search(state):
    print("---WEB SEARCH---")
    question = state["question"]
    documents = state.get("documents", [])
    steps = state["steps"]
    steps.append("web_search")
    web_results = web_search_tool.invoke({"query": question})
    documents.extend(
        [
            Document(page_content=d["content"], metadata={"url": d["url"]})
            for d in web_results
        ]
    )
    return {"documents": documents, "question": question, "steps": steps}
STEP
WEB検索を判断する関数

WEB検索をするか、RAGによる生成をするかを判断します。

この関数は「search」または「generate」を返します。この返り値は、後続のステップで利用されます。

#Web検索を判断する関数
def decide_to_generate(state):
    print("---GENERATE OR WEB SEARCH---")
    search = state["search"]
    if search == "Yes":
        print("---TO WEB SEARCH---")
        return "search"
    else:
        print("---TO GENERATE---")
        return "generate"

LangGraphワークフローの構築

見出し画像

下図のようなLangGraphのワークフローを構築していきます。

workflow
ワークフローの説明
  • Retriver:質問をもとにベクトルストアからドキュメントを取得する
  • ドキュメントの関連性評価:ベクトルストアから取得したドキュメントが質問に関連しているか評価する
  • WEB検索:質問と取得したドキュメントとの関連性がないときにWeb検索を行う
  • WEB検索の判定:関連性評価に応じて、回答を生成するか、Web検索をするかの判定をします。
  • 回答の生成:質問と取得したドキュメントとの関連性がある場合は、RAGで回答を生成する
STEP
ライブラリのインポート

必要なライブラリをインポートします。

from langgraph.graph import END, StateGraph, START
from IPython.display import Image, display
from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeStyles
STEP
Graphの初期化とノードの追加

Graphのワークフローを作成し、各ノードを追加します。

#Graphのワークフローを作成
workflow = StateGraph(GraphState)

#ノードの追加
workflow.add_node("retrieve", retrieve)
workflow.add_node("grade_documents", grade_documents)
workflow.add_node("generate", generate)
workflow.add_node("web_search", web_search)
コードの説明

StateGraph(GraphState)

StateGraphは、複数の処理ステップ(ノード)をグラフ構造として扱い、それぞれのノード間で状態を渡しながら処理を進めることができます。

workflow.add_node(“web_search”, web_search)

web_searchという名前のノードがワークフローに追加され、web_search関数がそのノードで実行されます。

STEP
エッジの追加

ワークフローにおけるエッジ(ノード間の接続)の設定を行います。

エッジは、各ノードをどの順序で実行するか、どのような条件で次のノードに進むかを定義します。

# Edgeの追加
workflow.add_edge(START, "retrieve")
workflow.add_edge("retrieve", "grade_documents")
workflow.add_conditional_edges(
    "grade_documents",
    decide_to_generate,
    {
        "search": "web_search",
        "generate": "generate",
    },
)
workflow.add_edge("web_search", "generate")
workflow.add_edge("generate", END)
コードの説明

workflow.add_edge()エッジの追加

workflow.add_edge(START, "retrieve")

ワークフローの開始点 START から "retrieve" ステップに進みます。

workflow.add_edge("retrieve", "grade_documents")

"retrieve" ステップから "grade_documents" ステップに進みます。

workflow.add_edge("web_search", "generate")

"web_search" が完了した後に "generate" ステップに進みます。

workflow.add_edge("generate", END)

"generate" ステップが完了すると、ワークフローが終了ENDします。

workflow.add_conditional_edges():条件付きエッジの追加

workflow.add_conditional_edges(
"grade_documents",
decide_to_generate,
{
"search": "web_search",
"generate": "generate",
},
)

"grade_documents" ステップから次のステップへの接続を条件付きで追加します。

decide_to_generate 関数が返す値によって次のステップが決まります:

"search" の場合、"web_search" ステップに遷移します。

"generate" の場合、"generate" ステップに遷移します。

STEP
ワークフローのコンパイル

全体のワークフローをコンパイルして、実行可能な形にします。

# コンパイル
app = workflow.compile()
STEP
ワークフローの可視化

定義したLangGraphワークフローのグラフ構造を、Mermaid形式で可視化します。

# ワークフローの可視化
display(
    Image(
        app.get_graph().draw_mermaid_png(
            draw_method=MermaidDrawMethod.API,
        )
    )
)
crag

CRAGエージェントに『ヒロアカ』について聞く

見出し画像

構築したCRAGエージェントに『ヒロアカ』について質問をしてみます。

RAGエージェントに質問する(1)

「デクとは誰ですか?」と質問してみます。

import uuid

def graph_output(inputs: dict):
    config = {"configurable": {"thread_id": str(uuid.uuid4())}}
    state_dict = app.invoke(
        {"question": inputs["input"], "steps": []}, config
    )
    return {"response": state_dict["generation"], "steps": state_dict["steps"]}

inputs = {"input": "デクとは誰ですか?"}
response = graph_output(inputs)
response

デクとは誰ですか?

—RETRIEVE—
—CHECK DOCUMENT RELEVANCE TO QUESTION—
—GRADE: DOCUMENT RELEVANT—
—GRADE: DOCUMENT RELEVANT—
—GRADE: DOCUMENT RELEVANT—
—GRADE: DOCUMENT RELEVANT—
—GENERATE OR WEB SEARCH—
—TO GENERATE—
—GENERATE—
{‘response’: ‘緑谷出久(みどりや いずく)は、主人公の男子生徒で、9代目ワン・フォー・オール継承者です。’,
‘steps’: [‘retrieve_documents’,
‘grade_document_retrieval’,
‘generate_answer’]}

取得したドキュメントが全て質問に関連しているため、RAGによる生成が行われました。

RAGエージェントに質問する(2)

「ウラビティとは誰ですか?」と質問してみます。

def graph_output(inputs: dict):
    config = {"configurable": {"thread_id": str(uuid.uuid4())}}
    state_dict = app.invoke(
        {"question": inputs["input"], "steps": []}, config
    )
    return {"response": state_dict["generation"], "steps": state_dict["steps"]}

inputs = {"input": "ウラビティとは誰ですか?"}
response = graph_output(inputs)
response

ウラビティとは誰ですか?

—RETRIEVE—
—CHECK DOCUMENT RELEVANCE TO QUESTION—
—GRADE: DOCUMENT RELEVANT—
—GRADE: DOCUMENT RELEVANT—
—GRADE: DOCUMENT RELEVANT—
—GRADE: DOCUMENT NOT RELEVANT—
—GENERATE OR WEB SEARCH—
—TO WEB SEARCH—
—WEB SEARCH—
—GENERATE—
{‘response’: ‘ウラビティは、漫画『僕のヒーローアカデミア』に登場する麗日お茶子のヒーロー名です。’,
‘steps’: [‘retrieve_documents’,
‘grade_document_retrieval’,
‘web_search’,
‘generate_answer’]}

一部関連性のないドキュメントを取得したため、WEB検索で回答がされています。

RAGエージェントに質問する(3)

「かっちゃんとは誰ですか?」と質問してみます。

def graph_output(inputs: dict):
    config = {"configurable": {"thread_id": str(uuid.uuid4())}}
    state_dict = app.invoke(
        {"question": inputs["input"], "steps": []}, config
    )
    return {"response": state_dict["generation"], "steps": state_dict["steps"]}

inputs = {"input": "かっちゃんとは誰ですか?"}
response = graph_output(inputs)
response

質問:
かっちゃんとは誰ですか?

—RETRIEVE—
—CHECK DOCUMENT RELEVANCE TO QUESTION—
—GRADE: DOCUMENT RELEVANT—
—GRADE: DOCUMENT NOT RELEVANT—
—GRADE: DOCUMENT NOT RELEVANT—
—GRADE: DOCUMENT NOT RELEVANT—
—GENERATE OR WEB SEARCH—
—TO WEB SEARCH—
—WEB SEARCH—
—GENERATE—
{‘response’: ‘かっちゃんは、「僕のヒーローアカデミア」に登場するキャラクターで、爆豪勝己のあだ名です。幼馴染の出久から「かっちゃん」と呼ばれています。強力な個性と戦闘センスを持ち、自尊心が強く攻撃的な性格ですが、妙な部分では冷静に判断するという複雑な人物です。’,
‘steps’: [‘retrieve_documents’,
‘grade_document_retrieval’,
‘web_search’,
‘generate_answer’]}

関連性のないドキュメントを取得したため、WEB検索で回答がされています。

生成AI・LLMのコストでお困りなら

GPUのスペック不足で生成AIの開発が思うように進まないことはありませんか?

そんなときには、高性能なGPUをリーズナブルな価格で使えるGPUクラウドサービスがおすすめです!

GPUSOROBAN
GPUSOROBAN

GPUSOROBANは、高性能GPU「NVIDIA H200」を業界最安級の料金で使用することができます。

NVIDIA H200は、生成AI・LLMの計算にかかる時間を大幅に短縮することが可能です。

クラウドで使えるため、大規模な設備投資の必要がなく、煩雑なサーバー管理からも解放されます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
EdgeHUBロゴ

メールマガジン登録

Stable Diffusion・LLM・RAGに関する最新情報をいち早くお届けします。

無料メルマガの配信をご希望の方は、下記フォームよりご登録ください。

    EdgeHUB編集部からのお知らせ

    無料ウェビナーのお知らせ

    RAG進化のさらに先へ!自立型AIエージェント ウェビナー【12/25無料開催】

    RAG進化のさらに先へ! 大好評につきRAGシリーズ第3弾が開催決定!

    開催日時:
    2024年12月25日(水) 14:00~15:00

    内容:

    • RAGの精度を向上させる「自律型AIエージェント」のデモ
    • 生成AI開発の強い味方「GPUSOROBAN」の活用方法

    このウェビナーでは、オープンソース「LangGraph」で構築したAIエージェントの使い方や、デモを紹介します。

    生成AIに関心のある方、AI技術をビジネスに活かしたい方は、ぜひこの貴重な機会にご参加ください!

    こんな方におすすめ!

    • 自律型AIエージェントに興味がある方
    • RAGの高度化を検討しているエンジニアや開発者
    • 日本語のローカルLLMの利用を検討している方
    • GPUリソースに課題を感じている方

    \簡単1分で申し込み!/

    この記事を書いた人

    EdgeHUBは、NVIDIAクラウドパートナーである株式会社ハイレゾが運営しています。「AIと共にある未来へ繋ぐ」をテーマに、画像生成AI、文章生成AI、動画生成AI、機械学習・LLM、Stable Diffusionなど、最先端の生成AI技術の使い方をわかりやすく紹介します。

    目次