コンテンツにスキップ

mastra_basics: Mastra で学ぶ TypeScript AI エージェント開発

一言の本質: Mastra は「エージェント(LLM が流れを決める)とワークフロー(コードが流れを決める)を、同じ登録簿(Mastra インスタンス)に載る対等な部品として提供する TS フレームワーク」。 LangChain + LangGraph が2つのライブラリで担う役割を、1つの @mastra/core が最初から統合設計で持っている — この「全部入りだが薄い」感覚を10本の写経で掴む。

書籍『MastraによるAIエージェント開発・運用[実践入門]』(技術評論社、2026-07)の1〜6章・9〜11章の核概念に対応する(対応表は README.md)。

全体像

Mastra の部品は「Mastra インスタンス=登録簿」を中心に組み上がる。

flowchart TD
    MastraCore[("Mastra インスタンス<br>アプリ全体の登録簿<br>ex03")]

    subgraph AgentSide["Agent — LLM が流れを決める"]
        AgentNode["Agent<br>instructions + model<br>ex01"]
        ToolNode["tools: createTool<br>zod が仕様書<br>ex02"]
        MemNode["memory: Memory<br>履歴・付箋・想起<br>ex05"]
        SubAgents["agents: 部下エージェント<br>委譲ツール化<br>ex06"]
        ProcNode["processors: 入出力の関所<br>tripwire<br>ex09"]
    end

    subgraph WfSide["Workflow — コードが流れを決める"]
        StepNode["createStep<br>zod で配線を型付け"]
        ChainNode["then / parallel / branch<br>ex03"]
        SuspendNode["suspend / resume<br>HITL ex07"]
    end

    subgraph DataSide["データと評価"]
        RagNode["MDocument + LibSQLVector<br>RAG ex08"]
        EvalNode["createScorer + runEvals<br>ex10"]
    end

    MastraCore --> AgentNode
    MastraCore --> ChainNode
    AgentNode --> ToolNode
    AgentNode --> MemNode
    AgentNode --> SubAgents
    AgentNode --> ProcNode
    StepNode --> ChainNode
    ChainNode --> SuspendNode
    RagNode -.->|検索ツールとして| ToolNode
    EvalNode -.->|golden dataset で採点| AgentNode

読む順もこの図の通り: Agent 側(ex01→02→04→05→06)と Workflow 側(ex03→07)を往復し、最後にデータ(ex08)・防御(ex09)・評価(ex10)で外堀を埋める。

使用ライブラリ・原理

Mastra とは何か(LangChain / LangGraph との対比)

観点 LangChain + LangGraph (Python) Mastra (TypeScript)
エージェント create_agent() が実行ループを構築 new Agent({...}) クラスを直接 new
決定論フロー LangGraph の StateGraph(ノード+エッジのグラフ) createWorkflow().then().parallel().commit()(メソッドチェイン)
モデル指定 プロバイダ SDK を import して注入 'openai/gpt-4o-mini' 文字列1つ(モデルルーティング。AI SDK 由来)
状態永続化 checkpointer を差す Mastra / Memory に storage(LibSQL 等)を差す
ガードレール Middleware(@before_model 等)を自分で書く processors に検知器クラスを並べる
評価 別ライブラリ(Ragas 等)か素手 @mastra/core/evals が同梱

ポイントは「グラフを描かずにメソッドチェインで済ませる」設計。LangGraph でノード・エッジ・State 型を書いていた作業が、Mastra では zod スキーマ付きステップの .then() 連結になる。表現力はグラフに劣る場面もあるが、TS の型推論が「前段の出力=次段の入力」を静的に検査してくれる(ex03 で inputSchema をわざと変えると tsc が落ちるのを試すとよい)。

モデルルーティング(ex01)

model: 'openai/gpt-4o-mini' と書くだけで、env の OPENAI_API_KEY を見て OpenAI に解決される。プロバイダ SDK の import がゼロ。Anthropic に切り替えるなら文字列を 'anthropic/claude-...' に書き換えるだけ(book 4章の「AI SDK によるマルチ LLM 対応」の土台がこれ)。local_models/ レクチャーでやった「モデルは base_url 1つで差し替え可能な部品」の TS 版と考えると早い。

