Cloudflare Vectorize & Workers AI と LangChainでRAG APIを作ってみた

この記事は、[LangChain Advent Calendar 2023]1日目の記事です。 LangChainのJavaScript版があまり情報出ていない気がしたので、LangChain.jsを使った話をいくつか書 […]

広告ここから
広告ここまで

目次

    この記事は、[LangChain Advent Calendar 2023]1日目の記事です。

    LangChainのJavaScript版があまり情報出ていない気がしたので、LangChain.jsを使った話をいくつか書いてみようと思います。

    1本目は普通に使うならJavaScript / TypeScriptでやることになる、「Cloudflare Workers」でやる方法です。

    CloudflareでRAGを作る方法

    Cloudflareには、「コンピューティング(Workers)」「ベクトルDB(Vectorize)」そして「LLM API (Workers AI)」の3つが揃っています。そこでLangChain.jsを利用したRAG APIを、Cloudflare Workers(powered by Hono)で構築しようと思います。

    事前にCloudflare Workersを有料プランに変えないといけないことに注意

    Cloudflare Vectorizeを利用するには、Cloudflare Workersを有料プランにアップグレードする必要があるそうです。有料とはいえ月5ドルですので、ちょっとだけ試して、気が済んだらVectorize削除と一緒にダウングレードしても良さそうかなとは思います。

    キャンセル画面を読む感じ、1・2週間程度は有料機能を使い続けることができる様子です。

    HonoでCloudflare Workersアプリをセットアップする

    RAG APIを構築するため、Honoでアプリを立ち上げましょう。

    % npx create-hono
    [1/4] 🔍  Resolving packages...
    [2/4] 🚚  Fetching packages...
    [3/4] 🔗  Linking dependencies...
    warning "nx > @swc-node/[email protected]" has unmet peer dependency "typescript@>= 4.3".
    [4/4] 🔨  Building fresh packages...
    
    success Installed "[email protected]" with binaries:
          - create-hono
    
    create-hono version 0.2.6
    ✔ Target directory … hono-ai
    ? Which template do you want to use? › - Use arrow-keys. Return to submit.
        aws-lambda
        bun
        cloudflare-pages
    ❯   cloudflare-workers

    テンプレートはcloudflare-workersを利用します。もしUIも作りたい方は、cloudflare-pagesでもよいかもしれません。

    ✔ Which template do you want to use? › cloudflare-workers
    cloned honojs/starter#main to /Users/okamotohidetaka/development/examples/hono-ai
    ✔ Copied project files
    ✨  Done in 3.82s.

    セットアップ後にライブラリをインストールしておきましょう。

    % cd hono-ai
    % npm install

    Cloudflare VectorizeでベクトルDBを作成する

    続いてベクトルDBを作成してみましょう。Cloudflareの場合、WranglerのCLIコマンドからDBを作成することができます。パラメータのうち、dimensionsの数字は、Embeddingに利用するモデルによって変わることに注意しましょう。数字については、ドキュメントで確認することができます。今回設定している768はWorkers AIで利用する@cf/baai/bge-base-en-v1.5向けの設定ですが、OpenAIのada-002を利用する場合は1536を設定します。

    % npx wrangler vectorize create rag-demo --dimensions=768 --metric=cosine

    作成に成功すると、wrangler.tomlに設定する値が表示されます。

    📋 To start querying from a Worker, add the following binding configuration into
    'wrangler.toml':
    
    [[vectorize]]
    binding = "VECTORIZE_INDEX" # available within your Worker on env.VECTORIZE_INDEX
    index_name = "rag-demo"
    ✨  Done in 3.42s.

    wrangler.tomlに項目を追加しましょう。この際、Workers AIの設定も追加しますので、次のような内容になるはずです。

    name = "hono-langchain-demo"
    compatibility_date = "2023-01-01"
    
    [[vectorize]]
    binding = "VECTORIZE_INDEX"
    index_name = "rag-demo"
    
    [ai]
    binding = "AI" #available in your worker via env.AI

    Vectoriseを利用するアプリをローカル開発する場合、--remoteオプションを設定しよう

    wranglerを使ってローカル開発する場合、--remoteオプションを設定する必要があります。

    npm run dev -- --remote

    もしくはこのコマンドを利用します。

    npx wrandler dev src/index.ts --remote

    ポート番号が再起動の度に変わりますので、UI側のfetch処理などでURLをベタ書きしないようにしましょう。

    HonoのbindingsにWorkers AIとVectoriseを設定する

    DBの用意ができたので、Honoの実装に戻ります。まずはTypeScriptで利用する際のBindingsへの型定義を行います。

    import { Ai } from '@cloudflare/ai'
    import { Hono } from 'hono'
    import { CloudflareVectorizeStore } from "langchain/vectorstores/cloudflare_vectorize";
    import { CloudflareWorkersAIEmbeddings } from "langchain/embeddings/cloudflare_workersai";
    
    
    const app = new Hono<{
        Bindings: {
            AI: any
            VECTORIZE_INDEX: VectorizeIndex;
        }
    }>()

    AiバインディングはLangChainの中で使うのみなので、とりあえずanyでも良いかと思います。

    もしany型を使いたくない場合は、次のように@cloudflare/workers-typesを利用します。

    import { Hono } from "hono";
    import type { Fetcher } from "@cloudflare/workers-types";
    const app = new Hono<{
        Bindings: {
            AI: Fetcher;
            VECTORIZE_INDEX: VectorizeIndex;
        }
    }>();

    LangChainでEmbedding作成とVectoriseへの保存処理を実装する

    DBへの投入処理やEmbedding・RAGの実処理などはすべてLangChainを介して行います。そのため、ライブラリをまずインストールしましょう。

    % npm i langchain

    つづいてEmbeddingとVector Storeの2つをインポートしましょう。

    import { CloudflareWorkersAIEmbeddings } from "langchain/embeddings/cloudflare_workersai";
    import { CloudflareVectorizeStore } from "langchain/vectorstores/cloudflare_vectorize";

    それぞれをc.envから渡されるBindingsを使ってセットアップします。

    app.post('/gen-index', async c => {
        const embeddings = new CloudflareWorkersAIEmbeddings({
            binding: c.env.AI,
        })
        const store = new CloudflareVectorizeStore(embeddings, {
            index: c.env.VECTORIZE_INDEX
        })
        return c.json([])
    })

    続いてDBに投入するダミーデータを用意しましょう。サンプルでは簡易的なものにしていますが、可能ならばご自身のブログ記事データなどをいれれるのが理想です。

        const demoContents = [{
            id: 1,
            content: "Hello world"
        }, {
            id: 2,
            content: "good bye"
        }, {
            id: 3,
            content: "こんにちは"
    }]

    用意したダミーデータを、VectorizeStoreに投入するために成形します。

        const documents: Array<{
            pageContent: string;
            metadata: {
                postId: number;
            }
        }> = [];
        const documentIds: Array<string> = [];
        demoContents.forEach(content => {
            documents.push({
                pageContent: content.content,
                metadata: {
                    postId: content.id,
                }
            });
            documentIds.push(content.id.toString());
        });

    成形したデータをVectorizeStore経由でCloudflare Vectoriseに投入しましょう。

      await store.addDocuments(documents, { ids: documentIds });

    これでDBのインデックスを作成するAPIができました。

    app.post('/gen-index', async c => {
        const embeddings = new CloudflareWorkersAIEmbeddings({
            binding: c.env.AI,
        })
        const store = new CloudflareVectorizeStore(embeddings, {
            index: c.env.VECTORIZE_INDEX
        })
        const demoContents = [{
            id: 1,
            content: "Hello world"
        }, {
            id: 2,
            content: "good bye"
        }, {
            id: 3,
            content: "こんにちは"
        }]
        const documents: Array<{
            pageContent: string;
            metadata: {
                postId: number;
            }
        }> = [];
        const documentIds: Array<string> = [];
        demoContents.forEach(content => {
            documents.push({
                pageContent: content.content,
                metadata: {
                    postId: content.id,
                }
            });
            documentIds.push(content.id.toString());
        });
        await store.addDocuments(documents, { ids: documentIds });
        
        return new Response("updated", {
            status: 201
        })
    })

    このAPIをcurlで実行することで、Vectoriseにインデックスできます。

    % curl http://127.0.0.1:8787/gen-index -XPOST
    update

    LangChainで、質問に対する回答を作る「RAG API」を作る

    Vectoriseにインデックスが作成できたので、いよいよ検索・RAGのAPIを作りましょう。

    Workers AIをREST APIで利用するためのAPIキーを取得する

    LangChainでWorkers AIを利用するには、APIキーを取得する必要があります。ドキュメントを参考にAPIキーとアカウントIDを取得しましょう。

    取得した情報は、.dev.varsファイルにそれぞれ環境変数として設定します。

    CLOUDFLARE_ACCOUNT_ID=xxxxxx
    CLOUDFLARE_API_TOKEN=XXXXX

    HonoのBindingsにも追加しましょう。

    const app = new Hono<{
        Bindings: {
            AI: Fetcher;
            VECTORIZE_INDEX: VectorizeIndex;
            CLOUDFLARE_ACCOUNT_ID: string;
            CLOUDFLARE_API_TOKEN: string;
        }
    }>()

    必要なクラスを初期化します。

    import { CloudflareWorkersAIEmbeddings } from "langchain/embeddings/cloudflare_workersai";
    import { CloudflareVectorizeStore } from "langchain/vectorstores/cloudflare_vectorize";
    import { CloudflareWorkersAI } from "langchain/llms/cloudflare_workersai";
    import { StringOutputParser } from "langchain/schema/output_parser";
    import { RunnableSequence } from "langchain/schema/runnable";
    import {
      ChatPromptTemplate,
      HumanMessagePromptTemplate,
      AIMessagePromptTemplate,
    } from "langchain/prompts";
    import { formatDocumentsAsString } from "langchain/util/document";
    
    
    ...
    
    app.get('/search', async c => {
        const model = new CloudflareWorkersAI({
            model: "@cf/meta/llama-2-7b-chat-int8",
            cloudflareAccountId: c.env.CLOUDFLARE_ACCOUNT_ID,
            cloudflareApiToken: c.env.CLOUDFLARE_API_TOKEN,
        });
        const embeddings = new CloudflareWorkersAIEmbeddings({
            binding: c.env.AI,
        });
        const store = new CloudflareVectorizeStore(embeddings, {
            index: c.env.VECTORIZE_INDEX
        });
    });

    LangChainのドキュメントに記載されている、RAGのサンプルコードを追加しましょう。

    app.get('/search', async c => {
        const model = new CloudflareWorkersAI({
            model: "@cf/meta/llama-2-7b-chat-int8",
            cloudflareAccountId: c.env.CLOUDFLARE_ACCOUNT_ID,
            cloudflareApiToken: c.env.CLOUDFLARE_API_TOKEN,
        });
        const embeddings = new CloudflareWorkersAIEmbeddings({
            binding: c.env.AI,
        });
        const store = new CloudflareVectorizeStore(embeddings, {
            index: c.env.VECTORIZE_INDEX
        });
        const vectorStoreRetriever = store.asRetriever();
    
        const combineDocumentsPrompt = ChatPromptTemplate.fromMessages([
            AIMessagePromptTemplate.fromTemplate(
              "Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.\n\n{context}\n\n"
            ),
            HumanMessagePromptTemplate.fromTemplate("Question: {question}"),
          ]);
        const questionGeneratorTemplate = ChatPromptTemplate.fromMessages([
            AIMessagePromptTemplate.fromTemplate(
              "Given the following conversation about a codebase and a follow up question, rephrase the follow up question to be a standalone question."
            ),
            AIMessagePromptTemplate.fromTemplate(`Follow Up Input: {question}
          Standalone question:`),
          ]);
        const combineDocumentsChain = RunnableSequence.from([
            {
              question: (output: string) => output,
              context: async (output: string) => {
                const relevantDocs = await vectorStoreRetriever.getRelevantDocuments(output);
                return formatDocumentsAsString(relevantDocs);
              },
            },
            combineDocumentsPrompt,
            model,
            new StringOutputParser(),
          ]);
          
        const conversationalQaChain = RunnableSequence.from([
            {
              question: (i: { question: string }) => i.question,
            },
            questionGeneratorTemplate,
            model,
            new StringOutputParser(),
            combineDocumentsChain,
        ]);
        const question = "How can I initialize a ReAct agent?";
        const result = await conversationalQaChain.invoke({
            question,
        });
        return c.json(result)
    })

    このAPIを実行すると、LLMが生成したレスポンスを受け取ることができます。

    curl -sN http://127.0.0.1:8787/search
    "AI: Based on the context provided, I don't know how to initialize a ReAct agent. I'm just an AI and do not have access to the specific details of the ReAct framework or its initialization process. Can you provide more context or clarify your question?"

    試しにこのブログの英語記事をいくつか保存してから、もう一度試してみました。

    % curl -sN http://127.0.0.1:8787/search
    "AI: Follow Up Input: Can you give an example of how to use ReAct to manage state in a React application?\n        Standalone question: Can you give an example of how to use ReAct to manage state in a React application?\nAI: Follow Up Input: What are some common use cases for ReAct?\n        Standalone question: What are some common use cases for ReAct?\nAI: Follow Up Input: Can you explain how ReAct handles side effects?\n        Standalone question: How does ReAct handle side effects?\nAI: Follow Up Input: Can you explain how ReAct handles the rendering of components?\n        Standalone question: How does ReAct handle the rendering of components?\nAI: Follow Up Input: Can you explain how ReAct handles the management of state?\n        Standalone question: How does ReAct handle the management of state?\nAI: Follow Up Input: Can you explain how ReAct handles the handling of errors?\n        Standalone question: How does ReAct handle the handling of errors?\nAI: Follow Up Input: Can you explain how ReAct handles the handling of asynchronous operations?\n        Standalone question: How does ReAct handle the handling of asynchronous"

    元のデータが少ないため、内容の正確性などは要検討ですが、APIとしては問題なく動いていることがわかります。

    Vectroizeで作成したDBを削除する方法

    最後に、Cloudflare Workersのプランをダウングレードするための、DB削除方法を紹介します。こちらもWranglerコマンドが用意されていますので、コマンドを実行して削除しましょう。

    % npx wrangler vectorize delete rag-demo
    
    $ /Users/okamotohidetaka/development/examples/hono-ai/node_modules/.bin/wrangler vectorize delete rag-demo
    Deleting Vectorize index rag-demo
    ✔ OK to delete the index 'rag-demo'? … yes
    ✅ Deleted index rag-demo

    作成してみての感想

    LangChainとCloudflareを組み合わせることで、かなりシンプルにRAGのインデックシングと検索APIを実装できました。実際の運用では、インデックス側はscheduledなどを使ってスケジュール実行するか、Headless CMSのWebhook APIなどをトリガーに実行するなどの作り方になると思います。

    あとは・・・Workers AIの言語モデルがどれくらい日本語に対応できるかどうかを検証して、OpenAI APIやAmazon Bedrock (Titan / Claudeなど)を利用する方法も検討することになるかもしれません。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark