💨

Mastra でローカルの LLM を使ったエージェントを作る

に公開

こんにちは、初めましての方は初めまして。かわらです。気付けば 2025 年も終わりに近づき、今年もびっくりするくらい速く駆け抜けていってしまったなと感じています。今年の目標は達成できていませんが、めげずに来年も目標を立てて頑張っていきます。まあ目標なんて高ければ高いほど良いですからね。

この記事は「AIエージェント構築&運用 Advent Calendar 2025」の 14 日目の記事です。タイトルの通り、ローカルで動かせる LLM を使って Mastra でエージェントを作ってみたものになります。いつも実装するときは LangGraph を使うことが多い(僕が Python しか分からないので)のですが、TS も今後使えた方がいいだろうと思ったのと、アドベントカレンダーでエージェントを作る記事を書くという機会もあって以前から気になっていた Mastra を使うことにしました。

この記事でやりたいこと

  • Mastra を使って AI エージェントを動かす
  • エージェントの LLM をローカルでホストする

以上の二点を目標に実装していきます。エージェントとしては以下の二つを実装してみます。

  • URL を与えると、fetch してその内容を要約してくれるエージェント(要約エージェント)
  • 汎用的なエージェントとして機能しつつ、ツールが足りない場合にツールの実装を行うエージェント(自己拡張エージェント)

実際にやってみる

Mastra と LMStudio の準備

Mastra と LMStudio のインストールをします。

Mastra は以下のコマンドを実行するとアプリ用のディレクトリが構成され、その中にデモ用のファイルが作成されます。どのプロジェクトの名前やどのプロバイダーを選ぶか、どの IDE を選ぶかなどが聞かれるので、お好みの設定を選択しましょう。--no-example を付けることでデモ用のファイルを作らないようにしたり、--template で指定することでテンプレートを変えたりも出来ます。

npm create mastra@latest

次に LMStudio のインストールをします。GUI アプリなので、https://lmstudio.ai/ からファイルをダウンロードしてインストールしましょう。インストール後に好きなモデルを選択してダウンロードしておき、LMStudio 側で API の設定をしておきます。この記事では gpt-oss-20b を使っていこうと思います(これ以上の LLM は僕の PC では動かないため…)ちなみに LMStudio は OpenAI 互換の API を用意している[1]ため、Mastra 以外からも(OpenAI の API を叩けるなら)使用することが可能です。

以上二つで準備は完了です。次にエージェントの実装をしていきます。

要約エージェントの実装

まずは LMStudio を使うための実装をします。とはいってもこの記事 [2] を参考にすればすぐに完成します。AI SDK の createOpenAICompatible クラスを使い、URL をローカルに向けるだけでオッケーです。

summarize-agent.ts
import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/libsql';
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { summarizeURLTool } from '../tools/summarize-tools';

const localLLM = createOpenAICompatible({
  name: "lmstudio",
  baseURL: "http://192.168.11.2:1234/v1",
})

export const SummarizeAgent = new Agent({
  name: "Agent for Summarizing of URL contents",
  instructions: `
    あなたは与えられた URL からその内容を要約するエージェントです。

    ユーザーのメッセージに URL が含まれている場合:
    1. summarizeURLTool を使ってユーザーのメッセージを渡してください
    2. ツールが返した summary をユーザーに返答してください
    3. 処理を終了してください

    URL がない場合:
    - 「URL を教えてください」と返してください
    `,
  model: localLLM("openai/gpt-oss-20b"),
  tools: {
    summarizeURLTool,
  },
  maxRetries: 1,
  memory: new Memory({
    storage: new LibSQLStore({
      url: "file:../../mastra.db",
      // url: ":memory:",
    }),
  }),
});

AI SDK の createOpenAICompatible で OpenAI 互換の API を持ったモデルを定義し、localLLM("openai/gpt-oss-20b") を model として渡すことで、LMStudio の openai/gpt-oss-20b を使ってくれるようになります。とても簡単にローカル LLM が使用できますね。

