👻

LangChain でチャットボットを構築(Node.js)

2024/05/24に公開

はじめに

この記事では、LangChain で Chatbot を作成します。具体的にはこの記事に記載されている例を実装します。

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

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

https://github.com/hayato94087/langchain-chatbot-application

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

LangChain とは

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

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

コンセプト

はじめに重要となるコンセプトについて記載します。

  • Chat Model
    • LLM はステートを持ちません。つまり、過去の会話に基づいて会話できませんが、過去の会話履歴を LLM のモデルに入力することで、あたかもステートがあるように会話を続けることができます(参考)。
  • Prompt Templates
    • プロンプトのテンプレートをの活用によりプロンプトを組みたセルプロセスを簡素化できます(参考)。
  • Chat History
    • 過去の会話を LLM に渡すことで、会話を続けることができます。が、過去の会話を全て渡すとデータ量が増え、LLM の入力制限の上限を超えてしまいます。LangChain では、会話履歴を管理する機能が提供されています。この機能を利用することで、過去の会話をデータベースに保存したり、必要な会話のみを LLM に渡せたりできます(参考)。

作業プロジェクトの準備

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

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

package.json を作成

package.json を作成します。

$ mkdir -p langchain-chatbot-application
$ cd langchain-chatbot-application
$ pnpm init

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

package.json
{
  "name": "langchain-chatbot-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 "環境変数を設定"

言語モデルを利用

コードの作成

言語モデルを利用します。demo01.ts を作成します。

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

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

// result
const result = await model.invoke([new HumanMessage({content: "こんにちは!私の名前は太郎といいます!"})]);
console.log(result);

ローカルで実行します。

$ pnpm vite-node demo01.ts

AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: 'はじめまして、太郎さん!どうぞよろしくお願いします。ご用件はありますか?',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'はじめまして、太郎さん!どうぞよろしくお願いします。ご用件はありますか?',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 34, promptTokens: 22, totalTokens: 56 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}

コードの解説

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

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

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

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

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

// messages
const result = await model.invoke([new HumanMessage({content: "こんにちは!私の名前は太郎といいます!"})]);

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

const result = await model.invoke([new HumanMessage({content: "こんにちは!私の名前は太郎といいます!"})]);

コミットします。

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

状態について

モデル自体は状態を持ちません。過去の会話履歴を渡さないと、モデルは新しい会話を開始します。

コードの作成

demo02.ts を作成します。

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

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

// result
const result = await model.invoke([new HumanMessage({content: "こんにちは!私の名前は太郎といいます!"})]);
console.log(result);

const result2 = await model.invoke([new HumanMessage({content: "私の名前を覚えていますか?"})]);
console.log(result2);

ローカルで実行します。

$ pnpm vite-node demo02.ts

AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: 'こんにちは太郎さん!どうも初めまして。何か質問やお話しがあれば何でも聞いてくださいね。',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'こんにちは太郎さん!どうも初めまして。何か質問やお話しがあれば何でも聞いてくださいね。',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 38, promptTokens: 22, totalTokens: 60 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: 'はい、あなたの名前はわかりません。お名前を教えていただけますか?',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'はい、あなたの名前はわかりません。お名前を教えていただけますか?',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 31, promptTokens: 19, totalTokens: 50 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}

コードの解説

まず LLM に自分が太郎であることを伝えています。LLM は太郎であることを認識しています。

// result
const result = await model.invoke([new HumanMessage("こんにちは!私の名前は太郎といいます!")]);
console.log(result);
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: 'こんにちは太郎さん!どうも初めまして。何か質問やお話しがあれば何でも聞いてくださいね。',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'こんにちは太郎さん!どうも初めまして。何か質問やお話しがあれば何でも聞いてくださいね。',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 38, promptTokens: 22, totalTokens: 60 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}

次に、LLM に自分の名前を聞くと LLM は覚えていません。

