VOOZH about

URL: https://zenn.dev/hayato94087/articles/3026b7ba9aa1c2

⇱ LangChain で外部からデータを参照 前編(Node.js)


JavaScript
TypeScript
OpenAI
LangChain
tech

はじめに

この記事では、公式のドキュメントを使いながら LangChain で外部から入力された情報を参照する方法を紹介します。こちらが記事です。チャットモデル以外に知識をあたえるために外部データを読み込ませて応答を拡張できます。本記事ではその方法について記述します。

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

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

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

記事が長いため、前編後編 に分けて記述しています。本記事は 前編 です。

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

LangChain とは

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

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

作業プロジェクトの準備

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

LangChain をインストール

LangChain をインストールします。

$ pnpm add langchain @langchain/core

言語モデルの選択

LangChain は、多くの異なる言語モデルをサポートしており、それらを自由に選んで使用できます。

例えば、以下のような言語モデルを選択できます。

  • OpenAI
  • Anthropic
  • FireworksAI
  • MistralAI
  • Groq
  • VertexAI

ここでは OpenAI を利用します。OpenAI を LangChain で利用するためのパッケージをインストールします。

$ pnpm add @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

基礎編

まず、シンプルに Tools を使わない方法で 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

コードの解説

OpenAI の言語モデルを利用するために ChatOpenAI をインポートします。

import { ChatOpenAI } from "@langchain/openai";

gpt-3.5-turbo のモデルを選択します。temperature は 0 に設定します。temperature が低いほど、モデルの出力はより予測可能になります。

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

invoke メソッドを実行し、LLM にリクエストを送信します。AIMessage のオブジェクトが返されます。

const result = await model.invoke("猫についてジョークを言ってください");
console.log(result)

Retriever の作成

ここではウェブからデータを取得、ベクターストアにデータをストア、ベクターとストアからデータを取得する Retriever を作成します。

インストール

cheerioをインストールします。

$ pnpm add @langchain/openai cheerio @langchain/community

コードの作成

コードを作成します。

$ touch demo02.ts
demo02.ts
import "cheerio";
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";

const loader = new CheerioWebBaseLoader(
 "https://ja.wikipedia.org/wiki/LangChain"
);
const rawDocs = await loader.load();
console.log(rawDocs);

const textSplitter = new RecursiveCharacterTextSplitter({
 chunkSize: 500,
 chunkOverlap: 0,
});
const allSplits = await textSplitter.splitDocuments(rawDocs);
console.log(allSplits);

const vectorstore = await MemoryVectorStore.fromDocuments(
 allSplits,
 new OpenAIEmbeddings()
);

const retriever = vectorstore.asRetriever(2);
const docs = await retriever.invoke("LangChainのライセンス形式は?");
console.log(docs);
$ pnpm vite-node demo02.ts

コードの解説

cheerioを利用して、指定した URL の HTML を取得します。

import "cheerio";
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";

const loader = new CheerioWebBaseLoader(
 "https://ja.wikipedia.org/wiki/LangChain"
);
const rawDocs = await loader.load();
console.log(rawDocs);

LLM のコンテキストウィンドウが処理できるより小さなチャンクに分割します。

import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";

const textSplitter = new RecursiveCharacterTextSplitter({
 chunkSize: 500,
 chunkOverlap: 0,
});
const allSplits = await textSplitter.splitDocuments(rawDocs);
console.log(allSplits);

ベクターストアを作成します。先程、特徴量化したチャンクをベクターストアに保存詩ます。ベクターストアはメモリー上に展開します。

import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";

const vectorstore = await MemoryVectorStore.fromDocuments(
 allSplits,
 new OpenAIEmbeddings()
);

ベクターストアから情報を取得する Retriever を作成します。

const retriever = vectorstore.asRetriever(2);
const docs = await retriever.invoke("LangChainのライセンス形式は?");
console.log(docs);

Document chains

理想は、先ほど作成した Retriever を利用し、LLM にユーザーから質問があった場合、ベクターストアを検索し、検索結果から回答を作成することです。

が、今回は、Retriever には固定の質問「ライセンス形式は?」にしておきベクターストアから情報を取得します。その情報をもとに、ユーザーからの質問に回答するプロンプトを作成します。LLM にはハルシネーションがあるため、そのハルシネーションが抑えられているかこの例では確かめます。

