🎃

LangChain で簡易LLMアプリを構築(Node.js)

2024/05/24に公開

はじめに

この記事では、LangChain で簡易的なアプリをここでは作成します。具体的には以下の記事を参考に記述します。

https://js.langchain.com/v0.2/docs/tutorials/llm_chain/

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

https://github.com/hayato94087/langchain-simple-llm-demo

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

LangChain とは

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

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

作業プロジェクトの準備

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

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

package.json を作成

package.json を作成します。

$ mkdir -p langchain-simple-llm-application
$ cd langchain-simple-llm-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 & ts-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 "環境変数を設定"

言語モデルを利用

言語モデルを利用しシステムと対話します。

コードの作成

コードを作成します。

$ touch demo01.ts
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
import 'dotenv/config'

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

// system
const system =  `あなたは入力された文章を英語に変換するアシスタントです`;

// messages
const messages = [
  new SystemMessage(system),
  new HumanMessage("昔々あるところにおじいさんとおばあさんがいました"),
];

// result
const modelResult = await model.invoke(messages);
console.log(modelResult);

ローカルで実行します。

$ pnpm vite-node demo01.ts

AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: 'Once upon a time, there was an old man and an old woman.',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'Once upon a time, there was an old man and an old woman.',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 15, promptTokens: 58, totalTokens: 73 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}

コミットします。

$ git add .
$ git commit -m "言語モデルを利用"

コードの解説

コードを解説します。LangChain が提供する ChatOpenAI を利用しモデルをインタンス化します。今回は gpt-3.5-turbo モデルを使用します。

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

モデルに送信するメッセージを設定します。メッセージにはいくつか種類がありますが、ここでは HumanMessageSystemMessage を利用します。端的に以下のように理解するとよいです。

  • SystemMessageはモデルがどのような振る舞いをするかを設定するメッセージです。
  • HumanMessageはユーザーからモデル(あるいはチャットボット)へのメッセージです。
  • AIMessageはモデル(あるいはチャットボット)からユーザーへのメッセージです。

SystemMessage を通してユーザーのメッセージを翻訳するという役割を設定します。

// system
const system = `あなたは入力された文章を英語に変換するアシスタントです`;

// messages
const messages = [
  new SystemMessage(system),
  new HumanMessage("昔々あるところにおじいさんとおばあさんがいました"),
];

HumanMessage にユーザーのメッセージを設定し、英語に訳したい文章を設定します。

// messages
const messages = [
  new SystemMessage(system),
  new HumanMessage("昔々あるところにおじいさんとおばあさんがいました"),
];

.invoke() で言語モデルを呼び出し、生成された結果を取得します。出力される型は AIMessage です。

const result = await model.invoke(messages);
console.log(modelResult);
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: 'Once upon a time, there was an old man and an old woman.',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'Once upon a time, there was an old man and an old woman.',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 15, promptTokens: 61, totalTokens: 76 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}

出力結果をパース

LLM のレスポンスは AIMessage 型です。StringOutputParser を利用することで AIMessage をパースし、content を取得し、出力結果として返します。

AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: 'Once upon a time, there was an old man and an old woman.',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'Once upon a time, there was an old man and an old woman.',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 15, promptTokens: 61, totalTokens: 76 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}

コードの作成

$ touch demo02.ts
demo02.ts
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
import { StringOutputParser } from "@langchain/core/output_parsers";
import 'dotenv/config'

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

// system
const system =  `あなたは入力された文章を英語に変換するアシスタントです`;

// messages
const messages = [
  new SystemMessage(system),
  new HumanMessage("昔々あるところにおじいさんとおばあさんがいました"),
];

// result
const modelResult = await model.invoke(messages);

// parser
const parser = new StringOutputParser();

// parsed result
const parsedResult = await parser.invoke(modelResult);
console.log(parsedResult);

// chain
const chain = model.pipe(parser);
const pipedResult = await chain.invoke(messages);
console.log(pipedResult);

ローカルで実行します。

$ pnpm vite-node demo02.ts

Once upon a time, there lived an old man and an old woman.
Once upon a time, there was an old man and an old woman living somewhere.

コミットします。

$ git add .
$ git commit -m "StringOutputParserで出力結果をパース"

コードの解説

StringOutputParser をインポートします。

import { StringOutputParser } from "@langchain/core/output_parsers";

const parser = new StringOutputParser();

Chat Model から取得した取得した結果(modelResult)をパーサーに渡しパースします。

const modelResult = await model.invoke(messages);
const parsedResult = await parser.invoke(modelResult);
console.log(parsedResult);
Once upon a time, there lived an old man and an old woman.

