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でAIエージェントを作りたい

Posted at

01 基本エージェントの作成

概要

  • LLMのみで応答する最小構成のMastraエージェントを定義する。
  • 目的は、以降の拡張(ツール/メモリ等)を差し込める土台を整えること。

実装場所

  • src/agents/basicAgent.ts: Agent定義とLLM設定を記述するAgent層。
  • src/index.ts: ランタイムを起動し、CLIやテストから呼べるようにするアプリ層。

実装内容

// src/agents/basicAgent.ts
import { Agent } from '@mastra/core';
import { OpenAIChatModel } from '@mastra/llms-openai';

export const basicAgent = new Agent({
  name: 'basic-support',
  instructions: `あなたはプロダクトサポート担当です。ユーザーの質問には正確かつ簡潔に回答してください。`,
  llm: new OpenAIChatModel({
    apiKey: process.env.OPENAI_API_KEY!,
    model: 'gpt-4o-mini',
    temperature: 0.2,
  }),
});
// src/index.ts
import { AgentRuntime } from '@mastra/runtime';
import { basicAgent } from './agents/basicAgent';

const runtime = new AgentRuntime({ agents: [basicAgent] });

export async function askBasicAgent(prompt: string) {
  const result = await runtime.run('basic-support', {
    messages: [{ role: 'user', content: prompt }],
  });
  return result.output;
}

if (require.main === module) {
  askBasicAgent(process.argv.slice(2).join(' ') || 'Hello').then(console.log);
}
  • basicAgentはLLMのみを保持し、まだツールやメモリ機構を持たない。
  • AgentRuntimeは今後も使い回せるエントリーポイントなので、この段階で用意しておく。

ポイント解説

  • MastraのAgentはLLMクライアントとシステムプロンプト(instructions)を基礎構成とする。
  • ここではAPIキーを環境変数から読み、テスト時はMock LLMに差し替えられるようにファクトリを1箇所に集約している。
  • まだ状態を持たないため、各リクエストはステートレスに完結する。

次の段階へのつながり

  • LLM呼び出しの流れが固まったので、次はAgentにToolを登録して外部機能を呼び出せるようにする。
  • 以降の拡張はbasicAgent定義を順に拡張していく形で積み重ねる。

02 ツールの追加

概要

  • Agentに外部ツールを登録し、LLMが必要に応じてデータを取得できるようにする。
  • ユーザーの質問に対してAPI経由の最新情報(例: 天気)を返せるようになる。

実装場所

  • src/tools/weatherTool.ts: Toolクラスを定義するTool層。
  • src/agents/basicAgent.ts: Agentにツールを登録する。
  • src/index.ts: ランタイムに渡すAgentは変わらないが、利用例をツール想定に更新する。

実装内容

// src/tools/weatherTool.ts
import { Tool, ToolExecutionContext } from '@mastra/core';
import fetch from 'node-fetch';

export class WeatherTool extends Tool {
  name = 'weatherLookup';
  description = '指定都市の現在の天気を返す';

  async run(input: { city: string }, _ctx: ToolExecutionContext) {
    const response = await fetch(
      `https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${encodeURIComponent(input.city)}`
    );
    const data = await response.json();
    return {
      condition: data.current.condition.text,
      temperatureC: data.current.temp_c,
    };
  }
}
// src/agents/basicAgent.ts (追記)
import { WeatherTool } from '../tools/weatherTool';

export const basicAgent = new Agent({
  name: 'basic-support',
  instructions: `...`,
  llm: new OpenAIChatModel({ /* 省略 */ }),
  tools: [new WeatherTool()], // ← LLMがfunction callで呼び出せるようになる
});
// src/index.ts (利用例の更新)
const runtime = new AgentRuntime({ agents: [basicAgent] });

runtime.run('basic-support', {
  messages: [
    { role: 'user', content: '東京の現在の天気を教えて' },
  ],
});
  • ToolはnamedescriptionでLLMに用途を説明し、runで実際の処理を行う。
  • Agent構成にtools配列を渡すだけで、Mastraが自動でfunction callプロトコルを扱う。

