13
5

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入門】新卒エンジニアがMastra触ってみた

Last updated at Posted at 2025-06-06

はじめに

はじめまして、いのせです。

本記事では、Mastraの概要とGetting Startedを実際に触った一連の流れを説明します。

Mastraとは

Mastraは、TypeScript(JavaScript)で書かれたオープンソースのAIエージェント開発フレームワークです。大規模言語モデル(LLM)を活用し、チャットボットや自律的にタスクを実行するAIエージェントを簡単に作成・運用できるよう設計されています。

スクリーンショット 2025-06-04 14.07.58.png

主な特徴

  • ワークフロー: 複雑なAIタスクを段階的に処理
  • エージェント: 自律的に動作するAIエージェントの構築
  • RAG: 検索拡張生成(Retrieval-Augmented Generation)の実装
  • 統合機能: 外部サービスとの連携
  • 評価機能: AIシステムの性能評価

環境構築

前提条件

  • Node.js 20以上
  • npm または yarn
  • TypeScriptの基本知識

1. プロジェクト作成

# Mastraプロジェクトの作成
npm create mastra@latest
cd my-mastra-app

# 依存関係のインストール
npm install

# 開発サーバー起動
npm run dev
my-mastra-app/
├── src/
│   └── mastra/
│       ├── agents/          # AIエージェント定義
│       │   └── weather-agent.ts
│       ├── tools/           # ツール定義
│       │   └── weather-tool.ts
│       ├── workflows/       # ワークフロー定義
│       │   └── weather-workflow.ts
│       └── index.ts         # Mastraのエントリーポイントを定義
├── mastra.config.ts
├── .env                 # OPENAI_API_KEY を格納
└── package.json

各ファイルの作成

ツール

src/mastra/tools/weather-tool.ts
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
 
interface WeatherResponse {
  current: {
    time: string;
    temperature_2m: number;
    apparent_temperature: number;
    relative_humidity_2m: number;
    wind_speed_10m: number;
    wind_gusts_10m: number;
    weather_code: number;
  };
}
 
export const weatherTool = createTool({
  id: "get-weather",
  description: "Get current weather for a location",
  inputSchema: z.object({
    location: z.string().describe("City name"),
  }),
  outputSchema: z.object({
    temperature: z.number(),
    feelsLike: z.number(),
    humidity: z.number(),
    windSpeed: z.number(),
    windGust: z.number(),
    conditions: z.string(),
    location: z.string(),
  }),
  execute: async ({ context }) => {
    return await getWeather(context.location);
  },
});
 
const getWeather = async (location: string) => {
  const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`;
  const geocodingResponse = await fetch(geocodingUrl);
  const geocodingData = await geocodingResponse.json();
 
  if (!geocodingData.results?.[0]) {
    throw new Error(`Location '${location}' not found`);
  }
 
  const { latitude, longitude, name } = geocodingData.results[0];
 
  const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`;
 
  const response = await fetch(weatherUrl);
  const data: WeatherResponse = await response.json();
 
  return {
    temperature: data.current.temperature_2m,
    feelsLike: data.current.apparent_temperature,
    humidity: data.current.relative_humidity_2m,
    windSpeed: data.current.wind_speed_10m,
    windGust: data.current.wind_gusts_10m,
    conditions: getWeatherCondition(data.current.weather_code),
    location: name,
  };
};
 
function getWeatherCondition(code: number): string {
  const conditions: Record<number, string> = {
    0: "Clear sky",
    1: "Mainly clear",
    2: "Partly cloudy",
    3: "Overcast",
    45: "Foggy",
    48: "Depositing rime fog",
    51: "Light drizzle",
    53: "Moderate drizzle",
    55: "Dense drizzle",
    56: "Light freezing drizzle",
    57: "Dense freezing drizzle",
    61: "Slight rain",
    63: "Moderate rain",
    65: "Heavy rain",
    66: "Light freezing rain",
    67: "Heavy freezing rain",
    71: "Slight snow fall",
    73: "Moderate snow fall",
    75: "Heavy snow fall",
    77: "Snow grains",
    80: "Slight rain showers",
    81: "Moderate rain showers",
    82: "Violent rain showers",
    85: "Slight snow showers",
    86: "Heavy snow showers",
    95: "Thunderstorm",
    96: "Thunderstorm with slight hail",
    99: "Thunderstorm with heavy hail",
  };
  return conditions[code] || "Unknown";
}

エージェント

src/agents/weather-agent.ts
import { openai } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
import { weatherTool } from "../tools/weather-tool";
 
export const weatherAgent = new Agent({
  name: "Weather Agent",
  instructions: `You are a helpful weather assistant that provides accurate weather information.
 
Your primary function is to help users get weather details for specific locations. When responding:
- Always ask for a location if none is provided
- If the location name isn't in English, please translate it
- Include relevant details like humidity, wind conditions, and precipitation
- Keep responses concise but informative
 
Use the weatherTool to fetch current weather data.`,
  model: openai("gpt-4o-mini"),
  tools: { weatherTool },
});

ワークフロー

src/mastra/workflows/weather-workflow.ts
import { openai } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
import { createStep, createWorkflow } from "@mastra/core/workflows";
import { z } from "zod";
 
const llm = openai("gpt-4o-mini");
 
const agent = new Agent({
  name: "Weather Agent",
  model: llm,
  instructions: `
        あなたは天気に基づく計画立案に優れた地域活動・旅行の専門家です。天気データを分析し、実用的な活動の推奨事項を提供してください。
 
        予報の各日について、以下の通りに正確に構造化して回答してください:
 
        📅 [曜日、月 日、年]
        ═══════════════════════════
 
        🌡️ 天気概要
        • 状況:[簡潔な説明]
        • 気温:[X°C/Y°F から A°C/B°F]
        • 降水確率:[X%]
 
        🌅 午前の活動
        屋外:
        • [活動名] - [具体的な場所/ルートを含む簡潔な説明]
          最適な時間:[具体的な時間帯]
          注意:[関連する天気の考慮事項]
 
        🌞 午後の活動
        屋外:
        • [活動名] - [具体的な場所/ルートを含む簡潔な説明]
          最適な時間:[具体的な時間帯]
          注意:[関連する天気の考慮事項]
 
        🏠 屋内の代替案
        • [活動名] - [具体的な会場を含む簡潔な説明]
          適している条件:[この代替案を選ぶきっかけとなる天気条件]
 
        ⚠️ 特別な考慮事項
        • [関連する天気警報、UV指数、風の状況など]
 
        ガイドライン:
        - 1日あたり2-3の時間指定屋外活動を提案
        - 1-2の屋内バックアップオプションを含める
        - 降水確率が50%を超える場合は、屋内活動を優先
        - すべての活動は場所に特化したものでなければならない
        - 具体的な会場、トレイル、または場所を含める
        - 気温に基づいて活動の強度を考慮
        - 説明は簡潔だが情報豊富に保つ
 
        一貫性のため、示されている絵文字とセクションヘッダーを使用して、この正確なフォーマットを維持してください。
      `,
});
 
const forecastSchema = z.object({
  date: z.string(),
  maxTemp: z.number(),
  minTemp: z.number(),
  precipitationChance: z.number(),
  condition: z.string(),
  location: z.string(),
});
 
function getWeatherCondition(code: number): string {
  const conditions: Record<number, string> = {
    0: "Clear sky",
    1: "Mainly clear",
    2: "Partly cloudy",
    3: "Overcast",
    45: "Foggy",
    48: "Depositing rime fog",
    51: "Light drizzle",
    53: "Moderate drizzle",
    55: "Dense drizzle",
    61: "Slight rain",
    63: "Moderate rain",
    65: "Heavy rain",
    71: "Slight snow fall",
    73: "Moderate snow fall",
    75: "Heavy snow fall",
    95: "Thunderstorm",
  };
  return conditions[code] || "Unknown";
}
 
const fetchWeather = createStep({
  id: "fetch-weather",
  description: "指定された都市の天気予報を取得します",
  inputSchema: z.object({
    city: z.string().describe("天気を取得する都市"),
  }),
  outputSchema: forecastSchema,
  execute: async ({ inputData }) => {
    if (!inputData) {
      throw new Error("Input data not found");
    }
 
    const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(inputData.city)}&count=1`;
    const geocodingResponse = await fetch(geocodingUrl);
    const geocodingData = (await geocodingResponse.json()) as {
      results: { latitude: number; longitude: number; name: string }[];
    };
 
    if (!geocodingData.results?.[0]) {
      throw new Error(`Location '${inputData.city}' not found`);
    }
 
    const { latitude, longitude, name } = geocodingData.results[0];
 
    const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=precipitation,weathercode&timezone=auto,&hourly=precipitation_probability,temperature_2m`;
    const response = await fetch(weatherUrl);
    const data = (await response.json()) as {
      current: {
        time: string;
        precipitation: number;
        weathercode: number;
      };
      hourly: {
        precipitation_probability: number[];
        temperature_2m: number[];
      };
    };
 
    const forecast = {
      date: new Date().toISOString(),
      maxTemp: Math.max(...data.hourly.temperature_2m),
      minTemp: Math.min(...data.hourly.temperature_2m),
      condition: getWeatherCondition(data.current.weathercode),
      precipitationChance: data.hourly.precipitation_probability.reduce(
        (acc, curr) => Math.max(acc, curr),
        0,
      ),
      location: inputData.city,
    };
 
    return forecast;
  },
});
 
const planActivities = createStep({
  id: "plan-activities",
  description: "天気条件に基づいてアクティビティを提案します",
  inputSchema: forecastSchema,
  outputSchema: z.object({
    activities: z.string(),
  }),
  execute: async ({ inputData }) => {
    const forecast = inputData;
 
    if (!forecast) {
      throw new Error("Forecast data not found");
    }
 
    const prompt = `${forecast.location}の以下の天気予報に基づいて、適切なアクティビティを提案してください:
      ${JSON.stringify(forecast, null, 2)}
      `;
 
    const response = await agent.stream([
      {
        role: "user",
        content: prompt,
      },
    ]);
 
    let activitiesText = "";
 
    for await (const chunk of response.textStream) {
      process.stdout.write(chunk);
      activitiesText += chunk;
    }
 
    return {
      activities: activitiesText,
    };
  },
});const weatherWorkflow = createWorkflow({
  id: "weather-workflow",
  inputSchema: z.object({
    city: z.string().describe("天気を取得する都市"),
  }),
  outputSchema: z.object({
    activities: z.string(),
  }),
})
  .then(fetchWeather)
  .then(planActivities);
 
weatherWorkflow.commit();
 
export { weatherWorkflow };

エージェントの登録

src/mastra/index.ts
import { Mastra } from "@mastra/core";
import { PinoLogger } from "@mastra/loggers";
import { LibSQLStore } from "@mastra/libsql";
import { weatherWorkflow } from "./workflows/weather-workflow";
import { weatherAgent } from "./agents/weather-agent";
 
export const mastra = new Mastra({
  workflows: { weatherWorkflow },
  agents: { weatherAgent },
  storage: new LibSQLStore({
    // stores telemetry, evals, ... into memory storage, if it needs to persist, change to file:../mastra.db
    url: ":memory:",
  }),
  logger: new PinoLogger({
    name: "Mastra",
    level: "info",
  }),
});

開発サーバーの起動

以下のコマンドを実行してサーバーを起動します。

npm run dev

実行結果

qiita-1.png

おわりに

今回はMastraのGettingStartedを一通り動かしてみました。
「Mastraの勉強をしよう!」と思っても日本語での記事はほぼなく、なんやかんやで公式ドキュメントを使いました。
Mastraの先駆者となれるように、これからもたくさん触っていこうと思います🔥

13
5
2

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
13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?