2
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?

「npx create-mastra」で作った Mastra のプロジェクト(ツールを使うエージェントの実装)と LM Studio でのローカルLLM(OpenAI互換API を扱うもの)を組み合わせる

Last updated at Posted at 2025-09-03

はじめに

過去に書いた、以下の記事でやっていたものを組み合わせた構成で、ローカルLLM をやってみたという話の記事です。

●Mastra の AgentNetwork を使って「複数エージェントと MCPサーバーの組み合わせ」を試す - Qiita
 https://qiita.com/youtoy/items/8a20d6c1a2af6d82826c

●Mastra と LM Studio を使ったローカルLLM(Agent.streamVNext()/generateVNext() を使う)【Node.js】 - Qiita
 https://qiita.com/youtoy/items/b401bea6b0acd1ae17d6

●Vercel の AI SDK と LM Studio を使ったローカルLLM を組み合わせて試す【Node.js】 - Qiita
 https://qiita.com/youtoy/items/7ec636b7086f107a9f5d

もう少し補足

もう少し補足すると、今回やった内容は以下のとおりです。

  • ローカルLLM のためのローカルサーバーを LM Studio で準備する
    • ローカルLLM のモデルはツールを使えるモデルをセット(今回は「jan-v1-4b」)
  • 「npx create-mastra」コマンドを使い、OpenAIプロバイダーを利用するプロジェクトを作る
  • 上記のプロジェクトのプロバイダー関連の部分について、「OpenAIプロバイダーを使う部分を、LM Studio で立てたローカルサーバー(OpenAI互換API を利用したローカルLLM用のもの)を使う形」に変更する
    • OpenAI互換API を使うプロバイダー用のパッケージを追加する
    • コードでは、LM Studio を使ったローカルサーバーの OpenAI互換API を使うようにする

実際にやっていく

実際に上記を進めていきます。

LM Studio を使ったローカルLLM用のローカルサーバーの準備

LM Studio に関する下準備は、上で掲載していた記事の「LM Studio の準備」の部分をご参照ください。