// result
const result2 = await model.invoke([new HumanMessage("私の名前を覚えていますか?")]);
console.log(result2);
AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: 'はい、あなたの名前はわかりません。お名前を教えていただけますか?',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'はい、あなたの名前はわかりません。お名前を教えていただけますか?',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 31, promptTokens: 19, totalTokens: 50 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}

コミットします。

$ git add .
$ git commit -m "状態について"

過去の会話を渡す

過去の会話に基づいて、モデルが会話を続けることができるようにするには、過去の会話を渡す必要があります。

コードの作成

demo03.ts を作成します。

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

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

// messages
const messages = [
  new HumanMessage({content: "こんにちは!私の名前は太郎といいます!"}),
  new AIMessage({content: "こんにちは太郎さん!元気ですか?何かお手伝いできることはありますか?"}),
  new HumanMessage({content: "私の名前を覚えていますか?"}),
]

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

ローカルで実行します。

$ pnpm vite-node demo03.ts

AIMessage {
  lc_serializable: true,
  lc_kwargs: {
    content: 'はい、もちろん覚えていますよ!太郎さんですね。何か質問やお話があれば遠慮なくおっしゃってくださいね。',
    tool_calls: [],
    invalid_tool_calls: [],
    additional_kwargs: { function_call: undefined, tool_calls: undefined },
    response_metadata: {}
  },
  lc_namespace: [ 'langchain_core', 'messages' ],
  content: 'はい、もちろん覚えていますよ!太郎さんですね。何か質問やお話があれば遠慮なくおっしゃってくださいね。',
  name: undefined,
  additional_kwargs: { function_call: undefined, tool_calls: undefined },
  response_metadata: {
    tokenUsage: { completionTokens: 52, promptTokens: 71, totalTokens: 123 },
    finish_reason: 'stop'
  },
  tool_calls: [],
  invalid_tool_calls: []
}

コードの解説

過去の会話をこのように渡すことで、モデルは過去の会話に基づいて会話を続けることができます。

// messages
const messages = [
  new HumanMessage({content: "こんにちは!私の名前は太郎といいます!"}),
  new AIMessage({content: "こんにちは太郎さん!元気ですか?何かお手伝いできることはありますか?"}),
  new HumanMessage({content: "私の名前を覚えていますか?"}),
]

const result = await model.invoke(messages);

コミットします。

$ git add .
$ git commit -m "過去の会話を渡す"

会話履歴

いちいち過去の会話を渡すのは面倒です。そこで、MessageHistory を利用します。MessageHistory を利用することでモデルへの入出力が管理されます。必要に応じてデータベースへ保存するステップも追加できます。

コードの作成

demo04.ts を作成します。

$ touch demo04.ts
demo04.ts
import { ChatOpenAI } from "@langchain/openai";
import { BaseChatMessageHistory, BaseListChatMessageHistory, InMemoryChatMessageHistory } from "@langchain/core/chat_history";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import 'dotenv/config'

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

// message histories
const messageHistories: Record<string, InMemoryChatMessageHistory> = {};

// prompt
const prompt = ChatPromptTemplate.fromMessages([
  [
    "system",
    `あなたは私が送ったメッセージをすべて覚えている親切なアシスタントです。`,
  ],
  ["placeholder", "{chat_history}"],
  // equivalent to the following code:
  // new MessagesPlaceholder("chat_history"),
  ["human", "{input}"],
]);

// chain
const chain = prompt.pipe(model);

const withMessageHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory: async (sessionId) => {
    if (messageHistories[sessionId] === undefined) {
      messageHistories[sessionId] = new InMemoryChatMessageHistory();
    }
    return messageHistories[sessionId] as BaseChatMessageHistory | BaseListChatMessageHistory;
  },
  inputMessagesKey: "input",
  historyMessagesKey: "chat_history",
});

const configSession1 = {
  configurable: {
    sessionId: "abc2",
  },
};

const firstResponseOnSession1 = await withMessageHistory.invoke(
  {
    input: "こんにちは!私の名前は太郎といいます!",
  },
  configSession1
);

