LangChain で簡易LLMアプリを構築(Node.js)
はじめに
この記事では、LangChain で簡易的なアプリをここでは作成します。具体的には以下の記事を参考に記述します。
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)を活用したアプリケーションの開発を支援するフレームワークです。
作業プロジェクトの準備
TypeScript の簡易プロジェクトを作成します。
長いので折りたたんでおきます。
package.json を作成
package.json
を作成します。
$ mkdir -p langchain-simple-llm-application
$ cd langchain-simple-llm-application
$ pnpm init
下記で 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
を上書きします。
{
"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 "環境変数を設定"
言語モデルを利用
言語モデルを利用しシステムと対話します。
コードの作成
コードを作成します。
$ 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"});
モデルに送信するメッセージを設定します。メッセージにはいくつか種類がありますが、ここでは HumanMessage
と SystemMessage
を利用します。端的に以下のように理解するとよいです。
-
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
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()
を利用してパイプラインを構築します。model
と parser
をパイプラインに結合し、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
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()
で ChatPromptValue
の messages
を取得できます。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 を利用することで、簡単に言語モデルを利用したアプリを構築できます。
作業リポジトリ
作業リポジトリはこちらです。
Discussion