グラフ構築と状態管理の実装アプローチから見る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を使わず、簡略化した処理のみで実装しています)
- ユーザー入力を受け付ける (Start)
- 入力文書のタイプを判定 (条件分岐)
- [技術文書の場合]
- 技術用語を抽出し、専門用語辞書と照合
- 抽出した技術用語の正確性を検証し、最新の技術標準と照合
- [ビジネス文書の場合]
- ビジネス要点、ROI関連情報、予算項目を抽出
- 市場への潜在的影響を分析、競合分析を実行
- 分析結果を統合し、フォーマット済みレポートを生成 (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
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)
のような連鎖は、データの流れを直感的に表現できます
- 例:
- 各ステップの入出力が明確で追跡しやすい👍
-
inputSchema
とoutputSchema
の明示的な定義により、各ステップが何を受け取り何を返すかがハッキリします
-
デメリット:
- 分岐後の処理が少ない場合でも、ネストされたフローが必要になるのが少し面倒
- 例:サンプルコード内で単純な条件分岐のために
createWorkflow({...}).then().then().commit()
という冗長な構造が必要に
- 例:サンプルコード内で単純な条件分岐のために
- 複雑な分岐構造では、サブワークフローのネストが深くなり可読性が低下することも
- 例:3層以上のネストが発生した場合、
.branch([...]).map().then()
のネストが複雑になりコードの追跡が難しくなります
- 例:3層以上のネストが発生した場合、
- 現行バージョン(0.10.0)のPlaygroundではネスト構造を持つWorkflowの可視化は十分とは言えない状況です 😢
- ネストしたWorkflowは
View workflow
ボタンをクリックすることで、モーダル表示されます。そのため、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によるスキーマ定義により、型安全性が高いのが魅力的✨
- 前のステップの
outputSchema
とinputSchema
が一致していなければ型のエラーが出るので、早期に問題を発見できます!
- 前のステップの
- 各ステップが必要な入力のみを受け取るため、意図しない副作用が起きにくい👍
デメリット:
- グラフが複雑になると、データの受け渡しが煩雑になることも 😅
-
inputSchema
とoutputSchema
を全ステップで定義する必要があるので、コード量が増えがちです
-
- ワークフロー実行時、途中経過での全体の状態を一覧で確認するのが少し難しいです
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