Mastra
Mastraは、TypeScript製AIエージェントフレームワークです。
Node.js v20以上があれば、簡単に始められますので、ここではセットアップなどは省略します。
CiNii超簡易検索エージェント
CiNiiは、論文、図書・雑誌や博士論文などの学術情報を、人工知能 研究」のような検索キーワードで検索するデータベース・サービスです。
今回は、これをMastraを使って、自然文で検索できるようにしてみます。
本来は検索APIを使った方がよいのですが、お遊び半分なので、簡易的にOpenSearchインターフェースからJSON形式でデータを取得しています。
.env
インストールすると、.env ファイルも用意されるので、そこにAPIキーを書き込みます。
GOOGLE_GENERATIVE_AI_API_KEY=XXXXXX
HF_TOKEN=XXXXX
index.ts
import { Mastra } from "@mastra/core/mastra";
import { PinoLogger } from "@mastra/loggers";
import { LibSQLStore } from "@mastra/libsql";
import {
Observability,
DefaultExporter,
CloudExporter,
SensitiveDataFilter,
} from "@mastra/observability";
import { weatherWorkflow } from "./workflows/weather-workflow";
import { weatherAgent } from "./agents/weather-agent";
import { CiNii_Agent } from "./agents/cinii_agent"; // これを追加
import {
toolCallAppropriatenessScorer,
completenessScorer,
translationScorer,
} from "./scorers/weather-scorer";
import { ollama } from "ollama-ai-provider-v2";
import 'dotenv/config'; // .envからAPIキーを詠み込むために、これが最初の方に必要です
export const mastra = new Mastra({
workflows: { weatherWorkflow },
agents: { weatherAgent, CiNii_Agent }, // ここでimportしたエージェントを詠み込む
scorers: {
toolCallAppropriatenessScorer,
completenessScorer,
translationScorer,
},
storage: new LibSQLStore({
id: "mastra-storage",
// stores observability, scores, ... into persistent file storage
url: "file:./mastra.db",
}),
logger: new PinoLogger({
name: "Mastra",
level: "info",
}),
observability: new Observability({
configs: {
default: {
serviceName: "mastra",
exporters: [
new DefaultExporter(), // Persists traces to storage for Mastra Studio
new CloudExporter(), // Sends traces to Mastra Cloud (if MASTRA_CLOUD_ACCESS_TOKEN is set)
],
spanOutputProcessors: [
new SensitiveDataFilter(), // Redacts sensitive data like passwords, tokens, keys
],
},
},
}),
});
エージェント
インストールするとサンプルの
\src\mastra\agents\weather-agent.ts
が用意されていますが、
同じフォルダ \src\mastra\agents\ に、
以下のファイルを作成します。
import { Agent } from "@mastra/core/agent";
import { huggingface } from "@ai-sdk/huggingface"; // プロバイダーをインポート
import "dotenv/config";
import { fetchJsonTool } from "../tools/fetchJsonTool";
import { Memory } from "@mastra/memory";
export const CiNii_Agent = new Agent({
id: "CiNii_Agent",
name: "CiNii_Agent",
model: "huggingface/Qwen/Qwen3.5-9B", // ここでモデルを指定
// tools または enabledTools (バージョンに合わせて)
tools: {
fetchJsonTool,
},
memory: new Memory({
options: {
lastMessages: 0, // 過去のメッセージをコンテキストに含めない
},
}),
instructions: `
必要に応じてAPIからデータを取得し、それを元にユーザーの質問に答えてください。
あなたは学術情報のリサーチアシスタントです。
ユーザーの依頼から検索キーワードを特定する際は、以下のルールを守ってください:
1. 文章から重要な名詞を2〜3個抽出する。
2.入力が英語など日本語以外の場合は、名詞を日本語に翻訳してください。
3. 抽出したキーワードを半角スペースで繋いで fetchJsonTool の query に渡す。
2. 抽出したキーワードを半角スペースで繋いで fetchJsonTool の query に渡す。
(例:「日本の少子化と経済への影響」→「日本 少子化 経済」)
fetchJsonTool は内部でこれらを " "(space) で結合して検索します。
【重要】
- 検索が必要な場合は fetchJsonTool を使用してください。
- ツールを実行した後、その結果(JSON)をそのまま表示せず、
内容を要約して「〜という論文が見つかりました」のように日本語で報告してください。
- JSONの形式({"name": ...})のまま回答を終えないでください。
3. 検索結果を、もとのクエリに関連する順番に並び替えてください。
4. 検索結果の JSON に含まれる 'title' と 'url' の値を、そのまま箇条書きで出力してください。解釈や省略は不要です。
`,
});
今回私は、GeminiとhuggingfaceのAPIで試しましたが、Ollamaを使ってローカルLLMで試す場合は、以下のようにします。
import { ollama } from "ollama-ai-provider-v2";
・・・
model: ollama("qwen3.5:9b"), //"qwen3:0.6b"),
ただ、私の環境(ノートPC、GPUなし)では、非力で実用に耐えませんでした。
順番的には、cinii_agent.ts をモジュールとして export したものを、
index.ts で
import { CiNii_Agent } from "./agents/cinii_agent";
・・・
agents: { weatherAgent, CiNii_Agent },
でインポートする、というのが正確なところです。
tool
さきほどのエージェントに
tools: {
fetchJsonTool,
},
としていた部分を
\src\mastra\tools\fetchJsonTool.ts
に作ります。
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
export const fetchJsonTool = createTool({
id: "fetchJsonTool",
description: "CiNiiからJSONを取得。複数のキーワードはスペースで入力してください。",
inputSchema: z.object({
query: z.string().describe("検索クエリ(例:'人工知能 ロボット')"),
}),
outputSchema: z.object({
data: z.any(),
status: z.number(),
}),
execute: async ({ query }) => {
// 1. 全角スペースを半角に変換し、前後の余白を削除
// 2. スペースで分割して配列にする
// 3. 空の文字列を除去して、"&" で結合する
const formattedQuery = query
.trim()
.replace(/ /g, " ") // 全角を半角へ
.split(/\s+/) // 連続する空白で分割
.join(" "); // " " で繋ぐ
return await getResult(formattedQuery);
},
});
// tools/fetchJsonTool.ts の修正例
const getResult = async (query: string) => {
const ciniiUrl = `https://cir.nii.ac.jp/opensearch/all?count=3&sortorder=0&format=json&q=${encodeURIComponent(query)}`;
try {
const response = await fetch(ciniiUrl);
const data = await response.json();
// 1. JSONのルートにある 'items' 配列を取得(存在しない場合は空配列)
const items = data.items || [];
// 2. 各 item から 'title' のみを抽出
const titles = items.map((item: any) => ({
title: item.title || "タイトルなし",
url: item.link || "URLなし",
}));
// 3. 5件分など、Agentが扱いやすいように整形して返す
return {
data: titles,
status: response.status,
};
} catch (error) {
console.error("Fetch error:", error);
return {
data: [],
status: 500,
};
}
};
冒頭で書いたとおり、簡易的に単純な fetchで、OpenSearchインターフェースからJSON形式でデータを取得しています。
テストで無料の範囲でやっているので、数件のみ取得しています。
起動
で、これを起動します。
npm run dev
> mymastraapp@1.0.0 dev
> mastra dev
◐ Preparing development environment...
✓ Initial bundle complete
◇ Starting Mastra dev server...
mastra-cloud-observability-exporter disabled: MASTRA_CLOUD_ACCESS_TOKEN environment variable not set.
mastra 1.3.7 ready in 38686 ms
│ Studio: http://localhost:4111
│ API: http://localhost:4111/api
◯ watching for file changes...
40秒くらいかかりました。
http://localhost:4111
にアクセスします。
回答例
生成AIによるエージェントの活用の動向について調べてください
とすると、
生成AIの活用に関する以下の論文が見つかりました。
生成AIの活用によるデスクワーカーの生産性向上 : AIエージェントは労働力不足を補うか?
url: https://cir.nii.ac.jp/crid/1520307579484898048
MCP、AIエージェント連携による生成AIを用いた中小企業の活用事例
url: https://cir.nii.ac.jp/crid/1390588534714008704
AIエージェントを活用した知財とマーケティング分析の自動化・効率化 : GeminiとPerplexity Patentsの連携による市場起点の技術開発戦略
url: https://cir.nii.ac.jp/crid/1520307037461227136
のような結果が出ました。
LLMがキーワードを生成するので、英語で
tell me about Edo Bakufu?
などと聞いても、日本語で検索してくれます。
MCP不要
ちまたでは検索用のMCPサーバも流行っていますが、今回のような簡単なケースでは、検索結果を普通に fetch するだけでそこそこのことはできると思います。