別の方法として .pipe() を利用してパイプラインを構築します。modelparser をパイプラインに結合し、invoke() でメッセージを渡します。

const chain = model.pipe(parser);
const pipedResult = await chain.invoke(messages);
console.log(pipedResult);
Once upon a time, there was an old man and an old woman living somewhere.

Prompt Templates

先程の例では、システムメッセージとユーザーメッセージを直接指定しました。しかし、Prompt Templates を利用することで、メッセージを柔軟にカスタマイズできます。Prompt Template は、メッセージを生成するためのテンプレートです。

String PromptTemplates で指定した文字列を変数に代入できます。実際のコードで説明すると、{language} には翻訳後の言語を、{text} には翻訳したい文章を指定します。

// String PromptTemplates
const systemTemplate = `あなたは入力された文章を{language}に変換するアシスタントです`; 
// Prompt Templates
const promptTemplate = ChatPromptTemplate.fromMessages([
  ["system", systemTemplate],
  ["user", "{text}"],
]);

コードの作成

コードを作成します。テンプレートで柔軟に変換したい言語や文章を指定できるようにします。

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

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

// system
const systemTemplate = `あなたは入力された文章を{language}に変換するアシスタントです`; // String PromptTemplates

// Prompt Templates
const promptTemplate = ChatPromptTemplate.fromMessages([
  ["system", systemTemplate],
  ["user", "{text}"],
]);

// 動作確認
const promptTemplateResult = await promptTemplate.invoke({ language: "イタリア語", text:  "昔々あるところにおじいさんとおばあさんがいました" });
console.log(promptTemplateResult); // rowデータを表示
console.log(promptTemplateResult.toChatMessages()); // 変換後のモデルへのメッセージを確認

// parser
const parser = new StringOutputParser();

// chain
const chain = promptTemplate.pipe(model).pipe(parser);

// 英語に翻訳
const chainResult01 = await chain.invoke({ language: "英語", text: "ありがとう" });
console.log(chainResult01);

// イタリア語に翻訳
const chainResult02 = await chain.invoke({ language: "イタリア語", text: "ありがとう" });
console.log(chainResult02);

// ドイツ語に翻訳
const chainResult03 = await chain.invoke({ language: "ドイツ語", text: "ありがとう" });
console.log(chainResult03);

// 中国語に翻訳
const chainResult04 = await chain.invoke({ language: "中国語", text: "ありがとう" });
console.log(chainResult04);

// 関西弁に翻訳
const chainResult05 = await chain.invoke({ language: "関西弁", text: "ありがとう" });
console.log(chainResult05);

// 宇宙語に翻訳
const chainResult06 = await chain.invoke({ language: "宇宙語", text: "ありがとう" });
console.log(chainResult06);

ローカルで実行します。

$ pnpm vite-node demo03.ts

ChatPromptValue {
  lc_serializable: true,
  lc_kwargs: { messages: [ [SystemMessage], [HumanMessage] ] },
  lc_namespace: [ 'langchain_core', 'prompt_values' ],
  messages: [
    SystemMessage {
      lc_serializable: true,
      lc_kwargs: [Object],
      lc_namespace: [Array],
      content: 'あなたは入力された文章をイタリア語に変換するアシスタントです',
      name: undefined,
      additional_kwargs: {},
      response_metadata: {}
    },
    HumanMessage {
      lc_serializable: true,
      lc_kwargs: [Object],
      lc_namespace: [Array],
      content: '昔々あるところにおじいさんとおばあさんがいました',
      name: undefined,
      additional_kwargs: {},
      response_metadata: {}
    }
  ]
}
[
  SystemMessage {
    lc_serializable: true,
    lc_kwargs: {
      content: 'あなたは入力された文章をイタリア語に変換するアシスタントです',
      additional_kwargs: {},
      response_metadata: {}
    },
    lc_namespace: [ 'langchain_core', 'messages' ],
    content: 'あなたは入力された文章をイタリア語に変換するアシスタントです',
    name: undefined,
    additional_kwargs: {},
    response_metadata: {}
  },
  HumanMessage {
    lc_serializable: true,
    lc_kwargs: {
      content: '昔々あるところにおじいさんとおばあさんがいました',
      additional_kwargs: {},
      response_metadata: {}
    },
    lc_namespace: [ 'langchain_core', 'messages' ],
    content: '昔々あるところにおじいさんとおばあさんがいました',
    name: undefined,
    additional_kwargs: {},
    response_metadata: {}
  }
]
You're welcome.
Prego.
Bitte schön.
谢谢 (xièxiè)
おおきに〜
対話ありがとうございます