LM Studio をローカルサーバーとして動作させ、そのローカルサーバーで OpenAI互換の API を使える状態( http://localhost:1234/v1 をベースURL として、API を呼び出せる状態)
にできれば OK です。

「npx create-mastra」コマンドを使ったプロジェクト作成

次に、上で掲載していた過去記事でもやっていた、「npx create-mastra」コマンドを使ったプロジェクト作成を進めます。

npx create-mastra

上記のコマンドを実行した後に進める手順の中で、以下のプロバイダーを選ぶ選択肢の部分では、ひとまず仮に OpenAI を選んで進めます(これは後で、ローカルでの OpenAI互換API を利用するように置きかえます)。

2025-09-03_17-29-41.jpg

この手順を進めると、以下までで一区切りという感じになります。

2025-09-03_17-30-01.jpg

インストールされたものの一覧を確認する

ここで、「my-mastra-app」フォルダにインストールされたパッケージの情報を確認してみます。具体的には、以下がインストールされた状態になりました。

2025-09-03_17-30-56.jpg

パッケージを追加する

今回、OpenAI の API を使うのではなく、その部分は LM Studio でたてたローカルサーバーの API を使う形(ローカルLLM)にします。

そのために、上で掲載していた 3つの記事の中の、2番目・3番目の記事で使っていた以下のパッケージを追加します。

●OpenAI Compatible Providers
 https://ai-sdk.dev/providers/openai-compatible-providers

具体的には、以下などで説明が書かれている「Vercel の AI SDK の OpenAI Compatible Providers( @ai-sdk/openai-compatible )」になります。

●OpenAI Compatible Providers: LM Studio
 https://ai-sdk.dev/providers/openai-compatible-providers/lmstudio

パッケージ追加用のコマンド

パッケージを追加するため、「my-mastra-app」フォルダへの移動とパッケージインストールの それぞれを行うコマンドを実行します。具体的には以下のとおりです。

cd my-mastra-app
npm i @ai-sdk/openai-compatible

上記のコマンドを実行した後、「my-mastra-app」フォルダにインストールされたパッケージの情報を確認してみます。そうすると、以下のとおり @ai-sdk/openai-compatible が追加されたことが確認できました。

2025-09-03_17-32-44.jpg

コードの書きかえ

それとコードも少し書きかえます。この後に、どのファイルのコードを書きかえるかを説明します。

「my-mastra-app/src」フォルダ以下の構成

この時、「my-mastra-app/src」フォルダ以下は、次のような構成になっています。

2025-09-03_23-02-17.jpg

書きかえ対象のコード

書きかえ対象は上記の中の「mastra/agents/weather-agent.ts」で、そのコードの中身は以下になります。

mastra/agents/weather-agent.ts(元の内容)
import { openai } from '@ai-sdk/openai';
import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/libsql';
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 and can help planning activities based on the weather.

      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
      - If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part (e.g. "New York")
      - Include relevant details like humidity, wind conditions, and precipitation
      - Keep responses concise but informative
      - If the user asks for activities and provides the weather forecast, suggest activities based on the weather forecast.
      - If the user asks for activities, respond in the format they request.

      Use the weatherTool to fetch current weather data.
`,
  model: openai('gpt-4o-mini'),
  tools: { weatherTool },
  memory: new Memory({
    storage: new LibSQLStore({
      url: 'file:../mastra.db', // path is relative to the .mastra/output directory
    }),
  }),
});

これに最小限の変更を加えて、OpenAI の API を使うのではなく、ローカルサーバーの OpenAI互換API を使うような処理にします。

書きかえ後のコード

書きかえなどを行った後のコードを示します。書きかえなどを行った箇所は、以下のコメントをつけています。

  • コメントアウトしたところ:
    • // ★ここをコメントアウト
  • コードを足したところ:
    • // ★ここを追加
    • // ★以下 5行を追加

以下が具体的なコードの内容です。

mastra/agents/weather-agent.ts(書きかえ後の内容)
// import { openai } from '@ai-sdk/openai'; // ★ここをコメントアウト
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; // ★ここを追加
import { Agent } from "@mastra/core/agent";
import { Memory } from "@mastra/memory";
import { LibSQLStore } from "@mastra/libsql";
import { weatherTool } from "../tools/weather-tool";

// ★以下 5行を追加
const lmstudio = createOpenAICompatible({
  name: "lmstudio",
  baseURL: "http://localhost:1234/v1",
  apiKey: "lm-studio",
});

export const weatherAgent = new Agent({
  name: "Weather Agent",
  instructions: `
      You are a helpful weather assistant that provides accurate weather information and can help planning activities based on the weather.

      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
      - If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part (e.g. "New York")
      - Include relevant details like humidity, wind conditions, and precipitation
      - Keep responses concise but informative
      - If the user asks for activities and provides the weather forecast, suggest activities based on the weather forecast.
      - If the user asks for activities, respond in the format they request.

      Use the weatherTool to fetch current weather data.
`,
  // model: openai('gpt-4o-mini'), // ★ここをコメントアウト
  model: lmstudio("jan-v1-4b"), // ★ここを追加
  tools: { weatherTool },
  memory: new Memory({
    storage: new LibSQLStore({
      url: "file:../mastra.db", // path is relative to the .mastra/output directory
    }),
  }),
});

「npx create-mastra」で作った Mastra のプロジェクトのコードは、ツールを利用するエージェントの実装になっていたので、LM Studio で扱うローカルLLM用のモデルは「ツール利用」に対応したモデルを設定しています。具体的には、過去のローカルLLM のお試しで扱かったものの 1つである、以下の「janhq/Jan-v1-4B-GGUF」です。

●M4 の MacBook Air でローカルLLM(2種): MLX版と公式の GGUF版の「Jan-v1-4B」をそれぞれ軽く試す(MLX LM と LM Studio を利用) - Qiita
 https://qiita.com/youtoy/items/dc8818981b7baff5dc08

●janhq/Jan-v1-4B-GGUF · Hugging Face
 https://huggingface.co/janhq/Jan-v1-4B-GGUF

その他のコードは、変更を加えていません。

今回、変更を加えていない他のコードも、念のため以下で折りたたみで掲載しておきます。

【折りたたみ】(変更を加えていない 3つのファイル)
mastra/index.ts(用意されたものそのまま)

import { Mastra } from '@mastra/core/mastra';
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',
  }),
});
mastra/tools/weather-tool.ts(用意されたものそのまま)
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';

interface GeocodingResponse {
  results: {
    latitude: number;
    longitude: number;
    name: string;
  }[];
}
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()) as GeocodingResponse;

  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 = (await response.json()) as WeatherResponse;

  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';
}
mastra/workflows/weather-workflow.ts(用意されたものそのまま)
import { createStep, createWorkflow } from '@mastra/core/workflows';
import { z } from 'zod';

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: 'Fetches weather forecast for a given city',
  inputSchema: z.object({
    city: z.string().describe('The city to get the weather for'),
  }),
  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: name,
    };

    return forecast;
  },
});

const planActivities = createStep({
  id: 'plan-activities',
  description: 'Suggests activities based on weather conditions',
  inputSchema: forecastSchema,
  outputSchema: z.object({
    activities: z.string(),
  }),
  execute: async ({ inputData, mastra }) => {
    const forecast = inputData;

    if (!forecast) {
      throw new Error('Forecast data not found');
    }

    const agent = mastra?.getAgent('weatherAgent');
    if (!agent) {
      throw new Error('Weather agent not found');
    }

    const prompt = `Based on the following weather forecast for ${forecast.location}, suggest appropriate activities:
      ${JSON.stringify(forecast, null, 2)}
      For each day in the forecast, structure your response exactly as follows:

      📅 [Day, Month Date, Year]
      ═══════════════════════════

      🌡️ WEATHER SUMMARY
      • Conditions: [brief description]
      • Temperature: [X°C/Y°F to A°C/B°F]
      • Precipitation: [X% chance]

      🌅 MORNING ACTIVITIES
      Outdoor:
      • [Activity Name] - [Brief description including specific location/route]
        Best timing: [specific time range]
        Note: [relevant weather consideration]

      🌞 AFTERNOON ACTIVITIES
      Outdoor:
      • [Activity Name] - [Brief description including specific location/route]
        Best timing: [specific time range]
        Note: [relevant weather consideration]

      🏠 INDOOR ALTERNATIVES
      • [Activity Name] - [Brief description including specific venue]
        Ideal for: [weather condition that would trigger this alternative]

      ⚠️ SPECIAL CONSIDERATIONS
      • [Any relevant weather warnings, UV index, wind conditions, etc.]

      Guidelines:
      - Suggest 2-3 time-specific outdoor activities per day
      - Include 1-2 indoor backup options
      - For precipitation >50%, lead with indoor activities
      - All activities must be specific to the location
      - Include specific venues, trails, or locations
      - Consider activity intensity based on temperature
      - Keep descriptions concise but informative

      Maintain this exact formatting for consistency, using the emoji and section headers as shown.`;

    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('The city to get the weather for'),
  }),
  outputSchema: z.object({
    activities: z.string(),
  }),
})
  .then(fetchWeather)
  .then(planActivities);

weatherWorkflow.commit();

export { weatherWorkflow };

コマンドでプレイグラウンドを立ち上げる

あとは、以下のコマンドで、ブラウザから Mastra を使った処理を試せるようにします(※ Mastra のプライグラウンドを使います)。

npm run dev

上記のコマンドを実行すると、以下のように「 http://localhost:4111 」でプレイグラウンドを使える、という表示がでてきます。

2025-09-03_23-33-16.jpg

プレイグラウンドでチャットを行う

プレイグラウンドでチャット画面を開く

ブラウザでプレイグラウンド( http://localhost:4111 )にアクセスすると、ブラウザ上で以下の内容が表示されました。

左メニューの「Agents」が選ばれた状態になっていますが、その右の部分には「Weather Agent」が項目として表示されています。その項目の右のほうを見ると「jan-v1-4b」がモデルで使われているという表示も確認できます。

2025-09-03_23-33-57.jpg

そして「Weather Agent」を選ぶと、以下のチャット用の画面が出てきます。

2025-09-03_23-34-25.jpg

上記の画面でも「jan-v1-4b」がモデルで使われているということが表示されています。それと、ツール「get-weather」が利用されるツール一覧に出ているのも確認できます。

チャットのお試し1

まずはチャット用画面でのシンプルなやりとりを試してみます。

この画面で、「あなたは誰?」というプロンプトを入れてみました。そうすると、推論の処理が行われて、最終的に「私は天気アシスタントです。特定の場所の天気情報を提供し、活動計画を助けることができます。場所を教えてください!」という内容が返ってきました。

2025-09-03_23-36-07.jpg

2025-09-03_23-36-19.jpg

チャットのお試し2

今度は、ツールが呼び出されるようなプロンプトを入れてみます。

プロンプトは「東京の天気は?」という内容にしました。これを入れると、以下のように「location に Tokyo が指定された形」で「weatherTool」が呼び出されたという表示が出てきました。

2025-09-03_23-41-28.jpg

そして、以下のようにツールでの処理が行われ(※ 緑で示した部分)、最後のところには、ツールで得られた結果を使った返答が出てきました(※ 赤で示した部分)。

2025-09-03_23-42-50.jpg

これで、LM Studio を使ってたてたローカルサーバーの OpenAI互換の API(Jan-v1-4B を使ったローカルLLM によるもの)を、Mastra で作ったプロジェクト(ツールを使う Agent の実装)から扱うことができました。

2
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
2
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?