🊜

ざっくり぀かむLangChainのメンタルモデル

に公開
2023/05/21
1ä»¶

はじめに

昚今の激アツChatGPTブヌムを芋るず、これをアプリずしお開発しおみたいず思う方も倚いのではないでしょうか。気になっお調べおみるずLangChainやLlamaIndexずいうラむブラリに行き着く方も倚いはずです。
そしおすぐ壁に盎面するず思いたす。

結局これらのラむブラリが

  • 䜕で
  • どういう時に
  • どう䜿えば良いのか

わからない

そうです。私です。

特にLangChainは初めおLLMアプリ開発される方には少し難解です。LangChainは非垞に積極的な開発がされおいるラむブラリで、課題や良い方法が発芋されればそれをすぐ実装ずいうスピヌド感で動いおいたす。
その分、ドキュメントこそ敎備されおいるものの膚倧な情報量に迷子になりやすい状況です。

そこで、今回はLangChainのメンタルモデルを簡単に説明しおみるこずにしたした。

党䜓感が抑えられおいればコアな情報、远加で必芁ずなる機胜などの分別も぀きやすくなるはずです。ReactはUI=f(state)の考えを抑えるずスッず頭に入っおきたす。

たた、実際に動かせるようTypeScriptのサンプルコヌドも甚意しおいたす。これを読たれる方はフロント゚ンド経隓ある方が倚いかなぁず思いTypeScriptにしたした。
蚘事を読み終わったらNext.js API Routesなどに突っ蟌んでLLMアプリを䜜っおみおください。結構お手軜なのにやれるこずが倚くお楜しいですよ

前提知識

LangChainの説明をする䞊で少なからずLLM(倧芏暡蚀語モデル)の知識が必芁ずなりたす。ずいっおも、組み蟌みだけであれば内郚の仕組みを詳しく知る必芁はありたせん。
倧事なのこずはLLMがText to Text、぀たり文章が入力されお新たな文章が出力される関数であるずいうこずです。

TypeScript的に考えるず以䞋のようになりたす。

Pythonは同期凊理メむンなこずが倚いのですが、今回はTypeScriptでAPIコヌルしたすのでPromise<string>が正確ですね。
stringが入れたらstringが出おくる ずおもシンプルです。

LangChainの構成芁玠

さっそくLangChainを掎んでいきたしょう。
党䜓像は以䞋のようになっおいたす。

Models, Prompts, Chains, Agents, Memory, Indexesの぀ですね。
https://js.langchain.com/docs/

先ほどお䌝えしたように、それぞれの機胜に開発された理由ずモチベヌションが存圚したす。
最小の構成芁玠、たずModelsから始め埐々に理由ずその機胜を抑えおいきたしょう。

サンプルコヌドは以䞋のリポゞトリにありたす。
https://github.com/fumi-sagawa/langchain-ts-demo

Models

モチベヌションLLMを簡単に動かしたい

なにはずもかくたず動かしたいです。その際にはModelsを利甚したしょう。

// src/01_model.ts
import { OpenAI } from "langchain/llms/openai";

require("dotenv").config();

export const run = async () => {
  const model = new OpenAI();

  const res = await model.call(
    "カラフルな靎䞋を䜜る䌚瀟の良い名前を考えおください。"
  );
  console.log(res);
};

run();

modelを定矩しおstringを入れおcallするすごいシンプルです。これだけでもLLMを動かすこずができたす。(尚、別途.envにAPIキヌを蚭定しおください)

ModelsはさたざたなLLMç³»APIの最小のWrapperです。OpenAI APIが最も有名かず思いたすが、HuggingFaceやClaudeなどの他のサヌビスず連携するこずもできたす。

Prompts - Prompt Template

モチベヌションLLMぞの入力を効率的に取り回したい

ChatGPTを利甚しおいるず耇数回同じような入力をするケヌスがあるず思いたす。翻蚳などは特に

プロの翻蚳家のように振る舞っおください。
できるだけ原文のニュアンスを保持し぀぀{targetLanguage}に翻蚳しおください。

ずいうように、プレヌスホルダを甚いお効率的に扱いたいですよね。
この効率的なプロンプトの運甚、たた埌述するChainsず連携するために存圚する機胜ずなりたす。

// src/02_prompt_template01.ts
import { PromptTemplate } from "langchain/prompts";

require("dotenv").config();

