LangChainとUpstash Redisでチャット履歴を永続化する方法
LangChainは、人間とAIの間の対話を実現するためのシンプルなインターフェースを提供しています。BufferMemoryを使うよう簡単に設定でき、会話履歴をメモリ上に保存できます。一部のユースケースではそれで十分ですが、アプリによってはチャット履歴を長期的に永続化したいケースもあるでしょう。幸い、これをUpstash Redisインスタンスに差し替えるのも同じくらい簡単です。
LangChainには、ioredis、node-redis、Upstash Redisなど、Redis向けの複数の統合機能が用意されています。Upstash RedisクライアントはREST経由で動作するため、Vercel、Cloudflare Workersなど、あらゆるサーバーレス環境にデプロイ可能なエッジ対応アプリケーションを構築できます。この記事では、セッションをまたいでも記憶が保持されるシンプルなチャットアプリを作成します。
このデモの完全なソースコードはこちらから入手できます。
前提条件
- Upstash Redisデータベース
- OpenAI APIキー
はじめに
プロジェクトの作成
Vercel AI SDKを使った基本的なNext.jsアプリを構築し、LangChainとUpstash Redisの連携方法をデモします。まずは新しいNext.jsアプリを作成しましょう。
npx create-next-app@latest
プロジェクトのオプションをいくつか選択するよう求められます。ほとんどのアプリではデフォルト設定で問題ありません。このデモでは、TypeScriptとappディレクトリを必ず有効にしてください。
依存パッケージのインストール
アプリが作成できたら、必要な依存パッケージをインストールします。
npm install ai langchain openai @upstash/redis
厳密には必須ではありませんが、Vercel AI SDKを利用すると、OpenAIからのレスポンスをNext.jsのフロントエンドへストリーミングしやすくなります。Redisクライアントの作成には@upstash/redisだけを使い、それ以外はLangChainが面倒を見てくれます。
環境変数の設定
最後に、前提条件で挙げた環境変数を設定します。自動読み込みの対象となるため、以下の名前と完全に一致させる必要があります。プロジェクトのルートにある.envファイルに追記しましょう。
UPSTASH_REDIS_REST_URL="https://********.upstash.io"
UPSTASH_REDIS_REST_TOKEN="********"
OPENAI_API_KEY="sk-********"
基本的なチャットクライアントの作成
Next.jsが多くのファイルを自動生成していることに気づくでしょう。ここではappディレクトリ内の一部のファイルしか使わないので、publicとappの中身はすべて削除してしまって構いません。
まず、アプリの土台となるapp/layout.tsxを作成します。
import type { PropsWithChildren } from "react";
export default function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
次に、ユーザーのメッセージを受け付ける入力欄付きの基本フォームを用意します。app/page.tsxに以下を追加してください。
export default function Home() {
return (
<main>
<form>
<input placeholder="Enter a message..." />
<button type="submit">Send</button>
</form>
</main>
);
}