次にツールを定義しあmす。このエージェントでは「URL を渡すと、その内容を渡してくれるツール」を一つだけ定義しています。要約部分では LMStudio でホストしている LLM を使って行います。createOpenAICompatible で作ったインスタンスを generateText 関数に渡すことで、そのモデルを使ってテキストの生成が行えます(ちなみにツール内の実装は Claude Code で生成してもらいました。自分で書いたらエラーが出てビルド出来なかったので…)

summarize-tools.ts
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { generateText } from 'ai';
import { load } from 'cheerio';

const localLLM = createOpenAICompatible({
    name: "lmstudio",
    baseURL: "http://192.168.11.2:1234/v1",
});

export const summarizeURLTool = createTool({
    id: "summarize-url",
    description: "ユーザーのメッセージから URL を抽出し、その内容を取得して要約します。",
    inputSchema: z.object({
        userMessage: z.string().describe("ユーザーからのメッセージ(URL を含む)"),
    }),
    outputSchema: z.object({
        summary: z.string().describe("URL の内容を要約したテキスト"),
        url: z.string().describe("抽出した URL"),
    }),

    execute: async ({ context }) => {
        // 1. URL を抽出
        const extractedURL = await extractURL(context.userMessage);

        if (!extractedURL) {
            throw new Error("URL が見つかりませんでした");
        }

        // 2. URL の内容を取得
        const content = await fetchURL(extractedURL);

        // 3. 内容を要約
        const summary = await summarizeContent(content);

        return {
            summary,
            url: extractedURL,
        };
    }
});

const extractURL = async (text: string): Promise<string> => {
    const result = await generateText({
        model: localLLM("openai/gpt-oss-20b"),
        prompt: `次のテキストから URL を抜き出してください。URL らしいテキストであれば抜き出してください(例えば最初の h が抜けていたり、全角であっても抜き出してください)\n生成するテキストは抽出した URL のみにしてください。\n\n${text}`,
    });

    return result.text.trim();
};

const fetchURL = async (url: string): Promise<string> => {
    try {
        const response = await fetch(url);

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const html = await response.text();

        // cheerio で HTML をパースしてテキストのみを抽出
        const $ = load(html);

        // script, style, nav, header, footer などの不要な要素を削除
        $('script, style, nav, header, footer, iframe, noscript').remove();

        // body のテキストを抽出(body がなければ全体から)
        const bodyText = $('body').length > 0
            ? $('body').text()
            : $.text();

        // 連続する空白や改行を整理
        const cleanedText = bodyText
            .replace(/\s+/g, ' ')  // 連続する空白を1つに
            .trim();

        // 文字数制限(例: 最大10000文字)
        const maxLength = 10000;
        return cleanedText.length > maxLength
            ? cleanedText.substring(0, maxLength) + '...'
            : cleanedText;

    } catch (error) {
        throw new Error(`Failed to fetch URL: ${error instanceof Error ? error.message : String(error)}`);
    }
};

const summarizeContent = async (content: string): Promise<string> => {
    const result = await generateText({
        model: localLLM("openai/gpt-oss-20b"),
        prompt: `次のテキストはあるWebページから抜き出してきたものです。このテキストを簡潔にまとめてください。思考は英語で構いませんが、最終的な要約は日本語で出力してください。\n\n${content}`,
    });

    return result.text;
};

エージェントとツールが出来たら、最後に Mastra 側へエージェントの設定をします。ここでは単純に Mastra に使用できるエージェントを追加するだけです。

index.ts
import { Mastra } from '@mastra/core/mastra';
import { PinoLogger } from '@mastra/loggers';
import { LibSQLStore } from '@mastra/libsql';
import { SummarizeAgent } from './agents/summarize-agent';
import { selfExpandingAgent } from './agents/self-expanding/self-expanding-agent';

