AI

LangChainをローカルLLMで試してみる

Takuya Kobayashi⛰
2024.06.18

LangChainは、LLMを利用したアプリケーションを構築するためのフレームワークです。これにより、開発者は自然言語処理を活用したさまざまな高度なアプリケーションを迅速かつ効果的に作成できます。

この記事ではOllamaでローカルLLMを動かし、LangChainを使って推論を行ってみます。

環境:Windows10、WSL(Kali Linux)

環境の準備

Ollamaのインストール

ここではOllamaでローカルLLMを動かします。

Ollamaをインストール:

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

インストールが完了したら、Ollamaのデーモンを起動:

nohup ollama serve &

モデルを落とします。

ollama pull phi3

軽量で賢いphi3を選択しました。
(llama3やgemmaなどを試してみましたがphi3が一番賢かったです。)

必要なパッケージをインストール

次のコマンドで必要なパッケージをインストールします。

pip install langchain langchain-community langchain-experimental pandas tabulate

以上で準備は完了です。

LangChainでCSVファイルを参照して推論

create_pandas_dataframe_agentはユーザーのクエリからデータフレームに対して何の処理をすべきかを判断し、実行してくれます。

タイタニック号の名簿のCSVファイルを参照させて指示を送ってみます。
https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv

import pandas as pd
from langchain_experimental.agents.agent_toolkits import create_pandas_dataframe_agent
from langchain_community.llms import Ollama

llm = Ollama(model="phi3")

df = pd.read_csv("https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv")

agent = create_pandas_dataframe_agent(
    llm, df,
    allow_dangerous_code=True,
    #verbose=True,
)

rep = agent.invoke("平均年齢を教えて")
print(rep["output"])

verbose=Trueにすると、思考や処理を表示してくれます。

エージェントを使用する際は「allow_dangerous_code=True」を付けないとセキュリティ警告で動きません。なぜなら、エージェントはREPLツールで任意のコードを実行できて危ないからです。

出力:

The average age is approximately 29.7 years old.

正しい情報が得られました。

次は女性の人数を聞いてみます。

女性の人数は314です。(Females number is 314)

こちらも正しく推論できました。

「生存者は何人?」と聞いてみます:

生存者は342人です。(The survivors are 342.)

完璧ですね。

LangChainでURLに質問する(ベクトルRAG)

LLMがプロンプトで扱える情報量には上限があります。
LLMに大きな情報を扱わせる手段として、動的に切り抜いた情報を提供する方法があります。

次のコードでは、URLからテキストを取得してベクトルデータベース(Chroma)に保存。クエリに応じてデータベースから類似のテキストを読みだしてプロンプトに追加して推論します。RAGと呼ばれる手法です。

必要なパッケージをインストール:

pip install langchain-chroma langchain-comunity langchain-huggingface langchain-text-splitters

コード全文:

from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import CharacterTextSplitter

loader = WebBaseLoader("https://ja.wikipedia.org/wiki/%E7%94%B7%E4%BD%93%E5%B1%B1")
documents = loader.load()

text_splitter = CharacterTextSplitter(chunk_size=800, chunk_overlap=50)
docs = text_splitter.split_documents(documents)

embedding_function = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")

db = Chroma.from_documents(docs, embedding_function)

from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_community.llms import Ollama
import readline

retriever = db.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 4},
)

#使ったモデル:https://huggingface.co/DataPilot/ArrowPro-7B-KillerWhale
model = Ollama(model="killerwhale")

prompt_template = """
命令:提供する"情報"の中からユーザーの"質問"に回答してください。

情報:{context}

質問:{question}

回答:"""

prompt = PromptTemplate(
            template=prompt_template,
            input_variables=["context", "question"],
        )

chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

while True:
    user_input = input(">>> ")
    if user_input == "exit":
        break
    result = chain.invoke(user_input)
    print(result+"\n")

出力:

>>> 飛銚子とは?

飛銚子(とびちょうし)とは、昔の日本、特に江戸時代に言われていた自然現象で、男体山や女体山などの高峰から鉄製の銚子状の ものが飛来するとされる怪異現象を指します。これは、高山の風化によって生じた金属片が、強風に乗って遠くまで運ばれる自然現象だと考えられています。植田孟縉の著書『日光山志』に記述されており、山の修行者たちが目撃したとされています。なお、「飛銚子」はあくまでも伝説や怪異譚の範疇にある現象であり、科学的に厳密に解明されているわけではありませんが、当時の人々の間では実際に信じられ、語り継がれた出来事として知られています。(参考文献:橘守部家集)

>>> 男体山の標高は何メートル?

長らく日本百名山にも選ばれている栃木県の男体山の伝統的な標高は、従来より言われてきたのが2,484メートルでした。しかし、2003年10月に国土地理院の調査によって、実際の最高標高地点は三角点から南西に約11.2メートルに位置する岩場が「2,486メートル」 であることが判明しました。なお、栃木県民によって覚えやすいように標高を「2484(にしっぱし)」とも呼ばれていますが、調査結果に基づくと公式な標高は「2,486メートル」となります。

「飛銚子」は江戸時代の修行僧が男体山で目撃した怪異ですが、非常に情報が少ないのでLLMが知らないであろう情報です。男体山の標高や「にしっぱし」というニッチな情報もWikipediaから引っ張ってうまくまとめられています。

コードの説明:

次の部分で男体山のWikipediaを取得します。

loader = WebBaseLoader("https://ja.wikipedia.org/wiki/%E7%94%B7%E4%BD%93%E5%B1%B1")
documents = loader.load()

取得したテキストをchunkに区切ります。
chunk_overlapは各チャンクを重複させることで、情報の連続性を保ちます。

text_splitter = CharacterTextSplitter(chunk_size=800, chunk_overlap=50)
docs = text_splitter.split_documents(documents)

HuggingFaceEmbeddingsでembeddingします。

embedding_function = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
db = Chroma.from_documents(docs, embedding_function)

類似テキストをデータベースで検索するretreverを設定します。k:4はコサイン類似度トップ4件を取得します。

retriever = db.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 4},
)

{context}に取得した情報、{question}にユーザーの入力が入るようにプロンプトを設定します。

prompt_template = """
命令:提供する"情報"の中からユーザーの"質問"に回答してください。

情報:{context}

質問:{question}

回答:"""

prompt = PromptTemplate(
            template=prompt_template,
            input_variables=["context", "question"],
        )

めっちゃかっこいいLCEL(LangChain Expression Language)記法でChainを設定

chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

chain.invokeでChainを実行します。
whileで「exit」と入力されるまで繰り返し質問できるようにしています。

while True:
    user_input = input(">>> ")
    if user_input == "exit":
        break
    result = chain.invoke(user_input)
    print(result+"\n")

ベクトルRAGの限界

ここで紹介した方法はユーザーが入力したプロンプトに意味合い的に近い文章をDBから抜き出して推論するというものです。
キーワードが決まっているような単純なQAタスクであれば、おおよそ的を得た回答をしてくれますが、質問の抽象度が上がるとDBにまったく引っ掛からなくなり、不正確な返答しかしなくなってしまいます。

その問題を改善する方法としてGraphRAGがあるのですが、また別の記事で書こうと思います。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です