ポイント解説

  • ツールのI/O型はLLMのfunction schemaとして利用されるため、曖昧さを避けるためにcityフィールドなどを明確にする。
  • ネットワーク呼び出しはLLMトークン消費と独立しており、失敗時のリトライやタイムアウトをTool側で制御できる。
  • 複数ツールを登録するときはtools: [new WeatherTool(), new AnotherTool()]のように並べればよい。

次の段階へのつながり

  • ツール呼び出しによって応答がリッチになったが、会話の文脈はまだ保持していない。
  • 次はAgentMemoryを導入し、ユーザーとの継続的な会話を扱えるようにする。

03 記憶(Memory)の導入

概要

  • 過去の対話履歴を保持するAgentMemoryを追加し、会話の連続性を確保する。
  • 目的は、ユーザーの要望や前提を覚えてフォローアップ質問に自然に答えること。

実装場所

  • src/memory/supportMemory.ts: Agent専用のメモリストアを定義するMemory層。
  • src/agents/basicAgent.ts: Agent設定にmemoryを紐付ける。
  • src/index.ts: ランタイム呼び出し時に会話IDを指定し、同じ会話を続けられるようにする。

実装内容

// src/memory/supportMemory.ts
import { AgentMemory } from '@mastra/memory';

export const supportMemory = new AgentMemory({
  namespace: 'support-thread',
  window: 12, // 直近12ターンを保持(後段で要約を差し込めるようwindow型に)
  storage: {
    provider: 'redis',
    url: process.env.REDIS_URL!,
  },
});
// src/agents/basicAgent.ts (メモリを紐付け)
import { supportMemory } from '../memory/supportMemory';

export const basicAgent = new Agent({
  name: 'basic-support',
  instructions: `...`,
  llm: new OpenAIChatModel({ /* 省略 */ }),
  tools: [new WeatherTool()],
  memory: supportMemory, // ← AgentMemoryをセット
});
// src/index.ts (会話IDを指定)
export async function askBasicAgent(prompt: string, threadId = 'demo-thread') {
  const result = await runtime.run('basic-support', {
    threadId, // 同じthreadIdを使えばメモリが共有される
    messages: [{ role: 'user', content: prompt }],
  });
  return result.output;
}
  • AgentMemoryはストレージ層を抽象化し、ローカルでもクラウドでも同じAPIで使える。
  • threadIdがメモリのキーになるため、マルチユーザー環境ではユーザーIDと紐付けて生成する。

ポイント解説

  • window型メモリはトークン消費を一定に保ちつつ、後段で要約フローを噛ませやすい。
  • MastraのメモリはLLMへの入力テンプレートに自動で挿入されるため、呼び出し側は特別な処理を追加する必要がない。
  • Redisなどの外部ストアを使う場合は、接続ライフサイクルをアプリ起動時に1回だけ作ると効率的。

次の段階へのつながり

  • 会話文脈を保持できたので、次は外部知識を参照するRAGを導入し、メモリ+知識ベースの両立を図る。

04 RAG連携の追加

概要

  • ベクトル検索を行うvectorQueryToolを組み込み、社内ドキュメントやFAQから根拠ある回答を生成する。
  • 目的は、LLM単体では保持していない最新・正確なナレッジを参照可能にすること。

実装場所

  • src/vector/store.ts: ベクトルストアと埋め込みモデルの初期化。
  • src/tools/vectorQueryTool.ts: vectorQueryToolの定義。
  • src/agents/basicAgent.ts: AgentにRAGツールを追加。

実装内容

// src/vector/store.ts
import { VectorStore } from '@mastra/vector';
import { OpenAIEmbeddings } from '@mastra/embeddings-openai';

