🔀

Mastraのマルチエージェントワークフローにおける条件分岐

に公開

背景

前回の記事ではMastraのマルチエージェントワークフローの公式サンプルコードを動かすところまでを検証しました。今回はもう少し複雑なワークフローとして、条件分岐を行うようなワークフローを実現したいと思います。

環境

項目 バージョン
OS Windows 11
ランタイム Node.js v22.14.0
主要ライブラリ "@ai-sdk/azure": "^1.3.23", "@mastra/core": "^0.10.4", "zod": "^3.25.76"
LLM Azure OpenAI(gpt-4.1-mini)

検証内容

現在、社内ナレッジ情報の検索ができるAIチャットボットを開発しているのですが、社内ナレッジ情報で検索ヒットしない場合はWeb検索した結果を返すといったワークフローを作成したいと思っています。今回の検証では以下のようなStepをワークフローで実現したいと思います。

Step 1. 社内ナレッジ情報を検索
Step 2. 社内ナレッジ情報の検索結果が回答に十分な内容か判断。十分でない場合、Step3へ。十分である場合Step4へ
Step 3. Web検索
Step 4. 検索ヒットした結果を返す

検証コード概要

Mastraで作成した検証コードとしては以下の構成要素があります。

  • 社内ナレッジ情報検索MCPクライアント
  • 社内ナレッジ情報検索エージェント
  • 社内ナレッジ情報検索エージェントを呼び出すStep
  • Web検索MCPクライアント
  • Web検索エージェント
  • Web検索エージェントを呼び出すStep
  • 社内ナレッジ情報で十分な情報が得られたか判定するエージェント
  • 社内ナレッジ情報で十分な情報が得られたか判定するStep
  • 社内ナレッジ情報で十分な情報が得られた場合社内ナレッジ情報を返すStep

これらの構成要素を元に以下を実行します

  • ワークフロー定義
  • Stepを順番にワークフローへ登録
  • ワークフロー実行

検証コード

http://localhost:8000/mcpで社内ナレッジ検索のMCPサーバが起動している前提です

インポート

import { Agent } from '@mastra/core/agent';
import { createWorkflow, createStep } from '@mastra/core/workflows';
import { createAzure } from "@ai-sdk/azure";
import { config } from "./config.js";
import { z } from "zod";
import { MCPClient } from "@mastra/mcp";

Azure接続

const azure = createAzure({ apiKey: config.apiKey, resourceName: config.resourceName });

社内ナレッジ情報検索MCPクライアント

const knowledgeSearchMcp = new MCPClient({
  servers:{
    growi_knowledge: {
      url: new URL("http://localhost:8000/mcp"),
    }
  }
});

社内ナレッジ情報検索エージェント

const knowledgeSearchAgent = new Agent({
  name: "knowledgeSearchAgent",
  instructions: "あなたは社内ナレッジ情報検索エージェントです。社内ナレッジ情報検索から検索をして情報を取得してください。 \
      以下のルール・形式に必ず従って回答してください。\
      <以下略>",
  model: azure('gpt-4.1-mini'),
  tools: await knowledgeSearchMcp.getTools()
});

社内ナレッジ情報検索エージェントを呼び出すStep

const knowledgeSearchStep = createStep({
  id: "knowledge-search-step",
  description: "社内ナレッジ検索エージェントを使って情報を検索します。",
  inputSchema: z.object({
    topic: z.string(),
  }),
  outputSchema: z.object({
    topic: z.string(),
    knowledgeResult: z.string(),
  }),
  execute: async (context) => {
    const topic = context?.inputData?.topic;
    const result = await knowledgeSearchAgent.generate(`${topic}`);
    return {
      topic: topic,
      knowledgeResult: result.text
    };
  },
});

Web検索MCPクライアント

const webSearchMcp = new MCPClient({
  servers: {
    "ddg-search": {
        "command": "uvx",
        "args": ["duckduckgo-mcp-server"]
    },
  },
});