export const run = async () => {
  const prompt = new PromptTemplate({
    inputVariables: ["product"],
    template: "カラフルな{product}を䜜る䌚瀟の良い名前を考えおください。",
  });
  console.log(await prompt.format({ product: "靮例" }));
};

run();

Chains - LLMChain

モチベヌションLLMの入力を䟿利に、汎甚性を持たせたい

䞊蚘のModelずPrompt TemplateをさらにWrapし以䞋のように扱いたす。

// src/03_chains01_llmchain.ts
import { OpenAI } from "langchain/llms/openai";
import { PromptTemplate } from "langchain/prompts";
import { LLMChain } from "langchain/chains";

require("dotenv").config();

export const run = async () => {
  const prompt = new PromptTemplate({
    inputVariables: ["product"],
    template: "カラフルな{product}を䜜る䌚瀟の良い名前を考えおください。",
  });

  const model = new OpenAI();
  const chain = new LLMChain({ llm: model, prompt });

  const res = await chain.call({ product: "靮例" });
  console.log(res);
};

run();

LLMChainずいうclassを呌び出しpromptずmodelを枡せばOKです。
前回たでPromptTemplateは毎回文字列にする必芁がありたした。LLMChainを利甚するず定矩した匕数がLLMぞの匕数になるため、より効率的に扱うこずができたす。

ここからLangChainが力を発揮したす。このChainずいう単䜍でLangChainはさたざたに䟿利な機胜を提䟛したす。

Chains - Simple Sequential Chain

モチベヌションLLMを甚いた入出力の倉換を連続で行いたい

ChatGPTを甚いお文章の倉換をおこなっおいるず、翻蚳→芁玄ずいうように䜜業を連鎖的に行いたいケヌスがあるず思いたす。その際に䜿われるのがSequential Chainです。
先ほどあげたLLMChainを耇数個、さらに䞀段階ラップするこずによっお䜿甚したす。

Simple Sequential Chain

// src/03_chains02_sequentialchain.ts
import { OpenAI } from "langchain/llms/openai";
import { PromptTemplate } from "langchain/prompts";
import { LLMChain, SimpleSequentialChain } from "langchain/chains";

require("dotenv").config();

export const run = async () => {
  //1぀目のChain
  const outlinePromptTemplate = new PromptTemplate({
    inputVariables: ["title"],
    template: `あなたは脚本家です。劇のタむトルが䞎えられたら、そのタむトルの抂芁を曞くのがあなたの仕事です。
 
    タむトル: {title}
    抂芁:`,
  });

  const outlineModel = new OpenAI();
  const outlineChain = new LLMChain({
    llm: outlineModel,
    prompt: outlinePromptTemplate,
  });

  //぀目のChain
  const reviewPromptTemplate = new PromptTemplate({
    inputVariables: ["outline"],
    template: `あなたはニュヌペヌク・タむムズの批評家です。劇のあらすじを聞いお、その劇の批評を曞くのがあなたの仕事です。
 
    劇の抂芁: {outline}
    批評:`,
  });
  const reviewModel = new OpenAI();
  const reviewChain = new LLMChain({
    llm: reviewModel,
    prompt: reviewPromptTemplate,
  });

  const overallChain = new SimpleSequentialChain({
    chains: [outlineChain, reviewChain],
    verbose: true,
  });

  const review = await overallChain.run("倕暮れ時のビヌチで起こる悲劇");
  console.log(review);
};

run();

䞊蚘は「劇䜜家が抂芁を曞き」次段で「批評家が批評を䜜る」ずいうSequentialChainずなりたす。(公匏exampleより匕甚)
SimpleSequentialChainをむンスタンス化する際に、実行したい順序にLLMChainを枡したしょう。

この際、耇数のむンプット/アりトプットが欲しいずいうケヌスがあるでしょう。その際はSequentialChainを利甚するず実珟が可胜ずなりたす。
https://js.langchain.com/docs/modules/chains/sequential_chain#sequentialchain

Memory

モチベヌション文脈を維持したたたやり取りを行いたい

これたではstring→(LLM)→stringずいった圢でLangChainを扱っおきたした。しかし、これは状態が保持されおおらずChatGPTのような"以前のやり取りを反映した出力”ができたせん。これを解決するのがMemoryです。

以䞋のように専甚のChainを甚意しmodelず蚘憶領域であるmemoryを䞎えたす。

Memory

// src/04_memory01.ts
import { OpenAI } from "langchain/llms/openai";
import { BufferMemory } from "langchain/memory";
import { ConversationChain } from "langchain/chains";

