LangChain の実行

## はじめに

LangChain を使用して、次の機能を実行します。
* プロンプトテンプレート
* 会話履歴の扱い
* データベース連携（RAG）
* チャットボット

cheerio を利用してウェブページを読み込み、情報源としています。
本文では、LangChain の機能に含まれる会話スレッドとエージェントは行っていません。
 

## Ubuntu に JavaScript (Node.js) をインストール

使用した環境： Windows 11、WSL2 の Ubuntu 22.04

```console
sudo apt update
sudo apt install nodejs
```
バージョンの確認（.exit を入力すると node を終了します）

```console
node
Welcome to Node.js v12.22.9.
Type ".help" for more information.
>.exit
```

 デフォルトでインストールされるバージョンが古いため、nvm を使用して Node.js をバージョンアップします。

nvm インストール（リンク先の Install & Update Script のコマンドを実行します）
https://github.com/nvm-sh/nvm#install--update-script

```console
nvm ls-remote
nvm install 20.18.1
```

バージョンの再確認
```console
node
Welcome to Node.js v20.18.1.
Type ".help" for more information.
>.exit
```
　

## LangChain のセットアップ

作業用フォルダを作成し、そのフォルダに移動します。
```console
mkdir /home/(username)/20241214_langchain
cd /home/(username)/20241214_langchain
```

Node.js にライブラリを追加
```console
npm install --save cheerio readline-sync

npm i langchain @langchain/core
npm i @langchain/community
npm i @langchain/openai
```

OpenAI API と LangChain の API キーを環境変数に登録
```console
export OPENAI_API_KEY="..."

export LANGCHAIN_TRACING_V2="true"
export LANGCHAIN_API_KEY="..."
```

以下の1番簡単なサンプルプログラムが動作することを確認します。コンソールで「node ファイル名」を入力するとプログラムが実行されます。 

```console
node 01_SimpleLLM.mjs
```

Build a Simple LLM Application with LCEL
https://js.langchain.com/docs/tutorials/llm_chain

```javascript:01_SimpleLLM.mjs
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";

const model = new ChatOpenAI({ model: "gpt-4o-mini" });

const messages = [
  new SystemMessage("Translate the following from English into Italian"),
  new HumanMessage("hi!"),
];
// SystemMessage：以下を英語からイタリア語に翻訳してください
// HumanMessage：hi!

const response = await model.invoke(messages);

console.log(response.content);
// AIMessage：Ciao!
```
　

## LangChain の実行

下記の「02_LangChain.mjs」が、LangChain を実行するプログラムです。

OpenAI API を使用します。LangChain では Google Gemini、Microsoft Azure、Amazon AWS、Anthropic Claude などに簡単に切り替えることが可能です。

まず、質問への回答元となる情報源（コンテンツ）を、ウェブページから読込みます。gpt-4o-mini は2023年10月時点までの情報しか持っていないため（カットオフ）、最新の情報や優先的に扱いたい自社の情報を追加します。

プログラムは ChatGPT で使用するようなプロンプトを作成します。LangChain を使用しているので、
* システムメッセージ
* 情報源
* 会話履歴
* ユーザー質問

が OpenAI API に送信されます。ここで、会話履歴は（内部的に）毎回 OpenAI API に送信する必要があります。これを行わないと、引き続きの対話になりません。

RAG（データベース連携）は、ユーザーの質問に対して、事前に準備されたデータベースを検索し、見つかった情報（コンテンツ、記事）をユーザーの質問に追加したうえで ChatGPT に問合せる仕組みです。
「以下の記事を使用して質問に答えてください。答えが分からなければ『分からない』と伝えてください。」＋「記事」＋「ユーザー質問」
のような形になります。

プログラムは、クローリングまたはスクレイピングを行う cheerio を使用してウェブページから情報を取得します。その後、LangChain の機能を利用して、記事を1000文字ごとに区切りつつ、各区切りで200文字を重複させて分割し、ベクトルデータベースに登録します。英語だと良く動作するのかもしれませんが、日本語では章や節ごとに区切って意味のあるデータを持ったほうが良いかもしれません。

LangChain は、OpenAI API の Embedding 機能を使用して、与えられた文章や記事を1536個の数値パラメータに変換します（1536 次元ベクトル、text-embedding-3-small モデル、埋め込み）。変換されたデータは、PCメモリ上またはどこかのベクトルデータベースに保存します。検索時には、ベクトルの類似性を計算してデータを検索します。
参考：https://cookbook.openai.com/examples/question_answering_using_embeddings

データベース検索を行う際、ユーザーの質問に「それ」や「この」などの表現が含まれていると、クエリの結果として正しい答えが返って来ない可能性があります。LangChain の機能を使用すると、自動的に ChatGPT に問合せて会話履歴の中から、「それ」や「この」を具体的な内容に置き換えて、補完された質問を作成してくれます。
「会話履歴がなくても理解できる、独立した質問を作成してください。質問に答えてはいけません。」
のような形になります。回答はデータベース検索のために使用されるものであり、最終的に ChatGPT に送信される問合せ内容は「会話履歴」＋「（元の）ユーザー質問」となります。

プログラムは while 文を使用して繰り返し処理を行い、ユーザーからの質問を受け付けて回答します。「exit」を入力すると終了します。
　

```console
node 02_LangChain.mjs
```