コミットします。

$ git add .
$ git commit -m "String PromptTemplatesを利用"

コードの解説

システムメッセージに利用するテンプレートの文字列を生成します。{language} には翻訳後の言語を指定します。

const systemTemplate = "Translate the following into {language}:";

Prompt Template をインポートします。{text} には翻訳したい文章を指定します。

import { ChatPromptTemplate } from "@langchain/core/prompts";

// Prompt Templates
const promptTemplate = ChatPromptTemplate.fromMessages([
  ["system", systemTemplate],
  ["user", "{text}"],
]);

langeuage には翻訳後の言語を、text には翻訳したい文章を指定して、テンプレートを作成します。テンプレートの型は、ChatPromptValue です。

// 動作確認
const promptTemplateResult = await promptTemplate.invoke({ language: "イタリア語", text:  "昔々あるところにおじいさんとおばあさんがいました" });
console.log(promptTemplateResult); // rowデータを表示
ChatPromptValue {
  lc_serializable: true,
  lc_kwargs: { messages: [ [SystemMessage], [HumanMessage] ] },
  lc_namespace: [ 'langchain_core', 'prompt_values' ],
  messages: [
    SystemMessage {
      lc_serializable: true,
      lc_kwargs: [Object],
      lc_namespace: [Array],
      content: 'あなたは入力された文章をイタリア語に変換するアシスタントです',
      name: undefined,
      additional_kwargs: {},
      response_metadata: {}
    },
    HumanMessage {
      lc_serializable: true,
      lc_kwargs: [Object],
      lc_namespace: [Array],
      content: '昔々あるところにおじいさんとおばあさんがいました',
      name: undefined,
      additional_kwargs: {},
      response_metadata: {}
    }
  ]
}

.toChatMessages()ChatPromptValuemessages を取得できます。2 つのメッセージで構成されることがわかります。

console.log(promptTemplateResult.toChatMessages()); // 変換後のモデルへのメッセージを確認
[
  SystemMessage {
    lc_serializable: true,
    lc_kwargs: {
      content: 'あなたは入力された文章をイタリア語に変換するアシスタントです',
      additional_kwargs: {},
      response_metadata: {}
    },
    lc_namespace: [ 'langchain_core', 'messages' ],
    content: 'あなたは入力された文章をイタリア語に変換するアシスタントです',
    name: undefined,
    additional_kwargs: {},
    response_metadata: {}
  },
  HumanMessage {
    lc_serializable: true,
    lc_kwargs: {
      content: '昔々あるところにおじいさんとおばあさんがいました',
      additional_kwargs: {},
      response_metadata: {}
    },
    lc_namespace: [ 'langchain_core', 'messages' ],
    content: '昔々あるところにおじいさんとおばあさんがいました',
    name: undefined,
    additional_kwargs: {},
    response_metadata: {}
  }
]

pipe() を利用してパイプラインを構築します。「ありがとう」を英語、イタリア語、ドイツ語、中国語、関西弁、宇宙語に翻訳します。なお、宇宙語は存在しない言語ですが、LLM が頑張って応えようとしています(つまりハルシネーション)。

// chain
const chain = promptTemplate.pipe(model).pipe(parser);

// 英語に翻訳
const chainResult01 = await chain.invoke({ language: "英語", text: "ありがとう" });
console.log(chainResult01);

// イタリア語に翻訳
const chainResult02 = await chain.invoke({ language: "イタリア語", text: "ありがとう" });
console.log(chainResult02);

// ドイツ語に翻訳
const chainResult03 = await chain.invoke({ language: "ドイツ語", text: "ありがとう" });
console.log(chainResult03);

// 中国語に翻訳
const chainResult04 = await chain.invoke({ language: "中国語", text: "ありがとう" });
console.log(chainResult04);

// 関西弁に翻訳
const chainResult05 = await chain.invoke({ language: "関西弁", text: "ありがとう" });
console.log(chainResult05);

// 宇宙語に翻訳
const chainResult06 = await chain.invoke({ language: "宇宙語", text: "ありがとう" });
console.log(chainResult06);
You're welcome.
Prego.
Bitte schön.
谢谢 (xièxiè)
おおきに〜
対話ありがとうございます

さいごに

LangChain を利用し、大規模言語モデル(LLM)を活用した簡易的なアプリを構築しました。LangChain は TypeScript でも問題なく実践できます。LangChain を利用することで、簡単に言語モデルを利用したアプリを構築できます。

作業リポジトリ

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

https://github.com/hayato94087/langchain-simple-llm-demo

Discussion