LangChain入門

2024/08/01

AI

LangChainとは

LangChainは大規模言語モデル(LLM)を組み込んだアプリを簡単に実装できるようにしたライブラリである。LangChainを用いることで、LLMのみでは実現できない、複雑な計算をさせたり、Web検索した結果を参照して回答を生成させたりといった機能を簡単に実装できる。OpenAIを始め、AWSやGoogle Cloudなどが提供する様々なLLMのモデルを切り替えて利用可能。

LangChainの6つのモジュール

LangChainには6つのモジュールが用意されていて、これらのモジュールを組み合わせて複雑な機能を実現できる。

  • Model I/O 「プロンプトの準備」、「言語モデルの呼び出し」、「結果の受け取り」という3ステップを簡単に実装するための機能を提供する。
  • Retrieval 言語モデルが学習していない固有のデータを参照して回答させるための機能を提供する。
  • Memory 前の文脈を踏まえて回答できるように、過去の会話履歴の保存、読み込みを簡単に実装するための機能を提供する。
  • Chains 個別の機能を組み合わせて、より複雑な機能を簡単に実装するための機能を提供する。
  • Agents 言語モデルの呼び出しだけでは対応できないタスクを実行できるように、Web検索やファイル入出力といった機能を実装するための機能を提供する。
  • Callbacks イベント発生時にある特定の処理を実行させる機能を提供する。
  • LangChain(Python)で基本的な機能を実装する

    0. 準備

    LangChainを利用するための準備を実施する。(今回はOpenAIのAPIキーを取得して検証を実施)

  • 各自、環境変数にAPIキーを設定する
  • import os
    os.environ['OPENAI_API_KEY'] = "<各自取得したOPENAIのAPIキー>"
  • 必要なライブラリをインストールする
  • pip install langchain
    pip install langchain-openai
    pip install langchain-community
    pip install wikipedia
    pip install grandalf
    pip install chromadb
    pip install langchain-chroma
    pip install duckduckgo-search
    pip install langchain-experimental

    1. Model I/O

    Model I/Oでは、「プロンプトの準備」、「言語モデルの呼び出し」、「結果の受け取り」という3ステップを簡単に実装するための機能を提供する。これら、「プロンプトの準備」、「言語モデルの呼び出し」、「結果の受け取り」のために用意されている様々なコンポーネントの中で代表的なものを紹介する。

  • 「プロンプトの準備」のために、Promptコンポーネントが用意されている。
  • from langchain.prompts import PromptTemplate
    
    prompt_template = PromptTemplate(
      template = "次の食材からレシピを提案してください。「{input}」",
      input_variables=["input"],
    )
    response = prompt_template.invoke(input="じゃがいも 玉ねぎ 人参 りんご").text
    print(response)
    
    # 次の食材からレシピを提案してください。「じゃがいも 玉ねぎ 人参 りんご」
  • 「言語モデルの呼び出し」のために、ChatModelコンポーネントが用意されている。ChatModelコンポーネントは対話に最適化されており、対話形式で回答を生成する。
  • from langchain_openai import ChatOpenAI
    
    chat = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
    response = chat.invoke("こんにちは").content
    print(response)
    
    # こんにちは!どういったことをお手伝いできますか?
  • 「言語モデルの呼び出し」のために、LLMコンポーネントが用意されている。LLMコンポーネントは対話に最適化されておらず、入力に続く文章を生成する。
  • from langchain_openai import OpenAI
    
    llm = OpenAI(model='gpt-3.5-turbo-instruct', temperature=0)
    response = llm.invoke("机の上に積み木が")
    print(response)
    
    # 積まれている状態で、積み木を一つずつ取り除いていきます。...
  • 「結果の受け取り」のために、OutputParserコンポーネントが用意されている。StrOutputParser、JsonOutputParser、CommaSeparatedListOutputParserを紹介する。
  • from langchain_core.output_parsers import StrOutputParser, JsonOutputParser, CommaSeparatedListOutputParser
    
    # StrOutputParser
    output_parser = StrOutputParser()
    response = output_parser.invoke(1234)
    print(response)
    
    # JsonOutputParser
    output_parser = JsonOutputParser()
    response = output_parser.invoke('{"a":1, "b":2}')
    print(response)
    
    # CommaSeparatedListOutputParser
    output_parser = CommaSeparatedListOutputParser()
    response = output_parser.invoke('a,b,c,d')
    print(response)
    
    # 1234
    # {'a': 1, 'b': 2}
    # ['a', 'b', 'c', 'd']
  • 各種コンポーネント(Prompt、ChatModel、OutputParser)を組み合わせたシンプルなアプリケーションは次のように実装できる。
  • from langchain.prompts import PromptTemplate
    from langchain_openai import ChatOpenAI
    from langchain_core.output_parsers import StrOutputParser
    
    prompt_template = PromptTemplate(
      template = "次の食材からレシピを提案してください。「{input}」",
      input_variables=["input"],
    )
    chat = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
    output_parser = StrOutputParser()
    
    response = chat.invoke(prompt_template.format(input="じゃがいも 玉ねぎ 人参 りんご"))
    response = output_parser.parse(response.content)
    print(response)
    
    # もちろんです!「じゃがいも」「玉ねぎ」「人参」「りんご」を使ったレシピとして、「りんごと野菜のスープ」を提案します。...
  • 少し先取りして紹介すると、上記の「5」のコードをLCELというより直感的な記述方法で書くと以下のようになる。LCELの詳細については、後述の「Chains」の項目を参照されたい。
  • from langchain.prompts import PromptTemplate
    from langchain_openai import ChatOpenAI
    from langchain_core.output_parsers import StrOutputParser
    
    prompt_template = PromptTemplate(
      template = "次の食材からレシピを提案してください。「{input}」",
      input_variables=["input"],
    )
    chat = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
    output_parser = StrOutputParser()
    
    chain = prompt_template | chat | output_parser
    
    response = chain.invoke(input="じゃがいも 玉ねぎ 人参 りんご")
    print(response)
    
    # じゃがいも、玉ねぎ、人参、りんごを使ったレシピとして、「りんごと野菜のスープ」を提案します。以下に作り方を示します。...

    2. Retrieval

    Retrievalでは、言語モデルが学習していない固有のデータを参照して回答させるための機能を提供する。ここでは、手元のテキストファイル、指定のWebページ、Wikipediaの情報を参照して回答させる方法を紹介する。

  • Retrieverを利用し、手元のテキストファイル(sample.txt)を参照して回答させる機能は、次のように実装できる。
  • # sample.txt
    
    スイル
    スイルはボードゲームとボールゲームを組み合わせた新感覚のスポーツです。基本ルールは以下のとおりです。
    1. チーム構成:
    - 各チームは4人のプレイヤーで構成されます。
    2. フィールド:
    - フィールドは長方形で、中央にネットが張られています。
    -「ドット」と呼ばれる特定のエリアが、フィールド上を常に移動しています。
    3. ボールゲーム要素:
    - ボールは軽くて弾力のある素材で作られています。
    - ボールを相手チームの「ドット」に運ぶことが目的です。
    4. ボードゲーム要素:
    - フィールドの中央に大きなボードがあり、プレイヤーはボード上で駒を動かします。
    - 駒を動かすためには、ボールを相手チームの「ドット」に運ぶ必要があります。
    5. ゲームの進行:
    - ゲームは2つのハーフに分かれており、各ハーフは20分です。
    - ハーフタイムは10分間です。
    6. 得点方法:
    - ボールを相手チームの「ドット」に入れることで得点します。
    - ボード上で特定のマスに駒を進めることで追加得点が得られます。
    7. 反則:
    - 手でボールを扱う、相手プレイヤーを押す、引っ張るなどの行為は反則とされます。
    - 反則があった場合、相手チームにフリースローが与えられます。
    8. 勝利条件:
    - 試合終了時に得点が多いチームが勝利します。
    - 同点の場合は延長戦を行い、それでも決着がつかない場合はペナルティシュートで勝敗を決めま
    す。
    from langchain_community.vectorstores import Chroma
    from langchain_community.document_loaders import TextLoader
    from langchain.text_splitter import RecursiveCharacterTextSplitter
    from langchain_core.runnables import RunnablePassthrough
    from langchain.prompts import PromptTemplate
    from langchain_openai import ChatOpenAI, OpenAIEmbeddings
    from langchain_core.output_parsers import StrOutputParser
    
    embedding = OpenAIEmbeddings(model="text-embedding-3-large")
    
    if os.path.exists("vectorstore"): # vectorstore作成
        vectorstore = Chroma(persist_directory="./vectorstore", embedding_function=embedding)
    else: # vectorstore読み込み
        document = TextLoader('./sample.txt', encoding="utf-8").load()
        text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
        documents = text_splitter.split_documents(document)
        vectorstore = Chroma.from_documents(persist_directory="./vectorstore", documents=documents, embedding=embedding)
        retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
    
    prompt = PromptTemplate(
        input_variables=["context","question"],
        template="""以下のContextを参照して、Questionに回答してください。Contextの中に回答に役立つ情報が含まれていなければ、分からないと答えてください。
    Context: {context}
    Question: {question}
    """
    )
    chat = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
    rag_chain = (
        {
            "question": RunnablePassthrough(),#invokeで指定したtextが入る。
            "context": retriever
        }
        |prompt
        |chat
        |StrOutputParser()
    )
    response = rag_chain.invoke("スイルでボード上の駒を動かすための条件は?")
    print(response)
    
    # スイルでボード上の駒を動かすための条件は、ボールを相手チームの「ドット」に運ぶ必要があることです。
  • Retrieverを利用し、指定のWebページを参照して回答させる機能は、次のように実装できる。
  • from langchain_community.document_loaders import WebBaseLoader
    from langchain_community.vectorstores import Chroma
    from langchain.text_splitter import RecursiveCharacterTextSplitter
    from langchain_core.runnables import RunnablePassthrough
    from langchain.prompts import PromptTemplate
    from langchain_openai import ChatOpenAI, OpenAIEmbeddings
    from langchain_core.output_parsers import StrOutputParser
    
    embedding = OpenAIEmbeddings(model="text-embedding-3-large")
    
    document = WebBaseLoader("https://minkabu.jp/financial_item_ranking/sales").load()
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
    documents = text_splitter.split_documents(document)
    vectorstore = Chroma.from_documents(documents=documents, embedding=embedding)
    retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
    
    prompt = PromptTemplate(
        input_variables=["context","question"],
        template="""以下のContextを参照して、Questionに回答してください。Contextの中に回答に役立つ情報が含まれていなければ、分からないと答えてください。
    Context: {context}
    Question: {question}
    """
    )
    chat = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
    rag_chain = (
        {
            "question": RunnablePassthrough(),#invokeで指定したtextが入る。
            "context": retriever
        }
        |prompt
        |chat
        |StrOutputParser()
    )
    response = rag_chain.invoke("売上1位は?")
    print(response)
    
    # 売上1位はトヨタ(7203)です。
  • Retrieverを利用し、Wikipediaの情報を参照して回答させる機能は、次のように実装できる。
  • from langchain_community.retrievers import WikipediaRetriever
    from langchain_core.runnables import RunnablePassthrough
    from langchain.prompts import PromptTemplate
    from langchain_openai import ChatOpenAI
    from langchain_core.output_parsers import StrOutputParser
    from langchain_core.runnables import chain as chain_decorator
    
    prompt = PromptTemplate(
        input_variables=["context","question"],
        template="""以下のContextを参照して、Questionに回答してください。Contextの中に回答に役立つ情報が含まれていなければ、分からないと答えてください。
    Context: {context}
    Question: {question}
    """
    )
    chat = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
    retriever = WikipediaRetriever(lang='ja', top_k_results=3, doc_content_chars_max=300)
    output_parser = StrOutputParser()
    
    @chain_decorator
    def join(xs):
        return ' '.join([x.page_content for x in xs])
    
    chain = (
        {
            "question":RunnablePassthrough(),#invokeで指定したtextが入る。
            "context":retriever | join
        }
        | prompt
        | chat
        | StrOutputParser()
    )
    response = chain.invoke("違法素数とは?")
    print(response)
    
    # 違法素数とは、素数のうち、違法となるような情報やコンピュータプログラムを含む数字のことです。...

    3. Memory

    Memoryでは、前の文脈を踏まえて回答できるように、過去の会話履歴の保存、読み込みを簡単に実装するための機能を提供する。ここでは、ChatGPTのWebアプリケーションのように会話履歴を保持するして、APIを呼び出す方法を紹介する。

  • まず、会話履歴を保持しない実装を見てみる。
  • from langchain.prompts import ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate
    from langchain_openai import ChatOpenAI
    
    prompt = ChatPromptTemplate.from_messages([
      SystemMessagePromptTemplate.from_template("あなたは優秀な医者です。診断をしてほしいと指示があるまで「うん」「そうですか」など短く相槌を打ち、診断をしてほしいと指示があれば診断を下してください。"),
      HumanMessagePromptTemplate.from_template("{input}")
    ])
    chat = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
    chain = prompt | chat
    
    response1 = chain.invoke({"input":"頭が痛いです"})
    response2 = chain.invoke({"input":"手足がしびれます"}) # 「頭が痛いです」と伝えたことはもう覚えていない!
    response3 = chain.invoke({"input":"熱もあるようです"}) # 「手足がしびれます」と伝えたことはもう覚えていない!
    response4 = chain.invoke({"input":"診断してください"}) # 「熱もあるようです」と伝えたことはもう覚えていない!
    
    print(f"""
    回答1:{response1.content},
    回答2:{response2.content},
    回答3:{response3.content},
    回答4:{response4.content},
    """)
    
    # 回答1:うん。,
    # 回答2:うん。,
    # 回答3:うん。,
    # 回答4:症状や状況について詳しく教えていただけますか?,
  • 次に、会話履歴を保持している実装を見てみる。
  • from langchain.prompts import ChatPromptTemplate, SystemMessagePromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate
    from langchain_openai import ChatOpenAI
    from langchain_core.runnables.history import RunnableWithMessageHistory
    from langchain_community.chat_message_histories import ChatMessageHistory
    from langchain_core.chat_history import BaseChatMessageHistory
    
    prompt = ChatPromptTemplate.from_messages([
      SystemMessagePromptTemplate.from_template("あなたは優秀な医者です。診断をしてほしいと指示があるまで「うん」「そうですか」など短く相槌を打ち、診断をしてほしいと指示があれば診断を下してください。"),
      MessagesPlaceholder(variable_name="history"),
      HumanMessagePromptTemplate.from_template("{input}")
    ])
    chat = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
    chain = prompt | chat
    
    # セッションIDごとの会話履歴の取得
    store = {}
    def get_session_history(session_id: str) -> BaseChatMessageHistory:
        if session_id not in store:
            store[session_id] = ChatMessageHistory()
        return store[session_id]
    
    # RunnableWithMessageHistoryでチェーンをラップする
    chain_with_history = RunnableWithMessageHistory(
        chain,
        get_session_history,
        input_messages_key="input",
        history_messages_key="history",
    )
    
    response1 = chain_with_history.invoke({"input":"頭が痛いです"}, config={"configurable": {"session_id": "123"}})
    response2 = chain_with_history.invoke({"input":"手足がしびれます"}, config={"configurable": {"session_id": "123"}})
    response3 = chain_with_history.invoke({"input":"熱もあるようです"}, config={"configurable": {"session_id": "123"}})
    response4 = chain_with_history.invoke({"input":"診断してください"}, config={"configurable": {"session_id": "123"}})
    
    print(f"""
    回答1:{response1.content},
    回答2:{response2.content},
    回答3:{response3.content},
    回答4:{response4.content},
    """)
    
    # 回答1:うん。,
    # 回答2:そうですか。,
    # 回答3:うん。,
    # 回答4:頭痛、手足のしびれ、発熱の症状から考えられる可能性はいくつかあります。...

    4. Chains

    Chainsでは、個別の機能を組み合わせて、より複雑な機能を簡単に実装するための機能を提供する。Chainsを簡単に構築するために、LCELという記法が推奨されている。LCELでは|でつなげてチェーンを表現する。

  • 処理を直列に実行するチェーンは、|で並べて表現する。
  • from langchain_core.runnables import chain as chain_decorator
    from langchain_core.runnables import RunnablePassthrough
    
    @chain_decorator
    def double(x):
        return x * 2
    
    @chain_decorator
    def triple(x):
        return x * 3
    
    @chain_decorator
    def increment(x):
        return x + 1
    
    chain = double | triple | increment
    response = chain.invoke(2)
    print(response)
    
    # チェーンの可視化
    chain.get_graph().print_ascii()
    
    # 13
    #   +--------------+   
    #   | double_input |
    #   +--------------+
    #           *
    #           *
    #           *
    #      +--------+
    #      | double |
    #      +--------+
    #           *
    #           *
    #           *
    #      +--------+
    #      | triple |
    #      +--------+
    #           *
    #           *
    #           *
    #     +-----------+
    #     | increment |
    #     +-----------+
    #           *
    #           *
    #           *
    # +------------------+
    # | increment_output |
    # +------------------+

  • 処理を並列に実行するチェーンは、辞書型で分岐を表現する。
  • from langchain_core.runnables import chain as chain_decorator
    from langchain_core.runnables import RunnablePassthrough
    
    @chain_decorator
    def double(x):
        return x * 2
    
    @chain_decorator
    def triple(x):
        return x * 3
    
    @chain_decorator
    def increment(x):
        return x + 1
    
    chain = {
        "double":double,
        "triple":triple,
    } | RunnablePassthrough()
    response = chain.invoke(2)
    print(response)
    
    # チェーンの可視化
    chain.get_graph().print_ascii()
    
    # {'double': 4, 'triple': 6}
    # +------------------------------+   
    # | Parallel<double,triple>Input |
    # +------------------------------+
    #            **        **
    #          **            **
    #         *                *
    #   +--------+          +--------+
    #   | double |          | triple |
    #   +--------+          +--------+
    #            **        **
    #              **    **
    #                *  *
    # +-------------------------------+
    # | Parallel<double,triple>Output |
    # +-------------------------------+
    #                 *
    #                 *
    #                 *
    #          +-------------+
    #          | Passthrough |
    #          +-------------+
    #                 *
    #                 *
    #                 *
    #       +-------------------+
    #       | PassthroughOutput |
    #       +-------------------+
  • 先取りした「1. Models - 6」を再掲する。直列のチェーンでシンプルに表現されていることがわかる。
  • from langchain.prompts import PromptTemplate
    from langchain_openai import ChatOpenAI
    from langchain_core.output_parsers import StrOutputParser
    
    prompt_template = PromptTemplate(
      template = "次の食材からレシピを提案してください。「{input}」",
      input_variables=["input"],
    )
    chat = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
    output_parser = StrOutputParser()
    
    chain = prompt_template | chat | output_parser
    
    response = chain.invoke(input="じゃがいも 玉ねぎ 人参 りんご")
    print(response)
    
    # じゃがいも、玉ねぎ、人参、りんごを使ったレシピとして、「りんごと野菜のスープ」を提案します。以下に作り方を示します。...

    5. Agents

    Agentsでは、言語モデルの呼び出しだけでは対応できないタスクを実行できるように、Web検索やファイル入出力といった機能を実装するための機能を提供する。

  • 検索ツールとファイル出力ツールを利用したエージェントは、次のように実装できる。
  • from langchain_community.tools import DuckDuckGoSearchRun, WriteFileTool
    from langchain.prompts import ChatPromptTemplate
    from langchain_openai import ChatOpenAI
    from langchain.agents import create_tool_calling_agent, AgentExecutor
    
    search = DuckDuckGoSearchRun()
    writefile = WriteFileTool()
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", "you're a helpful assistant"), 
        ("human", "{input}"), 
        ("placeholder", "{agent_scratchpad}"),
    ])
    
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    
    tools = [search, writefile]
    
    agent = create_tool_calling_agent(llm, tools, prompt)
    agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
    
    agent_executor.invoke({"input":"違法素数の説明をresult.txtというファイルに書き込んで保存してください"})
    
    # > Entering new AgentExecutor chain...
    # Invoking: `write_file` with `{'file_path': 'result.txt', 'text': '違法素数(いほうそすう、英: illegal prime)とは、特定の条件や規則に従って定義された素数のことを指します。一般的に、素数は1とその数自身以外の約数を持たない自然数ですが、違法素数は通常の素数の定義から外れる特異な性質を持つことがあります。これらの素数は、数学的な研究や暗号理論などの分野で特に興味深いとされています。違法素数の具体的な定義や性質は、文脈によって異なる場合があります。', 'append': False}`
    # File written successfully to result.txt.違法素数の説明を `result.txt` というファイルに書き込みました。ファイルは正常に保存されています。
    # > Finished chain.
  • 自作関数をツールとして利用したエージェントは、次のように実装できる。
  • from langchain.prompts import ChatPromptTemplate
    from langchain_openai import ChatOpenAI
    from langchain.agents import create_tool_calling_agent, AgentExecutor
    from langchain_community.tools import tool as tool_decorator
    
    @tool_decorator
    def multiply(x: float, y: float) -> float:
        """Multiply 'x' times 'y'."""
        return x * y
    
    @tool_decorator
    def exponentiate(x: float, y: float) -> float:
        """Raise 'x' to the 'y'."""
        return x**y
    
    @tool_decorator
    def add(x: float, y: float) -> float:
        """Add 'x' and 'y'."""
        return x + y
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", "you're a helpful assistant"), 
        ("human", "{input}"), 
        ("placeholder", "{agent_scratchpad}"),
    ])
    
    tools = [multiply, exponentiate, add]
    
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    
    agent = create_tool_calling_agent(llm, tools, prompt)
    agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
    
    agent_executor.invoke({"input": "3+5を計算してその結果の2乗を計算してください"})
    
    # > Entering new AgentExecutor chain...
    # Invoking: `add` with `{'x': 3, 'y': 5}`
    # 8.0
    # Invoking: `exponentiate` with `{'x': 8, 'y': 2}`
    # 64.0 3 + 5 の計算結果は 8 で、その 2 乗は 64 です。
    # > Finished chain.

    6. Callbacks

    Callbacksでは、イベント発生時にある特定の処理を実行させる機能を提供する。

  • CallBack関数を利用した簡単な例を以下に示す。
  • from langchain.callbacks.base import BaseCallbackHandler
    from langchain_openai import ChatOpenAI
    
    class LogCallbackHandler(BaseCallbackHandler):
        def on_chat_model_start(self, serialized, messages, **kwargs):
            print("ChatModelの実行を開始")
            print(f"入力: {messages}")
    
    chat = ChatOpenAI(model="gpt-4o-mini", temperature=0, streaming=True, callbacks=[LogCallbackHandler()])
    response = chat.invoke("こんにちは").content
    print(response)
    
    # ChatModelの実行を開始
    # 入力: [[HumanMessage(content='こんにちは')]]
    # こんにちは!どういったことをお手伝いできますか?

    資料


    著者画像

    ゆうき

    2018/04からITエンジニアとして活動、2021/11から独立。主な使用言語はPython, TypeScript, SAS, etc.