🤖

グラフ構築と状態管理の実装アプローチから見るMastraとLanggraphの比較

に公開

1. はじめに

FastDOCTORの技術部門(通称FDT:FastDOCTOR Technologies)のIncubationグループでインターンをしている河内(かわち)です!

実は私は2023年にFastDOCTORが開催したハッカソンを通じて参加することになりました。当時のハッカソン参加体験はこちらのブログ記事にまとめていますので、興味のある方はぜひご覧ください。そして、嬉しいことにFastDOCTORでは近日中に再びハッカソンの開催が予定されています!ぜひチェックしてみてください。 connpass

FastDOCTORの開発環境は多様で、プロジェクトによって異なるフレームワークを採用しています。一部のプロジェクトではLangGraphを活用していますが、別のプロジェクトではMastraを使用しており、それぞれの特性を活かした開発が行われています。


今回は大規模言語モデル(LLM)を活用したアプリケーション開発で注目されている2つのフレームワーク、MastraとLangGraphについて比較してみました🔍

LLMワークフロー構築で同じ目的を持ちながらも異なるアプローチをとるこれらのフレームワーク、どちらが自分のプロジェクトに適しているのか迷っている方も多いのではないでしょうか?

LangGraphはPythonがメイン実装でありながらJS/TSサポートも充実させている一方、MastraはTypeScriptファーストに設計されていて、静的型システムの恩恵を最大限に活かした型安全性が魅力です!

新興のMastraはすでにGitHubスター数でLangGraphを上回るなど、この分野の技術競争は激しさを増しています!(最近は拮抗しているようです💥)

この記事では、抽象的な特徴比較でなく、具体的なコード例を通じて2つのフレームワークの実装アプローチの違いを比較検証します。グラフ構築や状態管理の違いを理解して、プロジェクトに合ったフレームワーク選択の参考にしていただければ嬉しいです!

2. MastraとLangGraphの概要

LangGraph

  • 開発元 : LangChain
  • 対応言語 : Python / JavaScript(TypeScript)
  • GitHubリポジトリ : https://github.com/langchain-ai/langgraph
  • 検証時の最新バージョン : 0.3.0 (LangGraph.js) / 0.4.8 (LangGraph)
  • 特徴 : 最大手クラスのAI Framework🔥。ノードやエッジを直接操作できるため、複雑なワークフローの構築に強みがあります。PlaygroundやLangSmith Cloudなどのエコシステムや機能の網羅性、大規模なコミュニティなど、他のツールよりも充実した環境が整っています✨

Mastra

  • 開発元 : Mastra AI
  • 対応言語 : JavaScript(TypeScript)
  • GitHubリポジトリ : https://github.com/mastra-ai/mastra
  • 検証時の最新バージョン : 0.10.1
  • 特徴 : 新興勢力トップのAI Frameworkで、スター数ではすでにLangGraphを超えています🚀 LangGraphがPython主体なのに対し、MastraはTypeScript環境向けに設計されたフレームワークです! TracingやDebuggerなどの機能を標準搭載しており、TypeScriptの型システムを最大限に活用した開発者体験重視の設計が特徴です👍

3. MastraとLangGraphの比較

今回は、次のような複数の条件分岐を持つワークフローを実装例として見ていきます!
基本的なグラフ構造は次のとおりです。(AgentやToolを使わず、簡略化した処理のみで実装しています)

  1. ユーザー入力を受け付ける (Start)
  2. 入力文書のタイプを判定 (条件分岐)
  3. [技術文書の場合]
    1. 技術用語を抽出し、専門用語辞書と照合
    2. 抽出した技術用語の正確性を検証し、最新の技術標準と照合
  4. [ビジネス文書の場合]
    1. ビジネス要点、ROI関連情報、予算項目を抽出
    2. 市場への潜在的影響を分析、競合分析を実行
  5. 分析結果を統合し、フォーマット済みレポートを生成 (End)

LangGraph/MastraでこのWorkflowを実装したTypeScriptでのサンプルコードを示します。
今回はMastraがJavaScript/TypeScriptのみの対応であることを考慮し、LangGraphもPythonではなくLangGraph.jsを使用します。(LangGraphは、一部の高度な機能やエコシステム統合はPython版が先行してアップデートされていますが、基本機能や設計は同じです)。
それぞれ、検証を行った2025/06/05時点で最新のバージョンを使用しています。

  • LangGraph.js 0.3.0
  • Mastra 0.10.1