export const supportStore = new VectorStore({
  name: 'support-knowledge-base',
  embeddings: new OpenAIEmbeddings({
    apiKey: process.env.OPENAI_API_KEY!,
    model: 'text-embedding-3-small',
  }),
});

export async function upsertSupportDocs(documents: Array<{ id: string; text: string }>) {
  await supportStore.upsertMany(
    documents.map((doc) => ({ id: doc.id, values: doc.text, metadata: doc }))
  );
}
// src/tools/vectorQueryTool.ts
import { Tool } from '@mastra/core';
import { supportStore } from '../vector/store';

export class SupportRagTool extends Tool {
  name = 'supportDocsLookup';
  description = '社内ナレッジベースから関連情報を検索して要約する';

  async run(input: { query: string }) {
    const matches = await supportStore.similaritySearch(input.query, { k: 4 });
    return matches
      .map((match) => `#${match.metadata.id}\n${match.metadata.text}`)
      .join('\n---\n');
  }
}
// src/agents/basicAgent.ts (RAGツールを追加)
import { SupportRagTool } from '../tools/vectorQueryTool';

export const basicAgent = new Agent({
  name: 'basic-support',
  instructions: `...`,
  llm: new OpenAIChatModel({ /* 省略 */ }),
  tools: [
    new WeatherTool(),
    new SupportRagTool(), // ← 質問がドキュメント参照を必要とするときに使用
  ],
  memory: supportMemory,
});
  • RAGツールは検索結果をテキスト化してLLMに渡すだけでよい。引用形式や区切り文字を揃えておくとLLMが扱いやすい。
  • upsertSupportDocsはデプロイ前にバッチで呼び出すか、CIで同期する。

ポイント解説

  • ベクトルストアはLLMとは別のリソースなので、更新頻度が高い場合は差分アップサート用のタスクを用意する。
  • Toolとして包むと、LLMが「社内情報が必要」と判断した時だけ呼び出されるため、毎回すべてのドキュメントを入力する必要がない。
  • マッチ結果にはmetadataを含められるので、回答中で参照IDを提示したい場合はLLMプロンプト内で利用する。

次の段階へのつながり

  • メモリ+RAGの両立により情報量が増えるため、会話履歴を適宜要約する仕組みが必要になる。
  • 次は要約ワークフローを導入し、メモリウィンドウを保ちながら長時間の会話を維持する。

05 要約ワークフローの追加

概要

  • 長い会話履歴を圧縮する要約ワークフローを追加し、メモリ消費とトークンコストを抑える。
  • 目的は、windowサイズを超える履歴が発生した際に自動要約を実行し、重要情報だけを残すこと。

実装場所

  • src/workflows/summarizeConversation.ts: メモリ圧縮用ワークフローを定義するWorkflow層。
  • src/memory/supportMemory.ts: 要約結果を格納するフィールドを追加。
  • src/index.ts: AgentRuntimeにワークフローを登録し、イベントをフックする。

実装内容

// src/workflows/summarizeConversation.ts
import { defineWorkflow } from '@mastra/workflows';
import { OpenAIChatModel } from '@mastra/llms-openai';

export const summarizeConversation = defineWorkflow({
  id: 'conversation-summary',
  trigger: 'memory.window.exceeded', // AgentMemoryのwindowを超えたタイミング
  run: async ({ threadId, memory, logger }) => {
    const transcript = await memory.exportThread(threadId);
    const llm = new OpenAIChatModel({
      apiKey: process.env.OPENAI_API_KEY!,
      model: 'gpt-4o-mini',
      temperature: 0.1,
    });

    const summary = await llm.invoke([
      { role: 'system', content: '次の会話をサマリし、重要なTODO/決定のみ残してください。' },
      { role: 'user', content: transcript.join('\n') },
    ]);

    await memory.saveSummary(threadId, summary.output);
    logger.info('conversation summary stored', { threadId });
  },
});
// src/memory/supportMemory.ts (サマリ格納メソッドを利用)
export const supportMemory = new AgentMemory({
  namespace: 'support-thread',
  window: 12,
  storage: { provider: 'redis', url: process.env.REDIS_URL! },
  summaryField: 'compressedSummary', // summarizeConversationで書き込むキー
});
// src/index.ts (ワークフロー登録)
import { summarizeConversation } from './workflows/summarizeConversation';

