👋

LangChain で Runnable を並列実行(Node.js)

2024/05/31に公開

はじめに

この記事では、LangChain で複数の Runnable を並列して事項する方法を紹介します。具体的には以下の記事を参考に記述します。

https://js.langchain.com/v0.2/docs/how_to/parallel/

TypeScript / JavaScript での GitHub リポジトリーを公開している実装例はすくないので記事化しました。作業リポジトリはこちらです。

https://github.com/hayato94087/langchain-runnable-parallel-sample

LangChain x TypeScript での実装例を以下の記事で紹介しています。

LangChain とは

LangChain は、大規模言語モデル(LLM)を活用したアプリケーションの開発を支援するフレームワークです。

https://js.langchain.com/v0.2/docs/introduction/

Runnableとは

Runnableとは LangChain で実装されているプロトコールです。

シクエンシャルに実行

このプロトコールに準拠しているコンポーネントは、コンポーネントを結合することで、処理をシクエンシャルに実行できます。LangChain で実装されている chat model, output prser, retriever, propt template など多くのコンポーネントが Runnable プロトコルに準拠しています。

.pipe() で Runnable のコンポーネントを結合します。.invoke() を実行し次の Runnable に準拠したコンポーネントへ結果を渡します。結果としてシクエンシャルに結合した処理を実装できます。

こちらの記事で詳細を説明しています。

https://zenn.dev/hayato94087/articles/d913b9d3e8dfe4

並列に実行

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 を上書きします。

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 を上書きします。

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
.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
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 キーの取得方法はこちらを参照してください。

https://zenn.dev/hayato94087/articles/85378e1f7bc0e5#openai-の-apiキーの取得

環境変数の設定

環境変数に OpenAI キーを追加します。<your-api-key> に自身の API キーを設定してください。

$ touch .env
.env
# OPENAI_API_KEY は OpenAI の API キーです。
OPENAI_API_KEY='<your-api-key>'

Node.js で環境変数を利用するために dotenv をインストールします。

$ pnpm i -D dotenv

コミットします。

$ touch .env.example
.env
# OPENAI_API_KEY は OpenAI の API キーです。
OPENAI_API_KEY='<your-api-key>'
$ git add .
$ git commit -m "環境変数を設定"

基礎編

まず、シンプルに LLM を使ってみます。

コードの作成

コードを作成します。

$ touch demo01.ts
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
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 へと処理を繋げます。

promotmodelStringOutputParser はいずれも Runnable です。

// chain with pipe
const chain = prompt.pipe(model).pipe(new StringOutputParser());

.invoke() を実行します。すると、jokepoem の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
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 を並列して事項する方法を紹介します。具体的には以下の記事を参考に記述します。

作業リポジトリ

こちらが作業リポジトリです。

https://github.com/hayato94087/langchain-runnable-parallel-sample

Discussion