エージェントとワークフローの使い分け(ex01 vs ex03)

判定基準は1文: 「実行経路を毎回 LLM に決めさせたいか(Agent)、コードで固定したいか(Workflow)」。 書籍6章の Deep Research が好例で、「検索→評価→レポート」という大枠は Workflow で固定し、各ステップの中身(何を検索するか)は Agent に任せる。ex03/ex07 の execute 内で mastra.getAgent() する形がその縮図。

メモリの3層(ex05)

実体 効く範囲 一言
会話履歴 (lastMessages) 直近 N 件をそのまま注入 同一 thread 「議事録をそのまま読む」
ワーキングメモリ (workingMemory) LLM が更新し続ける要約テンプレ scope: 'resource' なら thread 跨ぎ 「相手の名刺に書き足す付箋」
セマンティックリコール (semanticRecall) 過去ログの埋め込み検索 同上 「過去の議事録を検索して該当ページだけ開く」

実出力で確認できたこと: thread-2(履歴ゼロの新会話)で「石川県に住んでいる」を言い当てた。これは履歴ではなく resource スコープの記憶(付箋+検索)が効いた証拠。

プロセッサ=関所、ブロック=tripwire(ex09)

inputProcessors / outputProcessors は LLM の前後に挟む関所。検知器(PromptInjectionDetector / ModerationProcessor / PIIDetector 等)はそれ自体が小さな LLM 判定なので、guardrails_basics/ で学んだ「確率の防御」に分類される。ブロックは例外を投げず、response.tripwire に理由が入って結果として返る(アプリ側で分岐しやすい設計)。

ファイル別の役割