console.log(`session1`, firstResponseOnSession1.content)

const secondResponseOnSession1 = await withMessageHistory.invoke(
  {
    input: "私の名前を覚えていますか?",
  },
  configSession1
);

console.log(`session1`, secondResponseOnSession1.content)

const configSession2 = {
  configurable: {
    sessionId: "abc3",
  },
};

const firstResponseOnSession2 = await withMessageHistory.invoke(
  {
    input: "私の名前を覚えていますか?",
  },
  configSession2
);

console.log(`session2`, firstResponseOnSession2.content)

const thirdResponseOnSession1 = await withMessageHistory.invoke(
  {
    input: "私の名前を覚えていますか?",
  },
  configSession1
);

console.log(`session1`, thirdResponseOnSession1.content)

ローカルで実行します。

$ pnpm vite-node demo04.ts

session1 こんにちは太郎さん!どのようにお手伝いしましょうか?
session1 はい、もちろん太郎さんですね!
session2 はい、もちろん覚えています。あなたの名前は「ユーザー」ですね。
session1 はい、もちろん太郎さんですね!

コードの解説

データベースに保持もできますが、今回はメモリ上に保持します。

import { BaseChatMessageHistory, BaseListChatMessageHistory, InMemoryChatMessageHistory } from "@langchain/core/chat_history";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";

placeholder{chat_history} に過去の会話が格納され、{input} にユーザーの入力が格納されます。

// prompt
const prompt = ChatPromptTemplate.fromMessages([
  [
    "system",
    `あなたは私が送ったメッセージをすべて覚えている親切なアシスタントです。`,
  ],
  ["placeholder", "{chat_history}"],
  // equivalent to the following code:
  // new MessagesPlaceholder("chat_history"),
  ["human", "{input}"],
]);

ポイントは getMessageHistory で受け取っているセッション ID をもとに会話履歴が管理されます。

// chain
const chain = prompt.pipe(model);

const withMessageHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory: async (sessionId) => {
    if (messageHistories[sessionId] === undefined) {
      messageHistories[sessionId] = new InMemoryChatMessageHistory();
    }
    return messageHistories[sessionId] as BaseChatMessageHistory | BaseListChatMessageHistory;
  },
  inputMessagesKey: "input",
  historyMessagesKey: "chat_history",
});

会話を始める場合は、セッション ID を作成します。セッション ID を指定して会話を続けることができます。

const configSession1 = {
  configurable: {
    sessionId: "abc2",
  },
};

const firstResponseOnSession1 = await withMessageHistory.invoke(
  {
    input: "こんにちは!私の名前は太郎といいます!",
  },
  configSession1
);

console.log(`session1`, firstResponseOnSession1.content)

const secondResponseOnSession1 = await withMessageHistory.invoke(
  {
    input: "私の名前を覚えていますか?",
  },
  configSession1
);

console.log(`session1`, secondResponseOnSession1.content)
session1 こんにちは太郎さん!どのようにお手伝いしましょうか?
session1 はい、もちろん太郎さんですね!

セッション ID が異なる場合は、別の会話となります。

const configSession2 = {
  configurable: {
    sessionId: "abc3",
  },
};

const firstResponseOnSession2 = await withMessageHistory.invoke(
  {
    input: "私の名前を覚えていますか?",
  },
  configSession2
);

console.log(`session2`, firstResponseOnSession2.content)
session2 はい、もちろん覚えています。あなたの名前は「ユーザー」ですね。

改めて過去のセッション ID を指定することで、会話が続けられます。

const thirdResponseOnSession1 = await withMessageHistory.invoke(
  {
    input: "私の名前を覚えていますか?",
  },
  configSession1
);

console.log(`session1`, thirdResponseOnSession1.content)
session1 はい、もちろん

コミットします。

$ git add .
$ git commit -m "Message History"

今はメモリー上で保持していますが、データベースに保持することでいつでも会話を再開できます。問題としては会話が長くなると LLM に送るデータが増えることです。

