Langchainのコアモジュールと、LLM応用プログラム開発の基礎
こんにちは!ONDAで開発全般を担当しているElliot(エリオット、イ・ウンギュ)です。
最近、GPT-4やBard、LLaMAをはじめとするビッグテックの大規模言語モデル(LLM)競争が市場で大きな話題になっています。こうしたパラダイムシフトのなかで、オープンソース主導の小規模言語モデルとその応用プロジェクトも目覚ましい成長を遂げており、生成AIと言語モデル活用能力の重要性に対する共感も急速に広がっています。
LLMの活用は、テキストデータを処理するこれまでの方法を完全に革新し、データを分析・生成・応用する新しいアプローチを提示します。LLMを使えば、あるテーマについての長文を自動で生成・要約したり、質問に能動的な回答を受けられます。非構造化データを構造化データへスマートに変換する機能を実装するなど、以前は試みることすら困難だったさまざまな発想を簡単に実現できるのです。
特に最近は、LLMを活用した面白い実装や試みが次々と登場しています。たとえば、LLMを使って互いに交流可能なAIキャラクター集団を構成し仮想空間でシミュレートした研究プロジェクトや、与えられた命令を遂行するため自律的に思考・計画・行動するAuto-GPTエージェントプロジェクトのように。
誰しも一度は想像したアイデアが実際に実装されているのを見て、こうしたプロジェクトがLLMをどう操作・構成しているのか気になるでしょう。
なんとなく複雑な工学的設計と理解不能な数学的難解さの上に構築されているように感じるプロジェクトたち。実は、それらを構成する核心アイデアが私たちの想像よりはるかにシンプルだと知れば、ほとんどの方が驚くはずです。
今日ご紹介するLangchainは、LLMの複雑な連鎖作用を制御しやすく構成・モジュール化し、言語モデルを原動力として動作するアプリケーションを簡単に作れるよう支援するフレームワークです。
Langchainとは?
Langchainは2022年10月に登場したオープンソースフレームワークです。2023年6月現在、GitHubで4万4千近いスターを獲得し、自体コミュニティの爆発的成長とともにフレームワーク上にエコシステムを構成中の巨大プロジェクトです。
Langchainの核心概念は、名前からもわかるようにLLMプロンプトの実行と外部ソースの実行(電卓、Google検索、Slackメッセージ送信、ソースコード実行など)を紐づけて連鎖(Chaining)することです。
入力と出力を持つ媒体を数珠つなぎにして1つのフロー(Flow)にし、そうしたフローをさらにモジュール化・連鎖させて1つのアプリケーションにする——そう考えると理解しやすいでしょう。これはLangchain設計のためのGUIプロジェクトであるLangflowの画面を見ればより直感的に理解できます。
![[出典:Langflow]](https://zqcfqfqgiyckyhazcfrk.supabase.co/storage/v1/object/public/blog-images/webflow/6316f3f75130788ec2d762dd/65942ba2890a8b0c8d799b02_648845871d4f69cdb0ebe3db_OMC3ATW-nMrwn7joRxGwjFVpZtMvR8cKUwry9VSIPq4Mfb9QnS69ykpfr7vOmSm1P89qu8nDFVc19Yb1o9OSlj4bJu2o2FQno0m45foi1vhnOB1vncnxeLZ8-Nh5DWTHg7PyfQRC2AiMM4BT9dw70zA.gif)
Langchainには事前設定されたモジュール(Module)があり、ユーザーはこのモジュールを適切に組み合わせてさまざまな構成要素(Component)に紐づけた後、各構成要素間のパイプラインを設定できます。
モジュールには多様な種類があり、フレームワークエコシステムの発展に伴い継続的に増加しています。複数のモジュールが集まって1つの構成要素を成し、さらに集まった構成要素がチェーンとなり、まるでレゴブロックを積み上げるように1つの完成されたアプリケーションをビルドする役割を果たします。
Langchainには多くの種類のモジュールがありますが、その中でも最も核心的な役割を果たすモジュールは以下の通りです。
- LLMs
- Prompt Templates
- Agents
- Tools
この記事では各モジュールの役割を説明し、簡単な例を挙げてモジュールがどう動作するかを見ていきます。注意すべき点は、Langchainのモジュールはすべてが同じレベルで動作するわけではなく(あるモジュールは他の種類のモジュールを必須構成要素として要求することもあります)、同じモジュールでも例の性格によって異なる用途で使われることがあるということです。人に例えると、同じ職務でも会社によってR&Rが異なる場合を考えればわかりやすいでしょう。
💡言及したモジュール以外にも、Memory、Vectorstore、Retriever、Text Splitter、Document Loaderなど多様なドメインのモジュールがあります。また、Agent Executor、Toolkitなど、より詳細または抽象的なモジュールをサポートしており継続的に追加されていますので、より高度なフレームワーク利用を希望される方は一度ご覧になることをお勧めします。
では各モジュールについて見ていきましょう。この記事の例では、Langchainの公式JavaScriptポーティング版であるlangchain-jsを使用します。
1. LLMs(Large Language Models)
LLMモジュールはLangchainのエンジンです。各異なる言語モデルまたは言語モデル提供サービスが持つAPIをLangchainの他の多様なモジュールで使用できるよう正規化したインターフェースとして提供する役割を担います。
モジュールに接続するLLMはどんな種類のモデルでも構いません。OpenAIのGPT-4、GPT-3.5やDavinci、Adaモデルでもいいですし、Hugging FaceのInference APIを通じてホストしているモデルでも、ローカルで実行中のLLaMAベースモデルでも構いません。コミュニティが急速に発展するにつれ、ほとんどのエンドポイントをサポートしており、必要であればインターフェースを直接構成することもそれほど難しくありません。
また、1つのアプリケーションで必ず1つのLLMを使う必要もありません。アプリケーションを作る際、軽い推論能力を要求する簡単なプロンプトの実行にはAdaモデルを、より深化した推論能力を要する作業には自己ホスティング中のVicunaモデルを、複雑な推論と行動計画立案を要求するAgentモジュールにはGPT-4モデルを使う——といった形でアプリケーションをうまく構成すれば、不要なリソース浪費を減らしコストを削減できます。
LLMモジュールインスタンスの生成は非常に簡単です。以下はGPT-4モデルを使用したLLMモジュールインスタンス生成の例です。
import { ChatOpenAI } from 'langchain/chat_models';
export const gpt4Model = new ChatOpenAI({
temperature: 0.6,
modelName: 'gpt-4',
verbose: true,
streaming: true,
});
このように生成したLLMインスタンスは他のモジュールを生成する際に渡され、プロンプトの実行を行う役割を担うことになります。
2. Prompt Templates
Prompt Templateは事前設定されたPromptに指定された変数を簡単に挿入できるよう構成したテンプレートモジュールです。
たとえば、以下のようなシンプルな言語検出プロンプトを設計して使用するとしましょう。
Detect the language of text in <input></input>. Your answer must be in english. don't use language code, return full name of the language in english.
if the language seems like one of programming language (like code blocks) return "english".
if detected language is ambigous, return most popular one.
if you can't detect language, return "english".
use the following format for your answer:
Detected: <detected language>
Example:
<input>
안녕하세요.
</input>
Detected: korean
User Input:
<input>
この文章の言語を検出しようとしています。
</input>
上記プロンプトを再活用し、[この文章の言語を検出しようとしています。]の部分に動的な入力値を渡してテンプレートにしたい場合、次のようなコードを書けます。
import { BaseOutputParser } from 'langchain/schema';
import { PromptTemplate } from 'langchain';
const LANG_DETECTION = `Detect the language of text in <input></input>. Your answer must be in english. don't use language code, return full name of the language in english.
if the language seems like one of programming language (like code blocks) return "english".
if detected language is ambigous, return most popular one.
if you can't detect language, return "english".
use the following format for your answer:
Detected: <detected language>
Example:
<input>
안녕하세요.
</input>
Detected: korean
User Input:
<input>
{input}
</input>`
class LanguageDetectionParser extends BaseOutputParser {
parse = async (output: string): Promise<string> => {
const result = output.match(/Detected: (.*)/);
if (result) {
return result[1];
}
return 'english';
};
getFormatInstructions(): string {
return `Your response should be in following format.
Detected: <detected language>`;
}
}
export const languageDetectionPrompt = new PromptTemplate({
template: LANG_DETECTION,
inputVariables: ['input'],
outputParser: new LanguageDetectionParser(),
});
LANG_DETECTION文字列は、作成されたプロンプトのうち置換したい部分を**{変数名}で置き換えたものです。文字そのものとして中括弧を使う場合は{{}}**の形で中括弧を2つ使う必要があります。
LanguageDetectionParserは、プロンプトの実行結果をパースして必要な結果物だけを抽出するOutputParserです。例ではDetected:の後に続く文字列だけをパースするようにしています。もしLLMモデルが該当プロンプトの実行結果値としてDetected: koreanという応答を送った場合、パースされたOutputはkoreanになります。
languageDetectionPromptは完成されたPromptTemplateです。上記の構成要素とともにinputVariablesとしてテンプレートに使用された変数名であるinputを渡しています。
このように宣言したPrompt Templateは、LLMChainモジュールを通じてLLMに接続されて直接使用されたり、Agentに渡されてAgentによって実行されたり、プロンプト入力を要求するさまざまなモジュールで入力として使用されます。
3. Agents
AgentはLangchainで最も核心的な役割を果たすモジュールであるだけでなく、最も複雑で精巧な思考作業の実行を担当するモジュールです。
Agentの概念は、いくつかの核心的な生成AI論文にそのルーツを置いており、新しいアプローチ方法が提示されるたびにそれに合わせて継続的に改善されています。Agentの概念をより完璧に理解するためのいくつかの重要な参考文献を挙げると以下の通りです。
- Chain-of-Thought Prompting Elicits Reasoning in Large Language Models
- Toolformer: Language Models Can Teach Themselves to Use Tools
- ReAct: Synergizing Reasoning and Acting in Language Models
上記の資料をすべて読む必要はありませんが、読めばAgentの概念とその動作原理を把握するのに大いに役立ちます。
簡単に説明すると、Agentは
-
自分に与えられたTaskを遂行するため、与えられたツール(Tools)と現在の状況を元に思考(Thought)して必要な次の行動を設計します。(Action planning/Reasoning)
-
設計が終わったら現在必要なActionを適切なInputとともに実行します。
-
Actionの実行が終わったらその実行結果を分析(Observation)し、分析した結果と現在まで実行したActionの結果を元に1〜3の作業を繰り返します。(Chain of thought)
-
結果を分析してTaskが完了したまたは完了可能であれば作業実行を完了し終了します。
このようにAgentは主体的に与えられた資源を活用して何らかの作業を実行する役割を果たします。
Agentは担う役割が複雑な分、その実装も画一的ではありません。同じLangchainを使っても、Agentを構成する方法はさまざまあり、使用する用途によって必要なモジュールの構成やプロンプトの形態が完全にバラバラになります。したがって、この記事ではLangchain公式ドキュメントで案内しているAgentの基本例を見て、使用された各要素について説明します。
💡同じ役割を果たすAgentでも、Agent Executorの実装によってその実行様相まで変わることがあります。上で説明した段階は最も基本的なAction Agentについての説明であり、現在段階のActionだけを推論するのではなく全体実行の段階を最初からすべて構想して段階別に実行するPlan-and-Execute Agent等のAgentも存在します。
import { initializeAgentExecutorWithOptions } from "langchain/agents";
import { OpenAI } from "langchain/llms/openai";
import { SerpAPI } from "langchain/tools";
import { Calculator } from "langchain/tools/calculator";
const model = new OpenAI({ temperature: 0 });
const tools = [
new SerpAPI(process.env.SERPAPI_API_KEY, {
location: "Austin,Texas,United States",
hl: "en",
gl: "us",
}),
new Calculator(),
];
const executor = await initializeAgentExecutorWithOptions(tools, model, {
agentType: "zero-shot-react-description",
verbose: true,
});
const input = `Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?`;
const result = await executor.call({ input });
LangchainJsの公式ドキュメントから参照したMRKL(Modular Reasoning, Knowledge and Language;ミラクルと読みます)Agentの例です。
上で話したのとは違い、例のコードが非常にシンプルなのですが、これはzero-shot-react-descriptionとして定義されたagentTypeが一種の事前設定プリセットとしてAgent構成に必要な多くの要素を含んでいるためです。
I need to find out who Olivia Wilde's boyfriend is and then calculate his age raised to the 0.23 power.
Action: Search
Action Input: "Olivia Wilde boyfriend"
Observation: Olivia Wilde started dating Harry Styles after ending her years-long engagement to Jason Sudeikis — see their relationship timeline.
Thought: I need to find out Harry Styles' age.
Action: Search
Action Input: "Harry Styles age"
Observation: 29 years
Thought: I need to calculate 29 raised to the 0.23 power.
Action: Calculator
Action Input: 29^0.23
Observation: Answer: 2.169459462491557
Thought: I now know the final answer.
Final Answer: Harry Styles, Olivia Wilde's boyfriend, is 29 years old and his age raised to the 0.23 power is 2.169459462491557.
該当コードを実行してみると、Agentがユーザーの最初の質問[Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?]を受けて
-
SerpAPIを使ってGoogleでOlivia Wildeのボーイフレンドが誰か検索
-
上でHarry StylesがOlivia Wildeのボーイフレンドであることを突き止めたので、彼の年齢を検索
-
彼の年齢が29歳であることがわかったので、Calculator Toolを使って29の0.23乗を計算
-
計算結果が出たので、ユーザーの質問に最終回答を生成
上記の過程を自ら判断して順次実行することを観察できます。
💡上記の過程がどのように動作するのか気になるなら、LLMモデルのIn/OutputをモニタリングできるようカスタムCallBackManagerを提供して分析してみてください。また公式ドキュメントのCustom Agentsパートを読むことで、コードレベルでの動作過程を分析できます。Langchainフレームワークは提供する機能に比べて文書化(Documentation)された機能の範囲がかなり限定的です。コードリポジトリを直接参考にしながら使えば、より多様な高度機能をより簡単に使用できます。
Auto GPTを使用された方なら、こうした動作過程がかなり馴染み深いはずです。Auto GPTも、よくオーケストレーションされたAgentのチェーン上に使用可能な多様なToolを提供する形で設計された巨大なAgentなので、Auto GPTがどう動作するのか気になっていた方なら、その疑問が一部解消されたと思います。
4. Tools
ToolはAgentが各Actionを実行する際に使用できるよう抽象化された一種の関数です。
Agentは自分が使用できるToolのリストを以下の値とともにプロンプトの一部として受け取ります。
- Toolの名前
- 該当ToolのDescription(どんな役割を果たすToolか、Inputはどう渡すべきか等)
interface Tool {
call(arg: string): Promise<string>;
name: string;
description: string;
}
Langchainに定義されたToolのInterfaceは上記の通りです。シンプルですが柔軟で、文字列で結果値を返しさえすればcall内部でどんな動作を実行しても構いません。
さまざまな異なるモジュールで構成された複雑なAgentさえも、他のAgentにToolの形態で提供できます。このシンプルだが強力なInterfaceがLangchainの活用可能性を無限に引き上げる要素の1つです。
シンプルなLLMChainを使用した以下の例は、Calculatorに入力された数式値を渡す前に、Calculatorが理解できる形態で数式値が入力されることを保証するため入力値を精製するよう設計されたCalculatorツールです。
import { LLMChain, PromptTemplate } from 'langchain';
import { BaseOutputParser } from 'langchain/schema';
import { Calculator, Tool } from 'langchain/tools';
const CALC_EQUATION_GENERATE = `
Generate a calculation equation from contents of <input> block. generated equation must be valid numeric calculation equation.
You need to convert words to numbers, if feasible.
Do not try to calculate the equation, just generate it.
If the input cannot be converted to a valid equation, return <EOF/>.
If the input is already a valid equation, return itself.
Provide your response in <output> block.
Example
\`\`\`
<input>
(1 billion plus 1 million) * 12
</input>
<output>
(1000000000 + 1000000) * 12
</output>
\`\`\`
Now, Here's your input.
<input>
{input}
</input>
`;
class CalcEquationParser extends BaseOutputParser {
parse = async (output: string): Promise<string> => {
try {
if (/^<EOF\/>$/.test(output)) {
return '';
}
return /<output>([\s\S]*)<\/output>/.exec(output)[1].trim();
} catch (err) {
return '';
}
};
getFormatInstructions(): string {
return `Provide your response in <output> block.`;
}
}
const calcEquationPrompt = new PromptTemplate({
template: CALC_EQUATION_GENERATE,
inputVariables: ['input'],
outputParser: new CalcEquationParser(),
});
const generateCalcEquationChain = new LLMChain({
llm: gpt3Model,
prompt: calcEquationPrompt,
outputKey: 'data',
});
class GenericCalculator extends Tool {
name = 'calculator';
async _call(input: string) {
const { data: generatedEquation } = await generateCalcEquationChain.call({
input,
});
if (!generatedEquation) {
return `Invalid equation. Please provide a valid equation. Try not to use word for numbers. For example, use 2 instead of two.`;
}
return new Calculator().call(generatedEquation);
}
description =
'Useful for getting the result of a math expression. The input to this tool should be a valid mathmatical expression that could be executed by a simple calculator.';
}
上記の例には、Agentを除いたこの記事で扱ったすべてのモジュールとLLMChainというチェーンモジュールを一緒に使いました。例を見てLLMChainがどう動作するか、PromptTemplateとLLMモデルがどう連携されて実行されるかを一度見てみてください。
この記事ではLangchain及びLangchainの核心的ないくつかのモジュールとともに、該当フレームワークを使って簡単にLLMの応用プログラムを開発する基礎的なアイデアをご紹介しました。
Langchainは今もとても速いスピードで開発されているプロジェクトです。記事で扱えなかった応用例やより多様なモジュールが存在し、その中にはVectorStoreを応用した長期記憶実装など非常に興味深い使用例も数多く存在します。生成AIを応用してアプリケーションを開発しようとするなら、ぜひ一度参考にしてみてください。ありがとうございました。