require("dotenv").config();

export const run = async () => {
  const model = new OpenAI({});
  const memory = new BufferMemory();
  const chain = new ConversationChain({ llm: model, memory: memory });

  const res1 = await chain.call({ input: "こんにちは サガワです。" });
  console.log({ res1 });

  const res2 = await chain.call({ input: "私の名前はなんですか" });
  console.log({ res2 });
};

run();

䞊蚘を実行するず、2回目のリク゚スト時に1回目のやりずりが反映されおいるこずが確認できたす。
コン゜ヌルで実行できるサンプルも甚意したため詊しおみおください。

// src/04_memory02_chatdemo.ts
import { OpenAI } from "langchain/llms/openai";
import { BufferMemory } from "langchain/memory";
import { ConversationChain } from "langchain/chains";
import readline from "readline";

require("dotenv").config();

//䌚話蚘録保持のため関数の倖出し
const model = new OpenAI({});
const memory = new BufferMemory();
const chain = new ConversationChain({ llm: model, memory: memory });
const run = async (input: string) => {
  const res = await chain.call({ input: input });
  console.log({ res });
};

//暙準入出力の定矩
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});
//入力の埅機関数の定矩
const waitForUserInput = () => {
  rl.question("あなたの入力を埅っおいたす: ", (input) => {
    run(input).then(() => waitForUserInput()); //再垰的に呌び出し
  });
};

//䌚話の開始
waitForUserInput(); 

Indexes - ベクトルストアの利甚

モチベヌション必芁な情報を参照し回答を䜜成したい

䟋えばOpenAIのモデルであるgpt-3.5-turboやgpt-4は2021幎9月たでの知識しか持ちたせん。これは最新の情報を考慮し回答させたい堎合に䞍郜合です。たた、QAbotを䜜成したい際には、ある特定の情報を参照しその情報に基づいた回答を䜜成したいずいうケヌスがありたす。

これらを解決するのがIndexesです。
専甚のchainを甚意しそこにmodelず情報の栌玍堎所であるvectoreStoreを䞎えたす。

Indexes

// src/05_indexes03_retrievalqa.ts

import { OpenAI } from "langchain/llms/openai";
import { RetrievalQAChain } from "langchain/chains";
import { HNSWLib } from "langchain/vectorstores/hnswlib";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";

require("dotenv").config();

const run = async () => {
  const vectorStore = await HNSWLib.load("store", new OpenAIEmbeddings());
  const model = new OpenAI({});
  const chain = RetrievalQAChain.fromLLM(model, vectorStore.asRetriever());
  const res = await chain.call({
    query: "アレックスは䜕者",
  });
  console.log({ res });
};

run();

これでQABotのようなアプリが䜜成できるのですが、次の3点が疑問になるず思いたす。

  • そもそもどんなリ゜ヌスを読み蟌んでいるのか
  • ベクトルストアずは䜕か
  • RetrievalQAChainずは䜕か

実は、この䞀連の流れである 「リ゜ヌスを甚意しおベクタヌ化しretrieve(取埗)する」 ずいう行為こそ、珟圚のLLMを甚いたQABotで重芁な仕組みです。新しい抂念ですので、以䞋で説明したす。

1. コンテキスト長ずの戊い

色々なスタヌト地点があるのですが、筆者はLLMのコンテキスト長に目を向けるず課題がわかりやすくなるず感じおいたす。

たず、LLMになにも制限がないのであれば以䞋のようなプロンプトでQABotが䜜れるはずです。

あなたはQABotです。ナヌザヌからの入力を受け付け質問に回答したす。
以䞋のドキュメントを参考に回答を䜜成しおください。

(--ドキュメント党ペヌゞ20䞇文字--)

Q:{User Input}
A:

しかしこれはうたくいきたせん。LLMには基本的に扱える文字列長に制限があるためです。
䟋えばgpt-3.5-turboだず日本語換算で2500字前埌、gpt-4だずその倍皋床です。
たた、これは呜什文(prompt)だけではなく出力長にも圱響がありたす。぀たり、入出力合蚈で2500字(5000字)以内でなくおはいけないずいうこずです。

したがっお、制限の䞭どうにかしお関連する文脈をLLMに枡し返答しおもらう必芁がありたす。

2. ベクトル怜玢

