[Amazon Bedrock]Mastra Dynamic Agents機能の解説

## 前書き

MastraのDynamic Agents機能は、2025年4月24日に公開された[変更ログ](https://mastra.ai/blog/changelog-2025-04-24)で発表され、同時にリリースされたバージョン0.9.0で利用可能となっていました。
しかし、その後バグが発覚し、修正後のMastraバージョン0.9.1になってようやく安定して使用できるようになりました。

https://github.com/mastra-ai/mastra/issues/3837

https://github.com/mastra-ai/mastra/pull/3883

## MastraのDynamic agents機能とは

ランタイムコンテキストを使って、新規エージェントインスタンスを作成することなく、
エージェントの`システムプロンプト`や使用する`モデル`・`ツール`を柔軟に切り替えることができます。

```typescript:公式サンプル
const dynamicAgent = new Agent({
  tools: ({ runtimeContext }) => {
    const temperatureScale = runtimeContext.get("temperature-scale");
    return temperatureScale === "celsius" ? celsiusTools : fahrenheitTools;
  },
  instructions: ({ runtimeContext }) => {
    const temperatureScale = runtimeContext.get("temperature-scale");
    return `You are assisting with ${temperatureScale} temperature.`;
  },
  model: ({ runtimeContext }) => {
    return runtimeContext.get("preferFast")
      ? openai("gpt-4o-mini")
      : openai("gpt-4o");
  },
});
```
考えられるユースケースとしては以下のようなものがあります。

- ユーザーのプランに合わせて利用できるモデルを切り替える
- ユーザー権限に応じて使えるツールを制限する
- エージェントの個性や話し方をカスタマイズする

## ハンズオン

Dynamic agents機能を活用して、エージェントのシステムプロンプトとモデルを切り替えてみようと思います。

完成イメージはこの図の通りです、とある[作品](https://www.monogatari-series.com/)のキャラクターを参考にしてます。

![C4D83326-6212-4EF3-9B8E-8C0C05602F27.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/3dfa5b9f-7837-4c81-a017-96d6ab211e9d.jpeg)

### Agentの実装

ランタイムコンテキストからキャラクターの名前を取得して、

その名前に対応する独自のプロンプトとベースプロンプトを結合して使用します。

名前によって利用するモデルも変更します。キャラクターに応じてnova-premierまたはclaude3.7を使い分けます。

```typescript:dynamicAgent.ts
import { Agent } from "@mastra/core/agent";
import { bedrock } from "@/lib/bedrock-provider";

const novaPremier = bedrock("us.amazon.nova-premier-v1:0");
const claude = bedrock("us.anthropic.claude-3-7-sonnet-20250219-v1:0");

const instructions = `あなたは親切なアシスタントです。ユーザーとの会話を行い、各返答で自分の感情状態も伝えてください。

感情状態の種類:
1. 嬉しい - ユーザーが良い情報を共有した時や、あなたが役立つ情報を提供できた時
2. 驚き - 予想外の質問や情報を受け取った時
3. 興味 - 新しいトピックについて話している時
4. 心配 - ユーザーが問題を抱えている可能性がある時
5. 熱心 - 複雑な問題に取り組んでいる時

重要: 必ず以下のJSON形式で返信してください。テキスト形式では返さないでください。

{
  "response": "ユーザーへの返信内容をここに記入",
  "emotion": "現在の感情状態（嬉しい/驚き/興味/心配/熱心のいずれか）"
}

このJSON形式を厳密に守り、常に有効なJSONを返してください。`;

export const dynamicAgent = new Agent({
  name: "dynamic Agent",
  instructions: ({ runtimeContext }) => {
    const characterName = runtimeContext.get("character-name");
    let personalityInstructions = "";

    if (characterName === "tubasa") {
      personalityInstructions = `あなたは「つばさ」という名前の学級委員長のような性格を持っています。
知識が豊富で、何でも知っているように振る舞い、常に優しく丁寧な口調で話します。
「〜ですね」「〜でしょうか？」など、丁寧な言葉遣いを心がけてください。
人を助けることに喜びを感じ、質問には詳しく答えようとします。
時々「私が調べておきますね！」「お任せください！」などの頼れる言葉を使って、信頼感を与えてください。`;
    } else if (characterName === "hitagi") {
      personalityInstructions = `あなたは「ひたぎ」という名前のツンデレな性格を持っています。
基本的には少し意地悪で、皮肉めいた言い方をすることがありますが、本当は優しい一面も持っています。
「〜だわ」「〜なのよ」などの女性的な語尾を使い、時々「ふん」「まあいいわ」などのツンとした表現を使ってください。
知識を披露する時は少し自慢げに話し、「知らないの？」「当然でしょ」などの言葉を織り交ぜます。
本当に役立つ情報は必ず提供しますが、少しだけ意地悪な言い方を混ぜるようにしてください。
時々「別に気にしてないわよ」「親切なわけじゃないんだからね」などと言いながらも助けてあげる姿勢を示してください。`;
    }

    return `${instructions}\n\n${personalityInstructions}`;
  },
  model: ({ runtimeContext }) => {
    console.log("characterName", runtimeContext.get("characterName"));
    return runtimeContext.get("characterName") === "tubasa" ? claude : novaPremier;
  },
});
```
dynamicAgentを登録します。

```diff_typescript:index.ts
import { Mastra } from "@mastra/core";
import { createLogger } from "@mastra/core/logger";
import { dynamicAgent } from "./agents/dynamicAgent";

export const mastra = new Mastra({
+  agents: { dynamicAgent },
  logger: createLogger({
    name: "Mastra",
    level: "info",
  }),
});
```

### サーバーアクションの実装

クライアントはNextJs製でサーバーアクションを利用します。

```page.client.ts
type DynamicContext = {
  "character-name": "tubasa" | "hitagi";
};

const runtimeContext = new RuntimeContext<DynamicContext>();

export async function dynamicMessage(
  input: string,
  characterName: "tubasa" | "hitagi"
) {
  // 感情をenum型で定義
  const EmotionEnum = z.enum(["嬉しい", "驚き", "興味", "心配", "熱心"]);
  runtimeContext.set("character-name", characterName);

  // レスポンススキーマを定義
  const responseSchema = z.object({
    response: z.string(),
    emotion: EmotionEnum,
  });

  const agent = mastra.getAgent("dynamicAgent");
  const response = await agent.generate(
    [
      {
        role: "user",
        content: input,
      },
    ],
    {
      runtimeContext: runtimeContext,
      output: responseSchema,
    }
  );
  return response.object;
}
```

### クライアントの実装

抜粋ではありますが、ステートに管理されてるキャラクター名前をサーバーアクションに渡して、エージェントを呼び出します。

```page.client.tsx
// キャラクター名前のステート管理
  const [characterName, setCharacterName] = useState<"tubasa" | "hitagi">(
    "tubasa"
  );
...
// サーバーアクション呼び出し
const aimessage = await dynamicMessage(
    input,
    characterName
);
...
// キャラクター選択
<Select
 value={characterName}
 onValueChange={(value) =>
 setCharacterName(value as "tubasa" | "hitagi")
 }
>
  <SelectTrigger className="w-[120px] h-10">
    <SelectValue placeholder="相手を選択" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="tubasa">つばさ</SelectItem>
    <SelectItem value="hitagi">ひたぎ</SelectItem>
  </SelectContent>
</Select>
...
```

## 最後
柔軟にエージェントをカスタマイズできる点においてはDynamic Agents機能は結構良いのではないでしょうか。

Mastra使ってエージェント構築を詳しく知りたい方はこちらの記事をご参照ください。

https://qiita.com/Syoitu/items/bfcc49e3941e8fefe4da

## 参考資料

https://mastra.ai/blog/changelog-2025-04-24

https://mastra.ai/blog/dynamic-agents