会話履歴の管理(その1)

全ての会話履歴をモデルに送るのではなく、必要に応じて会話履歴の量を絞ることもできます。

コードの作成

demo05.ts を作成します。

$ touch demo05.ts
demo05.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 response = await chain.invoke({
  chat_history: messages,
  input: "私の名前は何ですか?"
} as { chat_history: BaseMessage[]; input: string });

console.log(response.content);

const response2 = await chain.invoke({
  chat_history: messages,
  input: "私の好きなアイスクリームは?"
} as { chat_history: BaseMessage[]; input: string });

console.log(response2.content);
$ pnpm vite-node demo05.ts

申し訳ありません、私はユーザーの個人情報を記憶することはできません。
バニラアイスクリームですね。

コードの解説

chat_history.slice(-10) で過去の会話を直近 10 件に絞ります。

// chain
const chain = RunnableSequence.from([
  RunnablePassthrough.assign({
    chat_history: ({ chat_history }: { chat_history: BaseMessage[] }) => {
      return chat_history.slice(-10);
    },
  }),
  prompt,
  model,
]);

直近の会話を配列として保存しています。

// 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: "はい!" }),
];

ユーザーが自身の名前が何か聞きます。しかし、直近 10 件にはユーザーの名前は含まれないため、モデルはユーザーの名前を答えられません。

const response = await chain.invoke({
  chat_history: messages,
  input: "私の名前は何ですか?"
} as { chat_history: BaseMessage[]; input: string });

console.log(response.content);
申し訳ありません、私はユーザーの個人情報を記憶することはできません。

続いて、アイスクリームについて聞きます。直近 10 件にアイスクリームについての会話が含まれているため、モデルは答えられます。

const response2 = await chain.invoke({
  chat_history: messages,
  input: "私の好きなアイスクリームは?"
} as { chat_history: BaseMessage[]; input: string });

console.log(response2.content);
バニラアイスクリームですね。

コミットします。

$ git add .
$ git commit -m "会話履歴の管理(その1)"

会話履歴の管理(その2)

コードの作成

demo06.ts を作成します。

$ touch demo06.ts
demo05.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 demo06.ts

バニラアイスクリームですね。
すみません、前回の会話を覚えていませんでした。お好きなアイスクリームを教えていただけますか?

コミットします。

$ git add .
$ git commit -m "会話履歴の管理(その2)"

コードの説明

このようにすることで、過去の会話を 10 件のみメモリ上で保持できます。

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 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);
すみません、前回の会話を覚えていませんでした。お好きなアイスクリームを教えていただけますか?

ストリーミング

ストリーミングを利用することで、モデルがリアルタイムで応答を返すことができます。これにより、よりリアルタイムに返信ができるチャットボットを作ることができます。

コードの作成

demo07.ts を作成します。

$ touch demo07.ts
demo07.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 stream = await withMessageHistory.stream(
  {
    input: "私の好きなアイスクリームは?",
  } as { chat_history: BaseMessage[]; input: string },
  config
) 

for await (const chunk of stream) {
  console.log("|", chunk.content);
}

ローカルで実行します。

$ pnpm vite-node demo07.ts

| 
||||||||| ーム
| です
||| 

コードの説明

stream メソッドを利用することで、リアルタイムで応答を受け取ることができます。

const stream = await withMessageHistory.stream(
  {
    input: "私の好きなアイスクリームは?",
  } as { chat_history: BaseMessage[]; input: string },
  config
) 

for await (const chunk of stream) {
  console.log("|", chunk.content);
}

コミットします。

$ git add .
$ git commit -m "ストリーミング"

さいごに

LangChain を使って、簡単にチャットボットを作成できました。

所感としてですが、公式サイトの掲載されている事例はそのまま動かくことができませんでした。また GitHub のリポジトリへのリンクも見つけることができなかったため、動かすのに少し時間がかかってしまいました。

作業リポジトリ

https://github.com/hayato94087/langchain-chatbot-application

Discussion