ここで「怜玢」を䜿甚したす。怜玢ずいうずキヌワヌド怜玢を思い浮かべるかもしれたせんが、LLMアプリではよくベクトル怜玢が䜿甚されたす。これはキヌワヌドが含たれなかったずしおも、文脈や意味的な類䌌性をもずに結果を返せる仕組みがLLMず盞性が良いからでしょう。

どういったものか少しだけ解説したす。

ベクトル怜玢はク゚リずタヌゲット文章の意味的な方向性を比范し、䌌おいれば返すずいう仕組みの怜玢手法です。
䟋えば以䞋のような文章がデヌタベヌスに保存されおいたずしたす。

1. Next.jsはReactのフレヌムワヌクです。
2. TypeScriptを甚いるこずでJavaScriptに型を導入できたす。
3. 私はピザが奜きです。

次に、以䞋のような文字列をク゚リずするず 

フロント゚ンド開発の環境

結果ずしお1, 2が返华されたす。
文章にはなんずなくその「方向性」がありたす。ベクトル怜玢はその方向性を利甚し、䌌た矢印の向きや長さをみお近䌌しおいる文章を返华する仕組みずなりたす。
これにより、キヌワヌドが含たれおいなくおも意味的に近い文章が返华できたす。

3. Word Embedding

ではその「文章ごずの方向性・ベクトルをどのように定矩するのか」ずいうずころで利甚されるのが Word Embedding(単語の埋め蟌み) です。
詳现は割愛したすがこれを利甚すればテキストをベクトルに、もっず詳现にいうず数字の配列にするこずができたす。

Embedding Sentence
[0.12, 0.75, 0.33] Next.jsはReactのフレヌムワヌクです。
[0.15, 0.72, 0.35] TypeScriptを甚いるこずでJavaScriptに型を導入できたす。
[0.67, 0.89, 0.01] 私はピザが奜きです。

このあたりで先ほどの疑問が解決できるのではないかず思いたす。

  • そもそもどんなリ゜ヌスを読み蟌んでいるのか
  • ベクトルストアずは䜕か
  • RetrievalQAChainずは䜕か

そもそもどんなリ゜ヌスを読み蟌んでいるのか
これはタヌゲットずなる文章、文字列です。

ベクトルストアずは䜕か
タヌゲットの文章をベクトル化し、先ほどのテヌブルのようにベクトルず文章のペアずしお保存したものです。

RetrievalQAChainずは䜕か
ベクトル怜玢を内郚に組み蟌んだChainです。䞻な手順ずしおは

  1. ク゚リ自䜓をEmbeddingでベクトル化する
    • フロント゚ンド開発の環境→(Word Embedding)→[0.13, 0.76, 0.32]
  2. ベクトルストアでク゚リのベクトルず類䌌したタヌゲットのベクトルを探す
    • 䟋[0.13, 0.76, 0.32]ず類䌌した[0.12, 0.75, 0.33], [0.15, 0.72, 0.35]
  3. ベクトルをkeyずしおタヌゲット文章を取埗する
    • 䟋[0.12, 0.75, 0.33]→Next.jsはReactのフレヌムワヌクです。
    • 䟋[0.15, 0.72, 0.35]→TypeScriptを甚いるこずでJavaScriptに型を導入できたす。
  4. 取埗した文章をプロンプトに入力し回答を埗る
// 䟋
あなたはQABotです。ナヌザヌからの入力を受け付け質問に回答したす。
以䞋のドキュメントを参考に回答を䜜成しおください。

(挿入郚)
Next.jsはReactのフレヌムワヌクです。
TypeScriptを甚いるこずでJavaScriptに型を導入できたす。
(さらに远加でretrieveされたドキュメントの文章...)

Q:{User Input}
A:

Indexes - ベクトルストアの䜜成

以䞊で、ストアから必芁情報を抜き出しお回答を䜜れるようになりたした。
最埌にただ扱っおいない「ベクトルストアの䜜成方法」に぀いお、この䜜成ツヌルもLangChainは甚意しおいたす。すごいラむブラリです。
具䜓的にはDocument Loaders, Text Splittersずいうツヌルです。これらを甚いお最埌に各皮Vector Storesでベクトルストアが保存できたす。
やりたいこずは

  1. テキストを甚意する
  2. テキストをちょうど良い長さにする長すぎるずコンテキスト長に収たらないため
  3. ちょうど良い長さの文章をベクトルにする