コードの作成

コードを作成します。

$ touch demo03.ts
demo03.ts
import "cheerio";
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import {
 ChatPromptTemplate,
 MessagesPlaceholder,
} from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";

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

const loader = new CheerioWebBaseLoader(
 "https://ja.wikipedia.org/wiki/LangChain"
);
const rawDocs = await loader.load();

const textSplitter = new RecursiveCharacterTextSplitter({
 chunkSize: 500,
 chunkOverlap: 0,
});
const allSplits = await textSplitter.splitDocuments(rawDocs);

const vectorstore = await MemoryVectorStore.fromDocuments(
 allSplits,
 new OpenAIEmbeddings()
);

const retriever = vectorstore.asRetriever(3);
const docs = await retriever.invoke("ライセンス形式は?");

const SYSTEM_TEMPLATE = `# 指示
以下の質問に回答してください。質問に対する情報がコンテキストによって提供されない場合、または明確な情報源が存在しない場合は、『わかりません』とだけ回答してください。推測や創作はしないでください。

質問に対する情報が見つからない場合、必ず『わかりません』と回答してください。例えば、以下の質問に対してコンテキストに情報が含まれない場合です。

質問:「少年ジャンプで掲載されていた『ナルト』について教えて

# コンテキスト
{context}

<context>
{context}
</context>
`;

const questionAnsweringPrompt = ChatPromptTemplate.fromMessages([
 ["system", SYSTEM_TEMPLATE],
 new MessagesPlaceholder("messages"),
]);

const documentChain = await createStuffDocumentsChain({
 llm,
 prompt: questionAnsweringPrompt,
});

const result = await documentChain.invoke({
 messages: [new HumanMessage("LangChainのライセンス形式は?")],
 context: docs,
});
console.log(result);

const result2 = await documentChain.invoke({
 messages: [new HumanMessage("LangChainのライセンス形式は?")],
 context: [],
});
console.log(result2);

ローカルで実行します。

$ pnpm vite-node demo03.ts

コードの解説

先ほどの説明通り、Retriever には固定の質問「ライセンス形式は?」にしておきベクターストアから情報を取得しています。

const docs = await retriever.invoke("ライセンス形式は?");

LLM に送信するプロンプトのテンプレートを作成します。ユーザーからの質問は {context} に指定した内容から回答するように指示を出します。{context} には先ほど取得したウェブから取得したドキュメントが入ります。

const SYSTEM_TEMPLATE = `# 指示
以下の質問に回答してください。質問に対する情報がコンテキストによって提供されない場合、または明確な情報源が存在しない場合は、『わかりません』とだけ回答してください。推測や創作はしないでください。

質問に対する情報が見つからない場合、必ず『わかりません』と回答してください。例えば、以下の質問に対してコンテキストに情報が含まれない場合です。

質問:「少年ジャンプで掲載されていた『ナルト』について教えて

# コンテキスト
{context}

<context>
{context}
</context>
`;

Prompt Template の system にはプロンプトのテンプレートが入り、messages にはユーザーからの質問が入ります。messages は配列になっていますが、要素は 1 つだけです。

createStuffDocumentsChain ヘルパー関数を使用して、すべての入力ドキュメントをプロンプトに詰め込みます。

import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import {
 ChatPromptTemplate,
 MessagesPlaceholder,
} from "@langchain/core/prompts";

const questionAnsweringPrompt = ChatPromptTemplate.fromMessages([
 ["system", SYSTEM_TEMPLATE],
 new MessagesPlaceholder("messages"),
]);

const documentChain = await createStuffDocumentsChain({
 llm,
 prompt: questionAnsweringPrompt,
});

.invoke() して、ユーザーからの質問に対して回答を取得します。ここでは、先程取得したドキュメントを context として渡します。すると先程取得したドキュメントに基づいて回答が返ってきます。

const result = await documentChain.invoke({
 messages: [new HumanMessage("LangChainのライセンス形式は?")],
 context: docs,
});
console.log(result);
LangChainのライセンス形式はMITライセンスです。

.invoke() して、ユーザーからの質問に対して回答を取得します。ここでは、context には空の情報を渡します。プロンプトで指示した通り、「わかりません」と返してきます。

