Mastra と Upstash で構築する AI 論文リサーチアシスタント完全ガイド
AI リサーチエージェントの概要
学術研究の世界は動きが速く、arXiv をはじめとするプレプリントサーバーには毎日新しい論文が投稿されています。すべてを目視で追いかけるのは現実的ではありません。そこで本記事では、次のようなことができる AI リサーチアシスタント の構築方法を解説します。
- 研究者が自然言語で入力した質問を理解する
- arXiv のアブストラクトを格納したベクトルデータベースから最も関連性の高い論文を検索する
- 重要な知見を要約し、それが質問にどう答えているのかを説明する
- さらに詳しく読むための PDF への直接リンクを提示する
実現には、AI エージェント構築用のオープンソース TypeScript フレームワークである Mastra と、サーバーレス Redis およびベクトルストレージを提供する Upstash を使用します。AI 研究に特化したこの記事エージェントのライブデモは Vercel 上に公開されているので、ぜひ試してみてください。

Mastra とは?
Mastra は、本番環境レベルの AI エージェントを簡単に作れる「電池込み」のフレームワークです。
- エージェント & ワークフロー — エージェント、ツール、複数ステップのワークフローを組み合わせて構築可能
- RAG(検索拡張生成) — メモリとベクトルストアが組み込み済み
- マルチ LLM 対応 — OpenAI、Claude など複数のモデルで動作
今回は、Upstash Redis をメモリとして使うエージェントを作成します。また、事前に Upstash Vector データベースへ埋め込み済みの研究論文を検索するツールも持たせます。
より詳しく知りたい方は、Mastra の公式ドキュメントをご覧ください。
プロジェクトの技術スタック
- Mastra フレームワーク — AI エージェントとツールの作成
- Upstash Redis — エージェントへの会話メモリの提供
- Upstash Vector — 論文アブストラクトの埋め込みデータ保存
- Next.js & Vercel — Web アプリケーションの構築とデプロイ
さらに、デモアプリへの過剰なリクエストを防ぐために Upstash Ratelimit も使用します。
実装の全体像
このアプリケーションは主に 2 つのコンポーネント、すなわち「Mastra サーバー」と「Web アプリケーション」で構成されます。同じプロジェクト内に置くこともできますが、分離しておく方が管理しやすくなります。まずは Mastra サーバーから始めましょう。
Mastra プロジェクトの作成
ターミナルで以下のコマンドを実行すると、新しい Mastra プロジェクトを作成できます。
npm create mastra@latest
いくつか質問されますが、このプロジェクトではデフォルト設定のままで問題ありません。
エージェントとツールの作成
エージェントを設定する最初のステップは、名前・目的・使用するツールの定義です。加えて、与えられたタスクに対して十分な性能を持つ言語モデルを選ぶことも重要です。このプロジェクトでは、1 つのエージェントと 1 つのツールを使用します。
export const articleAgent = new Agent({
name: "articleAgent",
instructions: instruction,
model: openai('gpt-4o'),
tools: { articleQueryTool },
memory: memory
});
エージェントの設定は上記のように非常にシンプルです。instruction(システムプロンプトとして機能)、専用の tools、model、そしてもう一つの重要な要素である memory を定義しています。
エージェントのメモリ
Mastra は、チャット履歴とセマンティックリコール(意味的想起)の両方の機能をエージェントに提供します。ストレージにメモリを保持することで、エージェントはよりパーソナライズされた正確な回答を返せるようになります。以下が今回のメモリ設定です。
export const memory = new Memory({
storage: myUpstashStore,
options: {
lastMessages: 10,
semanticRecall: false,
threads: {
generateTitle: true
}
}
});
チャット履歴を有効化するため、Upstash Redis をストレージとして使用します。MastraStorage を継承した UpstashStore オブジェクトとして初期化することで、Mastra エージェントとシームレスに連携します。
export const myUpstashStore = new UpstashStore({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
先ほどセマンティックリコール機能にも触れました。これは、現在の文脈に関連する過去のメッセージを参照できるようにするものです。そのためには、エージェントにベクトルデータベースとメッセージ処理用の埋め込みモデルが必要になります。公開デモでは個人利用向けではないため、スレッド間でメッセージを記憶する必要がなく、この機能はオフにしていますが、実装する場合は以下のようになります。
export const myUpstashVector = new UpstashVector({
url: process.env.UPSTASH_VECTOR_REST_URL!,
token: process.env.UPSTASH_VECTOR_REST_TOKEN!,
});
export const memory = new Memory({
storage: myUpstashStore,
vector: myUpstashVector,
embedder: openai.embedding("text-embedding-3-small"),
options: {
lastMessages: 10,
semanticRecall: {
topK: 3,
messageRange: 2,
scope: 'resource'
},
threads: {
generateTitle: true
}
}
});
セマンティックリコールの設定では、topK が取得する類似メッセージの数、messageRange が各一致に付随させる前後の文脈量を指定し、scope を 'resource' に設定すると、'resource' というユーザーに関連付けられた全スレッドを横断的に検索します。このスレッド横断型メモリは、Upstash ならではの強力な機能です。
ツール
ツールの作成もエージェントとほぼ同様に簡単です。名前、説明、入出力スキーマ、そしてエージェントがツールの能力を必要としたときに実行される関数を定義します。
export const articleQueryTool = createTool({
id: 'get-relevant-article',
description: 'Get relevant article information',
inputSchema: z.object({
question: z.string().describe('the question about the field'),
}),
outputSchema: z.object({
bestOption: z.object({
abstract: z.string().describe('the abstract of the article'),
title: z.string().describe('the title of the article'),
pdfUrl: z.string().describe('the PDF URL of the article')
})
}),
execute: async ({ context }) => {
return await querySimilar(context.question);
},
});
入出力スキーマのバリデーションには Zod を使用します。これにより応答の一貫性が保たれ、LLM による潜在的なエラーを最小限に抑えられます。ツールが呼び出す関数も定義します。このツールは、arXiv API 経由で定期的に更新され、Upstash Vector データベースに埋め込み済みの大規模な論文コレクションを照会します。
const querySimilar = async (query: string) => {
const { embedding } = await embed({
value: query,
model: openai.embedding("text-embedding-3-small"),
});
const results = await myMastraUpstashVector.query({
indexName: "arxiv",
queryVector: embedding,
topK: 3,
});
if (results && results.length > 0) {
const bestMatch = results[0];
const metadata = bestMatch.metadata as ArxivPaper;
return {
bestOption: {
abstract: metadata.abstract,
title: metadata.title,
pdfUrl: metadata.pdfUrl
}
};
}
throw new Error("No relevant information found");
}
MastraVector を継承した UpstashVector インスタンスを通じて、ベクトルデータベースに対するシンプルな操作を実行できます。上記のコードでは、事前に埋め込み済みの類似論文アブストラクトを照会し、最良の結果をツールに返しています。クエリ側でも、論文埋め込み時と同じ埋め込みモデルを使用している点に注目してください。論文の埋め込みについては後ほど詳しく説明します。
Mastra インスタンス
export const mastra = new Mastra({
storage: myMastraUpstashStore,
agents: { articleAgent },
deployer: new VercelDeployer()
});
使用するエージェントを指定すれば、Mastra オブジェクトの準備は完了です。インメモリ以外の永続化のためのストレージも渡しています。デプロイ設定も選択肢の中から選べますが、ここでは Vercel を使ってデプロイします。
create-mastra-app のデフォルトオプションを使えば、必要なファイル構成はすでに整っています。
.
└── mastra
├── agents
│ └── index.ts
├── tools
│ └── index.ts
└── index.ts
デプロイ前に残っているのは環境変数の設定だけです。
OPENAI_API_KEY=
UPSTASH_VECTOR_REST_URL=
UPSTASH_VECTOR_REST_TOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
ローカル開発では .env.local ファイルに記述し、デプロイ環境にも同じ変数を登録してください。
これで Mastra サーバーをビルドしてデプロイする準備が整いました。
npm run build && vercel --prod
デプロイ方法の詳細は Vercel のドキュメントを参照してください。
開発中は Mastra Playground を使えば、サーバーの出力を確認できます。以下のコマンドを実行してください。
npm run dev
Web インターフェースへのリンクが表示され、そこでエージェントとのチャットやツールの明示的な実行など、サーバーの機能を試すことができます。
続いて、アプリケーションのもう一方の部分について見ていきましょう。
Next.js サーバー
Mastra サーバーが整ったら、残りの作業は UI、Mastra サーバーとの通信、そして arXiv API とやり取りしてアブストラクトを Upstash Vector に埋め込む論文サービスの 3 点です。Mastra にはサーバーの機能を公開するためのクライアント SDK があり、これを通じてエージェント、ツール、メモリなどにアクセスできます。使い方は直感的ですが、いくつか例を紹介します。詳細は公式ドキュメントを確認してください。Next.js プロジェクトでは、クライアント SDK をインストールしてそのまま使えます。
npm install @mastra/client-js@latest
コード内で MastraClient のインスタンスを作成してプロジェクトで利用します。
import { MastraClient } from "@mastra/client-js";
export const mastra_sdk = new MastraClient({
baseUrl: process.env.NEXT_PUBLIC_MASTRA_API!,
retries: 3,
});
NEXT_PUBLIC_MASTRA_API には Mastra サーバーのアドレスを設定します。ローカル開発中であれば localhost のアドレスになります。ポート 3000 は競合する可能性が高いため、ローカル実行時は Mastra サーバーの設定を以下のように変更しましょう。
export const mastra = new Mastra({
storage: myMastraUpstashStore,
agents: { articleAgent },
server: {
port: 4111,
timeout: 10000,
}
});
こうすると、npm run dev で Mastra サーバーを起動した際にポート 4111 で待ち受けます。Next.js プロジェクトをローカルで実行する際は、NEXT_PUBLIC_MASTRA_API を https://localhost:4111 に設定すれば OK です。
それでは、Mastra のクライアント SDK の使い方を見てみましょう。
export const MASTRA_CONFIG = {
resourceId: process.env.NEXT_PUBLIC_RESOURCE_ID || "articleAgent",
agentId: "articleAgent",
baseUrl: process.env.NEXT_PUBLIC_MASTRA_API || "https://localhost:4111",
retries: 3,
}; // コードベースのどこからでも使えるよう別ファイルで export しています。
// エージェントを取得し、agent オブジェクト経由でレスポンスをストリーミングします。
const agent = mastra_sdk.getAgent(MASTRA_CONFIG.agentId);
const response = await agent.stream({
messages: [message],
resourceId: MASTRA_CONFIG.resourceId,
threadId: threadId
});
ツールやエージェントを取得できれば、クライアント SDK 経由で実際のオブジェクトとほぼ同等の操作が可能です。
このデモプロジェクトは一般公開するため、エージェントに過剰な負荷をかけないことが重要です。ここで活躍するのが Upstash Ratelimit です。ストリームリクエストのたびに、ユーザーがレート制限対象かどうかをチェックします。レートリミッターの設定には Upstash Redis が必要ですが、Mastra エージェント用に使っているのと同じ Redis データベースを流用できます。
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
// プロジェクト全体で同じ Redis DB を使用
export const rateLimit = new Ratelimit({
redis: new Redis({
url: process.env.UPSTASH_REDIS_MEMORY_URL!,
token: process.env.UPSTASH_REDIS_MEMORY_TOKEN!
}),
limiter: Ratelimit.slidingWindow(10, '10s'),
prefix: 'upstash-ratelimit',
});
// 各ストリームの前に以下の関数を呼び出す。
export async function isRateLimited(id: string): Promise<boolean> {
const { success } = await rateLimit.limit(id);
return !success;
}
これにより、エンドポイントが高負荷にさらされるのを防げます。
Mastra でチャットエージェントを作る際、スレッド生成に関するいくつかの仕様を知っておくと役立ちます。エージェントのメモリ設定時に、threads オブジェクト内の generateTitle を true にしたことを思い出してください。これにより、新しく作成されたスレッドのタイトルが自動生成されます。ただしここに落とし穴があります。スレッドは明示的に作成することもできますが、その場合は自動タイトル生成がトリガーされません。通常、新しいスレッドは次のように作成します。
const thread = await mastraClient.createMemoryThread({
title: "New Conversation",
metadata: { category: "support" },
resourceId: "resource-1",
agentId: "agent-1",
});
しかし、この方法ではタイトルを手動で指定しているため、エージェントによる自動タイトル生成の恩恵を受けられません。title フィールドを空にしても機能しません。そこで参考になるのが Playground の挙動です。開発中にサーバーの機能を体験できる Mastra 提供の Playground を覚えていますか?ブラウザの開発者ツールでネットワークタブを観察すると、新しいスレッドを作成する際に、実際にはスレッド作成用の API リクエストを送っていないことが分かります。代わりに、最初のメッセージ送信を待ち、その後に新しく生成されたスレッド ID を付けてストリームリクエストを送信します。Mastra 側は「この ID のスレッドは存在しない」と判断してスレッドを新規作成し、generateTitle が true であれば最初のメッセージをもとにタイトルを生成するのです。
続いて、プロジェクトの最後の要素である arXiv 論文について説明します。
arXiv 論文
arXiv は、さまざまな分野の約 240 万件の研究論文を収録するオープンアクセスアーカイブです。articleQueryTool が照会する Upstash Vector データベースには、arXiv API 経由で取得した論文が投入されます。API は扱いやすく、詳細は公式ドキュメントで確認できます。
このプロジェクトでは、論文を毎日取得して保存します。サーバー初回起動時には、指定カテゴリの論文を約 30,000 件取得し、以降は前日に公開された新しい論文のみを取得します。論文カテゴリの指定や初回の大量取得を行うかどうかは、対応する環境変数で設定します。カテゴリは arXiv の分類法(taxonomy)に従い、カンマ区切りで指定します。カテゴリの一覧は arXiv のサイトで確認できます。
CATEGORIES=cs.AI
RUN_BEGINNING_STACK=false
より包括的なデータベースが必要な場合は、arXiv のバルクデータアクセスを利用できます。利用しない場合、1 回の API クエリで取得できるのは 30,000 件までですが、今回の目的には十分です。
arXiv への基本的なクエリは次のようになります。
const categories = process.env.CATEGORIES?.split(',') || []; // 指定カテゴリを取得してクエリ用に分割。
const searchQuery = categories.length === 1 ? `cat:${categories[0]}` : `(${categories.map(c => `cat:${c}`).join(" OR ")})`;
const query = `search_query=${searchQuery}&sortBy=submittedDate&sortOrder=descending`;
const url = `https://export.arxiv.org/api/query?${query}`;
const response = await axios.get(url); // 構築した URL で API を呼び出す。
同様の呼び出しを使って、毎日の最新論文取得と初回の大量取得を行っています。
論文を取得したら、正規化して埋め込みを作成し、Upstash Vector に保存します。このとき使うのは、Mastra 側のツールが参照するのと同じベクトルデータベースでなければなりません。「正規化」とは、取得した論文をコードベース全体で共通して使う標準的な ArxivPaper 型にパースすることを指します。
export interface ArxivPaper {
id: string;
title: string;
abstract: string;
authors: string[];
published: string;
pdfUrl: string;
category: string;
}// コードベース全体で使う論文の型。
async function storeAbstracts(papers: ArxivPaper[]) {
const embeddingModel = openai.embedding("text-embedding-3-small"); // Mastra 側のクエリと同じモデル。
const embeddings = await embedArticles(papers, embeddingModel)
// メタデータとともに必要な形式に整形。
const vectorsToUpsert = getVectorsToUpsert(embeddings, papers)
for (let j = 0; j < vectorsToUpsert.length; j++) {
await vectorStore.upsert(vectorsToUpsert[j], { namespace: "arxiv" }); // 埋め込みとメタデータを Upstash Vector にアップサート。
}
}
データベースを最新の研究内容に保つため、定期タスク実行には Upstash QStash を採用しました。Vercel にデプロイしているため、長時間の処理で発生しがちな関数タイムアウトを回避する必要があります。そこで、サーバー上に公開 API エンドポイントを設け、QStash インスタンスから確実に日次のデータベース更新関数をトリガーできるようにしました。
// src/app/api/arxiv_reneval/route.ts
import { verifySignatureAppRouter } from "@upstash/qstash/nextjs"
import { fetchAndUpsertYesterday} from "@/services/arxiv"
async function handler(request: Request) {
console.log("Fetching and upserting yesterday's papers...")
await fetchAndUpsertYesterday()
console.log("Fetching and upserting yesterday's papers completed")
return Response.json({ success: true })
}
export const POST = verifySignatureAppRouter(handler)
スケジューラは Upstash Console から設定でき、毎日 UTC 午前 6 時にこのエンドポイントへリクエストを自動送信するように構成します。

このスケジューラ設定により、サーバーは毎朝自動的にデータベースを更新し、常に新鮮なデータを維持できるようになります。
なお、QStash インスタンスの認証情報も必要です。必須の環境変数はすべてサンプルの env ファイルに記載されています。
以上で完成です。コードを自由に触って遊んでみてください。リポジトリをフォークして開発を始めるだけで OK です。Mastra 側のリポジトリはこちら、もう一方のリポジトリはこちらからアクセスできます。フォーク後の手順は次のとおりです。
- 両方のリポジトリをローカルマシンにクローンする
- 環境変数を入力する(サンプルの
.envファイルが用意されています) - 両プロジェクトのルートディレクトリで別々のターミナルを開く
- 以下のコマンドを実行する
npm install
npm run dev
これで https://localhost:3000 でアプリケーションを確認できます。
Mastra を使えば、RAG、ワークフロー、ネットワークなどの他のテンプレートを活用して、さらに複雑なシステムを構築できます。どの用途においてもメモリとストレージが重要な役割を果たします。まさにそこで Upstash が真価を発揮するのです。
-
Upstash・Redis・QStashで実現するリアルタイム緊急対応システムの構築
本記事では、Upstashを活用して国の避難所マップに関する情報をRedisで安全に保存・アクセスする方法と、QStashを使ってデータベースをリアルタイムに更新する方法について解説します。 はじめに 今日の世界では、自然災害や軍事的な脅威がますます深刻化しており、それに伴い社会サービス分野におけるデジタル化のニーズも高まっています。 AMBERアラートのような緊急放送システムから、COVID-19の追跡アプリ、SOSシステムまで、国に影響を及ぼすあらゆる危機的状況に対して、幅広いテクノロジーが活用されてきました。 本記事では、Redisとサーバーレスワークロードがどのようにリアルタイム緊急対
-
Redis HLENコマンドの使い方 – ハッシュに含まれるフィールド数を取得する方法
このチュートリアルでは、RedisのHLENコマンドを使って、指定したキーに保存されているハッシュ値に含まれるフィールドの数を取得する方法を解説します。redis-cliからHLENコマンドを実行することで、ハッシュ内のフィールド総数を簡単に確認できます。 HLENコマンドの構文 redis host:port> HLEN <キー名> 指定したキーに対してHLENコマンドを実行すると、そのハッシュに登録されているフィールドの数が返されます。 実行結果(戻り値) - (integer) ハッシュに含まれるフィールドの数 - 0:キーが存在しない場合 - エラー:キーは存在するが