LangChain で Runnable を並列実行(Node.js)
はじめに
この記事では、LangChain で複数の Runnable を並列して事項する方法を紹介します。具体的には以下の記事を参考に記述します。
TypeScript / JavaScript での GitHub リポジトリーを公開している実装例はすくないので記事化しました。作業リポジトリはこちらです。
LangChain x TypeScript での実装例を以下の記事で紹介しています。
- LangChain で 簡易LLMアプリを構築(Node.js)
- LangChain でチャットボットを構築(Node.js)
- LangChain で構造化データを取得(Node.js)
- LangChain で Tools 呼び出す(Node.js)
- LangChain で Runnable をシクエンシャルに結合(Node.js)
- LangChain で Runnable を並列実行(Node.js)
- LangChain で 外部からデータを参照 前編(Node.js)
- LangChain で 外部からデータを参照 後編(Node.js)
- LangChain で Fallbacks(Node.js)
LangChain とは
LangChain は、大規模言語モデル(LLM)を活用したアプリケーションの開発を支援するフレームワークです。
Runnableとは
Runnable
とは LangChain で実装されているプロトコールです。
シクエンシャルに実行
このプロトコールに準拠しているコンポーネントは、コンポーネントを結合することで、処理をシクエンシャルに実行できます。LangChain で実装されている chat model, output prser, retriever, propt template など多くのコンポーネントが Runnable
プロトコルに準拠しています。
.pipe()
で Runnable のコンポーネントを結合します。.invoke()
を実行し次の Runnable
に準拠したコンポーネントへ結果を渡します。結果としてシクエンシャルに結合した処理を実装できます。
こちらの記事で詳細を説明しています。
並列に実行
RunnableParallel
を活用することで、複数の Runnable
を並列に実行できます。並列に実行した結果は最後に結合できます。
Input
/ \
/ \
Branch1 Branch2
\ /
\ /
Combine
作業プロジェクトの準備
TypeScript の簡易プロジェクトを作成します。
長いので折りたたんでおきます。
package.json を作成
package.json
を作成します。
$ mkdir -p langchain-runnable-parallel-sample
$ cd langchain-runnable-parallel-sample
$ pnpm init
下記で package.json
を上書きします。
{
"name": "langchain-runnable-parallel-sample",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "vite-node index.ts",
"build": "tsc"
},
"keywords": [],
"author": "",
}
TypeScript & vite-node をインストール
TypeScript と vite-node をインストールします。補足としてこちらの理由のため ts-node
ではなく vite-node
を利用します。
$ pnpm install -D typescript vite-node @types/node
TypeScriptの設定ファイルを作成
tsconfig.json
を作成します。
$ npx tsc --init
tsconfig.json
を上書きします。
{
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ES2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
/* Strictness */
"strict": true,
"noUncheckedIndexedAccess": true,
"checkJs": true,
/* Bundled projects */
"noEmit": true,
"outDir": "dist",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "preserve",
"incremental": true,
"sourceMap": true,
},
"include": ["**/*.ts", "**/*.js"],
"exclude": ["node_modules", "dist"]
}
git
を初期化します。
$ git init
.gitignore
を作成します。
$ touch .gitignore
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
dist/
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
動作確認コードを作成
動作を確認するためのコードを作成します。
$ touch index.ts
console.log('Hello, World');
型チェック
型チェックします。
$ pnpm run typecheck
動作確認
動作確認を実施します。
$ pnpm run dev
Hello, World
コミットします。
$ git add .
$ git commit -m "初回コミット"
LangChain をインストール
LangChain をインストールします。
$ pnpm add langchain @langchain/core
コミットします。
$ git add .
$ git commit -m "LangChainをインストール"
言語モデルの選択
LangChain は、多くの異なる言語モデルをサポートしており、それらを自由に選んで使用できます。
例えば、以下のような言語モデルを選択できます。
- OpenAI
- Anthropic
- FireworksAI
- MistralAI
- Groq
- VertexAI
ここでは OpenAI を利用します。OpenAI を LangChain で利用するためのパッケージをインストールします。
$ pnpm add @langchain/openai
コミットします。
$ git add .
$ git commit -m "LangChainでOpenAIを利用するためのパッケージをインストール"
OpenAI API キーを取得
OpenAI API キーの取得方法はこちらを参照してください。
環境変数の設定
環境変数に OpenAI キーを追加します。<your-api-key>
に自身の API キーを設定してください。
$ touch .env
# OPENAI_API_KEY は OpenAI の API キーです。
OPENAI_API_KEY='<your-api-key>'
Node.js で環境変数を利用するために dotenv
をインストールします。
$ pnpm i -D dotenv
コミットします。
$ touch .env.example
# OPENAI_API_KEY は OpenAI の API キーです。
OPENAI_API_KEY='<your-api-key>'
$ git add .
$ git commit -m "環境変数を設定"
基礎編
まず、シンプルに LLM を使ってみます。
コードの作成
コードを作成します。
$ touch demo01.ts
import { ChatOpenAI } from "@langchain/openai";
import 'dotenv/config'
const model = new ChatOpenAI({
model: "gpt-3.5-turbo",
temperature: 0
});
const result = await model.invoke("猫についてジョークを言ってください");
console.log(result)
ローカルで実行します。
$ pnpm vite-node demo01.ts
AIMessage {
lc_serializable: true,
lc_kwargs: {
content: '猫がパソコンを使うとき、何をクリックするか知っていますか?「マウス」です!',
tool_calls: [],
invalid_tool_calls: [],
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: {}
},
lc_namespace: [ 'langchain_core', 'messages' ],
content: '猫がパソコンを使うとき、何をクリックするか知っていますか?「マウス」です!',
name: undefined,
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: {
tokenUsage: { completionTokens: 34, promptTokens: 22, totalTokens: 56 },
finish_reason: 'stop'
},
tool_calls: [],
invalid_tool_calls: []
}
コミットします。
$ git add .
$ git commit -m "簡易的にLLMに問い合わせて答えをもらう"
コードの解説
OpenAI の言語モデルを利用するために ChatOpenAI
をインポートします。
import { ChatOpenAI } from "@langchain/openai";
gpt-3.5-turbo
のモデルを選択します。temperature
は 0 に設定します。temperature
が低いほど、モデルの出力はより予測可能になります。
const model = new ChatOpenAI({
model: "gpt-3.5-turbo",
temperature: 0
});
.invoke()
を実行し、LLM にリクエストを送信すると結果として AIMessage
のオブジェクトが返されます。
const result = await model.invoke("猫についてジョークを言ってください");
AIMessage {
lc_serializable: true,
lc_kwargs: {
content: '猫がパソコンを使うとき、何をクリックするか知っていますか?「マウス」です!',
tool_calls: [],
invalid_tool_calls: [],
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: {}
},
lc_namespace: [ 'langchain_core', 'messages' ],
content: '猫がパソコンを使うとき、何をクリックするか知っていますか?「マウス」です!',
name: undefined,
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: {
tokenUsage: { completionTokens: 34, promptTokens: 22, totalTokens: 56 },
finish_reason: 'stop'
},
tool_calls: [],
invalid_tool_calls: []
}
Runnable を 並列に実行
コードの作成
コードを作成します。
$ touch demo02.ts
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
import 'dotenv/config'
import { RunnableMap } from "@langchain/core/runnables";
// model
const model = new ChatOpenAI({
model: "gpt-3.5-turbo",
temperature: 0
});
// ジョークを生成するChain
const jokeChain = PromptTemplate.fromTemplate("{topic}についてジョークを言ってください").pipe(model);
// ポエムを生成するChain
const poemChain = PromptTemplate.fromTemplate("{topic}についてポエムを言ってください").pipe(model);
// chain with pipe
const mapChain = RunnableMap.from({
joke: jokeChain,
poem: poemChain,
});
const result = await mapChain.invoke({ topic: "熊" });
console.log(result);
ローカルで実行します。
$ pnpm vite-node demo02.ts
{
joke: AIMessage {
lc_serializable: true,
lc_kwargs: {
content: '熊がパンツを履いていたら、何色だと思いますか?「くま色」です!',
tool_calls: [],
invalid_tool_calls: [],
additional_kwargs: [Object],
response_metadata: {}
},
lc_namespace: [ 'langchain_core', 'messages' ],
content: '熊がパンツを履いていたら、何色だと思いますか?「くま色」です!',
name: undefined,
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: { tokenUsage: [Object], finish_reason: 'stop' },
tool_calls: [],
invalid_tool_calls: []
},
poem: AIMessage {
lc_serializable: true,
lc_kwargs: {
content: '森の中に住む熊\n' +
'力強く立ち上がる姿\n' +
'草食の食事を楽しむ\n' +
'季節の移り変わりを感じる\n' +
'\n' +
'冬眠の季節には\n' +
'ふわふわの毛皮で包まれ\n' +
'静かに眠りにつく\n' +
'春の訪れを待ちわびる\n' +
'\n' +
'熊よ、森の守護神\n' +
'力強さと優しさを持つ\n' +
'私たちに勇気を与え\n' +
'自然と調和する姿を見せてくれる\n' +
'\n' +
'熊よ、永遠に森の一部であり\n' +
'私たちの心にも住む存在\n' +
'あなたの存在は、この世界にとって\n' +
'かけがえのない尊さを持つ',
tool_calls: [],
invalid_tool_calls: [],
additional_kwargs: [Object],
response_metadata: {}
},
lc_namespace: [ 'langchain_core', 'messages' ],
content: '森の中に住む熊\n' +
'力強く立ち上がる姿\n' +
'草食の食事を楽しむ\n' +
'季節の移り変わりを感じる\n' +
'\n' +
'冬眠の季節には\n' +
'ふわふわの毛皮で包まれ\n' +
'静かに眠りにつく\n' +
'春の訪れを待ちわびる\n' +
'\n' +
'熊よ、森の守護神\n' +
'力強さと優しさを持つ\n' +
'私たちに勇気を与え\n' +
'自然と調和する姿を見せてくれる\n' +
'\n' +
'熊よ、永遠に森の一部であり\n' +
'私たちの心にも住む存在\n' +
'あなたの存在は、この世界にとって\n' +
'かけがえのない尊さを持つ',
name: undefined,
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: { tokenUsage: [Object], finish_reason: 'stop' },
tool_calls: [],
invalid_tool_calls: []
}
}
コミットします。
$ git add .
$ git commit -m "Runnable を 並列実行"
コードの解説
2つの Chain を作成します。1つ目はジョークを生成する Chain、2つ目はポエムを生成する Chain です。
// model
const model = new ChatOpenAI({
model: "gpt-3.5-turbo",
temperature: 0
});
// ジョークを生成するChain
const jokeChain = PromptTemplate.fromTemplate("{topic}についてジョークを言ってください").pipe(model);
// ポエムを生成するChain
const poemChain = PromptTemplate.fromTemplate("{topic}についてポエムを言ってください").pipe(model);
RunnableMap
を活用し、2つの Chain を並列に実行できるように定義し、.invoke()
で並列に実行します。
// chain with pipe
const mapChain = RunnableMap.from({
joke: jokeChain,
poem: poemChain,
});
const result = await mapChain.invoke({ topic: "熊" });
console.log(result);
.pipe()
を活用し、prompt
から model
へ、そして model
から StringOutputParser
へと処理を繋げます。
promot
、model
、StringOutputParser
はいずれも Runnable
です。
// chain with pipe
const chain = prompt.pipe(model).pipe(new StringOutputParser());
.invoke()
を実行します。すると、joke
と poem
の2つの結果が返されます。
// invoke
const result = await chain.invoke({ topic: "熊" });
console.log(result)
{
joke: AIMessage {
lc_serializable: true,
lc_kwargs: {
content: '熊がパンツを履いていたら、何色だと思いますか?「くま色」です!',
tool_calls: [],
invalid_tool_calls: [],
additional_kwargs: [Object],
response_metadata: {}
},
lc_namespace: [ 'langchain_core', 'messages' ],
content: '熊がパンツを履いていたら、何色だと思いますか?「くま色」です!',
name: undefined,
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: { tokenUsage: [Object], finish_reason: 'stop' },
tool_calls: [],
invalid_tool_calls: []
},
poem: AIMessage {
lc_serializable: true,
lc_kwargs: {
content: '森の中に住む熊\n' +
'力強く立ち上がる姿\n' +
'草食の食事を楽しむ\n' +
'季節の移り変わりを感じる\n' +
'\n' +
'冬眠の季節には\n' +
'ふわふわの毛皮で包まれ\n' +
'静かに眠りにつく\n' +
'春の訪れを待ちわびる\n' +
'\n' +
'熊よ、森の守護神\n' +
'力強さと優しさを持つ\n' +
'私たちに勇気を与え\n' +
'自然と調和する姿を見せてくれる\n' +
'\n' +
'熊よ、永遠に森の一部であり\n' +
'私たちの心にも住む存在\n' +
'あなたの存在は、この世界にとって\n' +
'かけがえのない尊さを持つ',
tool_calls: [],
invalid_tool_calls: [],
additional_kwargs: [Object],
response_metadata: {}
},
lc_namespace: [ 'langchain_core', 'messages' ],
content: '森の中に住む熊\n' +
'力強く立ち上がる姿\n' +
'草食の食事を楽しむ\n' +
'季節の移り変わりを感じる\n' +
'\n' +
'冬眠の季節には\n' +
'ふわふわの毛皮で包まれ\n' +
'静かに眠りにつく\n' +
'春の訪れを待ちわびる\n' +
'\n' +
'熊よ、森の守護神\n' +
'力強さと優しさを持つ\n' +
'私たちに勇気を与え\n' +
'自然と調和する姿を見せてくれる\n' +
'\n' +
'熊よ、永遠に森の一部であり\n' +
'私たちの心にも住む存在\n' +
'あなたの存在は、この世界にとって\n' +
'かけがえのない尊さを持つ',
name: undefined,
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: { tokenUsage: [Object], finish_reason: 'stop' },
tool_calls: [],
invalid_tool_calls: []
}
}
Runnable の入出力を管理
公式のドキュメントのタイトルが「How to invoke runnables in parallel」ですが、ここで記載する内容は並列実行と関係がありません。ですが、公式ドキュメントに記載があるので紹介します。
コードの作成
コードを作成します。
$ touch demo03.ts
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import {
RunnablePassthrough,
RunnableSequence,
} from "@langchain/core/runnables";
import { Document } from "@langchain/core/documents";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import "dotenv/config";
// model
const model = new ChatOpenAI({
model: "gpt-3.5-turbo",
temperature: 0,
});
// vector store
const vectorstore = await MemoryVectorStore.fromDocuments(
[
{ pageContent: "山田バクテリアは細胞の力源です", metadata: {} },
{ pageContent: "山田バクテリアは複数のセルから構成されます", metadata: {} },
{ pageContent: "吉田バクテリアは細胞の力源です", metadata: {} },
],
new OpenAIEmbeddings()
);
const retriever = vectorstore.asRetriever();
// prompt template
const template = `文脈に基づいて質問に答えてください:
{context}
質問: {question}`;
const prompt = PromptTemplate.fromTemplate(template);
const formatDocs = (docs: Document[]) => docs.map((doc) => doc.pageContent);
// chain with pipe
const retrievalChain = RunnableSequence.from([
{ context: retriever.pipe(formatDocs), question: new RunnablePassthrough() },
prompt,
model,
new StringOutputParser(),
]);
// invoke
const result = await retrievalChain.invoke(
"山田バクテリアとは何ですか?"
);
console.log(result);
ローカルで実行します。
$ pnpm vite-node demo03.ts
山田バクテリアは細胞の力源であり、複数のセルから構成されています。
コミットします。
$ git add .
$ git commit -m "Manipulating outputs/inputs"
コードの解説
Vector Store を作成します。MemoryVectorStore
は、メモリ内にベクトルを保存するための Vector Store です。OpenAIEmbeddings
は、入力されたテキストをベクトルに変換するための Embeddings です。retriever
は、Vector Store からベクトルを取得するためのものです。
// vector store
const vectorstore = await MemoryVectorStore.fromDocuments(
[
{ pageContent: "山田バクテリアは細胞の力源です", metadata: {} },
{ pageContent: "山田バクテリアは複数のセルから構成されます", metadata: {} },
{ pageContent: "吉田バクテリアは細胞の力源です", metadata: {} },
],
new OpenAIEmbeddings()
);
const retriever = vectorstore.asRetriever();
{context}
は、retriever
から取得したベクトルをフォーマットします。{question}
は、RunnablePassthrough
でパススルーされた質問を取得します。
// prompt template
const template = `文脈に基づいて質問に答えてください:
{context}
質問: {question}`;
const prompt = PromptTemplate.fromTemplate(template);
formatDocs
は、Document
の配列を受け取り、pageContent
の配列を返します。RunnableSequence
は、Runnable
の配列を受け取り、シクエンシャルに実行します。
// chain with pipe
const formatDocs = (docs: Document[]) => docs.map((doc) => doc.pageContent);
const retrievalChain = RunnableSequence.from([
{ context: retriever.pipe(formatDocs), question: new RunnablePassthrough() },
prompt,
model,
new StringOutputParser(),
]);
.invoke()
で LLM に問い合わせます。
// invoke
const result = await retrievalChain.invoke(
"山田バクテリアとは何ですか?"
);
console.log(result);
山田バクテリアは細胞の力源であり、複数のセルから構成されています。
さいごに
この記事では、LangChain で複数の Runnable を並列して事項する方法を紹介します。具体的には以下の記事を参考に記述します。
作業リポジトリ
こちらが作業リポジトリです。
Discussion