const result2 = await documentChain.invoke({
 messages: [new HumanMessage("LangChainのライセンス形式は?")],
 context: [],
});
console.log(result2);
わかりません。

Retrieval chains

Retriever を利用し、LLM にユーザーから質問があった場合、柔軟にベクターストアを検索し、検索結果に基づいた回答を返却します。

コードの作成

$ touch demo04.ts
demo04.ts
import "cheerio";
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import {
 ChatPromptTemplate,
 MessagesPlaceholder,
} from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";
import type { BaseMessage } from "@langchain/core/messages";
import {
 RunnablePassthrough,
 RunnableSequence,
} from "@langchain/core/runnables";

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

const loader = new CheerioWebBaseLoader(
 "https://ja.wikipedia.org/wiki/LangChain"
);
const rawDocs = await loader.load();

const textSplitter = new RecursiveCharacterTextSplitter({
 chunkSize: 500,
 chunkOverlap: 0,
});
const allSplits = await textSplitter.splitDocuments(rawDocs);

const vectorstore = await MemoryVectorStore.fromDocuments(
 allSplits,
 new OpenAIEmbeddings()
);

const retriever = vectorstore.asRetriever(3);

const SYSTEM_TEMPLATE = `# 指示
以下の質問に回答してください。質問に対する情報がコンテキストによって提供されない場合、または明確な情報源が存在しない場合は、『わかりません』とだけ回答してください。推測や創作はしないでください。

質問に対する情報が見つからない場合、必ず『わかりません』と回答してください。例えば、以下の質問に対してコンテキストに情報が含まれない場合です。

質問:「少年ジャンプで掲載されていた『ナルト』について教えて

# コンテキスト
{context}
`;

const questionAnsweringPrompt = ChatPromptTemplate.fromMessages([
 ["system", SYSTEM_TEMPLATE],
 new MessagesPlaceholder("messages"),
]);

const documentChain = await createStuffDocumentsChain({
 llm,
 prompt: questionAnsweringPrompt,
});

const parseRetrieverInput = (params: { messages: BaseMessage[] }) => {
 const lastMessage = params.messages[params.messages.length - 1];
 if (lastMessage) {
 return lastMessage.content;
 }
 return "";
};

const retrievalChain = RunnablePassthrough.assign({
 context: RunnableSequence.from([parseRetrieverInput, retriever]),
}).assign({
 answer: documentChain,
});

const result = await retrievalChain.invoke({
 messages: [new HumanMessage("LangChainのライセンス形式は?")],
});
console.log(result)

const result2 = await retrievalChain.invoke({
 messages: [new HumanMessage("転生したらスライムだった件の作者は誰?")],
});
console.log(result2)

ローカルで実行します。

$ pnpm vite-node demo04.ts

コードの解説

parseRetrieverInput は最後のメッセージを取得し Retrieval Chains に渡します。parseRetrieverInput はユーザーからの直近の問い合わせを取得するのに利用されます。

import type { BaseMessage } from "@langchain/core/messages";
import {
 ChatPromptTemplate,
 MessagesPlaceholder,
} from "@langchain/core/prompts";

const parseRetrieverInput = (params: { messages: BaseMessage[] }) => {
 const lastMessage = params.messages[params.messages.length - 1];
 if (lastMessage) {
 return lastMessage.content;
 }
 return "";
};

const retrievalChain = RunnablePassthrough.assign({
 context: RunnableSequence.from([parseRetrieverInput, retriever]),
}).assign({
 answer: documentChain,
});

LLM に問い合わせ回答を得ます。質問ごとにベクターストアへアクセスし、その結果をもとに回答を作成します。

const result = await retrievalChain.invoke({
 messages: [new HumanMessage("LangChainのライセンス形式は?")],
});
console.log(result)

LLM に Retriever で取得できない質問を投げると、Retriever が取得できない質問に対しては、わかりません と返します。

const result2 = await retrievalChain.invoke({
 messages: [new HumanMessage("転生したらスライムだった件の作者は誰?")],
});
console.log(result2)

さいごに

この記事では、公式のドキュメントを使いながら LangChain で外部から入力された情報を参照する方法を紹介しました。長いのでここで切って、続きは後編になります。

作業リポジトリ

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

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

Discussion

👁 Image