Web検索エージェント

const webSearchAgent = new Agent({
  name: "webSearchAgent",
  instructions: "あなたはWeb検索エージェントです。Web検索をして情報を取得してください。\
      以下のルール・形式に必ず従って回答してください。\
      <以下略>",
  model: azure('gpt-4.1-mini'),
  tools: await webSearchMcp.getTools()
});

Web検索エージェントを呼び出すStep

const webSearchStep = createStep({
  id: "web-search-step",
  description: "Web検索エージェントを使って情報を検索します。",
  inputSchema: z.object({
    topic: z.string(),
  }),
  outputSchema: z.object({
    answer: z.string(),
  }),
  execute: async ( context ) => {
    const topic = context?.inputData?.topic;
    const result = await webSearchAgent.generate(`${topic}`);
    return { 
      answer: result.text 
    };
  },
});

社内ナレッジ情報で十分な情報が得られたか判定するエージェント

const answerJudgeAgent = new Agent({
  name: "answerJudgeAgent",
  instructions: `あなたは質問した内容が社内ナレッジ情報から取得できたか判断するエージェントです。
  回答に十分な情報が得られた場合はtrueを返します。そうでなければfalseを返します。`,
  model: azure('gpt-4.1-mini'),
});

社内ナレッジ情報で十分な情報が得られたか判定するStep

const answerJudgeStep = createStep({
  id: "answer-judge-step",
  description: "社内ナレッジ情報の検索結果をもとに回答するかどうか判断します。",
  inputSchema: z.object({
    topic: z.string(),
    knowledgeResult: z.string(),
  }),
  outputSchema: z.object({
    topic: z.string(),
    knowledgeResult: z.string(),
    answerFlag: z.boolean()
  }),
  execute: async ( context ) => {
    const topic = context?.inputData?.topic;
    const knowledgeResult = context?.inputData?.knowledgeResult;
    const result = await answerJudgeAgent.generate(
      `質問内容:${topic}
       社内ナレッジ情報の検索結果:${knowledgeResult}`,
      {
        output: z.object({
          answerFlag: z.boolean()
        })
      }
    );
    return { 
      topic: topic,
      knowledgeResult: knowledgeResult,
      answerFlag: result.object.answerFlag
    };
  },
});

社内ナレッジ情報で十分な情報が得られた場合社内ナレッジ情報を返すStep

const returnknowledgeResultStep = createStep({
  id: "return-knowledge-result-step",
  description: "社内ナレッジ情報の検索結果を回答として返します。",
  inputSchema: z.object({
    knowledgeResult: z.string(),
  }),
  outputSchema: z.object({
    answer: z.string(),
  }),
  execute: async ({ inputData }) => {
    return {
      answer: inputData.knowledgeResult
    };
  },
});

ワークフロー定義

const knowledgeSearchWorkflow = new createWorkflow({
  name: "knowledge-search-workflow",
  triggerSchema: z.object({ topic: z.string() }),
});

Stepを順番にワークフローへ登録

knowledgeSearchWorkflow
.then(knowledgeSearchStep)
.then(answerJudgeStep)
.branch([
  [async ({ inputData }) => inputData.answerFlag === true, returnknowledgeResultStep],
  [async ({ inputData }) => inputData.answerFlag === false, webSearchStep]
])
.commit();

ワークフロー実行(knowledge) ※ 社内ナレッジ情報に世界遺産についての記事がある前提

let res = await knowledgeSearchWorkflow.createRun().start({ inputData: { topic: "世界遺産について" } });
let answer = Object.values(res.result).find(res => res.answer)?.answer;
console.log(answer);

ワークフロー実行(Web) ※ 社内ナレッジ情報に2025年の後楽園プランについての記事がない前提