ファイル 役割
ex01_agent_basics.ts Agent 最小構成。モデルルーティング文字列・generate()・usage の観察
ex02_tools.ts createTool。zod スキーマ=LLM への仕様書、toolCalls / toolResults の観察
ex03_workflow.ts createStep / createWorkflow / .then() / .parallel()、Mastra インスタンス経由の Agent 参照
ex04_streaming_structured.ts stream().textStreamstructuredOutput(検証済み response.object
ex05_memory_threads.ts Memory 3層と thread / resource の区画。LibSQL ファイル1個で永続化
ex06_multi_agent.ts agents: {...} によるスーパーバイザー。委譲=ツール呼び出しの再帰
ex07_hitl_workflow.ts suspend() / resume()。HITL の状態機械
ex08_rag.ts チャンク→埋め込み→ベクトル保存→検索ツール化(agentic RAG)+幻覚チェック
ex09_guardrails_processors.ts 入力プロセッサ2枚(injection / moderation)と tripwire
ex10_evals_scorers.ts 決定論 scorer 2本 + runEvals バッチ評価。content 構造の落とし穴込み

コードの挙動解説

ex07: suspend / resume の状態機械(HITL の心臓部)

ex07_hitl_workflow.ts:36-52 の承認ステップ:

const approveDraft = createStep({
  id: 'approve-draft',
  inputSchema: z.object({ draft: z.string() }),
  outputSchema: z.object({ finalText: z.string() }),
  resumeSchema: z.object({ approved: z.boolean(), comment: z.string() }),   // ①
  suspendSchema: z.object({ draftForReview: z.string() }),                  // ②
  execute: async ({ inputData, resumeData, suspend }) => {
    if (!resumeData) {                                                      // ③
      return await suspend({ draftForReview: inputData.draft })             // ④
    }
    if (!resumeData.approved) {
      return { finalText: `【差し戻し】${resumeData.comment}` }              // ⑤
    }
    return { finalText: `${inputData.draft}\n(承認者コメント: ...)` }
  },
})
やってること なぜそうする
人間から受け取るデータの型 resume() 時に zod 検証される。「承認フォームのスキーマ」に相当
人間に見せるデータの型 レビュー UI が表示すべき情報(ここでは下書き)を型で宣言
resumeData の有無で初回/再開を判別 同じ execute が2回呼ばれるのがこの API の肝。初回は undefined
suspend() で run 全体を停止 戻り値ではなく「状態遷移」。run は 'suspended' になり、プロセスが死んでも storage があれば後日再開できる
差し戻しも「正常な出力」として返す 却下=エラーではない。HITL の結果は常に outputSchema に載せる

呼び出し側(ex07_hitl_workflow.ts:72-85)の実行シーケンス:

sequenceDiagram
    participant App as 呼び出し側
    participant Run as Workflow Run
    participant Human as 人間の承認者

    App->>Run: run.start(topic)
    Run->>Run: write-draft 実行(LLM)
    Run->>Run: approve-draft 実行 → suspend()
    Run-->>App: status: 'suspended', suspended: [['approve-draft']]
    App->>Human: 下書きを提示(本来は Web UI)
    Human-->>App: 承認 + コメント
    App->>Run: run.resume(step, resumeData)
    Run->>Run: approve-draft を resumeData 付きで再実行
    Run-->>App: status: 'success', result.finalText

実行結果(実出力): status: suspended中断中の step: [["approve-draft"]] → resume 後 status: success で承認コメント付きの本文が返った。

ex10: 「エラーは出ないが常に 0」の壊れた scorer(実際に踏んだ穴)

初版の scorer は run.output.map((m) => m.content).join('') と書いて 常に 0 を返した。原因は content が文字列ではないこと。probe で実構造を見ると:

run.output = [{ "role": "assistant", "content": { "format": 2, "parts": [{ "type": "text", "text": "金沢市です。" }], "content": "金沢市です。" } }]

つまり m.content はオブジェクトで、暗黙の文字列化で '[object Object]' になり includes('金沢市') が常に false。修正は ex10_evals_scorers.ts:24-33 のテキスト抽出ヘルパー:

function outputText(output: ScorerRunOutputForAgent): string {
  return output
    .map((message) => {
      const { content } = message
      if (typeof content === 'string') return content
      return typeof content.content === 'string' ? content.content : ''
    })
    .join('')
}

これは testing.md の「green だが何も検証していないテスト」問題そのもの。scorer を書いたら、まず正解ケースで 1 が出ることを確認してから使う(scorer 自体の検証)。修正後は contains-ground-truth: 1 / is-concise: 1

学んだこと(要点)

  • モデルルーティング文字列だけで LLM が動く。SDK import ゼロ。プロバイダ差し替え=文字列書き換え(書籍4章のマルチ LLM 対応の土台)
  • toolCalls に出るツール名は tools: { fxRateTool } のキー名。id(get-fx-rate)ではない方が表示された — 命名はキー側も意識する
  • サブエージェントは agent-<id> という名前のツールとして親に見える(ex06 実出力: agent-researcher / agent-copywriter)。マルチエージェント=ツール呼び出しの再帰
  • storage 未設定の Mastra はメモリ内フォールバックで警告を出す(ex03/ex07)。suspend した run を「数日後に再開」するには LibSQL 等の永続 storage が必須 — 警告文がそのまま本番要件を教えてくれる
  • suspend/resume は「同じ execute が2回呼ばれる」モデルresumeData の有無が初回/再開のフラグ
  • プロセッサのブロックは tripwire という結果で返る(例外ではない)。ログに出るスタックトレースは内部ログでクラッシュではない
  • ハマりどころ: op run の並列実行は認証プロンプトが競合して落ちるauthorization prompt dismissed)。ex は直列で回す
  • ハマりどころ: scorer の run.output[].content はオブジェクト(上記)。String() 頼みの抽出は静かに壊れる

用語集

混同しやすい語の階層:

Mastra インスタンス(登録簿)
├── Agent ─ tools / memory / agents / processors / scorers を持つ
│     └── 実行 = generate() / stream()(1回の呼び出しで LLM+ツールのループが回る)
└── Workflow ─ Step を then / parallel / branch で配線
      └── 実行 = createRun() → run.start()(run が状態を持つ。suspended ⇄ resume)