export const mastra = new Mastra({
  agents: {
    SummarizeAgent,
    SelfExpandingAgent: selfExpandingAgent.getAgent(),
  },
  storage: new LibSQLStore({
    // stores observability, scores, ... into memory storage, if it needs to persist, change to file:../mastra.db
    url: "file:../../mastra.db",
  }),
  logger: new PinoLogger({
    name: 'Mastra',
    level: 'info',
  }),
  telemetry: {
    // Telemetry is deprecated and will be removed in the Nov 4th release
    enabled: false,
  },
  observability: {
    // Enables DefaultExporter and CloudExporter for AI tracing
    default: { enabled: true },
  },
});

先ほど実装したエージェントを agents に渡すことで、Mastra からエージェントが使えます(あとで実装する自己拡張エージェントも既に入っていますが、無視してください)

ここまで出来たら、Mastra Studio を起動してエージェントを実際に動かしてみます。プロジェクトルートディレクトリで npm run dev と打つと Mastra Studio が立ち上がると思うので、localhost:4111 にアクセスしてみましょう。


実際に URL を投げてみた例

回答を見るとちゃんとユーザーから投げられた URL の内容を読み取って、要約してくれていることが分かるかと思います。また、右側の Overview を見ると LMStudio の openai/gpt-oss-20b をちゃんと使えていることが分かります。以上のように簡単にツールを持ったエージェントをローカル LLM で実行することができました。

自己拡張エージェントの実装

ここでも上で説明したのとほとんど同じで、エージェントとツールの実装を行います。ただし、ツールは動的に増減させたいので、先ほどのように固定のツールをインポートするのではなくツール管理用のクラスを定義してそこからツールを読み出せるようにします。

dynamic-tool-loader.ts
import * as path from 'path';
import * as fs from 'fs/promises';
import { Tool } from '@mastra/core/tools';

/**
 * 生成されたツールを動的にロードするユーティリティ
 */
export class DynamicToolLoader {
    private toolsCache: Map<string, Tool> = new Map();
    private generatedToolsDir: string;

    constructor() {
        // mastra 実行環境では process.cwd() が .mastra/output を返すため、
        // プロジェクトルートからの相対パスを構築
        const projectRoot = process.cwd().replace(/\.mastra\/output$/, '');
        this.generatedToolsDir = path.join(projectRoot, 'src/mastra/tools/generated');
    }

    /**
     * 指定されたツールIDのツールをロード
     */
    async loadTool(toolId: string): Promise<Tool | null> {
        // キャッシュにあればそれを返す
        if (this.toolsCache.has(toolId)) {
            return this.toolsCache.get(toolId)!;
        }

        try {
            // ビルドプロセスを通すため、相対パスでインポート
            // 注: この実装では process.cwd() が .mastra/output なので、
            // ../../src/mastra/tools/generated からインポートする
            const relativeImportPath = `../../src/mastra/tools/generated/${toolId}.ts`;

            const module = await import(relativeImportPath);

            // エクスポートされたツールを探す
            const tool = this.findToolInModule(module);

            if (!tool) {
                console.error(`ツールが見つかりません: ${toolId}`);
                return null;
            }

            // キャッシュに保存
            this.toolsCache.set(toolId, tool);

            return tool;
        } catch (error) {
            console.error(`ツールのロードに失敗: ${toolId}`, error);
            return null;
        }
    }

    /**
     * すべての生成済みツールをロード
     */
    async loadAllGeneratedTools(): Promise<Record<string, Tool>> {
        const tools: Record<string, Tool> = {};

        try {
            // ディレクトリが存在しない場合は作成
            await fs.mkdir(this.generatedToolsDir, { recursive: true });

            const files = await fs.readdir(this.generatedToolsDir);
            const tsFiles = files.filter(f => f.endsWith('.ts'));

            for (const file of tsFiles) {
                const toolId = file.replace(/\.ts$/, '');
                const tool = await this.loadTool(toolId);

                if (tool) {
                    tools[this.getToolVariableName(toolId)] = tool;
                }
            }

            return tools;
        } catch (error) {
            console.error('生成済みツールのロードに失敗:', error);
            return {};
        }
    }

