LangChain で会話履歴を追加(Node.js)
はじめに
LLM に過去の会話履歴を覚えさせておくことで、記憶した情報を利用して、より自然な会話を実現できます。
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)を活用したアプリケーションの開発を支援するフレームワークです。
コンセプト
RunnableWithMessageHistory
に会話履歴を追加することで、会話履歴を利用して、会話のコンテキストを保持できます。
RunnableWithMessageHistory
は会話履歴のインプットとしてBaseMesssages
の塊を指定できます。
作業プロジェクトの準備
TypeScript の簡易プロジェクトを作成します。
長いので折りたたんでおきます。
package.json を作成
package.json
を作成します。
$ mkdir -p langchain-message_history-sample
$ cd langchain-message_history-sample
$ pnpm init
下記で package.json
を上書きします。
{
"name": "langchain-message_history-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 @langchain/community
コミットします。
$ 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 "環境変数を設定"
基礎編
まず、シンプルに OpenAI の 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
コミットします。
$ git add .
$ git commit -m "OpenAIを使ってみる"
コードの解説
OpenAI を利用します。
import { ChatOpenAI } from "@langchain/openai";
gpt-3.5-turbo
のモデルを選択します。temperature
は 0 に設定します。temperature
が低いほど、モデルの出力はより予測可能になります。
const model = new ChatOpenAI({
model: "gpt-3.5-turbo",
temperature: 0
});
.invoke()
を利用して、モデルにテキストを送信します。
const result = await model.invoke("猫についてジョークを言ってください");
console.log(result)
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: [],
usage_metadata: { input_tokens: 22, output_tokens: 52, total_tokens: 74 }
}
会話履歴を追加
LLM に会話履歴を追加します。
コードを作成
コードを作成します。
$ touch demo02.ts
import { ChatOpenAI } from "@langchain/openai";
import { BaseChatMessageHistory, BaseListChatMessageHistory, InMemoryChatMessageHistory } from "@langchain/core/chat_history";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnablePassthrough, RunnableSequence, RunnableWithMessageHistory } from "@langchain/core/runnables";
import 'dotenv/config'
import { AIMessage, HumanMessage } from "@langchain/core/messages";
import type { BaseMessage } from "@langchain/core/messages";
// model
const model = new ChatOpenAI({model: "gpt-3.5-turbo"});
// message
const messages = [
new HumanMessage({ content: "こんにちは!私の名前は太郎です" }),
new AIMessage({ content: "こんにちは!" }),
new HumanMessage({ content: "私はバニアアイスクリームが好きです" }),
new AIMessage({ content: "いいですね" }),
new HumanMessage({ content: "2 + 2 は?" }),
new AIMessage({ content: "4" }),
new HumanMessage({ content: "ありがとう" }),
new AIMessage({ content: "どういたしまして" }),
new HumanMessage({ content: "楽しんでますか?" }),
new AIMessage({ content: "もちろん!" }),
new HumanMessage({ content: "いいですね!" }),
new AIMessage({ content: "はい!" }),
];
// prompt
const prompt = ChatPromptTemplate.fromMessages([
[
"system",
`あなたは私が送ったメッセージをすべて覚えている親切なアシスタントです。`,
],
["placeholder", "{chat_history}"],
// equivalent to the following code:
// new MessagesPlaceholder("chat_history"),
["human", "{input}"],
]);
// chain
const chain = RunnableSequence.from([
RunnablePassthrough.assign({
chat_history: ({ chat_history }: { chat_history: BaseMessage[] }) => {
return chat_history.slice(-10);
},
}),
prompt,
model,
]);
const messageHistories: Record<string, InMemoryChatMessageHistory> = {};
const withMessageHistory = new RunnableWithMessageHistory({
runnable: chain,
getMessageHistory: async (sessionId): Promise<BaseListChatMessageHistory | BaseChatMessageHistory> => {
if (messageHistories[sessionId] === undefined) {
const messageHistory = new InMemoryChatMessageHistory();
await messageHistory.addMessages(messages);
messageHistories[sessionId] = messageHistory;
}
return messageHistories[sessionId]!;
},
inputMessagesKey: "input",
historyMessagesKey: "chat_history",
});
const config = {
configurable: {
sessionId: "abc4",
},
};
const response = await withMessageHistory.invoke(
{
input: "私の好きなアイスクリームは?",
} as { chat_history: BaseMessage[]; input: string },
config
)
console.log(response.content);
const response2 = await withMessageHistory.invoke(
{
input: "私の好きなアイスクリームは?",
} as { chat_history: BaseMessage[]; input: string },
config
)
console.log(response2.content);
ローカルで実行します。
$ pnpm vite-node demo02.ts
コミットします。
$ git add .
$ git commit -m "会話履歴を追加"
コードの解説
直近の会話履歴を擬似的に作成します。11 件あります。
// message
const messages = [
new HumanMessage({ content: "こんにちは!私の名前は太郎です" }),
new AIMessage({ content: "こんにちは!" }),
new HumanMessage({ content: "私はバニアアイスクリームが好きです" }),
new AIMessage({ content: "いいですね" }),
new HumanMessage({ content: "2 + 2 は?" }),
new AIMessage({ content: "4" }),
new HumanMessage({ content: "ありがとう" }),
new AIMessage({ content: "どういたしまして" }),
new HumanMessage({ content: "楽しんでますか?" }),
new AIMessage({ content: "もちろん!" }),
new HumanMessage({ content: "いいですね!" }),
new AIMessage({ content: "はい!" }),
];
Prompt Template を作成します。system
では役割を設定し、placeholder
では {chat_history}
つまり会話履歴を設定し、最後に human
ではユーザーからのインプットを指定します。
// prompt
const prompt = ChatPromptTemplate.fromMessages([
[
"system",
`あなたは私が送ったメッセージをすべて覚えている親切なアシスタントです。`,
],
["placeholder", "{chat_history}"],
// equivalent to the following code:
// new MessagesPlaceholder("chat_history"),
["human", "{input}"],
]);
このようにすることで、過去の会話を 10 件のみメモリ上で保持できます。
// chain
const chain = RunnableSequence.from([
RunnablePassthrough.assign({
chat_history: ({ chat_history }: { chat_history: BaseMessage[] }) => {
return chat_history.slice(-10);
},
}),
prompt,
model,
]);
const messageHistories: Record<string, InMemoryChatMessageHistory> = {};
getMessageHistory()
でセッション ID をキーにして、会話履歴(BaseChatMessageHistory
)を取得します。inputMessagesKey
は最後のユーザーのメッセージを指定します。historyMessagesKey
は会話履歴を指定します。
const withMessageHistory = new RunnableWithMessageHistory({
runnable: chain,
getMessageHistory: async (sessionId): Promise<BaseListChatMessageHistory | BaseChatMessageHistory> => {
if (messageHistories[sessionId] === undefined) {
const messageHistory = new InMemoryChatMessageHistory();
await messageHistory.addMessages(messages);
messageHistories[sessionId] = messageHistory;
}
return messageHistories[sessionId]!;
},
inputMessagesKey: "input",
historyMessagesKey: "chat_history",
});
会話履歴にはセッション ID が必要です。
const config = {
configurable: {
sessionId: "abc4",
},
};
「私の好きなアイスクリームは?」という質問に対して、モデルは「バニラアイスクリームですね。」と答えます。
const response = await withMessageHistory.invoke(
{
input: "私の好きなアイスクリームは?",
} as { chat_history: BaseMessage[]; input: string },
config
)
console.log(response.content);
バニラアイスクリームですね。
もう一度聞くと、ちょうどバニラアイスクリームが好きだという情報が直近 10 件の会話から漏れたため、モデルは答えられなくなりました。
const response2 = await withMessageHistory.invoke(
{
input: "私の好きなアイスクリームは?",
} as { chat_history: BaseMessage[]; input: string },
config
)
console.log(response2.content);
すみません、前回の会話を覚えていませんでした。お好きなアイスクリームを教えていただけますか?
セッションID
ここではセッション ID を変更したときの動作を確認します。会話はセッション ID に紐づきます。正しく紐づいているか確認します。
コードを作成
コードを作成します。
$ touch demo03.ts
import { ChatOpenAI } from "@langchain/openai";
import { BaseChatMessageHistory, BaseListChatMessageHistory, InMemoryChatMessageHistory } from "@langchain/core/chat_history";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnablePassthrough, RunnableSequence, RunnableWithMessageHistory } from "@langchain/core/runnables";
import 'dotenv/config'
import type { BaseMessage } from "@langchain/core/messages";
// model
const model = new ChatOpenAI({model: "gpt-3.5-turbo"});
// prompt
const prompt = ChatPromptTemplate.fromMessages([
[
"system",
`あなたは私が送ったメッセージをすべて覚えている親切なアシスタントです。`,
],
["placeholder", "{chat_history}"],
// equivalent to the following code:
// new MessagesPlaceholder("chat_history"),
["human", "{input}"],
]);
// chain
const chain = RunnableSequence.from([
RunnablePassthrough.assign({
chat_history: ({ chat_history }: { chat_history: BaseMessage[] }) => {
return chat_history.slice(-3);
},
}),
prompt,
model,
]);
const messageHistories: Record<string, InMemoryChatMessageHistory> = {};
const withMessageHistory = new RunnableWithMessageHistory({
runnable: chain,
getMessageHistory: async (sessionId): Promise<BaseListChatMessageHistory | BaseChatMessageHistory> => {
if (messageHistories[sessionId] === undefined) {
const messageHistory = new InMemoryChatMessageHistory();
messageHistories[sessionId] = messageHistory;
}
// console.log(messageHistories[sessionId]?.getMessages())
return messageHistories[sessionId]!;
},
inputMessagesKey: "input",
historyMessagesKey: "chat_history",
});
const configSessionId1 = {
configurable: {
sessionId: "abcd1",
},
};
const response1SessionId1 = await withMessageHistory.invoke(
{
input: "私の好きなアイスクリームはチョコレートですです",
} as { chat_history: BaseMessage[]; input: string },
configSessionId1
)
console.log(response1SessionId1.content);
const response2SessionId1 = await withMessageHistory.invoke(
{
input: "私の好きなアイスクリームは?",
} as { chat_history: BaseMessage[]; input: string },
configSessionId1
)
console.log(response2SessionId1.content);
const configSessionId2 = {
configurable: {
sessionId: "abcd2",
},
};
const response1SessionId2 = await withMessageHistory.invoke(
{
input: "私の好きなアイスクリームは?",
} as { chat_history: BaseMessage[]; input: string },
configSessionId2
)
console.log(response1SessionId2.content);
コミットします。
$ git add .
$ git commit -m "セッションIDを追加"
コードの解説
セッション ID を変更することで、会話履歴が変わります。
セッション IDabcd1
では、私の好きなアイスクリームはチョコレートです
というメッセージを送信します。
私の好きなアイスクリームについて聞くと、チョコレートです
と答えます。
const configSessionId1 = {
configurable: {
sessionId: "abcd1",
},
};
const response1SessionId1 = await withMessageHistory.invoke(
{
input: "私の好きなアイスクリームはチョコレートです",
} as { chat_history: BaseMessage[]; input: string },
configSessionId1
)
console.log(response1SessionId1.content);
const response2SessionId1 = await withMessageHistory.invoke(
{
input: "私の好きなアイスクリームは?",
} as { chat_history: BaseMessage[]; input: string },
configSessionId1
)
console.log(response2SessionId1.content);
わかりました。ありがとうございます!あなたの好きなアイスクリームはチョコレートですね。ありがとうございます。
あなたの好きなアイスクリームはチョコレートです。
では、別のセッション IDabcd2
で同じ質問をしてみます。すると、想定通り答えられません。
const configSessionId2 = {
configurable: {
sessionId: "abcd2",
},
};
const response1SessionId2 = await withMessageHistory.invoke(
{
input: "私の好きなアイスクリームは?",
} as { chat_history: BaseMessage[]; input: string },
configSessionId2
)
console.log(response1SessionId2.content);
申し訳ありませんが、私はユーザーがお好きなアイスクリームの情報を保持する能力を持っていません。そのような情報をお知りになりたい場合は、お尋ねいただき、適切なメモをとることができます。それ以外にも何かお手伝いできることがあればお知らせください。
さいごに
この記事では、LangChain で会話履歴を追加する方法を紹介しました。
作業リポジトリ
こちらが作業リポジトリです。
Discussion