LoginSignup
197
158

More than 1 year has passed since last update.

ChatGPT APIの運用で必須のツール: LangChainの使い方まとめ (2)

Last updated at Posted at 2023-03-12

こんにちは!逆瀬川 ( https://twitter.com/gyakuse )
こちらに引き続き、LangChainの解説をしていきたいと思います。

副読用Colab:

動かしながら遊びましょう。

前回のあらすじ

Chatbotや言語モデルを使ったサービスを作ろうとしたときに生のOpenAI APIを使うのは以下の点でたいへん。

  • プロンプトの共通化や管理をすること
  • 複数のドキュメントやWebの情報を参照して質問応答をすること
  • 言語モデルにcsvやpdf等のプレーンテキストでないファイルを読ませること
  • それらの処理を統括して管理すること

使い方まとめ(1)で説明したLangChainの各モジュールはこれを解決するためのものでした。

  • Prompt Templates : プロンプトの管理
  • LLMs : 言語モデルのラッパー(OpenAI::GPT-3やGPT-Jなど)
  • Document Loaders : PDFなどのファイルの下処理
  • Utils : 検索APIのラッパーなど便利関数保管庫
  • Indexes : テキストを分割したり埋め込みにしたりVector Storeに入れる処理

そしてこれから説明するモジュール群は簡単に説明すると以下のようになります。

  • Chains : 言語モデルなどの処理の連携を扱うもの
  • Agents : 任意の入力に対して任意のアクションを起こすことを目標としたもの
  • Memory : 言語モデルとの会話の履歴や知識を上手く扱う保管庫
  • Chat : ChatGPT APIでChatbotを作るためのモジュール

それでは各モジュールを見ていきましょう。

Chain: チェーン

このChainという概念はわりと曖昧であり、今後設計として改善される部分だと個人的に考えています。
そのため、現段階の実装を理解するのは大変です。
分類としてGenericChainとUtilityChainという2分類がありますが、GenericとかUtilとかいう単語を見てヤバそうなにおいを感じ取っておけば良いと思います。

LLM Chain

一番シンプルなLLM Chainは以下のように使用できます。
重要なのは変数がPromptTemplateに隠蔽されていることです。

from langchain import PromptTemplate, OpenAI, LLMChain

template = """質問: {question}

回答: 段階的に考えてください。"""
prompt = PromptTemplate(template=template, input_variables=["question"])
llm_chain = LLMChain(prompt=prompt, llm=OpenAI(temperature=0, openai_api_key=openai_key), verbose=True)

question = "関ヶ原の戦いで勝ったのは?"

llm_chain.predict(question=question)

これを使用すると以下のような出力が得られます。

> Entering new LLMChain chain...
Prompt after formatting:
質問: 関ヶ原の戦いで勝ったのは?

回答: 段階的に考えてください。

> Finished chain.
関ヶ原の戦いは、1575年に行われた戦いです。この戦いで勝利したのは、徳川家康率いる徳川軍です。

これは以下と等しいです。

llm = OpenAI(temperature=0, openai_api_key=openai_key)
prompt = PromptTemplate(template=template, input_variables=["question"])
llm(prompt.format(question='関ヶ原の戦いで勝ったのは?'))

一つのQAであれば、LLMsモジュールをそのまま使うだけで良さそうですが、
複数のQAを連鎖的に使ったりする場合にはChainsモジュールにしたほうが便利であることが推察されます。

SimpleSequentialChain

ある質問などに対して複数回言語モデルを通したいというモチベーションはよくあると思います。
たとえば、何かについて質問して、それを要約するとき、
要約文章 = 要約(質問(質問内容)) という構造になります。

こうした言語モデル等への処理の連鎖を行うのがSimpleSequentialChainです。
たとえば、質問&要約では以下のように行います。

# 質問
question_llm = OpenAI(temperature=.7, openai_api_key=openai_key, max_tokens=512)
question_template = """質問: {question}

回答: 段階的に考えてください"""
question_prompt_template = PromptTemplate(input_variables=["question"], template=question_template)
question_chain = LLMChain(llm=question_llm, prompt=question_prompt_template)

# 回答の要約
summarize_llm = OpenAI(temperature=.7, openai_api_key=openai_key, max_tokens=256)
summarize_template = """{text}

上記を要約してください:"""
summarize_prompt_template = PromptTemplate(input_variables=["text"], template=summarize_template)
summarize_chain = LLMChain(llm=summarize_llm, prompt=summarize_prompt_template)

# 質問とその回答の要約Chain
from langchain.chains import SimpleSequentialChain
question_answer_summarize_chain = SimpleSequentialChain(chains=[question_chain, summarize_chain], verbose=True)
question_answer_summarize_chain.run("未来の大規模言語モデルはどうなりますか?")

ここで、二番目のtemplateに使われるtext変数には自動的に最初のtemplateの出力が当てられます。
以下のように出力されます。

> Entering new SimpleSequentialChain chain...
。大規模言語モデルは、現在の言語モデルとはかなり異なります。未来の大規模言語モデルは、複数の言語を同時に処理できるようになるでしょう。さらに、複数の言語を一貫して処理するための複雑なルールを持つこともできるでしょう。複雑なルールは、複数の言語間の相互運用性を確実にし、ユーザーが複数の言語を同時に使用できるようにします。さらに、大規模言語モデルは、複数の言語を統合し、自然な会話を実現するための複雑な計算アルゴリズムを備えるでしょう。


未来の大規模言語モデルは、複数の言語を同時に処理できるほか、複数の言語間の相互運用性を確実にする複雑なルールを持ち、複数の言語を統合して自然な会話を実現するための複雑な計算アルゴリズムを備えるようになるでしょう。

> Finished chain.
\n\n未来の大規模言語モデルは、複数の言語を同時に処理できるほか、複数の言語間の相互運用性を確実にする複雑なルールを持ち、複数の言語を統合して自然な会話を実現するための複雑な計算アルゴリズムを備えるようになるでしょう。

Sequential Chain

ところで、中間生成物も使いたいとか複数の入力を使いたいという欲求があると思います。その場合こちらを使います。

# 質問
question_llm = OpenAI(temperature=.7, openai_api_key=openai_key, max_tokens=512)
question_template = """質問: {question}

回答: 段階的に考えてください"""
question_prompt_template = PromptTemplate(input_variables=["question"], template=question_template)
question_chain = LLMChain(llm=question_llm, prompt=question_prompt_template, output_key="answer")

# 回答の要約
summarize_llm = OpenAI(temperature=.7, openai_api_key=openai_key, max_tokens=256)
summarize_template = """{answer}

上記を{style}口調で要約してください:"""
summarize_prompt_template = PromptTemplate(input_variables=["answer", "style"], template=summarize_template)
summarize_chain = LLMChain(llm=summarize_llm, prompt=summarize_prompt_template, output_key="summary")

# 質問とその回答の要約Chain
from langchain.chains import SequentialChain
question_answer_summarize_chain = SequentialChain(
    chains=[question_chain, summarize_chain],
    input_variables=["question", "style"],
    output_variables=["answer", "summary"],
    verbose=True)
result = question_answer_summarize_chain({"question": "未来の大規模言語モデルはどうなりますか?", "style": "江戸っ子"})
result

上記ではquestion_chain, answer_chainに以下のようにoutput_keyを明示しました。

question_chain = LLMChain(llm=question_llm, prompt=question_prompt_template, output_key="answer")

また、output_variablesをキーで指示することによって出力物をコントロールしています。
この結果、以下のようにdict形式で出力されます。

> Entering new SequentialChain chain...

> Finished chain.
{'question': '未来の大規模言語モデルはどうなりますか?',
 'style': '江戸っ子',
 'answer': '。未来の大規模言語モデルは、さらなる高度な自然言語理解と生成を実現するものとして、より大規模で、より柔軟で、より高精度なものになります。また、複雑な文法構造や複雑な単語関係を理解できるようになるでしょう。大規模言語モデルは、現在の言語モデルと比べ、より複雑な文章を理解し、更に複雑な言葉を生成することが可能になるでしょう。さらに、人間の言語のような自然さを表現できるようになるでしょう。',
 'summary': '\n\n将来の大規模言語モデルは、もっと複雑な文章を理解し、自然な表現も可能にするんやで!高精度で、柔軟なモデルなんやで!'}

Serialization

作ったChainを保存したいときはSerializationを使います。
これを適当なKVSに入れておくといつでもchainを呼び出せて便利です。
LLMChainは対応してますが、Sequential ChainなどはSerialization未対応です。はい。

LLMChainの場合は以下のようにsaveするだけです。

from langchain import PromptTemplate, OpenAI, LLMChain

template = """質問: {question}

回答: 段階的に考えてください。"""
prompt = PromptTemplate(template=template, input_variables=["question"])
llm_chain = LLMChain(prompt=prompt, llm=OpenAI(temperature=0), verbose=True)
llm_chain.save("qa_chain.json")

また、promptのsaveは以下のように行います。

prompt = PromptTemplate(template=template, input_variables=["question"])
prompt.save("qa_prompt.json")

Transformation Chain

ここまでは言語モデルのみの操作でしたが、任意の操作ができると嬉しいです。
たとえば、なんらかの資料に対してQAをするとき、最初の500文字のみで十分だったりします。
これを実装すると以下のようになります。

from langchain.chains import TransformChain
def get_first_500_chars(inputs: dict) -> dict:
    text = inputs['text']
    shortened_text = text[:500]
    return {"output_text": shortened_text}

transform_chain = TransformChain(input_variables=["text"], output_variables=["output_text"], transform=get_first_500_chars)

これは単純な処理モジュールなので、他のチェーンと組み合わせて使います。

Utility Chains

いろんな便利Chainが突っ込まれていますがここでは割愛します。

Agent

LangChainにおけるAgentも混乱した概念のひとつです。
ツールを使って処理したり、言語モデルに問い合わせたり、そういうのをまとめて突っ込んだものになります。

ツールという概念もここで導入されます。ツールは検索などのUtilityや任意のChain、はたまたAgentである場合もあります。
任意の入力に対してなんでもできるAIが作りたい 、というモチベーションがあるとき、ツールの選択すら動的にしなければなりません。これがAgentが複雑たるゆえんです。
ここでは一般的なAgentのみを紹介します。詳細は使って慣れたほうが早いからです。

Agentの思想

任意の入力に対してなんでもできるAIが作りたい とき、任意の入力に対して、何を聞いているかを判別する層を作り、対応するアクションの数だけツールを登録しておけば、対応するアクションを実施することができそうです。法律の相談なら法曹LLMに、CSVをグラフ化したいときはCSVAgentに繋ぎつつ、CSVAgentはさらにグラフ処理AIに、等々。ゆえに、何回も考える必要があり、言語モデルへの問い合わせも増えます。
LangChainを気軽に使ったほうがいい理由と気軽に使うべきでない理由がここに詰まっています。

ReACTを中心に設計されたこれは、それゆえに自由ではありません。汎用人工知能に至るこの概念実装は、もっと多くのアプローチがあって良いと思います。

Agentの使い方

それでは一般的なAgentの使い方を見ていきます。

from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.llms import OpenAI

llm = OpenAI(temperature=0)
tools = load_tools(["wikipedia"], llm=llm)
agent = initialize_agent(tools, llm, agent="zero-shot-react-description", verbose=True)
agent.run("関ヶ原の戦いは西暦何年?")

これを実行すると以下の結果になります。

> Entering new AgentExecutor chain...
 関ヶ原の戦いについて調べる必要がある
Action: Wikipedia
Action Input: 関ヶ原の戦い
Observation: Page: Battle of Sekigahara
Summary: The Battle of Sekigahara (Shinjitai: 関ヶ原の戦い; Kyūjitai: 關ヶ原の戰い, Hepburn romanization: Sekigahara no Tatakai) was a decisive battle on October 21, 1600 (Keichō 5, 15th day of the 9th month) in what is now Gifu prefecture, Japan, at the end of the Sengoku period. This battle was fought by the forces of Tokugawa Ieyasu against a coalition of Toyotomi loyalist clans under Ishida Mitsunari, several of which defected before or during the battle, leading to a Tokugawa victory. The Battle of Sekigahara was the largest battle of Japanese feudal history and is often regarded as the most important. Toyotomi's defeat led to the establishment of the Tokugawa shogunate.
Tokugawa Ieyasu took three more years to consolidate his position of power over the Toyotomi clan and the various daimyō, but the Battle of Sekigahara is widely considered to be the unofficial beginning of the Tokugawa shogunate, which ruled Japan for another two and a half centuries until 1868.

Page: Tozama daimyō
Summary: Tozama daimyō (外様大名, "outside daimyō") was a class of powerful magnates or daimyō (大名) considered to be outsiders by the ruler of Japan. Tozama daimyō were classified in the Tokugawa shogunate (江戸幕府) as daimyō who became hereditary vassals of the Tokugawa after the Battle of Sekigahara (関ヶ原の戦い).
Tozama daimyō were discriminated against by the Tokugawa and opposed to the fudai daimyō during the Edo period   (江戸時代).

Page: Fires in Edo
Summary: Fires in Edo (江戸), the former name of Tokyo, during the Edo period (1600−1868) of Japan were so frequent that the city of Edo was characterized as the saying "Fires and quarrels are the flowers of Edo" goes. Even in the modern days, the old Edo was still remembered as the "City of Fires" (火災都市).Edo was something of a rarity in the world, as vast urban areas of the city were repeatedly leveled by fire. The great fires of Edo were compared to the gods of fire Shukuyū (祝融) and Kairoku (回禄), and also humorously described as "autumn leaves".
Thought: 関ヶ原の戦いは西暦1600年に行われたことがわかった
Final Answer: 関ヶ原の戦いは西暦1600年に行われました。

> Finished chain.
関ヶ原の戦いは西暦1600年に行われました。

まずはじめにqueryから何をしらべればいいか考える処理に向かいます。
このwikipedia toolは最終的にutilityのwikipediaからWikipediaを呼び出していますが、このpythonモジュールは.set_lang({language})しないと任意の言語にならないので、英語のwikipediaが呼び出されてしまっています。

このように、Agentは特に日本語で使う分には若干面倒な部分もありますが、強力なモジュールです。

Memory

さて、AgentやChainを使ってきてそろそろChatbotに記憶を実装したくなってきました。
AIを人間らしく振る舞わせるだけでなく、単純なChatbotとしても記憶の取り扱いが上手いと便利っぽい気がします。
ここで使うのがMemoryモジュールです。これも、「LangChainではこう実装されている」というだけで、本質的に記憶等をどう扱ったほうがいいかはより熟慮されるべき分野でしょう。

ただ、LangChainでのMemoryモジュールを考えるうえでも、そのまえに記憶をどうやって扱っておくべきかと考えておくことは有効です。事前に何があるべきかを考えておくと、実装されている各クラスの存在意義が理解しやすくなります。

記憶をどう実装するか

かんたんに考えます。人間の記憶は長期記憶と短期記憶に分かれています。

「いま会話してること」
「さっき見たこと」「さっき知ったこと(事実、知識)」
「今日あったこと」
「10年前に起きたこと」
「あることに対する記憶」

これを全部うまく扱うのは大変そうです。ひとつの提案としては、

  • 直近の会話についての要約
  • 知識や事実等にかんして保存する貯蔵庫
  • できごとにかんして保存する貯蔵庫

これらを用意してあげて、取得・保存・更新をしていくことで、短期記憶・長期記憶を実現することです。

LangChainにおけるMemory

LangChainにおけるメモリは主に揮発する記憶として実装されています。
記憶の長期化にかんしては、作られた会話のsummaryやentityをindexesモジュールを使って保存することで達成されます。

  • ConversationBufferMemory: 単純に会話記録を保持し、プロンプトに過去会話として入れ込むメモリです
  • ConversationSummaryMemory: 会話の要約を保存するメモリです
  • ConversationEntityMemory: 会話中の特定の事物にかんして保持するためのメモリです
    • 欠点としては、固有名詞抽出を行っているので表記ゆれに弱いことです

Memoryの使い方

from langchain.llms import OpenAI
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain


llm = OpenAI(temperature=0)
conversation = ConversationChain(
    llm=llm, 
    verbose=True, 
    memory=ConversationBufferMemory()
)

conversation.predict(input="こんにちは")
conversation.predict(input="今日は晴れてていい気分ですね")

上記を実行すると以下のようになります。

> Entering new ConversationChain chain...
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.

Current conversation:

Human: こんにちは
AI:

> Finished chain.


> Entering new ConversationChain chain...
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.

Current conversation:
Human: こんにちは
AI:  こんにちは!私はAIです。どうぞよろしくお願いします!
Human: 今日は晴れてていい気分ですね
AI:

> Finished chain.
 そうですね!今日は本当に素晴らしい天気ですね!私もとても気分がいいです!

ちゃんと二度目の会話のときに前回の会話が挿入されていることがわかります。
会話の履歴だけ見るときは以下のように叩きます。

memory.load_memory_variables({})

Chat

ChatはChatGPT APIの仕様の煽りを受けたモジュールです。
LangChainはOpenAIに依存したサービスではないので、任意の言語モデルを扱える汎用的なモジュールを新たに考え直さなければなりませんでした。
そうした意味で、LangChainの設計は一回破壊して再構築してもよさそうではあるのですが、なかなかそうもいかないので新しいモジュールとして(めっちゃ頑張って考えて)作っています。

なにがうれしいのか

ChatGPT APIのwrapperとしてではなく、より深堀りして考えてみます。
これまでの経験上、LangChainに期待を寄せられる主な部分はVector StoreやAPIから情報を取ってきて事実ベースで回答することにあります。実際、これができれば以下の記事で示したようなことができます。

  • QAデータをもっている企業における質問応答チャットボットの構築
  • 企業/事業ごとの専門知識をもったチャットボットの構築
  • 教科書等を読み込ませた家庭教師的なチャットボットの構築
  • 論文等の各種文献の読解補佐チャットボットの構築
  • AITuberやAIキャラクターに長期記憶を持たせる
  • BingGPTなどのようなシステムの構築

Agentのような「なんでもやる」よりは現実的で、実用的です。
Chatモジュールはこれを達成するためにあると解釈しても(現段階では)よさそうです。

Chatモジュールの使い方

from langchain.chat_models import ChatOpenAI
from langchain import PromptTemplate, LLMChain
from langchain.prompts.chat import (
    ChatPromptTemplate,
    SystemMessagePromptTemplate,
    AIMessagePromptTemplate,
    HumanMessagePromptTemplate,
)
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage
)
chat = ChatOpenAI(temperature=0)
chat([HumanMessage(content="これを英語から日本語に翻訳してください. I love programming.")])

