Mastraでチャットに紐づくメタデータを保存する方法
senpai.nvimというNeovimのAIプラグインをMastraを使って開発しています。その際に調べた「Mastraでのチャット(スレッド)に紐づくメタデータの保存・取得方法」について、備忘録を残しておきます。
senpai.nvimの紹介については別の記事Neovimの履歴管理ができるAIプラグインsenpai.nvimの使い方に書いています。
※ 今回の記事ではMastraのClient SDKについては触れていません。
メタデータってどんな時に必要?
ユーザーにモデルを選ばせるタイプのアプリであれば「プロバイダー名・モデルID」を保存しておけば、スレッドを復元する時に同じモデルをセットできます。同様にシステムプロンプトの記録・復元にも便利です。つまるところ、後から使う予定があるデータをメタデータに入れておきます。
ちなみに、Mastraでは各スレッドの初回のメッセージ送信時にタイトルが自動的に生成されます。つまりタイトルの管理は自力でやる必要は無いです。変に苦労しないようにしましょうね(戒め)。
Mastraのメタデータ管理のしくみ
メタデータの保存にはMemory
を使います。
次のコードはUsing Agent Memoryより引用です。
import { Agent } from "@mastra/core/agent";import { Memory } from "@mastra/memory";import { openai } from "@ai-sdk/openai";
// Basic memory setupconst memory = new Memory();
const agent = new Agent({ name: "MyMemoryAgent", instructions: "You are a helpful assistant with memory.", model: openai("gpt-4o"), memory: memory, // Attach the memory instance});
Memoryの実体はSQLの操作です。デフォルトだと、libSQLを使ってローカルにmemory.db
のようなファイルに保存されます。もちろん外部のDBにも保存できるようですが今回は割愛します。
メタデータの保存のタイミング
メッセージのやりとりが終わった後にチャットのタイトルなどが生成され、メモリへ保存されます。メタデータの保存はその後に発火されるonFinish
というフックで処理するとよいでしょう。
stream
のオプションにonFinish
を渡します(generate
でも同じオプションがあります)。
const agentStream = await agent.stream([userMessage], { threadId: command.thread_id, resourceId: "senpai", onFinish: async () => { // ここでメタデータを保存 },});
updateThreadを使う
メモリに対してupdateThread
を実行します。次のコードは筆者が実際に使った例です。
// 初回メッセージのみメタデータを保存する場合の分岐const thread = await memory.getThreadById({ threadId: command.thread_id });let isFirstMessage = false;if (thread == null) { isFirstMessage = true;}
const agentStream = await agent.stream([userMessage], { threadId: command.thread_id, resourceId: "senpai", onFinish: async () => { if (isFirstMessage) { // タイトルを取得して渡さないと初期化される const thread = await memory.getThreadById({ threadId: command.thread_id, }); await memory.updateThread({ id: command.thread_id, title: thread.title, metadata: { provider: command.provider, system_prompt: command.system_prompt ?? "", }, }); } },});
最初にgetThreadById
を呼んでいるのはメタデータの保存を「スレッドの初回メッセージのみ」に限定しているためです。そういう縛りが必要ではない場合、分岐不要です。updateThread
の呼び出しにはタイトルも必須のため、onFinish
の中でもgetThreadById
を呼んで取得しています。
このupdateThread
はドキュメントには載っていなかったため、どこかのバージョンで変更や削除されるかもしれません。バージョンアップの際は要注意です。
保存したメタデータを取得する
保存したメタデータはgetThreadById
で取得できます。
const result = await memory.getThreadById({ threadId: thread_id,});
戻り値の型はStorageThreadType | null
です。次が実際に筆者が使った時の例です。
{ "id": "test-2025-04-14-1741", "resourceId": "senpai", "title": "I'll analyze the content of the src/presentation/hello.ts file in Japanese.", "metadata": { "provider": { "name": "openrouter", "model_id": "anthropic/claude-3.7-sonnet" }, "system_prompt": "" }, "createdAt": "2025-04-14T08:41:36.325Z", "updatedAt": "2025-04-14T08:41:36.325Z"}
保存できる情報
実装を見ると、JSON.stringify
で変換されています。つまり、JSONで使えない値は入れられません(たとえばfunctionなど)。KVSみたいな感じですね。
型はRecord<string, unknown>
です。メタデータを取得したらバリデーションしておくとよいでしょう。
以上、Mastraにおけるメタデータの扱い方でした。
アップデートで変わったりするものなので最新の情報はMastraのドキュメントや実装をご覧ください。
というのが今までの定石でしたが、MastraがドキュメントのMCP@mastra/mcp-docs-serverを提供しているため、そちらを利用するのもアリです。