LangChain で Runnable をシクエンシャルに実行(Node.js)

2024/05/31に公開

はじめに

この記事では、LangChain で複数の LangChain のコンポーネントを結合し、シクエンシャルに処理する方法を紹介します。具体的には以下の記事を参考に記述します。

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

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

https://github.com/hayato94087/langchain-chain-runnable-application

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 に準拠したコンポーネントへ結果を渡します。結果としてシクエンシャルに結合した処理を実装できます。

参考までに、Runnable 以外に、RunnableSequenceというものもあります。RunnableSequence じたいも Runnable プロトコルに準拠しています。

作業プロジェクトの準備

TypeScript の簡易プロジェクトを作成します。

長いので折りたたんでおきます。

package.json を作成

package.json を作成します。

$ mkdir -p langchain-chain-runnable-application
$ cd langchain-chain-runnable-application
$ pnpm init

下記で package.json を上書きします。

package.json
{
  "name": "langchain-chain-runnable-application",
  "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: 'Q: 猫がパソコンを使うときに使うプログラミング言語は何でしょうか?\nA: プロクラスミング言語!',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'Q: 猫がパソコンを使うときに使うプログラミング言語は何でしょうか?\nA: プロクラスミング言語!',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 52, promptTokens: 22, totalTokens: 74 },
    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: 'Q: 猫がパソコンを使うときに使うプログラミング言語は何でしょうか?\nA: プロクラスミング言語!',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'Q: 猫がパソコンを使うときに使うプログラミング言語は何でしょうか?\nA: プロクラスミング言語!',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 52, promptTokens: 22, totalTokens: 74 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}

Runnable を 簡易的にChain

ここでは、3つの Runnable を結合します。

  • Prompt Template
  • Chat Model
  • String Output Parser

コードの作成

コードを作成します。

$ touch demo02.ts
demo01.ts
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import 'dotenv/config'

// prompot
const prompt = ChatPromptTemplate.fromTemplate("{topic}についてジョークを言ってください");

// model
const model = new ChatOpenAI({
  model: "gpt-3.5-turbo",
  temperature: 0
});

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

// invoke
const result = await chain.invoke({ topic: "熊" });
console.log(result)

ローカルで実行します。

$ pnpm vite-node demo02.ts

熊がパンツを履いていたら、何色だと思いますか?クマモン色!

コミットします。

$ git add .
$ git commit -m "Runnable を 簡易的にChain"

コードの解説

Prompt Template を活用し、ユーザーから指定された topic についてジョークを言うように指示します。

// prompot
const prompt = ChatPromptTemplate.fromTemplate("{topic}についてジョークを言ってください");

.pipe() を活用し、prompt から model へ、そして model から StringOutputParser へと処理を繋げます。

promotmodelStringOutputParser はいずれも Runnable です。

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

.invoke() を実行します。実行するさいに topic を指定します。トピックにあった、ジョークが LLM から返されます。

// invoke
const result = await chain.invoke({ topic: "熊" });
console.log(result)
熊がパンツを履いていたら、何色だと思いますか?クマモン色!

2つの Chain の結合

ここでは、ジョークを作成する Chain と、そのジョークを評価する Chain を結合します。

コードの作成

コードを作成します。

$ touch demo03.ts
demo03.ts
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableLambda } from "@langchain/core/runnables";
import 'dotenv/config'

// prompt
// ジョークを生成
const prompt = ChatPromptTemplate.fromTemplate("{topic}についてジョークを言ってください");

// model
const model = new ChatOpenAI({
  model: "gpt-3.5-turbo",
  temperature: 0
});

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

// prompt
// ジョークを評価
const analysisPrompt = ChatPromptTemplate.fromTemplate(
  "このジョークはどう面白いのか解説してください。 {joke}"
);

// chain with pipe
const composedChain = new RunnableLambda({
  func: async (input:{topic:string}) => {
    if(!input){
      throw new Error("ジョークがありません");
    }
    const result = await chain.invoke(input);
    console.log(result)
    return { joke: result };
  },
})
  .pipe(analysisPrompt)
  .pipe(model)
  .pipe(new StringOutputParser());

const analysisResult = await composedChain.invoke({ topic: "熊" });
console.log(analysisResult);

ローカルで実行します。

$ pnpm vite-node demo03.ts

熊がパン屋さんに行ったら、何を買うでしょうか?ハニーパン!
このジョークは、熊が蜂蜜を好むことから、熊がパン屋さんに行ったら蜂蜜が入ったパンを買うだろうという予想と、"ハニーパン"という言葉遊びが組み合わさっています。熊がパン屋さんに行って普通のパンを買うのではなく、蜂蜜が入ったパンを買うという意外性と、その結びつきが面白さを生み出しています。

コミットします。

$ git add .
$ git commit -m "RunnableLambdaで複数の対話をシクエンシャルに結合"

コードの解説

こちらが1つ目のジョークを生成する Chain です。

// prompt
// ジョークを生成
const prompt = ChatPromptTemplate.fromTemplate("{topic}についてジョークを言ってください");

// model
const model = new ChatOpenAI({
  model: "gpt-3.5-turbo",
  temperature: 0
});

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

こちらが2つ目のジョークを評価する Chain です。2 つ目の Chain を RunnableLambda でラップし、invoke を実行します。1 つ目の Chain の結果は 2 つ目の Chain に渡します。

// prompt
// ジョークを評価
const analysisPrompt = ChatPromptTemplate.fromTemplate(
  "このジョークはどう面白いのか解説してください。 {joke}"
);

// chain with pipe
const composedChain = new RunnableLambda({
  func: async (input:{topic:string}) => {
    if(!input){
      throw new Error("ジョークがありません");
    }
    const result = await chain.invoke(input);
    console.log(result)
    return { joke: result };
  },
})
  .pipe(analysisPrompt)
  .pipe(model)
  .pipe(new StringOutputParser());

評価結果がどうなのかは微妙ですが、とりあえず動作確認できました。

const analysisResult = await composedChain.invoke({ topic: "熊" });
console.log(analysisResult);
熊がパン屋さんに行ったら、何を買うでしょうか?ハニーパン!
このジョークは、熊が蜂蜜を好むことから、熊がパン屋さんに行ったら蜂蜜が入ったパンを買うだろうという予想と、"ハニーパン"という言葉遊びが組み合わさっています。熊がパン屋さんに行って普通のパンを買うのではなく、蜂蜜が入ったパンを買うという意外性と、その結びつきが面白さを生み出しています。

さいごに

この記事では、LangChain で複数の LangChain のコンポーネントを結合し、シクエンシャルに処理する方法を紹介しました。

作業リポジトリ

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

https://github.com/hayato94087/langchain-chain-runnable-application

Discussion