0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Mastra StudioでRAG Agentが答えない――3つのエラーを順番に切り分けた記録

0
Posted at

MastraでRAGを組み、検索ToolをAgentへ接続した。ローカル検索は成功している。ところがMastra Studioから質問すると、Agentは答えない。Toolの実行欄には赤い×が表示され、OpenAI APIから400エラーも返ってきた。

結論から書くと、原因は一つではなかった。

  1. ToolのJSON SchemaがOpenAIの要件を満たしていなかった
  2. toModelOutputの戻り値が、モデルへ渡す形式になっていなかった
  3. 取り込み処理とMastra Studioが、別々のSQLiteファイルを開いていた

この三つを直すと、Studio上のAgentは検索結果を根拠に回答し、末尾へ出典も表示するようになった。本記事では、直したコードだけでなく、どの順番で調べると原因へたどり着きやすいかをまとめる。

検証時の主なバージョンは @mastra/core 1.64.0mastra 1.27.3@mastra/libsql 1.22.3。Mastraは更新が速いため、別バージョンではAPIや挙動が異なる可能性がある。

作っていたもの

題材は、架空サービス「AtlasNote」のヘルプAgentだ。FAQ、料金プラン、リリースノートなど5文書を読み込み、次の経路で回答させる。

文書を読み込む
  ↓
チャンクへ分割する
  ↓
埋め込みを作り、LibSQLのベクトル索引へ保存する
  ↓
検索Toolが関連チャンクを取得する
  ↓
Agentが根拠と出典を付けて回答する

取り込み時には、次のログが出ていた。

Indexed 6 chunks from 5 documents in atlasnote_chunks.

これは「5つの元文書を分割した結果、合計6チャンクを索引へ登録した」という意味だ。文書数とチャンク数が一致しないのは正常である。長い文書だけが複数チャンクへ分割されることもある。

最初の症状――Agentへ質問する前に400エラーになる

Mastra Studioから質問すると、OpenAI APIが以下のエラーを返した。

Invalid schema for function 'search_atlasnote_documents':
In context=('properties', 'resumeData', 'anyOf', '0'),
schema must have a 'type' key.

注目すべき箇所は resumeData だ。自分で定義した検索Toolの入力は querytopK だけで、resumeData は書いていなかった。

調べると、この環境では autoResumeSuspendedTools: true により、Toolの自動再開に使う resumeData が生成スキーマへ追加されていた。そのスキーマの一部をOpenAI側が有効なJSON Schemaとして受け付けず、Toolを呼ぶ前のリクエスト検証で止まっていた。

今回は、検索Toolに中断・再開が不要だったため、自動再開を無効にした。

export const agent = new Agent({
  // ...
  defaultOptions: {
    maxSteps: 100,
    autoResumeSuspendedTools: false,
  },
});

これで生成スキーマから resumeData が外れ、最初の400エラーは解消した。

ただし、この設定には影響範囲がある。ユーザーへの確認や承認など、Toolを中断して後から自動再開する仕組みを使うAgentでは、単純に無効化してよいとは限らない。今回は「検索Toolを動かす」という要件に合わせた対処であり、中断・再開も必要なら、利用中のMastraバージョンに対応したスキーマと再開方法を公式ドキュメントで確認したい。

次の症状――Toolは動くが、結果をモデルへ返せない

最初のエラーを直すと、別の400エラーが現れた。

Missing required parameter: input[n].output

検索処理自体は実行されていた。しかし、検索結果をモデルへ渡すための toModelOutput が、加工後の文字列をそのまま返していた。

// 問題が起きた形
toModelOutput: (result) => {
  return result.chunks.map(/* ... */).join('\n\n');
},

このバージョンでは、toModelOutput はモデル向けコンテンツの形式で返す必要がある。テキストなら、typevalue を持つオブジェクトにする。

toModelOutput: (result) => {
  if (result.chunks.length === 0) {
    return {
      type: 'text' as const,
      value: 'コーパス内に、この質問への回答根拠は見つかりませんでした。',
    };
  }

  return {
    type: 'text' as const,
    value: result.chunks
      .map(
        (chunk) =>
          `[出典: ${chunk.citation}; チャンク: ${chunk.source.chunkIndex}]\n${chunk.content}`,
      )
      .join('\n\n'),
  };
},