LangGraphのサンプルコード全体
Mastraのサンプルコード全体

Workflowの構築の違い

LangGraph

LangGraphでは、Workflowに対してノードをまず定義し、そのノードに対してリレーションとしてエッジを貼るという流れになっています。

// グラフ構築
const workflow = new StateGraph(GraphAnnotation)
.addNode("classify", classifyDocument)
.addNode("extract_technical", extractTechnicalTerms)
.addNode("extract_business", extractBusinessPoints)
.addNode("verify_technical", verifyTechnicalAccuracy)
.addNode("analyze_market", analyzeMarketImpact)
.addNode("generate_report", generateReport)

// エッジの追加
workflow.addConditionalEdges(
  "classify",
  (state) => state.documentType,
  {
    "technical": "extract_technical",
    "business": "extract_business"
  }
);
workflow.addEdge("extract_technical", "verify_technical");
workflow.addEdge("extract_business", "analyze_market");
workflow.addEdge("verify_technical", "generate_report");
workflow.addEdge("analyze_market", "generate_report");

// 開始点の設定
workflow.addEdge(START, "classify");
メリット:
  • グラフ構造をノードとエッジを用いて定義できるので、複雑な分岐や合流が柔軟に表現できます✨
    • 例:workflow.addConditionalEdges での条件分岐は、ノード間の複雑な関係を単一の場所で定義できる
  • 後から新しいノードやエッジを追加するなど、グラフ構造の変更が簡単!
    • 例:workflow.addNode メソッドを使って、既存グラフに新機能を後から追加できる。
デメリット:
  • グラフが大きくなると、全体の依存関係を把握するのが難しくなる場合も。
    • 多数のノードでグローバル状態を更新すると、デバッグ時に「どのノードがこの値を設定したのか」を特定する手間がかかります。

Mastra

Mastraは、 then()branch() などのメソッドチェーンを用いてステップベースで制御フローを定義します。
0.10以降はさらにシンプルなワークフロー構築を目指した設計に変更されました。直列的な処理が基本で、複雑な分岐や合流はネストされたワークフローを使用することが推奨されています。

Nested Workflows are now first-class citizens and the first primitive to reach for when composing any nested control flows or circular execution structures

https://mastra.ai/blog/vNext-workflows

export const documentAnalysisWorkflow = createWorkflow({
  id: "document-analysis-workflow",
  description: "文書を分析し、種類に応じた適切な分析を行うワークフロー",
  inputSchema: z.object({
    document: z.string().describe("分析対象の文書内容"),
  }),
  outputSchema: z.object({
    finalReport: z.string().describe("生成された最終レポート"),
  }),
  steps: [classifyDocument, extractTechnicalTerms, extractBusinessPoints, verifyTechnicalAccuracy, analyzeMarketImpact, generateReport]
})
.then(classifyDocument)
.branch([
  [async ({ inputData }) => inputData.documentType === "technical", 
    createWorkflow({
      id: "technical-branch",
      inputSchema: z.object({
        documentType: z.enum(["technical", "business"]),
        document: z.string(),
      }),
      outputSchema: z.object({
        analysisResult: z.string(),
        extractedTerms: z.array(z.string()),
        document: z.string(),
        documentType: z.enum(["technical", "business"]),
      }),
      steps: [extractTechnicalTerms, verifyTechnicalAccuracy]
    })
    .then(extractTechnicalTerms)
    .then(verifyTechnicalAccuracy)
    .commit()
  ],
  [async ({ inputData }) => inputData.documentType === "business", 
    createWorkflow({
      id: "business-branch",
      inputSchema: z.object({
        documentType: z.enum(["technical", "business"]),
        document: z.string(),
      }),
      outputSchema: z.object({
        analysisResult: z.string(),
        extractedTerms: z.array(z.string()),
        document: z.string(),
        documentType: z.enum(["technical", "business"]),
      }),
      steps: [extractBusinessPoints, analyzeMarketImpact]
    })
    .then(extractBusinessPoints)
    .then(analyzeMarketImpact)
    .commit()
  ]
])
.map(async ({ inputData }) => {
  const branchData = inputData["technical-branch"] || inputData["business-branch"];
  return {
    analysisResult: branchData.analysisResult,
    extractedTerms: branchData.extractedTerms,
    document: branchData.document,
    documentType: branchData.documentType
  };
})
.then(generateReport)
.commit();
メリット:
  • 直列的な処理の流れが視覚的に理解しやすい✨
    • 例:.then(classifyDocument).then(extractTechnicalTerms) のような連鎖は、データの流れを直感的に表現できます
  • 各ステップの入出力が明確で追跡しやすい👍
    • inputSchemaoutputSchema の明示的な定義により、各ステップが何を受け取り何を返すかがハッキリします