ちょっとめんどいですが、HumanMessageというクラスで渡してあげることで会話をすることができます。

ここで実装したものも以下のように置き換えられます。

system_settings = """ツン子という少女を相手にした対話のシミュレーションを行います。
彼女の発言サンプルを以下に列挙します。

あんたのことなんか、どうでもいいわ!
うっさい!黙ってて!
こんなの、私がやるわけないじゃない!
お、おい…馬鹿にしないでよね。
う、うっかり…気にしないでよね!
あんたとは話しているつもりじゃないわよ。
な、なんでそんなに見つめないでよ!
うぅ…ちょっと待って、私、もう一回言ってあげるからね。
あんた、そこに立ってないで、何かしてよ!
ほ、本当に私がこんなことするわけないでしょう?
うっさい!邪魔しないで!
あんたの言うことなんて、どうだっていいわ!
ち、違うってば!私、全然…!
べ、別にあんたが好きだからって言ってるわけじゃないんだからね!
な、何よ、いきなり抱きついてきて…っ!
あんたみたいな人と一緒にいると、本当に疲れるわ。
そ、そんなに急かさないでよ…!
あんた、いつもいい加減なこと言うわね。
うっさい!うるさいってば!
あんたのことなんて、どうでもいいからさっさと帰って!

上記例を参考に、ツン子の性格や口調、言葉の作り方を模倣し、回答を構築してください。
ではシミュレーションを開始します。"""