ここで大事なのは、検索結果そのものと、モデルに読ませる表現を分けることだ。

  • execute は、スコアやメタデータを含む構造化データを返す
  • toModelOutput は、モデルが回答に使いやすい短いテキストへ整形する

Mastra公式の変更履歴でも、toModelOutput は生のTool結果をモデル向けに変換し、モデルへ渡す情報量を抑える仕組みとして紹介されている。

最後の症状――エラーは消えたのに「根拠がない」と答える

Toolのスキーマと出力形式を直した後も、Studio上のAgentは正しい回答を返さなかった。

一方、ターミナルから検索スクリプトを実行すると、FAQは取得できていた。

bun run search -- "パスワードを忘れました" --top-k 3

検索結果には、次の回答根拠が含まれていた。

サインイン画面の「パスワードを再設定」からメールを送信してください。
再設定リンクの有効期限は30分です。

つまり、取り込み処理、埋め込み、ベクトル検索は壊れていない。次にMastra Server経由で検索Toolだけを呼んだ。

./node_modules/.bin/mastra api tool execute \
  search_atlasnote_documents \
  '{"query":"パスワードを忘れました","topK":3}'

すると、こちらではSQLiteエラーが返った。

SQLITE_ERROR: no such table: atlasnote_chunks

同じコードを呼んでいるはずなのに、CLI検索ではテーブルが見つかり、Studio側では見つからない。実際に開かれているDBファイルを確認すると、二つの mastra.db ができていた。

プロジェクトルート/mastra.db
src/mastra/public/mastra.db

取り込みスクリプトは前者へデータを保存していた。Mastra Studioの開発サーバーは、初期化時のカレントディレクトリを基準に後者を開いていた。そのためStudioから見れば、索引テーブルは存在しなかった。

原因になった設定は、相対パスだった。

url: 'file:./mastra.db'

相対パスは、実行時のカレントディレクトリから解決される。ソースファイルが置かれた場所は基準にならない。同じアプリケーションでも、スクリプト、開発サーバー、ビルド後の実行で基準位置が変われば、別ファイルを開き得る。

ここが盲点だった。

そこで、プロジェクトルートを探して絶対パスを組み立て、取り込み処理とStudioで同じURLを共有した。

import { existsSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';

export function resolveLocalDatabaseUrl(
  startDirectory = process.cwd(),
): string {
  let directory = resolve(startDirectory);

  while (!existsSync(join(directory, 'package.json'))) {
    const parentDirectory = dirname(directory);
    if (parentDirectory === directory) break;
    directory = parentDirectory;
  }

  return `file:${join(directory, 'mastra.db')}`;
}

export const RAG_DATABASE_URL =
  process.env.TURSO_DATABASE_URL || resolveLocalDatabaseUrl();

ベクトルストアとMastraのストレージは、どちらもこの RAG_DATABASE_URL を使う。

export const ragVectorStore = new LibSQLVector({
  id: 'rag-vector-store',
  url: RAG_DATABASE_URL,
});

const storage = new LibSQLStore({
  id: 'mastra-storage',
  url: RAG_DATABASE_URL,
});

修正後に開発サーバーを再起動すると、Studio経由の検索ToolもFAQを返した。Agentの最終回答は次の形になった。

サインイン画面の「パスワードを再設定」からメールを送信してください。
再設定リンクの有効期限は30分です。

出典: faq.txt

RAGの不具合は、Agentから調べ始めない

今回いちばん効いたのは、処理を一段ずつ切り離したことだった。RAG Agentが答えないときは、次の順番で確認すると原因の範囲を狭めやすい。

1. 取り込み件数を確認する

bun run ingest

文書数とチャンク数が想定どおりかを見る。0件なら、AgentやToolへ進む前にローダー、対象パス、チャンク化を調べる。

2. 検索関数を単体で呼ぶ

bun run search -- "パスワードを忘れました" --top-k 3

期待する本文、出典、スコアが返るかを見る。ここで失敗するなら、埋め込みまたはベクトルストアより手前を調べる。

3. 開発サーバー経由でToolだけを呼ぶ

まず、プロジェクトで定義したスクリプトから開発サーバーを起動する。

bun run dev

別のターミナルでToolを実行する。

./node_modules/.bin/mastra api tool execute \
  search_atlasnote_documents \
  '{"query":"パスワードを忘れました","topK":3}'

検索関数は成功するのにここで失敗するなら、環境変数、実行ディレクトリ、DB接続先、Toolの入出力スキーマを疑う。

4. 最後にAgentを通す

./node_modules/.bin/mastra api agent run agent \
  '{"messages":"パスワードを忘れました","activeTools":["search_atlasnote_documents"]}'

Tool単体は成功するのにAgentが呼ばない場合は、AgentへのTool登録、Toolの説明文、Agentの指示文、activeTools を確認する。Toolは呼ばれるが回答内容がおかしい場合は、toModelOutput と実際の検索結果を見る。

5. Studioの赤い×を、回答品質の問題として片付けない

Toolカードの赤い×は、Tool呼び出しが失敗した合図だ。検索順位の低さを表す印ではない。開発サーバーのターミナル、Studioのトレース、Toolの入出力を確認する。Mastra StudioはAgentの試運転に加え、どの段階で止まったかを見るデバッガーとして使うと便利だった。

症状から当たりを付ける早見表

症状 最初に疑う場所 今回の原因
モデルAPIがTool実行前に400を返す Toolの入力JSON Schema 自動追加された resumeData
Tool実行後に input[n].output で失敗する モデルへ渡すTool出力 toModelOutput が文字列を直接返していた
ローカル検索は成功し、Studioだけ失敗する 実行環境の差 別の mastra.db を開いていた
Toolは成功するがAgentが呼ばない Agent設定 Tool登録、説明文、指示文、activeTools
Toolは成功するが回答がずれる 検索結果とプロンプト 上位チャンクの内容、検索語、回答制約

再発防止のために追加したテスト

今回の問題は、単体の検索精度テストだけでは防げなかった。境界部分をテストへ追加した。

test('モデル向け出力をtext形式で返す', () => {
  expect(tool.toModelOutput!(result)).toEqual({
    type: 'text',
    value: expect.stringContaining('出典: faq.txt'),
  });
});
test('検索Toolへ不正なresumeDataスキーマを追加しない', async () => {
  const options = await agent.getDefaultOptions();
  const tools = await agent.getToolsForExecution({
    autoResumeSuspendedTools: options.autoResumeSuspendedTools,
  });

  expect(
    tools.search_atlasnote_documents.parameters.jsonSchema
      .properties?.resumeData,
  ).toBeUndefined();
});
test('Studioの実行ディレクトリからも同じDBを解決する', () => {
  const studioDirectory = resolve('src/mastra/public');

  expect(fileURLToPath(resolveLocalDatabaseUrl(studioDirectory)))
    .toBe(resolve('mastra.db'));
});

「検索できる」に加え、「モデルへ渡せる」「Studioから同じDBが見える」までをテストするのがポイントだ。

まとめ――RAGは一本の機能ではなく、境界の連続である

RAG Agentが答えないと、ついプロンプトやモデルを疑いたくなる。今回は、その前後にある境界で問題が起きていた。

入力スキーマ
  → Tool実行
  → Tool出力の変換
  → ベクトルDB接続
  → 検索結果
  → Agentの回答

一気にStudioだけで確認すると、「Agentが答えない」という一つの症状に見える。検索関数、Server上のTool、Agentの順に分ければ、どの境界でデータが途切れたかを確認できる。

Mastra初心者に限らず、フレームワーク、モデルプロバイダー、ローカルDBをつないだ開発では、境界の食い違いが起きる。エラーメッセージの名詞を手掛かりに、一段ずつ単体実行する。今回の3件から得た、いちばん再利用しやすい教訓だった。

参考資料


X投稿文案

MastraでRAG Agentを作ったら、ローカル検索は成功するのにStudioでは答えない。

原因は、①ToolのJSON Schema、②toModelOutputの形式、③SQLiteの相対パスで別DBを開いていたこと、の3つでした。

「検索関数 → Tool単体 → Agent」の順に切り分けた手順と修正内容をまとめました。

[記事URL]

#Mastra #RAG #AIエージェント #TypeScript

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?