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など)を利用する方法も検討することになるかもしれません。