👻

LangChain で会話履歴を追加(Node.js)

2024/06/05に公開

はじめに

LLM に過去の会話履歴を覚えさせておくことで、記憶した情報を利用して、より自然な会話を実現できます。

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

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

https://github.com/hayato94087/langchain-message_history-sample

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

LangChain とは

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

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

コンセプト

RunnableWithMessageHistoryに会話履歴を追加することで、会話履歴を利用して、会話のコンテキストを保持できます。

RunnableWithMessageHistory は会話履歴のインプットとしてBaseMesssagesの塊を指定できます。

作業プロジェクトの準備

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

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

package.json を作成

package.json を作成します。

$ mkdir -p langchain-message_history-sample
$ cd langchain-message_history-sample
$ pnpm init

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

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

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

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

基礎編

まず、シンプルに OpenAI の 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

コミットします。

$ 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
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
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 で会話履歴を追加する方法を紹介しました。

作業リポジトリ

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

https://github.com/hayato94087/langchain-message_history-sample

Discussion