⛓️

TypeScript 版 LangChain で自前の情報を元に対話できるようにする

2023/03/25に公開

基本こちらのものを TS で書き直した感じです。
https://zenn.dev/tatsui/articles/langchain-chatbot

▼ langchain.js のドキュメント
https://hwchase17.github.io/langchainjs/docs/overview/

▼ サンプルコードがあるレポジトリ
https://github.com/kazuyaseki/langchain-js-tutorial

Vectorstore の準備

まず事前準備として Vectorstore なる AI のためのデータベースを用意します。
いくつかオプションがあるのですが、私は違いがよく分かっていないのでこちらの pinecone というものを使ってみました。
https://www.pinecone.io/

正直私は具体的に Vector とはなんなのか分かっておらず雰囲気で使っています。pinecone が解説記事を書いているのでこちらを読んでみると良いかもしれません。
https://www.pinecone.io/learn/vector-database/

こちらに SignUp した後、Indexes を選択し、 Create Your First Index! をクリックします。

Index Name に適当な名前をつけ、Dimensions には 1536 と入力します。なんか1536次元のベクトルらしいです。すごそう。

これで準備は完了です。

コードを書く

パッケージインストール

必須なのはこの辺りです。

npm install @pinecone-database/pinecone langchain openai axios dotenv

ドキュメントを Vectorstore に保存する

まず事前に読み込みたいドキュメントを用意しておきます。
例えば React のドキュメントサイトを読み込みたかったらこんな感じで用意しておきます。

mkdir docs
cd docs
git clone https://github.com/reactjs/react.dev.git

ちなみに手元にデータがなくても、LangChain では Web からスクレイピングして持ってくるツールもあります。
https://hwchase17.github.io/langchainjs/docs/modules/document_loaders/web_loaders/web_pages

というか今これを書いていて気づきましたが GitHub の Loader もありました。ただとりあえず今回は既に書いてしまったコードで進めます。
https://hwchase17.github.io/langchainjs/docs/modules/document_loaders/web_loaders/github

また、env ファイルを用意しておきます。
Pinecone の key などはダッシュボードの API Keys から取ってください。PINECONE_INDEX は先ほど index に付けた名前です。

OPENAI_API_KEY=
PINECONE_API_KEY=
PINECONE_ENVIRONMENT=
PINECONE_INDEX=experiment

次に先ほど用意したドキュメントを読み込んで Vectorstore にアップロードするスクリプトです。

import {
  DirectoryLoader,
  JSONLoader,
  JSONLinesLoader,
  TextLoader,
  CSVLoader,
} from 'langchain/document_loaders';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { OpenAIEmbeddings } from 'langchain/embeddings';
import { PineconeClient } from '@pinecone-database/pinecone';
import { PineconeStore } from 'langchain/vectorstores';

export const run = async () => {
  // 読み込みたいドキュメントのディレクトリを指定し、拡張子毎に Loader を指定
  const loader = new DirectoryLoader('./docs/react.dev', {
    '.json': (path) => new JSONLoader(path, '/texts'),
    '.jsonl': (path) => new JSONLinesLoader(path, '/html'),
    '.css': (path) => new TextLoader(path),
    '.txt': (path) => new TextLoader(path),
    '.tsx': (path) => new TextLoader(path),
    '.ts': (path) => new TextLoader(path),
    '.md': (path) => new TextLoader(path),
    '.csv': (path) => new CSVLoader(path, 'text'),
  });
  const docs = await loader.load();

  const textSplitter = new RecursiveCharacterTextSplitter({
    chunkSize: 800,
    chunkOverlap: 200,
  });

  const documents = await textSplitter.splitDocuments(docs);

  const client = new PineconeClient();
  await client.init({
    apiKey: process.env.PINECONE_API_KEY as string,
    environment: process.env.PINECONE_ENVIRONMENT as string,
  });

  const pineconeIndex = client.Index(process.env.PINECONE_INDEX as string);

  await PineconeStore.fromDocuments(documents, new OpenAIEmbeddings(), {
    pineconeIndex,
  });
};

run();

これで Vectorstore の準備ができました。

import { OpenAI } from 'langchain/llms';
import {
  DirectoryLoader,
  JSONLoader,
  JSONLinesLoader,
  TextLoader,
  CSVLoader,
} from 'langchain/document_loaders';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { OpenAIEmbeddings } from 'langchain/embeddings';
import { PineconeClient } from '@pinecone-database/pinecone';
import { PineconeStore } from 'langchain/vectorstores';
import { VectorDBQAChain } from 'langchain/chains';

export const run = async () => {
  const client = new PineconeClient();
  await client.init({
    apiKey: process.env.PINECONE_API_KEY as string,
    environment: process.env.PINECONE_ENVIRONMENT as string,
  });

  const pineconeIndex = client.Index(process.env.PINECONE_INDEX as string);

  const vectorStore = await PineconeStore.fromExistingIndex(
    new OpenAIEmbeddings(),
    { pineconeIndex }
  );

  const model = new OpenAI();
  const chain = VectorDBQAChain.fromLLM(model, vectorStore, {
    k: 1,
    returnSourceDocuments: true,
  });
  const response = await chain.call({
    query: 'show example cases of you should not use useEffect',
  });
  console.log(response);
};

run();

これでできましたやったね!!

また、モデルを変えたい場合は loadQAChain なるものを使って指定すれば良いそうです。
参考: https://github.com/mayooear/gpt4-pdf-chatbot-langchain/blob/main/utils/makechain.ts

export const makeChain = (
  vectorstore: PineconeStore,
  onTokenStream?: (token: string) => void,
) => {
  const questionGenerator = new LLMChain({
    llm: new OpenAIChat({ temperature: 0 }),
    prompt: CONDENSE_PROMPT,
  });
  const docChain = loadQAChain(
    new OpenAIChat({
      temperature: 0,
      modelName: 'gpt-4', //change this to older versions (e.g. gpt-3.5-turbo) if you don't have access to gpt-4
      streaming: Boolean(onTokenStream),
      callbackManager: onTokenStream
        ? CallbackManager.fromHandlers({
            async handleLLMNewToken(token) {
              onTokenStream(token);
              console.log(token);
            },
          })
        : undefined,
    }),
    { prompt: QA_PROMPT },
  );

  return new ChatVectorDBQAChain({
    vectorstore,
    combineDocumentsChain: docChain,
    questionGeneratorChain: questionGenerator,
    returnSourceDocuments: true,
    k: 2, //number of source documents to return
  });
};

Discussion