const runtime = new AgentRuntime({
  agents: [basicAgent],
  workflows: [summarizeConversation], // ← メモリイベントを監視
});
  • trigger: 'memory.window.exceeded'はAgentMemoryのバッファが満杯になった瞬間に発火するカスタムイベント。
  • memory.saveSummaryで保存した要約は次回のmemory.exportThreadに自動で含まれるため、LLMに与える前処理を挟む必要がない。

ポイント解説

  • 要約に専用のLLMインスタンスを使うと、メインLLMをブロックせずに済む。また低コストモデルに切り替えやすい。
  • ワークフローは独立したコンテキストで動作するため、ログを分けておくと監視しやすい。
  • 要約ポリシーはユースケース次第で変更でき、TODO抽出や数値ダイジェストなど複数パターンを用意可能。

次の段階へのつながり

  • 会話の凝縮ができたことで、Agentは長期利用に耐えられる。次は応答後に自己評価を行うReflection機構を追加し、品質を高める。

06 反省(Reflection)機構の追加

概要

  • Agentが生成した応答を自己評価し、改善が必要な場合は再生成またはメモリに学習事項を残すReflectionを導入する。
  • 目的は、ツールやRAGを利用した複雑な回答の品質を安定させること。

実装場所

  • src/workflows/reflectionWorkflow.ts: 応答後に走るReflectionワークフロー。
  • src/agents/basicAgent.ts: 反省結果を参照できるよう、Agent instructionsにフィードバックサマリを追加。
  • src/index.ts: RuntimeにReflectionワークフローを登録し、応答サイクルに組み込む。

実装内容

// src/workflows/reflectionWorkflow.ts
import { defineWorkflow } from '@mastra/workflows';
import { OpenAIChatModel } from '@mastra/llms-openai';
import { supportMemory } from '../memory/supportMemory';

export const reflectOnResponse = defineWorkflow({
  id: 'response-reflection',
  trigger: 'agent.response.completed',
  run: async ({ threadId, response }) => {
    const critic = new OpenAIChatModel({
      apiKey: process.env.OPENAI_API_KEY!,
      model: 'gpt-4o-mini',
      temperature: 0,
    });

    const critique = await critic.invoke([
      { role: 'system', content: '以下の回答の正確性・根拠・トーンを100文字以内で評価し、改善案があれば指摘してください。' },
      { role: 'user', content: response.output },
    ]);

    await supportMemory.appendNote(threadId, {
      type: 'reflection',
      content: critique.output,
      timestamp: new Date().toISOString(),
    });

    if (critique.output.includes('再回答')) {
      return { action: 'retry' }; // Mastraが同じ入力で再実行する
    }
  },
});
// src/agents/basicAgent.ts (instructionsへ反映)
import { supportMemory } from '../memory/supportMemory';

export const basicAgent = new Agent({
  name: 'basic-support',
  instructions: `...\n常に最新のreflection noteを考慮して回答を改善してください。`,
  llm: new OpenAIChatModel({ /* 省略 */ }),
  tools: [new WeatherTool(), new SupportRagTool()],
  memory: supportMemory,
});
// src/index.ts
import { reflectOnResponse } from './workflows/reflectionWorkflow';
import { summarizeConversation } from './workflows/summarizeConversation';

const runtime = new AgentRuntime({
  agents: [basicAgent],
  workflows: [summarizeConversation, reflectOnResponse],
});
  • Reflectionワークフローはresponseペイロードを受け取り、LLMで簡易評価してからメモリに保存する。
  • return { action: 'retry' }を返すと、Mastraは同じツール呼び出しを保持したまま再生成を行うため、自己修正ループを組める。

