業務でMastraを使ったので忘れないうちに記事としてまとめておこうと思います。
mastraでの開発の流れを記すだけで本記事ではRAGを作成したりtoolsを使ったりはしません(AIエージェントの意味は無いです)
Mastraに関する記事等は今の時点ではあまり多くないのでどなたかの一助になれば幸いです。しかしMastraは現在、頻繁に更新が行われているので常にドキュメントから最新の情報を確認するようにお願いします。
業務ではハードな作業をAIにさせていたのでその懺悔も込めてAI同士がしりとりをするアプリを作っていきます。ぜひとも楽しんでほしいですね。
セットアップ
非常に簡単です。
npx create-mastra@latest mastra-word-chain
終わりです。
この状態で(apiキーがあれば)npm run dev
で実際にサンプルのワークフローやエージェントが使えます。
ワークフローを作ってみる
最初からAIを使ってしりとりをさせるのではなく、ワークフローが正しく回せるかどうかを確認してから実際にしりとりをさせてます。(無限ループとかでリクエストいっぱいしても困るし)
以下が入力値を+1して+10するワークフローです。
import { createStep, createWorkflow } from "@mastra/core";
import z from "zod";
const add1 = createStep({
id: "add-1",
description: "Adds 1 to a number",
inputSchema: z.object({
initialNumber: z.number().describe("1足される数"),
}),
outputSchema: z.object({
added1Number: z.number().describe("1足したあとの数"),
}),
execute: async ({ inputData }) => {
if (!inputData) {
throw new Error("Input data not found");
}
return { added1Number: inputData.initialNumber + 1 };
},
});
const add10 = createStep({
id: "add-10",
description: "Adds 10 to a number",
inputSchema: z.object({
added1Number: z.number().describe("10足される数"),
}),
outputSchema: z.object({
added10Number: z.number().describe("10足したあとの数"),
}),
execute: async ({ inputData }) => {
if (!inputData) {
throw new Error("Input data not found");
}
return { added10Number: inputData.added1Number + 10 };
},
});
const additionWorkflow = createWorkflow({
id: "addition-workflow",
inputSchema: z.object({
initialNumber: z.number().describe("初期値"),
}),
outputSchema: z.object({
result: z.number().describe("最終結果"),
}),
})
.then(add1)
.then(add10)
.commit();
export default additionWorkflow;
zodに馴染みが無いとinputSchemaとoutputSchemaに関しては難しいかもしれません。
workflowではoutputSchemaとinputSchemaが同じでないといけません。(これを知らなくてだいぶ詰まった)
そして今回作成したワークフローを登録しましょう。登録しないと使えないです。忘れないようにしましょう
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";
import additionWorkflow from "./workflows/addition-workflow";
export const mastra = new Mastra({
workflows: { weatherWorkflow, additionWorkflow },
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",
}),
});
サンプルのweatherWorkflow
が残っていますがこれは消しても問題ないです。
では実際に動かして見ましょう。
npm run dev
してからhttp://localhost:4111/
でMastra Playgroundが開けます。
サイドバーのworkflowから自分が作成したワークフローを選択するとこんな感じでワークフローが表示されます。
もし自分のワークフローが表示されなかった場合
src\mastra\index.tsにワークフローを登録しているかどうか確認してください。
では実際に動かしていきます。今回は初期値に7を入れて実行します
無事にエラーなく完了しましたね。
Open Workflow Execution (JSON)からワークフローの結果を確認できます。今回は7+1+10なので18が出ていて正しく成功しているのが確認できます。
次はループするタイプのワークフローを作ってみます。
ワークフローでループさせる
では次にループするワークフローを作ります。
今回は先程作ったadd1
をループさせて10になるまでやりたいと思います。
import { createStep, createWorkflow } from "@mastra/core";
import z from "zod";
const add1 = createStep({
id: "add-1",
description: "Adds 1 to a number",
inputSchema: z.object({
number: z.number().describe("1足される数"),
}),
outputSchema: z.object({
number: z.number().describe("1足したあとの数"),
}),
execute: async ({ inputData }) => {
if (!inputData) {
throw new Error("Input data not found");
}
return { number: inputData.number + 1 };
},
});
const additionLoopWorkflow = createWorkflow({
id: "addition-loop-workflow",
inputSchema: z.object({
number: z.number().describe("初期値"),
}),
outputSchema: z.object({
result: z.number().describe("最終結果"),
}),
})
.dountil(add1, async ({ inputData }) => inputData.number >= 10)
.commit();
export default additionLoopWorkflow;
dountil
でループ条件を付けるだけです。
正しく10になっています。
AIを使う
本題です。AIを使ってしりとりをさせていきます。それってAIエージェントじゃなくていいじゃん
エージェントを作る
import { openai } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
export const wordChainAgent = new Agent({
name: "Word Chain Agent",
instructions: `
あなたはしりとりをするエージェントです。
出力結果は必ずJSON形式で以下のフォーマットに従ってください。
{
"word": "次の単語",
"status": "in the game" // "in the game" または "game over"
}
`,
model: openai("gpt-5-nano"),
});
とりあえず簡単につくりました。本来MCPとしてtoolを設定する場合はここで設定します。
ではワークフローと同様にこのエージェントが動くかどうか試してみましょう。
このときsrc\mastra\index.ts
に作成したエージェントの登録を忘れずに行いましょう。
いい感じですね。
ついでにしりとりを終わらせるエージェントも作っておきます。
import { openai } from "@ai-sdk/openai";
import { Agent } from "@mastra/core/agent";
// しりとりエージェント
export const wordChainAgent = new Agent({
name: "Word Chain Agent",
instructions: `
あなたはしりとりをするエージェントです。
出力結果は必ずJSON形式で以下のフォーマットに従ってください。
{
"word": "次の単語",
"status": "in the game" // "in the game" または "game over"
}
`,
model: openai("gpt-5-nano"),
});
// しりとりを終わらせるエージェント
export const wordChainEndAgent = new Agent({
name: "Word Chain End Agent",
instructions: `
あなたはしりとりを終わらせるエージェントです。
必ず「ん」で終わる単語を出力してください。
出力結果は必ずJSON形式で以下のフォーマットに従ってください。
{
"word": "任意の「ん」で終わる単語",
"status": "game over" // 必ず "game over"
}
`,
model: openai("gpt-5-nano"),
});
AIをワークフローに組み込む
では加算ワークフローと同様にワークフローを作ります。
import { createStep, createWorkflow } from "@mastra/core";
import z from "zod";
const wordChainStep = createStep({
id: "word-chain",
description: "Generates the next word in the word chain",
inputSchema: z.object({
word: z.string().describe("現在の単語"),
status: z.string().describe("ゲームの状態"),
}),
outputSchema: z.object({
word: z.string().describe("次の単語"),
status: z.string().describe("ゲームの状態"),
}),
execute: async ({ mastra, inputData, bail }) => {
if (!inputData) {
throw new Error("Input data not found");
}
// if (input.status === "game over") {
// bail({ word: input.word, status: input.status });
// }
let result;
// 20%の確率でしりとりを終わらせるエージェントを使う
if (Math.random() < 0.2) {
result = await mastra
.getAgent("wordChainEndAgent")
.generate(inputData.word);
} else {
result = await mastra.getAgent("wordChainAgent").generate(inputData.word);
}
let output;
try {
output = JSON.parse(result.text);
} catch (e) {
output = { word: "ん", status: "game over" };
}
console.log("output", output);
return { word: output.word, status: output.status };
},
});
const wordChainWorkflow = createWorkflow({
id: "word-chain-workflow",
inputSchema: z.object({
word: z.string().describe("現在の単語"),
status: z.string().describe("ゲームの状態"),
}),
outputSchema: z.object({
result: z.number().describe("最終結果"),
}),
})
.dountil(
wordChainStep,
async ({ inputData }) => inputData.status === "game over"
)
.commit();
export default wordChainWorkflow;
80%で通常のしりとりエージェントに、20%でしりとり終了エージェントに振り分けられるようなステップにしています。
今回はdountil
でstatus
が"game over"
の場合に終了する実装としました。
コメントアウトしていますがbail
でワークフローを終わらせる事も出来ます。
出力も問題なく出来ているのを確認出来ます。(20%をなかなか引かなくてちょっと焦った)
output { word: 'ラッパ', status: 'in the game' }
output { word: 'パンダ', status: 'in the game' }
output { word: 'ダンス', status: 'in the game' }
output { word: 'スープ', status: 'in the game' }
output { word: 'プール', status: 'in the game' }
output { word: 'ルーレット', status: 'in the game' }
output { word: 'トマト', status: 'in the game' }
output { word: 'トリ', status: 'in the game' }
output { word: 'リス', status: 'in the game' }
output { word: 'スイカ', status: 'in the game' }
output { word: 'カメ', status: 'in the game' }
output { word: 'メロン', status: 'in the game' }
output { word: 'なし', status: 'game over' }
本当はtoolsで用意した単語リストから選んで来るとかRAG使ってそのリストから類似性の高い物を選ぶとかしたかったのですが今回は一旦ここまでとします。
今回作成した物は以下に公開してあります。