    /**
     * ツールのキャッシュをクリア(再ロード時に使用)
     */
    clearCache(): void {
        this.toolsCache.clear();
    }

    /**
     * 特定のツールをキャッシュから削除
     */
    removeTool(toolId: string): void {
        this.toolsCache.delete(toolId);
    }

    /**
     * モジュールからツールを探す
     */
    private findToolInModule(module: any): Tool | null {
        // default export をチェック
        if (module.default && this.isValidTool(module.default)) {
            return module.default;
        }

        // named exports をチェック
        for (const key of Object.keys(module)) {
            if (this.isValidTool(module[key])) {
                return module[key];
            }
        }

        return null;
    }

    /**
     * オブジェクトがツールとして有効かチェック
     */
    private isValidTool(obj: any): obj is Tool {
        return (
            obj &&
            typeof obj === 'object' &&
            'id' in obj &&
            'execute' in obj &&
            typeof obj.execute === 'function'
        );
    }

    /**
     * ツールIDから変数名を生成(例: weather-tool -> weatherTool)
     */
    private getToolVariableName(toolId: string): string {
        return toolId.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) + 'Tool';
    }

    /**
     * 生成されたツールのファイルパスを取得
     */
    getToolFilePath(toolId: string): string {
        return path.join(this.generatedToolsDir, `${toolId}.ts`);
    }

    /**
     * ツールファイルが存在するかチェック
     */
    async toolExists(toolId: string): Promise<boolean> {
        try {
            await fs.access(this.getToolFilePath(toolId));
            return true;
        } catch {
            return false;
        }
    }
}

// シングルトンインスタンスをエクスポート
export const toolLoader = new DynamicToolLoader();

また、ファイルを増やすだけだと Mastra 側で自動で読み込んでくれないため、ツールの実装後に index.ts のタイムスタンプを更新してリロードしてツールの再読み込みを行っています。エージェントとコード生成ツールの実装は長いので以下のアコーディオンの中に入れておきます。Claude Code にやりたいことを伝えてほとんど実装してもらったら一発でチャット自体は出来るものができました。生成 AI 本当にいつもありがとうの気持ちでいっぱいです。

自己拡張エージェントの実装
self-expanding-agent.ts
import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/libsql';
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { summarizeURLTool } from '../../tools/summarize-tools';
import { toolGeneratorTool } from '../../tools/meta/tool-generator';
import { toolRegeneratorTool } from '../../tools/meta/tool-regenerator';
import { toolLoader } from '../../utils/dynamic-tool-loader';

const localLLM = createOpenAICompatible({
  name: "lmstudio",
  baseURL: "http://192.168.11.2:1234/v1",
});

/**
 * 自己拡張型エージェント
 *
 * 初期状態では限られたツールしか持たないが、
 * ユーザーのリクエストに応じて必要なツールを自動生成・追加できる
 */
export class SelfExpandingAgent {
  private agent: Agent;
  private availableTools: Record<string, any>;
  private maxRetries: number = 3;

  constructor() {
    // 初期ツールセット
    this.availableTools = {
      summarizeURLTool,
      toolGeneratorTool,
      toolRegeneratorTool,
    };

    this.agent = this.createAgent();
  }