ポイント解説

  • 評価LLMは低温度で決定的なフィードバックを返す設定にする。
  • 反省結果をメモリ内でtype: 'reflection'として分離すると、Agentがinstructionsで取り出しやすい。
  • リトライ条件は任意のルール(信頼度スコア、禁則語検出など)に置き換えられる。

次の段階へのつながり

  • Reflectionまで揃ったので、最後に全層(Agent/Tool/Memory/RAG/Workflow)の構成図とファイル一覧をまとめ、運用の全体像を共有する。

07 最終構成まとめ

概要

  • これまで追加したLLM・ツール・メモリ・RAG・要約・反省を統合した完成構成を整理する。
  • 目的は、運用チームが全体像を把握し、追加開発やデバッグを容易にすること。

実装場所

  • Agent層: src/agents/basicAgent.ts
  • Tool層: src/tools/weatherTool.ts, src/tools/vectorQueryTool.ts
  • Memory層: src/memory/supportMemory.ts
  • Workflow層: src/workflows/summarizeConversation.ts, src/workflows/reflectionWorkflow.ts
  • エントリーポイント: src/index.ts

実装内容

// src/index.ts(完成形)
import { AgentRuntime } from '@mastra/runtime';
import { basicAgent } from './agents/basicAgent';
import { summarizeConversation } from './workflows/summarizeConversation';
import { reflectOnResponse } from './workflows/reflectionWorkflow';

export const runtime = new AgentRuntime({
  agents: [basicAgent],
  workflows: [summarizeConversation, reflectOnResponse],
});

export async function askSupportAgent({ prompt, threadId }: { prompt: string; threadId: string }) {
  const result = await runtime.run('basic-support', {
    threadId,
    messages: [{ role: 'user', content: prompt }],
  });
  return result.output;
}
// src/agents/basicAgent.ts(機能統合済み)
import { Agent } from '@mastra/core';
import { OpenAIChatModel } from '@mastra/llms-openai';
import { WeatherTool } from '../tools/weatherTool';
import { SupportRagTool } from '../tools/vectorQueryTool';
import { supportMemory } from '../memory/supportMemory';

export const basicAgent = new Agent({
  name: 'basic-support',
  instructions: `プロダクトサポート担当として正確さ・根拠・トーンを重視してください。\n反省ノートと要約を常に参照し、必要ならツールを使って確認しましょう。`,
  llm: new OpenAIChatModel({
    apiKey: process.env.OPENAI_API_KEY!,
    model: 'gpt-4o-mini',
    temperature: 0.2,
  }),
  tools: [new WeatherTool(), new SupportRagTool()],
  memory: supportMemory,
});
src/
├─ agents
│  └─ basicAgent.ts
├─ tools
│  ├─ weatherTool.ts
│  └─ vectorQueryTool.ts
├─ memory
│  └─ supportMemory.ts
├─ vector
│  └─ store.ts
├─ workflows
│  ├─ summarizeConversation.ts
│  └─ reflectionWorkflow.ts
└─ index.ts
  • Agentが中心となり、Tool/Memory/Workflowが依存逆転で差し込まれている。
  • 各層は単体でテストできる構成になっているため、LLMをMockしても動作確認が容易。

ポイント解説

  • AgentはLLM以外をすべてDI(依存性注入)で受け取るため、テスト・環境差分・モデル切り替えに強い。
  • Memory→Workflow→Agentのデータフローを明確にしておくと、トークン使用量や遅延のボトルネックを追跡しやすい。
  • ツールはLLMのfunction schemaに直結するため、I/O型をzod等で検証すると安全性がさらに向上する。

次の段階へのつながり

  • この完成形を基点に、権限管理(RBAC)、モニタリング、A/Bテストなど運用レベルの機能を追加できる。
  • 複数エージェント連携や自動オーケストレーションを検討する場合も、本構成をテンプレートとして流用可能。
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?