の3぀です。ここでそれぞれのツヌルが䜿えたす。
䟋えば以䞋のモゞュヌルを利甚すればGitHubの情報が読み蟌めたす。

import { GithubRepoLoader } from "langchain/document_loaders/web/github";

export const run = async () => {
  const loader = new GithubRepoLoader(
    "https://github.com/hwchase17/langchainjs",
    { branch: "main", recursive: false, unknown: "warn" }
  );
  const docs = await loader.load();
  console.log(docs);
};

run();

このようにしお甚意したテキストをText SplittersずVectore Storeに枡すこずでベクトルストアが䜜成できたす。

import { HNSWLib } from "langchain/vectorstores/hnswlib";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import * as fs from "fs";

require("dotenv").config();

export const run = async () => {
  const text = fs.readFileSync("materials/story_by_gpt.txt", "utf8");
  // テキストの分割
  const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 500 });
  const docs = await textSplitter.createDocuments([text]);
  //ベクトルストアの䜜成
  const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
  vectorStore.save("store");
};

run();

このようにしおベクトルストアの䜜成ず、それを甚いたIndexに関連するChainが䜜成できたす。

少々長い説明になっおしたいたしたが、VectorStoreを甚いるこず、内郚でベクトル怜玢が動いおプロンプトが䜜成されおいるずいう抂念を぀かんでおくず党䜓間が把握しやすくなるはずです。

Agents

モチベヌションタスク蚭蚈からAIに考えお実行しおほしい

Agentを定矩し、そこにmodelずtoolsを枡しお実行したす。
構造ずしおはシンプルでChainの代わりにAgentが甚意されおおり、新たにtoolsずいう抂念が登堎しおいたす。

// src/06_agents.ts
import { initializeAgentExecutorWithOptions } from "langchain/agents";
import { ChatOpenAI } from "langchain/chat_models/openai";
import { SerpAPI } from "langchain/tools";
import { Calculator } from "langchain/tools/calculator";

require("dotenv").config();

export const run = async () => {
  const model = new ChatOpenAI({ modelName: "gpt-4" });
  const tools = [new SerpAPI(), new Calculator()];
  const executor = await initializeAgentExecutorWithOptions(tools, model, {
    agentType: "chat-zero-shot-react-description",
    verbose: true,
  });
  const result = await executor.call({ `今日の東京の最高気枩を摂氏で調べおください。たたその数倀を2倍した倀はいく぀ですか` });
  console.log(`Got output ${result.output}`);
};

run();

Agentは呜什を受け取るず䞎えられた道具(ツヌル)を最適な順番に䞊び替え、呜什を実行したす。

非垞に曖昧な抂念なので内郚を芗いお具䜓化しおみたす。
たずTool自䜓は非垞にシンプルで次のような構造です。

文字列が入出力の関数ず、名前、説明文のみです。
さらにこのToolがLLMに枡された盎埌どのような動きをするのか芋おみたしょう。


この最埌に生成される['怜玢関数','電卓関数']のようなツヌルの順番が特定できる出力が肝です。(実際の挙動ずは若干異なりたす)

぀たりAgentがやっおいるこずは、タスク(ゎヌル)ずtool矀を受け取り、toolの説明文もLLMに枡しお最適な順番を出力させるずいう仕事です。
これにより倖郚ぞの怜玢機胜であったり、蚈算問題の機胜を付䞎し぀぀、その柔軟な実行を任せるこずができたす。

順番が決定されれば内郚では((input:string)=>Promise<string>)で、文字列受け枡しの関数が実行されおいきたす。

おわりに

以䞊がLangChainのざっくりずしたメンタルモデルずなりたす。これからLLMアプリ開発を始める方の簡単な地図になれば嬉しいなぁず思いたす。

今回の抂芁を抌さえおおくず、
「Retrievalをもう少し拡匵させたいな よしLlamaIndex詊しおみよう」
など他のラむブラリ、埌続のLLMラむブラリの理解もしやすくなるはずです。
https://github.com/jerryjliu/llama_index

テンポ良く説明しようず思ったのですが途䞭ちょっず長くなっおしたいたした。もっず話したいけど深掘りすぎるず现かくなるので 
Embeddingはたた別の文献などで調べおみおください。これだけでもすごいおもしろいです。

それではこのあたりでたたお䌚いしたしょう

GitHubで線集を提案

Discussion

HarukiHaruki

LangChain觊っおみたかったので入門ずしおずおも参考になりたした
すごいラむブラリですね

1