  /**
   * エージェントインスタンスを作成
   */
  private createAgent(): Agent {
    return new Agent({
      name: "Self-Expanding Agent",
      instructions: `
あなたは自己拡張型のAIエージェントです。ユーザーのリクエストに応じて、必要なツールを動的に生成・使用できます。

## 動作フロー

1. **リクエスト分析**
   - ユーザーのリクエストを分析します
   - 既存のツールで対応可能か判断します

2. **既存ツールで対応可能な場合**
   - 適切なツールを使用してリクエストに応答します
   - 例: URLの要約が求められた場合は summarizeURLTool を使用

3. **新しいツールが必要な場合**
   - toolGeneratorTool を使って新しいツールを生成します
   - 生成時には以下の情報を提供してください:
     * toolName: ツールの名前(キャメルケース、例: weatherTool, calculatorTool)
     * toolDescription: ツールが何をするかの説明
     * userRequest: ユーザーの元のリクエスト全文
   - ツール生成後、"TOOL_GENERATED: <toolId>" という形式でツールIDを返してください
   - ユーザーには「新しいツールを生成しました。もう一度リクエストしてください。」と伝えてください

## 利用可能なツール
- summarizeURLTool: URLの内容を取得して要約
- toolGeneratorTool: 新しいツールのコードを生成
- toolRegeneratorTool: エラーが発生したツールを修正して再生成

## 重要な注意事項
- ツール生成は慎重に行い、本当に必要な場合のみ実行してください
- 生成したツールは次回のリクエストから使用可能になります
- エラーが発生した場合は、ユーザーに分かりやすく説明してください
      `,
      model: localLLM("openai/gpt-oss-20b"),
      tools: this.availableTools,
      maxRetries: 1,
      memory: new Memory({
        storage: new LibSQLStore({
          // url: ":memory:",
          url: "file:../../mastra.db"
        }),
      }),
    });
  }

  /**
   * エージェントを実行
   */
  async generate(messages: any[]): Promise<any> {
    const response = await this.agent.generate(messages);

    // ツール生成のシグナルをチェック
    const toolGeneratedMatch = response.text?.match(/TOOL_GENERATED:\s*(\S+)/);

    if (toolGeneratedMatch) {
      const toolId = toolGeneratedMatch[1];
      await this.loadGeneratedTool(toolId);
    }

    return response;
  }

  /**
   * 生成されたツールを動的にロード(エラー時の自動リトライ機能付き)
   */
  async loadGeneratedTool(toolId: string, attemptCount: number = 1): Promise<boolean> {
    console.log(`🔧 ツールをロード中: ${toolId} (試行 ${attemptCount}/${this.maxRetries})`);

    try {
      const tool = await toolLoader.loadTool(toolId);

      if (!tool) {
        console.error(`❌ ツールのロードに失敗: ${toolId}`);
        return false;
      }

      // ツールを追加
      const toolVariableName = this.getToolVariableName(toolId);
      this.availableTools[toolVariableName] = tool;

      // エージェントを再作成(新しいツールセットで)
      this.agent = this.createAgent();

      console.log(`✅ ツールをロード完了: ${toolId} (${toolVariableName})`);
      return true;
    } catch (error) {
      console.error(`❌ ツールのロードでエラー発生: ${toolId}`, error);

      // 最大リトライ回数に達していない場合は再生成を試みる
      if (attemptCount < this.maxRetries) {
        console.log(`🔄 ツールの再生成を試みます...`);
        // エラー情報を含めて再生成が必要であることを通知
        // 実際の再生成は toolRegeneratorTool を通じて行われる
        return false;
      }

      return false;
    }
  }

  /**
   * すべての生成済みツールをロード
   */
  async loadAllGeneratedTools(): Promise<void> {
    console.log('📦 生成済みツールをロード中...');

    const generatedTools = await toolLoader.loadAllGeneratedTools();

    Object.assign(this.availableTools, generatedTools);

    // エージェントを再作成
    this.agent = this.createAgent();

    console.log(`${Object.keys(generatedTools).length} 個のツールをロード完了`);
  }

  /**
   * ツールIDから変数名を生成
   */
  private getToolVariableName(toolId: string): string {
    return toolId.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) + 'Tool';
  }

  /**
   * 現在利用可能なツールのリストを取得
   */
  getAvailableTools(): string[] {
    return Object.keys(this.availableTools);
  }

  /**
   * エージェントインスタンスを取得(低レベルアクセス用)
   */
  getAgent(): Agent {
    return this.agent;
  }
}