```javascript:02_LangChain.mjs
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { createRetrievalChain } from "langchain/chains/retrieval";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { createHistoryAwareRetriever } from "langchain/chains/history_aware_retriever";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import readlineSync from "readline-sync";


// OpenAI API を使用
const llm = new ChatOpenAI( { model: "gpt-4o-mini", temperature: 0 } ); // temperature=0 は質問が同じならばいつも同じ回答を返す


// コンテンツの読込み
const loader = new CheerioWebBaseLoader( "https://lilianweng.github.io/posts/2023-06-23-agent/" );
const docs = await loader.load();
const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1000,
  chunkOverlap: 200
});
const splits = await textSplitter.splitDocuments(docs);
const vectorstore = await MemoryVectorStore.fromDocuments( splits, new OpenAIEmbeddings() );
const retriever = vectorstore.asRetriever();


// システムテンプレート
const systemPrompt =
  "You are an assistant for question-answering tasks. " +
  "Use the following pieces of retrieved context to answer " +
  "the question. If you don't know the answer, say that you " +
  "don't know. Use three sentences maximum and keep the " +
  "answer concise." +
  "\n\n" +
  "{context}";
// あなたは質問応答タスクのためのアシスタントです。以下の提供された
// コンテンツを使用して質問に答えてください。答えが分からない場合は、
// 分からないと伝えてください。最大で3文以内に簡潔に答えてください。


// RAG で使用するために、会話履歴を利用して最新の質問を補完するためのプロンプト
const contextualizeQSystemPrompt =
  "Given a chat history and the latest user question " +
  "which might reference context in the chat history, " +
  "formulate a standalone question which can be understood " +
  "without the chat history. Do NOT answer the question, " +
  "just reformulate it if needed and otherwise return it as is.";
// チャット履歴と、チャット履歴の文脈を参照している可能性がある最新の
// ユーザーの質問をもとに、チャット履歴なしでも理解できる独立した質問
// を作成してください。質問に答えてはいけません。必要に応じて再構成し、
// それ以外の場合はそのまま返してください。


const contextualizeQPrompt = ChatPromptTemplate.fromMessages([
  ["system", contextualizeQSystemPrompt],
  new MessagesPlaceholder("chat_history"),
  ["human", "{input}"],
]);

const historyAwareRetriever = await createHistoryAwareRetriever( { llm, retriever, rephrasePrompt: contextualizeQPrompt } );

const qaPrompt = ChatPromptTemplate.fromMessages([
  ["system", systemPrompt],
  new MessagesPlaceholder("chat_history"),
  ["human", "{input}"],
]);

const questionAnswerChain = await createStuffDocumentsChain( { llm, prompt: qaPrompt } );
const ragChain = await createRetrievalChain( { retriever: historyAwareRetriever, combineDocsChain: questionAnswerChain } );

let chatHistory = [];


// メインループ
(async () => {
  console.log("Welcome to the chatbot! Type 'exit' to quit.");

  while (true) {
    const userInput = readlineSync.question("\nYour question: ");

    if (userInput.toLowerCase() === "exit") {
      console.log("Thank you for using the chatbot. Have a great day!");
      break;
    }
  
    const output = await ragChain.invoke({
      input: userInput,
      chat_history: chatHistory,
    });
    
    chatHistory = chatHistory.concat([
      new HumanMessage(userInput),
      new AIMessage(output.answer),
    ]);

    console.log(output.answer);
  }
})();

// 入力例１：What is Task Decomposition?
// 入力例２：What are common ways of doing it?

```
　

## 実行結果

![1215_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/e1341b8f-621b-3c8d-439b-cd12d0861752.png)

　
実行後、何が行われたのかを LangChain のウェブページで確認できます。

https://www.langchain.com/
Home > Tracing projects > default > Runs

![1215_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/35354ef4-9097-4509-d056-5153d38533e6.png)

　
1回目の問合せでは、コンテンツの内容を追加したシステムメッセージとユーザーの質問を使用して、OpenAI に問合せ（ChatOpenAI）を行います。コンテンツの検索（VectorStoreRetriever）では、ユーザーが入力した文章がそのまま使用されています。
　

![1215_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/defc51b0-4c08-acbb-c25f-175e4edbda34.png)

![1215_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/d9624873-5d31-46ee-4176-d9881ce54a1b.png)

![1215_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/cc30afdd-d3e4-3d47-2a11-cedbde102e2b.png)

![1215_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/b485b360-821c-ac14-9e45-d1452b69faac.png)

![1215_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/b9f93954-06ab-d681-c244-c3bed413d386.png)

　
2回目の問合せでは、実際には OpenAI への問合せ（ChatOpenAI）が2回行われています。最初に、会話履歴を利用して最新の問合せを補完します。 そして、補完された文章を使用してコンテンツを検索（VectorStoreRetriever）します。
　

補完前：
What are common ways of doing it?

補完後：
What are some common methods for breaking down complex tasks into smaller, manageable steps?
　

![1215_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/12ed9631-6188-7b3f-3b0d-fa4d57f6e860.png)


![1215_21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/b32dc88e-fca5-1d7e-6d3b-74854c55652b.png)

![1215_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/b00dc4a3-3c50-2eeb-e40c-36ce4a54b5eb.png)

　
次に、コンテンツの内容を追加したシステムメッセージ（会話履歴を含む）とユーザーの質問を使用して、OpenAI に問合せを行います。前述の補完した文章は RAG のために使用されるものであり、OpenAI への問合せには補完前の文章がそのまま使用されています。また、会話履歴も送信されています。
　

![1215_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/5817d7bb-7f67-f851-a7b9-4fb39086485e.png)

![1215_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/37c7d068-8d5f-86bd-de17-be1f4bbbc102.png)

![1215_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3956789/981a08d7-5557-7e40-eb88-37210669e688.png)

　

参考ページ：

Build a Chatbot
https://js.langchain.com/docs/tutorials/chatbot

Conversational RAG
https://js.langchain.com/docs/tutorials/qa_chat_history