デメリット:
  • 分岐後の処理が少ない場合でも、ネストされたフローが必要になるのが少し面倒
    • 例:サンプルコード内で単純な条件分岐のために createWorkflow({...}).then().then().commit() という冗長な構造が必要に
  • 複雑な分岐構造では、サブワークフローのネストが深くなり可読性が低下することも
    • 例:3層以上のネストが発生した場合、.branch([...]).map().then() のネストが複雑になりコードの追跡が難しくなります
  • 現行バージョン(0.10.0)のPlaygroundではネスト構造を持つWorkflowの可視化は十分とは言えない状況です 😢
    • ネストしたWorkflowは View workflow ボタンをクリックすることで、モーダル表示されます。そのため、Workflow全体をそのまま概観することができません。

      ネストされたWorkflowの中身は表示されていない

      モーダルとして別に表示される様子

ノードとエッジをそのまま扱うことのできるLangGraphと、並列・直列・ループ・分岐などの単純な要素を抽象化して独自の記法でチェーンさせるMastraというイメージですね!

状態管理(入出力)の違い

LangGraphのグローバルステート方式

LangGraphでは、グラフ全体で共有される単一の状態オブジェクトを使用します。各ノードは状態の一部を更新し、更新された状態を次のノードに渡します。State はその時点でのワークフローのスナップショットを表すものになります。

// 前の例から状態管理に関連する部分を抜粋
const GraphAnnotation = Annotation.Root({
  ...MessagesAnnotation.spec,
  document: Annotation<DocumentContent>,
  documentType: Annotation<DocumentType>,
  extractedTerms: Annotation<ExtractedTerms>,
  analysisResult: Annotation<AnalysisResult>,
  finalReport: Annotation<Report>,
})

function generateReport(state: typeof GraphAnnotation.State): typeof GraphAnnotation.State {
  const documentType = state.documentType;
  const analysis = state.analysisResult;
 
  state.finalReport = `
【文書分析レポート】
文書タイプ: ${documentType === "technical" ? "技術文書" : "ビジネス文書"}
分析結果: ${analysis}

詳細:
${state.extractedTerms.map((item, index) => `${index + 1}. ${item}`).join('\n')}
  `;
  
  return {
    ...state,
    messages: [
      ...state.messages,
      new AIMessage(state.finalReport)
    ]
  };
}
メリット:
  • 状態が一元管理されるため、複雑なメッセージング機構や更新処理が不要でシンプル!
デメリット:
  • 複雑なグラフでは状態の依存関係や更新タイミングが不明確になりやすいです 😔
    • 例:サンプルコードの function generateReport では、他のノードで更新された複数の状態フィールドにアクセスする必要があり、どのノードが何を更新したかを追跡するのが難しくなります
  • 無関係なノードも全状態にアクセスできるため、意図しない副作用が起きる可能性があるのも注意点です!

Mastraのステップごとの入出力方式

Mastraでは、各ステップが明示的に入力を受け取り出力を返します。次のステップはその出力を入力として受け取る形になります(バケツリレー形式)

export const extractTechnicalTerms = createStep({
  id: "extract_technical_terms",
  description: "技術文書から技術用語を抽出します",
  inputSchema: z.object({
    documentType: z.enum(["technical", "business"]).describe("文書の種類"),
    document: z.string().describe("分析対象の文書内容"),
  }),
  outputSchema: z.object({
    extractedTerms: z.array(z.string()).describe("抽出された技術用語のリスト"),
    document: z.string().describe("元の文書内容"),
  }),
  execute: async ({ inputData }) => {
    // 略
  }
});