// デフォルトインスタンスをエクスポート
export const selfExpandingAgent = new SelfExpandingAgent();
tool-generator.ts
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { generateText } from 'ai';
import * as fs from 'fs/promises';
import * as path from 'path';

const localLLM = createOpenAICompatible({
    name: "lmstudio",
    baseURL: "http://192.168.11.2:1234/v1",
});

export const toolGeneratorTool = createTool({
    id: "tool-generator",
    description: "新しいツールのTypeScriptコードを生成し、ファイルシステムに保存します。ユーザーのリクエストを分析して、必要なツールの仕様を決定し、完全な実装を生成します。",
    inputSchema: z.object({
        toolName: z.string().describe("生成するツールの名前(例: weatherTool, calculatorTool)"),
        toolDescription: z.string().describe("ツールの機能説明"),
        userRequest: z.string().describe("ユーザーからの元のリクエスト"),
        errorContext: z.string().optional().describe("前回の生成でエラーが発生した場合、そのエラー情報"),
    }),
    outputSchema: z.object({
        toolId: z.string().describe("生成されたツールのID"),
        filePath: z.string().describe("生成されたファイルのパス"),
        success: z.boolean().describe("生成が成功したかどうか"),
        message: z.string().describe("生成結果のメッセージ"),
    }),

    execute: async ({ context }) => {
        const { toolName, toolDescription, userRequest, errorContext } = context;

        // ツールコードを生成
        const toolCode = await generateToolCode(
            toolName,
            toolDescription,
            userRequest,
            errorContext
        );

        // ファイルに保存
        const toolId = toKebabCase(toolName);
        const fileName = `${toolId}.ts`;

        // mastra 実行環境では process.cwd() が .mastra/output を返すため、
        // プロジェクトルートからの相対パスを構築
        const projectRoot = process.cwd().replace(/\.mastra\/output$/, '');
        const generatedDir = path.join(projectRoot, 'src/mastra/tools/generated');
        const filePath = path.join(generatedDir, fileName);

        try {
            // ディレクトリが存在しない場合は作成
            await fs.mkdir(generatedDir, { recursive: true });
            await fs.writeFile(filePath, toolCode, 'utf-8');

            // mastra dev のホットリロードをトリガーするため、index.ts のタイムスタンプを更新
            const indexPath = path.join(projectRoot, 'src/mastra/index.ts');
            try {
                const now = new Date();
                await fs.utimes(indexPath, now, now);
                console.log('🔥 ホットリロードをトリガーしました');
            } catch (touchError) {
                console.warn('⚠️ index.ts のタッチに失敗(ホットリロードされない可能性):', touchError);
            }

            return {
                toolId,
                filePath,
                success: true,
                message: `ツール「${toolName}」を ${filePath} に生成しました`,
            };
        } catch (error) {
            return {
                toolId,
                filePath,
                success: false,
                message: `ファイル保存エラー: ${error instanceof Error ? error.message : String(error)}`,
            };
        }
    }
});

/**
 * LLMを使ってツールのTypeScriptコードを生成
 */