res = await knowledgeSearchWorkflow.createRun().start({ inputData: { topic: "2025年の後楽園の観光プラン" } });
answer = Object.values(res.result).find(res => res.answer)?.answer;
console.log(answer);

process.exit(0);

ポイント

以下のコードによりワークフローのStepを登録してきます。
thenにより逐次実行、branchにより条件分岐を実現しています。

thenからbranchStep間ではinputDataで値が渡されており、inputData.answerFlaganswerJudgeStepの条件分岐フラグが取得できます。
このinputData.answerFlagtrueの場合は、社内ナレッジ情報検索で回答に十分な情報が得られたと判断され、returnknowledgeResultStepにより社内ナレッジ情報検索の結果を回答として返します。そうでない場合(inputData.answerFlagfalseの場合)はWeb検索がwebSearchStepにより実行され、Web検索結果を回答として返します。

このようにしてAIエージェントに条件分岐の判断を任せながらStepを実行することができました。

// Stepを順番に登録
knowledgeSearchWorkflow
.then(knowledgeSearchStep)
.then(answerJudgeStep)
.branch([
  [async ({ inputData }) => inputData.answerFlag === true, returnknowledgeResultStep],
  [async ({ inputData }) => inputData.answerFlag === false, webSearchStep]
])
.commit();

実行結果

社内ナレッジ検索結果が回答となる場合とWeb検索結果が回答となる場合の実行結果は、それぞれ以下のようになりました。

  • 社内ナレッジ検索結果が回答となる場合
社内ナレッジ情報検索から「世界遺産」に関する情報が見つかりましたので、ご紹介します。

---

# 世界遺産とは?—人類共通の宝を守るために

世界遺産は、**文化的価値と自然的価値の両方を兼ね備えた遺産**です。

---

## 世界遺産の種類

1. **文化遺産(Cultural Heritage)**
   人類の歴史や文化を物語る建造物、遺跡、宗教施設、芸術作品などが対象です。
   例:エジプトのピラミッド、京都の古都、パリのセーヌ河岸

2. **自然遺産(Natural Heritage)**
   地球の自然の美しさや科学的価値を持つ地形、生態系、動植物の生息地などが含まれます。
   例:グレート・バリア・リーフ(オーストラリア)、イエローストーン国立公園(アメリカ)

3. **複合遺産(Mixed Heritage)**
   文化的価値と自然的価値の両方を兼ね備えた遺産です。
   例:マチュ・ピチュ(ペルー)、テワカン渓谷(メキシコ)

---

## 世界遺産をめぐる課題

- **気候変動**:海面上昇や異常気象により、自然遺産が危機にさらされています。
- **戦争・紛争**:文化遺産が破壊されるケースもあり、国際社会の対応が急務です。
- **観光公害**:観光客の増加による環境負荷や地域住民との摩擦が問題となっています。
- **資金不足**:保護活動には多額の資金が必要であり、途上国では十分な対応が難しい場合もあります。

---

ご参考になれば幸いです。
  • Web検索結果が回答となる場合
Web検索から2025年の後楽園の観光プランに関する情報が見つかりました。

---

## 2025年後楽園観光プランのポイント

1. **幻想庭園の夜間特別開園(秋)**
   - 期間:2025年11月14日(金)~11月24日(月・振休)
   - 時間:17:00~20:30
   - 内容:ライトアップされた庭園を楽しめる特別な夜間開園イベントです。幻想的な雰囲気の中で後楽園の四季を感じられます。

2. **岡山後楽園の所要時間とモデルコース**
   - 所要時間は約1~2時間程度が目安。
   - 岡山城とのセット観光がおすすめ。
   - 園内で楽しめる食べ物もあり、周辺の駐車場情報も充実しています。

3. **入園案内**
   - 高校生以下は2026年3月31日まで入園無料(証明書提示必要)。
   - 団体やグループは事前申込が必要。