export const verifyTechnicalAccuracy = createStep({
  id: "verify_technical_accuracy",
  description: "抽出された技術用語の正確性を検証します",
  inputSchema: z.object({
    extractedTerms: z.array(z.string()).describe("抽出された技術用語のリスト"),
    document: z.string().describe("元の文書内容"),
  }),
  outputSchema: z.object({
    analysisResult: z.string().describe("技術用語の正確性検証結果"),
    extractedTerms: z.array(z.string()).describe("抽出された技術用語のリスト"),
    document: z.string().describe("元の文書内容"),
  }),
  execute: async ({ inputData }) => {
    // 略
  }
})
メリット:
  • TypeScriptの型システムとZodによるスキーマ定義により、型安全性が高いのが魅力的✨
    • 前のステップの outputSchemainputSchema が一致していなければ型のエラーが出るので、早期に問題を発見できます!
  • 各ステップが必要な入力のみを受け取るため、意図しない副作用が起きにくい👍
デメリット:
  • グラフが複雑になると、データの受け渡しが煩雑になることも 😅
    • inputSchemaoutputSchema を全ステップで定義する必要があるので、コード量が増えがちです
  • ワークフロー実行時、途中経過での全体の状態を一覧で確認するのが少し難しいです

4. 現実的なシナリオでの選択ガイド

シナリオ1: 複雑な条件分岐を持つRAG(検索拡張生成)システム

RAGシステムでは、ユーザークエリに基づいて検索を行い、結果に応じて異なる処理パスをたどる必要があります。

LangGraphが有利な点:

  • 検索結果の数や品質に基づいて、動的に異なるノードへ分岐させるグラフ構造が構築しやすい!🔀
  • グローバル状態により、検索結果や中間生成物を複数のノードで共有しやすい👍

Mastraが有利な点:

  • 検索結果の型が明確に定義され、後続処理との型の整合性が保証されるので安全に開発できます✨

シナリオ2: 並列処理と結果統合が必要なマルチエージェントシステム

複数のエージェントが並行して動作し、結果を統合するシステムでは、処理の並列性と状態管理が重要になります!

LangGraphが有利な点:

  • 同じ始点から複数のエッジを貼ることで、並列処理を簡潔に定義できます👍
  • 複数の並列ノードからの結果をグローバル状態に集約しやすいです✨

Mastraが有利な点:

  • 並列実行の結果を型安全に統合できるので、安心して開発を進められます!👍

シナリオ3: フロントエンド統合を重視するWeb アプリケーション

フロントエンドと密に連携するAIアプリケーションでは、統合のしやすさが重要です!

LangGraphが有利な点:

  • PythonとJavaScriptの両バージョンがあり、バックエンドが主にPythonの場合は統合しやすいです👍

Mastraが有利な点:

  • TypeScriptファースト で、React/Next.jsなどのフロントエンドフレームワークとの親和性が抜群に高いです!🔥
  • 型定義が充実しているので、FE-BE間の型の一貫性を維持しやすく開発体験が向上します✨

5. まとめ

MastraとLangGraphは、どちらも強力なAI Frameworkですが、設計思想や実装アプローチに大きな違いがあります!

LangGraphのアプローチでは、状態全体を直接操作するため、複数のプロパティへのアクセスや更新が簡単です。しかし、大規模アプリケーションでは何がどこで更新されたかの追跡が難しくなる可能性があります 🤔

一方、Mastraのアプローチでは、各ステップが必要とする入力と生成する出力を明示することで、データの流れがよりトレーサブルになります。ただし、多数のフィールドを持つ状態の場合、毎回必要なフィールドを列挙する必要があり冗長になることがあります。📝

プロジェクトの性質や開発チームの好みによって、どちらが適しているかは変わってくると思います!ぜひ自分のプロジェクトに合った方を選んでみてください✨

皆さんのプロジェクトでの経験や、どちらを選んだのかなどコメントをいただけると嬉しいです!🙌

ファストドクター テックブログ

Discussion