async function generateToolCode(
    toolName: string,
    toolDescription: string,
    userRequest: string,
    errorContext?: string
): Promise<string> {
    const errorFeedback = errorContext
        ? `\n\n前回の生成でエラーが発生しました:\n${errorContext}\n\nこのエラーを修正したコードを生成してください。`
        : '';

    const prompt = `
あなたは TypeScript のツール生成エキスパートです。以下の仕様に基づいて、mastra フレームワークで使用できるツールを生成してください。

## ユーザーのリクエスト
${userRequest}

## ツールの仕様
- ツール名: ${toolName}
- 説明: ${toolDescription}

## 要件
1. mastra の createTool API を使用すること
2. zod を使って inputSchema と outputSchema を定義すること
3. execute 関数内でツールのロジックを実装すること
4. 外部APIを呼び出す場合は fetch を使用すること
5. エラーハンドリングを適切に行うこと
6. TypeScript の型安全性を保つこと
7. 必要に応じて cheerio などのライブラリをインポートすること(既にインストール済み)

## コード生成の制約
- import文から始まる完全なTypeScriptファイルとして出力すること
- コメントや説明は最小限にし、実装を優先すること
- 生成したコードのみを出力し、マークダウンのコードブロック記号(\`\`\`)は含めないこと
- export const で1つのツールをエクスポートすること

## 参考: mastra ツールの実装例
\`\`\`typescript
import { createTool } from "@mastra/core/tools";
import { z } from "zod";

export const exampleTool = createTool({
    id: "example-tool",
    description: "ツールの説明",
    inputSchema: z.object({
        param1: z.string().describe("パラメータ1の説明"),
    }),
    outputSchema: z.object({
        result: z.string().describe("結果の説明"),
    }),
    execute: async ({ context }) => {
        const { param1 } = context;
        // ツールのロジックを実装
        return {
            result: "結果",
        };
    }
});
\`\`\`
${errorFeedback}

それでは、上記の仕様に基づいてツールのコードを生成してください。生成するコードのみを出力し、説明文やマークダウンは含めないでください。
`;

    const result = await generateText({
        model: localLLM("openai/gpt-oss-20b"),
        prompt,
    });

    // マークダウンのコードブロックを除去(念のため)
    let code = result.text.trim();
    if (code.startsWith('```typescript') || code.startsWith('```ts')) {
        code = code.replace(/^```(?:typescript|ts)\n/, '').replace(/\n```$/, '');
    } else if (code.startsWith('```')) {
        code = code.replace(/^```\n/, '').replace(/\n```$/, '');
    }

    return code;
}

/**
 * キャメルケースをケバブケースに変換
 */
function toKebabCase(str: string): string {
    return str
        .replace(/Tool$/, '') // 末尾の "Tool" を除去
        .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
        .toLowerCase();
}

実装が出来たので、実際にエージェントを動かしてみます。

天気を取得するツールは最初はないため、LLM が「ツールが必要だ」と判断して実装を行っていることが分かると思います。実際にファイル構成を見ると天気を取得するためのコードが(今回は tools/generated ディレクトリ以下に)実装されるのですが、使用できるツールとしてエージェントに反映させる方法が現時点で分かっていません。アドベントカレンダーまでに解決したかったのですが、うまくいきませんでした… 「緩募:アドバイス 出:感謝の気持ち」って感じです。

感想

今回はローカルの LLM で Mastra を使ったエージェントを作ってみました。LMStudio を使うことで API 使用料がかからずにローカルでエージェントを動かせるため、自分だけが使いたいツールやさくっと検証したいときに良さそうです。また、LangGraph といい Mastra といい、エージェントがサクッと作れるのもとても便利です。最近では Strands Agents も出てきて、エージェントを作成できるライブラリがかなり充実してきているように思えます。今回実装した自己拡張エージェントもそれほど手間をかけずにコードの生成までうまく出来ており、それをうまく読み込ませられれば自身の出来ることを拡張していけるエージェントが出来ると思うので、何とかうまくできないか方法を探っていきたいです。また、ツールが増えすぎると LLM が判断できなくなるので、ただただ拡張するだけではなく適切に判断できるようにツールの管理をよりうまく出来る方法も調査していきたいです(が、とりあえずは作ったツールを使える仕組みを作らないと…)

脚注
  1. https://lmstudio.ai/docs/developer/openai-compat ↩︎

  2. https://qiita.com/youtoy/items/b401bea6b0acd1ae17d6 ↩︎

GitHubで編集を提案

Discussion