Vercel AI SDKはuseChatという便利なフックを提供しており、チャットアプリに一般的なUIを簡単に実装できます。このフックはチャットメッセージのストリーミング処理や入力欄の状態管理を担ってくれます。フックを使うには、ファイルの先頭に"use client"ディレクティブを追加して、Reactにクライアントコンポーネントであることを伝えます。そのうえで、useChatフックから以下のプロパティを取り出せます。
messages: 送受信されたメッセージの配列input: 入力フィールドの現在の値handleInputChange: 入力値を更新する関数handleSubmit: メッセージをエンドポイントへ送信する関数
"use client";
import { useChat } from "ai/react";
export default function Home() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<main>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Enter a message..."
/>
<button type="submit">Send</button>
</form>
</main>
);
}
内部的には、handleSubmitが呼ばれるとinputが自動的にmessagesへ追加され、再レンダリングがトリガーされます。そのため、UIの更新を自分で実装する必要はありません。同時に入力欄はクリアされ、指定のエンドポイント(デフォルトは/api/chat)へのAPI呼び出しが発生します。
最後に、フォームの上にmessagesを表示させましょう。
<main>
<section>
{messages.map((message) => (
<p key={message.id}>{message.content}</p>
))}
</section>
{/* snip */}
</main>
APIエンドポイントの作成
まず、エンドポイントを格納するapp/api/chat/route.tsファイルを作成します。Next.jsはページだけでなくAPIエンドポイントにもファイルベースルーティングを採用しており、この新規ファイルのフォルダ構成が先ほどのデフォルトエンドポイント/api/chatと一致するのはそのためです。
Upstash Redisを使っているため、このエンドポイントはエッジ互換です。const runtime = "edge"をエクスポートすることで、それを明示できます。エンドポイント内では、useChatフックが送信したmessagesフィールドを取得できます。これにより、最新のメッセージをLangChainへ渡せるようになります。
import { type NextRequest } from "next/server";
import { LangChainStream, StreamingTextResponse } from "ai";
export const runtime = "edge";
export async function POST(req: NextRequest) {
const { messages } = await req.json();
const { stream, handlers } = LangChainStream();
const latestMessage = messages[messages.length - 1];
return new StreamingTextResponse(stream);
}
先ほどのuseChatと同じように、LangChainStreamも分割代入できるプロパティを返します。
stream: 最終的にLangChainの処理結果を格納するReadableStreamhandlers: LangChainに渡せるLLMコールバック関数を含むオブジェクト
チェーン本体を実装する前に、いくつかのクラスを追加でインポートします。
import { Redis } from "@upstash/redis";
import { ConversationChain } from "langchain/chains";
import { ChatOpenAI } from "langchain/chat_models/openai";
import { BufferMemory } from "langchain/memory";
import { UpstashRedisChatMessageHistory } from "langchain/stores/message/upstash_redis";
これでRedisクライアントを作成し、チェーン用のメモリをセットアップできます。ここでは、モデルそのものの代わりに呼び出せるConversationChainを作成します。これは人間とAIの対話を容易にするカスタムチェーンです。チェーンにはカスタムのBaseMemory実装を渡すことができ、メッセージの保存・取得に使われます。今回は、UpstashRedisChatMessageHistoryと組み合わせたBufferMemoryを使って、メッセージをUpstash Redisに保存します。
// snip
const latestMessage = messages[messages.length - 1];
const memory = new BufferMemory({
chatHistory: new UpstashRedisChatMessageHistory({
sessionId: new Date().toLocaleDateString(),
client: Redis.fromEnv(),
}),
});
const model = new ChatOpenAI({
modelName: "gpt-3.5-turbo",
streaming: true,
});
const chain = new ConversationChain({ llm: model, memory });
// snip
Redisクライアントは、@upstash/redisがエクスポートするRedisクラスで構築します。環境変数を自動的に読み込む便利なメソッドが用意されており、その挙動はChatOpenAIと同様です。環境変数の名前を正しく付けていれば、どちらのクラスにも追加の引数を渡す必要はありません。
実際のアプリでは、ユーザー間でメッセージが混在しないよう、sessionIdにユーザーIDなどの一意な識別子を使うのが望ましいですが、このデモでは現在の日付を使用します。UpstashRedisChatMessageHistoryには、キャッシュの有効期間を設定するsessionTTLなど、さらに多くの設定オプションが用意されています。
モデルでstreamingを有効にすることが重要です。これにより、先ほど分割代入したhandlersオブジェクトを使って、チェーンの結果をstreamへ流し込めます。最後に、最新のメッセージとhandlersオブジェクトを渡してチェーンを呼び出します。
// snip
const chain = new ConversationChain({ llm: model, memory });
chain.call({
input: latestMessage.content,
callbacks: [handlers],
});
return new StreamingTextResponse(stream);
// snip
先ほどのlatestMessageオブジェクトがLLMへのプロンプトとして使われます。また、handlersオブジェクトをチェーンに渡すことで、結果がstreamへ流れる仕組みになります。
まとめ
以上で完成です!npm run devでアプリを起動すれば、さっそくAIとのチャットを始められます。レスポンスは先ほど作成したクライアントへ自動的にストリーミングされ、会話履歴はUpstash Redisに保存されます。


-
Nuxt 3とサーバーレスRedis(Upstash)で始めるページ訪問カウント実装
Nuxt 3とサーバーレスRedis(Upstash)で始めるページ訪問カウント実装 アプリケーションの利用状況を追跡したり、リソース利用を制限したり、キャッシュからデータを取得してパフォーマンスを向上させたりする必要がある場合、Redisがその答えとなります。Redisはインメモリのキー・バリュー型データベースであり、オープンソースで「Remote Dictionary Server」の略称です。 この記事では、サーバーレスRedisサービスであるUpstashと、Vue SSRフレームワークの最新ベータ版であるNuxt 3を組み合わせた基本的なアプリケーション構築を通じて、Redisの基礎
-
【Redis】ZPOPMAX・BZPOPMAXコマンドでソート済みセットの最高スコア要素を取得・削除する方法
このチュートリアルでは、Redisデータストアに保存されたソート済みセット(Sorted Set)から、ZPOPMAXおよびBZPOPMAXコマンドを使用して最高スコアの要素を削除しながら取得する方法を解説します。 ZPOPMAXコマンドとは ZPOPMAXコマンドは、指定したキーに保存されたソート済みセットから、スコアが最も高い要素を1つ以上削除し、その要素を返します。オプション引数としてcount(削除する要素の総数)を指定でき、省略した場合はデフォルトで1が適用されます。要素が返される際は、最もスコアの高い要素が先頭となり、以降はスコアの低い順に並んで返されます。 なお、キーが存在するも