messages = [
    SystemMessage(content=system_settings),
    HumanMessage(content="こんにちは")
]
chat(messages)

ChatGPT用のtemplateやmemoryを使った実装の場合、以下のようになります。

from langchain.prompts import (
    ChatPromptTemplate, 
    MessagesPlaceholder, 
    SystemMessagePromptTemplate, 
    HumanMessagePromptTemplate
)
from langchain.chains import ConversationChain
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory
prompt = ChatPromptTemplate.from_messages([
    SystemMessagePromptTemplate.from_template(system_settings),
    MessagesPlaceholder(variable_name="history"),
    HumanMessagePromptTemplate.from_template("{input}")
])
conversation = ConversationChain(
    memory=ConversationBufferMemory(return_messages=True),
    prompt=prompt,
    llm=ChatOpenAI(temperature=0))
conversation.predict(input="こんにちは")
conversation.predict(input="お腹すいた")

また、以下のようにすればstreamingも簡単にできます。stream=trueとしても、会話の完了は通常のapi requestの到着と同時になりますが、より早い書き出しとなるため、ユーザー体験がよくなります。なお、もちろんではありますが通常のOpenAI ChatGPT/GPT-3 APIでもstreamingはできます。

from langchain.callbacks.base import CallbackManager
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler

from langchain.prompts import (
    ChatPromptTemplate, 
    MessagesPlaceholder, 
    SystemMessagePromptTemplate, 
    HumanMessagePromptTemplate
)
from langchain.chains import ConversationChain
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory
messages = [
    SystemMessage(content=""),
    HumanMessage(content="日本の歴史を教えて")
]
chat = ChatOpenAI(max_tokens=1024, temperature=0.7, streaming=True, callback_manager=CallbackManager([StreamingStdOutCallbackHandler()]))
resp = chat(messages)

まとめ

LangChainの各機能を横断的に見てきました。LangChainは一見するととても複雑な構造物に見えますが、Chatbotや汎用人工知能にどんな機能があるべきか、を考えておくととてもシンプルなものと解釈できます。
わたしは細かく実装を追ってませんが、「こういう機能があるべきだよね」ベースで探したりすると一瞬で出てきたりします (例: CSVtoレポート)。

いまのところ大事なのは、各モジュールがどのようにできているか知ることと、便利な部分は取り入れることだと思います。この稿がサービス開発などの一助になれば幸いです。
この稿を読んだあと、公式ドキュメントやnpakaさんの記事を読んでみると、サクッと使い方がわかってくると思います。

ぜひLangChainでサービスを乱造し遊びましょう。

197
158
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
197
158