4. **和文化体験コンテンツ**
   - 日本の精神文化「八百万の神」の概念を伝える体験が可能。
   - 外国人観光客だけでなく国内観光客も深い日本文化を体感できる特別な体験。

5. **夏の幻想庭園(2025年は終了予定)**
   - 例年8月1日~8月31日まで夜間ライトアップ。
   - 音楽イベントやワークショップも開催。

---

## おすすめ観光プラン例(1日)

| 時間帯       | 内容                                   |
|--------------|--------------------------------------|
| 午前         | 岡山城観光                           |
| 昼食         | 岡山後楽園周辺の飲食店で地元グルメを楽しむ |
| 午後         | 岡山後楽園散策(約1~2時間)          |
| 夕方~夜     | 11月中旬~下旬なら「秋の幻想庭園」夜間特別開園を楽しむ |

---

後楽園は日本三大庭園の一つで、四季折々の美しい景観と文化体験が魅力です。2025年の秋には幻想庭園のライトアップイベントが特におすすめです。

詳細や最新情報は公式サイトでご確認ください。

---

### 参照

1. [2025年「幻想庭園」について | 岡山後楽園夜間特別開園「幻想庭園」](http://genso-teien.okayama.jp/outline/10481/)
2. [岡山後楽園の所要時間は?観光モデルコースと見どころ](https://jp-travel.net/travel/cyuugoku/okayama/okayama-kourakuen/)
3. [入園案内/後楽園](https://okayama-korakuen.jp/goriyo/441.html)
4. [日本三大庭園・岡山後楽園で和文化体験コンテンツを開催](https://www.atpress.ne.jp/news/441997)
5. [夜間特別開園 夏の幻想庭園 | 岡山市の観光](https://okayama-kanko.net/sightseeing/event/10478/)

まとめ

  • Mastraにおいて条件分岐を使ったマルチエージェントワークフローを実現しました
  • 社内ナレッジ情報検索で回答に十分な情報が得られたかどうかをAIエージェントに判断させ、処理フローを分岐させることができました
コード全文
import { Agent } from '@mastra/core/agent';
import { createWorkflow, createStep } from '@mastra/core/workflows';
import { createAzure } from "@ai-sdk/azure";
import { config } from "./config.js";
import { z } from "zod";
import { MCPClient } from "@mastra/mcp";

// Azure接続
const azure = createAzure({ apiKey: config.apiKey, resourceName: config.resourceName });

// 社内ナレッジ情報検索MCPクライアント
const knowledgeSearchMcp = new MCPClient({
  servers:{
    growi_knowledge: {
      url: new URL("http://localhost:8000/mcp"),
    }
  }
});

// 社内ナレッジ情報検索エージェント
const knowledgeSearchAgent = new Agent({
  name: "knowledgeSearchAgent",
  instructions: "あなたは社内ナレッジ情報検索エージェントです。社内ナレッジ情報検索から検索をして情報を取得してください。 \
      以下のルール・形式に必ず従って回答してください。\
      <以下略>",
  model: azure('gpt-4.1-mini'),
  tools: await knowledgeSearchMcp.getTools()
});

// 社内ナレッジ情報検索エージェントを呼び出すStep
const knowledgeSearchStep = createStep({
  id: "knowledge-search-step",
  description: "社内ナレッジ検索エージェントを使って情報を検索します。",
  inputSchema: z.object({
    topic: z.string(),
  }),
  outputSchema: z.object({
    topic: z.string(),
    knowledgeResult: z.string(),
  }),
  execute: async (context) => {
    const topic = context?.inputData?.topic;
    const result = await knowledgeSearchAgent.generate(`${topic}`);
    return {
      topic: topic,
      knowledgeResult: result.text
    };
  },
});

// Web検索MCPクライアント
const webSearchMcp = new MCPClient({
  servers: {
    "ddg-search": {
        "command": "uvx",
        "args": ["duckduckgo-mcp-server"]
    },
  },
});

// Web検索エージェント
const webSearchAgent = new Agent({
  name: "webSearchAgent",
  instructions: "あなたはWeb検索エージェントです。Web検索をして情報を取得してください。\
      以下のルール・形式に必ず従って回答してください。\
      <以下略>",
  model: azure('gpt-4.1-mini'),
  tools: await webSearchMcp.getTools()
});

// Web検索エージェントを呼び出すStep
const webSearchStep = createStep({
  id: "web-search-step",
  description: "Web検索エージェントを使って情報を検索します。",
  inputSchema: z.object({
    topic: z.string(),
  }),
  outputSchema: z.object({
    answer: z.string(),
  }),
  execute: async ( context ) => {
    const topic = context?.inputData?.topic;
    const result = await webSearchAgent.generate(`${topic}`);
    return { 
      answer: result.text 
    };
  },
});

// 社内ナレッジ情報で十分な情報が得られたか判定するエージェント
const answerJudgeAgent = new Agent({
  name: "answerJudgeAgent",
  instructions: `あなたは質問した内容が社内ナレッジ情報から取得できたか判断するエージェントです。
  回答に十分な情報が得られた場合はtrueを返します。そうでなければfalseを返します。`,
  model: azure('gpt-4.1-mini'),
});

// 社内ナレッジ情報で十分な情報が得られたか判定するStep
const answerJudgeStep = createStep({
  id: "answer-judge-step",
  description: "社内ナレッジ情報の検索結果をもとに回答するかどうか判断します。",
  inputSchema: z.object({
    topic: z.string(),
    knowledgeResult: z.string(),
  }),
  outputSchema: z.object({
    topic: z.string(),
    knowledgeResult: z.string(),
    answerFlag: z.boolean()
  }),
  execute: async ( context ) => {
    const topic = context?.inputData?.topic;
    const knowledgeResult = context?.inputData?.knowledgeResult;
    const result = await answerJudgeAgent.generate(
      `質問内容:${topic}
       社内ナレッジ情報の検索結果:${knowledgeResult}`,
      {
        output: z.object({
          answerFlag: z.boolean()
        })
      }
    );
    return { 
      topic: topic,
      knowledgeResult: knowledgeResult,
      answerFlag: result.object.answerFlag
    };
  },
});

// 社内ナレッジ情報で十分な情報が得られた場合社内ナレッジ情報を返すStep
const returnknowledgeResultStep = createStep({
  id: "return-knowledge-result-step",
  description: "社内ナレッジ情報の検索結果を回答として返します。",
  inputSchema: z.object({
    knowledgeResult: z.string(),
  }),
  outputSchema: z.object({
    answer: z.string(),
  }),
  execute: async ({ inputData }) => {
    return {
      answer: inputData.knowledgeResult
    };
  },
});

// ワークフロー定義
const knowledgeSearchWorkflow = new createWorkflow({
  name: "knowledge-search-workflow",
  triggerSchema: z.object({ topic: z.string() }),
});

// Stepを順番にワークフローへ登録
knowledgeSearchWorkflow
.then(knowledgeSearchStep)
.then(answerJudgeStep)
.branch([
  [async ({ inputData }) => inputData.answerFlag === true, returnknowledgeResultStep],
  [async ({ inputData }) => inputData.answerFlag === false, webSearchStep]
])
.commit();

// ワークフロー実行(knowledge)
let res = await knowledgeSearchWorkflow.createRun().start({ inputData: { topic: "世界遺産について" } });
let answer = Object.values(res.result).find(res => res.answer)?.answer;
console.log(answer);

// ワークフロー実行(Web)
res = await knowledgeSearchWorkflow.createRun().start({ inputData: { topic: "2025年の後楽園の観光プラン" } });
answer = Object.values(res.result).find(res => res.answer)?.answer;
console.log(answer);

process.exit(0);
セリオ株式会社 テックブログ

Discussion