用語 何か 具体例(このレクチャー) 判定基準1文
Agent LLM に流れを決めさせる実行単位 ex01 new Agent({...}) 「実行経路が入力によって変わってよいか? Yes → Agent」
Workflow コードで流れを固定する実行単位 ex03 createWorkflow() 「毎回同じ順で走るべきか? Yes → Workflow」
Step Workflow の1マス。zod で入出力を型付け ex03 createStep({...}) 「配線の型(inputSchema/outputSchema)を持つ最小単位」
Run Workflow の1回の実行インスタンス ex07 workflow.createRun() 「状態(suspended 等)を持つのは Workflow ではなく Run」
Tool LLM が呼ぶと決め、TS が実行する関数 ex02 createTool({...}) 「zod スキーマが LLM への仕様書」
thread 会話1本の区画 ex05 thread: 'thread-1' 「チャット画面の1つのスレッド=1 thread」
resource ユーザー(等)の区画。thread を束ねる ex05 resource: 'user-yamamoto' 「thread を跨いで覚えたい主体=resource」
ワーキングメモリ LLM が更新する要約の付箋 ex05 workingMemory 「履歴そのものではなく、抽出された状態」
セマンティックリコール 過去ログの埋め込み検索 ex05 semanticRecall 「lastMessages に入らない昔の発言を意味で引く」
Processor LLM 前後の関所 ex09 inputProcessors 「メッセージが LLM に届く前/出た後に挟まるか?」
tripwire プロセッサがブロックした結果 ex09 response.tripwire 「ブロック=例外ではなく結果フィールド」
Scorer 1観点の採点関数 ex10 createScorer 「観点1つ=scorer 1本(合議は複数 scorer で)」
runEvals dataset × scorer の一括実行 ex10 「golden dataset を回す CI ゲートの本体」

困りごと → 見る/使うもの クイック早見表:

困りごと 見る/使うもの
LLM がツールを使ってくれない response.toolCalls(空なら instructions と description を疑う)
会話を覚えていない thread/resource の指定漏れ → ex05 の memory: { thread, resource }
suspend した run を後日再開したい Mastra に永続 storage(LibSQL 等)を設定
出力を JSON で受けたい structuredOutput: { schema }response.object
入力攻撃を止めたい inputProcessors + tripwire 確認
プロンプト変更の回帰が怖い runEvals を CI に(eval_basics/ の回帰ゲートと同型)

拡張アイデア

  1. Mastra Studio を触る(書籍2.6): npm create mastra@latest で正規プロジェクトを作り、ex の Agent/Workflow を src/mastra/index.ts に移植して mastra dev の GUI(実行・トレース閲覧)で観察する
  2. Langfuse 連携(書籍11.4): Mastra の observability exporter で ex06 のマルチエージェント実行を Langfuse に流し、langfuse_basics/ で見た trace ツリーと比較する
  3. Deep Research ミニ再現(書籍6章): ex07 の HITL を「検索ワークフロー→ユーザー評価→レポート生成」の3段に拡張する(検索は Tavily か mock)
  4. LLM-as-a-Judge scorer(書籍11章): ex10 に createScorer().generateScore() 内で judge LLM を呼ぶ採点を足し、eval_basics/ で実測した judge バイアスが Mastra でも出るか確かめる
  5. ブランチ: ex03 に .branch()(条件分岐)を足して flowchart との対応を確認する

既知の注意点

  • npm install 時に zod v3 系を求める深い依存の peer 警告が出るが、zod 4.4.3 に dedupe されて動作に問題なし(typecheck・全実行 green)
  • ex03 / ex07 の「No storage configured」警告は仕様(in-memory フォールバック)。学習用途では無視してよい
  • ex09 のブロック時スタックトレースは Mastra 内部ログ(プロセッサはワークフローとして実行されるため)。クラッシュではない
  • op run を複数並列で起動しない(1Password の認証プロンプトが競合)

参考


作成: 2026-07-14 / 最終更新: 2026-07-14