RAGを超えて:LangGraphを用いたエージェント検索の実装による、より賢明な知識取得
Onyx は LangGraph を活用したエージェント検索アーキテクチャを提案し、従来の RAG パターンを超えた自律的な知識探索と意思決定プロセスの確立を示している。
キーポイント
RAG からエージェント検索への進化
単なる情報検索(Retrieval)から、LLM が計画・推論・実行を行う自律的な「エージェント検索」へパラダイムシフトする必要性を説く。
LangGraph による複雑なワークフローの実装
LangChain の LangGraph を用いて、複数のドキュメントソース(Slack, Drive, Confluence)間を横断し、条件分岐や反復処理を行う高度なエージェントフレームワークを構築する手法を紹介。
実証事例と採用企業の動向
Klarna、Replit、AppFolio などの主要企業が LangGraph を基盤としたエージェントフレームワークの採用を進めている現状を報告し、Onyx の技術的優位性を裏付ける。
エンタープライズデータ統合と専門家の創出
多様な非構造化データを統合し、LLM を活用してチームごとの「専門知識を持つアシスタント」を構築することで、業務生産性を最大化するアプローチを提示。
影響分析・編集コメントを表示
影響分析
この記事は、LLM アプリケーション開発の潮流が「静的な検索(RAG)」から「動的な推論・実行(Agent)」へと急速にシフトしていることを示す重要な指標です。特に LangGraph の成熟と主要企業の採用事例を挙げることで、開発者がより複雑で自律的なシステムを構築する際の具体的な指針となり、エンタープライズ AI の実用化スピードを加速させる可能性があります。
編集コメント
RAG の限界を打破し、自律的な意思決定を行うエージェントの実装方法を詳細に解説した実践的な記事です。LangGraph の活用事例が豊富で、開発者にとって非常に参考になります。
編集者注:これは、*Onyx* のパートナー企業からの寄稿記事です。LangGraph が成熟するにつれて、私たちは *Klarna*、*Replit*、*AppFolio*、*その他* などの多くの企業が、エージェントフレームワークとしてこれを使用し始めているのを見てきました。私たちはこれが、その評価がどのように行われるかを詳細に説明する優れたブログ記事だと考えました。*彼らのブログでのこの投稿のバージョン* もご覧いただけます。
Evan Lohn、Joachim Rahmfeld 著
Onyx では、企業データからユーザーが得られる知識と洞察を拡大し、それによって各職務の生産性を向上させることに専念しています。
では、Onyx とは何か? Onyx は、ラップトップ上、オンプレミス、またはクラウド上で、いかなる規模でも展開できる AI アシスタントです。Slack、Google Drive、Confluence など、多くのソースからの文書化された知識を結びつけます。Onyx は大規模言語モデル(LLM: Large Language Model)を活用してチームのための専門知識を持つエージェントを作成し、ユーザーが関連するドキュメントを見つけるだけでなく、「機能 X はすでにサポートされていますか?」や「機能 Y のプルリクエストはどこにありますか?」といった質問に対する回答を得られるようにしています。
昨年、私たちはエンタープライズ検索(Enterprise Search)とナレッジリトリーバル(Knowledge Retrieval)の機能を強化するために、以下の目標を設定しました:
- 複雑で曖昧な質問に対してスケーラブルな回答を可能にする
- 複数のエンティティが関与する場合の回答品質を向上させる
- 質問の主要な側面について、より詳細で文脈豊かな情報を提供すること
これらのカテゴリに該当する質問は通常、ユーザーにとって高い価値を持ちますが、従来のRAG(Retrieval-Augmented Generation)のようなシステムはこれらの状況で苦戦しがちです。
例えば、「ナイキとプーマでの私たちのポジショニングにおける、販売結果の違いに影響を与えうる製品関連の差異は何ですか?」という質問を考えてみましょう。この質問には、複数のエンティティが含まれるだけでなく、曖昧さ(「製品関連の販売結果」には多くの意味があります)も含まれています。
もしコーパス内にこの正確な質問を扱った文書が存在しない限り、RAGシステムはここで適切な回答を見つけることが困難になります。
これこそが、私たちの新しいエージェント検索(Agent Search)の出番となる質問のタイプです。ここで提案されているアイデアとは何でしょうか?
高レベルでは、このアプローチは以下の3ステップから成ります。1)まず、質問をより狭い文脈に焦点を当て、かつ潜在的に曖昧な用語の曖昧さを解消できるサブ質問に分解する。2)回答済みのサブ質問と取得された文書を用いて初期回答を構成する。3)そして、初期プロセス中に学習した様々な事実に基づき、初期回答からさらに洗練された最終回答を生成する。
上記の例をより具体的にするために、有効な初期サブクエリの例としては、「プーマと議論した製品はどれか?」「ナイキと議論した製品はどれか?」「プーマから報告された課題は何だったのか?」などが挙げられます。この種の論理プロセスをカプセル化するには、多くのステップ、計算、および大規模言語モデル(LLM)呼び出しを整理し、オーケストレーションする必要があります。
このブログの目的は、i) 機能的なレベルでこの問題にどのようにアプローチしたかを説明し、ii) テクノロジーの選択プロセスをどのように進めたかを議論し、iii) LangGraph をバックボーンとしてどのように活用したか、および特にどのような教訓を得たかを詳細に共有することです。
この投稿が、この分野に関心がある読者や、LangGraph を使用してエージェントを構築したいと考えている読者に有用であり、また私たちの要件の一部を共有できることを願っています。
一般的なフローと技術的要件
おおまかに言えば、私たちの目標とする論理フローは高レベルで以下のようになります:

このフローの主要な側面と要件は以下の通りです:
- 元の質問に関連する文書を検索するだけでなく、初期の質問をより狭く明確なサブ質問に分解します。これにより、曖昧さの解消と検索焦点の絞り込みが促進されます。
- 分解は初期検索に基づいて行われ、分解に文脈情報を提供します。
- 各サブ質問への回答には複数の要素が含まれます:クエリ拡張、検索、文書の検証、再ランク付け、サブ回答の生成、およびサブ回答の検証です。
- 初期回答は、検索結果と各サブ質問への回答に基づいて生成されます。
- 初期回答が不十分な場合、洗練された回答を生成するために別の分解を実行します。これは、不足点を補うことやサブ質問への回答に関する後続の問い合わせに対応するために設計されています。この洗練された分解は、以下の情報に基づきます:質問と初期回答(およびその不備)、サブ質問とその回答(および回答できなかったサブ質問)、初期検索に基づいた独立したエンティティ/関係/用語の抽出(これにより、文書セットの内容と分解をより適切に整合させます)。
- 全体として、並列処理は多くのレベルで不可欠です。これには以下が含まれます:各サブ質問に対する取得済み文書の検証処理、複数のサブ質問の並列処理、およびサブ質問の処理と並行して行うエンティティ/関係/用語の抽出。
- 同様に、依存関係の管理も重要です。例としては以下があります:洗練フェーズでの分解は、初期回答が生成され(かつ洗練の必要性が判定され)、エンティティ、関係、用語の抽出が完了するまで待機する必要があります。
- 将来のステップは、以前のステップの結果によって影響を受けます。
したがって、本質的に広範で曖昧な質問に対応できるという目標を達成するためには、多くのプロセスが行われる必要があります。
このフローは確かにワークフロー中心のものですが、より広範なエージェント検索フローへの第一歩を示すものとなっています。私たちは、このフローにさまざまなツールを接続し、精緻化プロセスを更新することを目指しています。また将来的には、ユーザーとのヒューマン・イン・ザ・ループ(人間関与型)のインタラクションを導入する可能性もあります。例えば、精緻化の前に回答を承認したり、手動の変更を加えてフローの一部を再実行したりすることです。
近い将来および中期的な見通しを見据えて、私たちの要件を満たすためには、以下の特性を備えたフレームワークが必要です。
- 制御がしっかりしていること
- 拡張や(再)設定が容易であること
- コスト効果が高いこと
- 高い並列化の度合いを可能にすること
- ロジカルな依存関係(AとBがCの開始前に完了する必要があり、Eはこれらすべてと並列に実行可能など)を管理できること
- トークンやその他のオブジェクトのストリーミングを可能にすること
- 将来的により複雑なインタラクションを許容すること
そして、もちろん!回答もユーザーの期待に応じた適切なタイミングで生成され、かつスケーラビリティを持って行われる必要がありました。
したがって、私たちが対処する必要があった核心的な質問は、「どのようにしてこれを最もよく実装するか?」でした。
フレームワークの選択肢と評価アプローチ
私たちの選択肢は、既存のフローを拡張してゼロからこのフローを実装するか、既存のエージェントフレームワークを活用するかのいずれかでした。後者の場合、どのフレームワークを選ぶかが問題となります。
上記で述べた優先順位を考慮し、実装フレームワークの主要候補としてLangGraphを採用することにしました。ゼロからの独自実装は、おそらくそれに次ぐ選択肢となります。
LangGraphの採用を後押しした初期の要因は以下の通りでした:
- 「私たちの描く図が、彼らの図と自然に一致する」という状況。つまり、私たちが設計したフローは、LangGraphのノード(Nodes)、エッジ(Edges)、状態(States)という概念と非常に良く適合していました。
- 強力なコミュニティを擁するオープンソースフレームワーク
- この分野におけるイノベーションの時間軸と比較して「新しすぎない」こと
- 高い制御可能性
- ネイティブなストリーミングサポート
- Human-in-the-Loop(ヒューマン・イン・ザ・ループ)や、一部のパラメータを変更してエージェントフローを再実行する機能など、将来のOnyx機能にとって興味深い可能性を秘めていること
一方で、ゼロからの独自実装を支持する懸念事項も確かに存在しました。これらには以下が含まれます:
- 第三者への依存と、エンドツーエンドの制御力低下
- LangGraphにおける可変状態変数の扱い(「誰がこの値を変更したのか?」という問題)
- デバッグ目的でのコールスタック(呼び出し履歴)の可視性が低いこと
- 既存フローの変更が必要となること
決定を下すにあたり、この種のプロジェクトで一般的に行われるように、プロトタイプ評価から始めました。具体的には、約1週間(フルタイムの担当者1名分、学習期間を含む)という短期間で、対象とするフローを簡略化したスタンドアロンのLangGraph実装を迅速に構築し、以下のテストを行いました:
- 許容できる実行時間を提供する、エンドツーエンドの機能の近似
- ファンアウト並列化
- サブグラフ並列化
- 状態管理に関する潜在的な問題
- ストリーミング
結果は励みになるものであり、私たちはアプリケーション内で LangGraph における実際のフローの実装に進みました。予想通り、その過程で私たちはさらに多くの教訓を学びました。以下に、私たちが学んだことと、エージェントフローの使用が拡大していく中で今後従う予定の規約を文書化します。
LangGraph の学習事項 - 今後のベストプラクティス
プロジェクトが急速に複雑化するにつれて、私たちが行った観察と遵守した実践の一部を以下に示します。
コードの組織化
ディレクトリとファイル構造
- 複雑なグラフ全体でノードの数が非常に多くなることがあります。私たちは、(関数の再利用を除き)1 ノード 1 ファイルのアプローチを使用することにしました。
- ノードが多い場合、明確なディレクトリ構造とファイル命名戦略が推奨されます。私たちは各サブグラフ用のディレクトリを作成し、一般的に <action>_<object>.py という命名規則を採用しました。ステップ番号を示す数字を追加することも役立ちますが、ノードの追加や削除時に多少の手間がかかります。
- 私たちは並列化(後述)と再利用のためにサブグラフを多用しています。各サブグラフディレクトリには、独自のエッジ、状態、モデルファイル、およびグラフビルダーが含まれています。
- グラフを可視化するために、全体のグラフの mermaid png を使用することが非常に有用であることがわかりました。
型定義と状態管理
私たちはコードベース全体でPydanticを使用しているため、LangGraphがTypeDictに加えてPydanticモデルをサポートしているのは素晴らしいことでした。その結果、LangGraphの実装全体でPydanticモデルを使用しています。(ただし、グラフの出力についてはPydanticはまだサポートされていません。)
サブグラフ内には独自のアクションや「出力」(状態の更新)を持つノードが多数あるため、私たちは通常、(サブ)グラフの状態はノードの更新によって駆動されると見なしています。したがって、サブグラフ状態内でキーを直接定義するのではなく、さまざまなノードの更新に対してPydanticの状態モデルを定義し、それらのノードの更新(およびその他の)モデルから継承することでグラフ状態を構築します。
利点:
- キーが自然にグループ化される
- キーを追加する際にノードの状態モデル(およびノード)を更新するだけでよい
- キーの重複が許可される
- デフォルト値の設定が可能
課題:
- このアプローチを採用すると、全体のグラフ状態におけるキーの完全なセットを確認するのが少し難しくなる。
- 継承の問題を避けるために適切な構造を選択する必要がある
当然のことながら、状態のキーに対してデフォルト値を設定するかしないかを意図的に行うことは非常に重要です。注意深く処理しないと、不適切な状況でデフォルト値を設定すると、検出がより困難な意図しない動作を引き起こす可能性があります。例えば、以下の問題のある構成を考えてみてください:
ここで、メイングラフのノードAは'my_key'を'my_data'に設定しています。この値は後で内部サブグラフのノードで使用される予定ですが、この例では(意図的に)そのキーを外部サブグラフに追加し忘れています。当然のことながら、内部サブグラフにおけるこのキーの値は空文字列になります。出力側でも同様の状況が発生します:内部サブグラフのノードCで'my_key'を更新した場合、メイングラフ内の'my_key'状態は更新されません。
もし代わりに、以下に示すように内部サブグラフで'my_key'のデフォルト値を設定せずに注意を払っていたとすると:

*エラーが発生します*。内部サブグラフに対して'my_key'が入力値を持っていないためです。その後、外部サブグラフで見逃された状態が追加され、適切な構成に到達します:

これはもちろん従来の入れ子関数とは大きく異なるものではありませんが、LangGraphの文脈ではこれらの問題の特定が少し難しいという経験があります。
私たちの推奨事項(これも予想通り)は以下の通りです:
- デフォルト値を持たないグラフ入力状態のすべてのキーを定義する(ただし、ネストされたサブグラフの文脈などで文書化された例外を除く)
- グラフ内で更新されるすべてのキーを <type> | None = None ... のように定義する(ただし、リスト型であり、複数のノードからリストに追加することが期待される場合の例外を除く)
グラフの構成要素と検討事項
並列処理
**私たちは並列実行されるプロセスに対して多くの要件を持っており、並列処理には複数の種類があります。
同一フローの並列処理:
Map-Reduce ブランチ
この種の並列処理の例として、取得した文書の検証があります。つまり、取得された文書リスト内の各文書が質問に関連しているかどうかをテストすることです。当然、これらのテストは並列で行いたいものであり、LangGraph の Map-Reduce ブランチはこうした状況で非常にうまく機能します。

上記において、太字のステートキーは fan-out(扇出)中に更新されるものであり、斜体キーは fan-out ノード内部の変数を指します。
異なるフローセグメントの並列実行:
サブグラフの広範な活用!以下の状況を想像してください。B_1 と B_2 の実行にそれぞれ 5 秒かかる場合、C は 8 秒かかります。左側のシナリオでは、B_2 が B1 *および* C の両方が完了してから開始されるため、D は A の完了から実際に 13 秒後に開始されます。一方、右側のシナリオでは、D は A の完了から 10 秒後に開始されます。B_1 と B_2 をサブグラフにラップすることで、親グラフの観点からは左側に 1 つのノード、右側に 1 つのノードが存在することになり、B_2 の実行は C の完了を待たなくなります。(注:親グラフのノード内でサブグラフを呼び出すのではなく、常に親グラフ内のノードとして *サブグラフを使用* します。)

再利用可能なコンポーネント:再びサブグラフ!
複数のノードから構成される繰り返しフローのセグメントが確かに存在します。一例として、拡張検索取得(Extended Search Retrieval)があります。これは、特定の(サブ)質問に対して関連文書を取得するプロセスです。この処理の中核は、検索に続き、取得された各文書の質問に対する関連性を検証するステップ、そして最終的に検証済み文書のリランク付けを行うステップで構成されます。この繰り返しプロセスを効率的にするため、これをサブグラフとしてラップし、メインのグラフや他のサブグラフから利用できるようにします。ただし、親グラフとサブグラフの間でキーの定義と共有を行う際には、常に注意が必要です。
- ノード構造
一般的に「1ノード=1アクション」のアプローチを採用していますが、連続する複数のアクションを1つのノードにまとめることで、ノードの散乱を防ぐことも容易です。
- サブグラフをフロー内の複数のステップで使用する場合、末尾に「フォーマットノード」を導入し、データを所望のキー更新に変換する役割を持たせることが便利なことがあります。
- ストリーミング
カスタムイベントのノード内ストリーミング:
良好なユーザー体験を提供するためには、ノード内で多くのイベントをストリーミング出力する必要があります。例としては、取得されたドキュメント、生成された回答やサブ回答のLLMトークンなどがあります。これらの多くはカスタムイベントであり、同時にストリーミング出力される可能性のある個別のサブ回答を区別する必要があるためです。当アプリケーションは同期操作を使用しており、LangGraphのストリーミングドキュメントに従うことで、カスタムイベントのストリーミングを円滑に行うことができました。
- LangGraphのバージョン
これは急速に進化する分野であり、また急速に開発が進むプロジェクトであるため、リリース情報に常に追随することが重要です。
LangGraphを用いた現在のエージェント検索
最後に、現在使用しているグラフを示します(複雑さを抑えるためx-rayレベルを1に設定しており、そのため複数のノードはさらにサブグラフを含む可能性があります):

このフローが、エージェント検索が選択されなかった場合に基本検索フローを補完するためのいくつかの追加要素を除き、当初設定した論理フローと非常に類似していることは明白です。
展望とアクションへの呼びかけ
私たちはこの実装を第一歩と見なしており、近い将来、フローをより自律的なエージェント機能を持つものへと拡張する計画です。これまでにLangGraphは私たちのニーズに非常に適したツールでした。
エージェント検索のコードをGitHubで確認し、デモの予約を行い、無料でクラウド版を試すことができます。また、SlackやDiscordの#agent-searchチャンネルに参加して、エンタープライズAI検索やエージェントについて広く議論してください。
原文を表示
*Editor's note: this is a guest post from our friends at *Onyx*. As LangGraph has matured, we've seen more and more companies (*Klarna*, *Replit*, *AppFolio*, *etc*) start to use it as their agent framework of choice. We thought this was a great blog describing in detail how that evaluation is done. You can read *a version of this post on their blog* as well.*
By Evan Lohn, Joachim Rahmfeld
At Onyx, we are dedicated to expanding the knowledge and insights users can gain from their enterprise data, thereby enhancing productivity across job functions.
So, what is Onyx? Onyx is an AI Assistant that companies can deploy at any scale—on laptops, on-premises, or in the cloud—to connect documented knowledge from many sources, including Slack, Google Drive, and Confluence. Onyx leverages LLMs to create a subject matter expert for teams, enabling users to not only find relevant documents and but also get answers for questions such as "Is feature X already supported?" or "Where's the pull request for feature Y?"
Last year, we embarked on enhancing our Enterprise Search and Knowledge Retrieval capabilities by setting the following goals:
- Enable scalable answers to complex and ambiguous questions
- Improve answer quality when multiple entities are involved
- Provide richer detail and context around key aspects of questions
Questions that fall into these categories are usually of high value to the user, however a traditional RAG-like system tends to struggle in these situations.
For example, consider the question: “*What are some of the product-related differences between what we positioned at Nike vs Puma that could factor into our differing sales outcomes?*”. This question involves both, multiple entities, as well as an ambiguity (*product-related sales outcomes* can mean many things).
Unless there happen to be documents in the corpus that deal pretty much with this exact question, a RAG system is challenged to find a good answer here.
These are the types of questions where our new Agent Search comes in. What is the idea here?
On a high level, the approach is to 1) first break up the question into sub-questions that can focus on more narrow contexts as well as disambiguation of potentially ambiguous terms, 2) compose an initial answer using the answered sub-questions and fetched documents, and then 3) produce a further refined answer based on the initial answer and various facts that have been learned during the initial process.
To make this more concrete for the example above, some valid initial sub-questions could be ‘Which products did we discuss with Puma?’, ‘Which products did we discuss with Nike?’, ‘Which issues were reported by Puma?’...**To encapsulate this type of logical process a lot of steps, calculations, and LLM calls need to be organized and orchestrated.
The purpose of this blog is i) to illustrate how we approached this problem on a functional level, ii) discuss how we went about our technology selection approach, and iii) share in good detail how we leveraged LangGraph as a backbone, and specifically which lessons we learned.
We hope this write-up will be useful to readers who are interested in this space and/or who want to build agents using LangGraph and share some of our requirements.
General Flow and Technical Requirements
Roughly, our targeted logical flow looks on the high level like this:

Key aspects and requirements of this flow are:
- In addition to searching relevant documents for the original question directly, we break up the initial question into more narrow, well-defined sub-questions. This helps with disambiguation and narrowing the search focus
- The decomposition is informed by an initial search, giving some context to the decomposition
- The answering of each subquestion has many parts: query expansion, search, document validations, reranking, subanswer generation, subanswer verification
- The initial answer is based on a search + the answers of the subquestions.
- If the initial answer is lacking, we perform another decomposition to generate a refined answer, which is designed to address shortcomings and/or follow-up on answers to subquestions. This refinement decomposition is informed by:the question and the original answer (and the fact that it is lacking)
- the sub-questions and their answers (and the sub-questions that were not answerable)
- a separate entity/relationship/term extraction based on the initial search to better align the decomposition with the contents of the document set
- Overall, parallelism is imperative on many levels, including:retrieved document verifications for each subquestion processing
- the processing of multiple sub-questions in parallel
- entity/relationship/term extraction in parallel with the handling of the subquestions
- Similarly, dependency management is essential. Examples are:the decomposition in the refinement phase has to wait until both, the initial answer is generated (and need for refinement has been determined), and the extraction of the entities, relationships, and terms are done
- future steps are informed by the outcome of earlier steps
So indeed, a lot has to happen to achieve our goal of being able to address substantially broader and more ambiguous questions.
While this flow is certainly quite workflow-centric, it presents an initial step towards a broad(er) Agent Search flow(s). We intend to hook various tools into the flow, update the refinement process, etc. We may at a later date potentially also introduce Human-in-the-Loop type interactions with the users, like approving answers before refinement or re-running part of the flow with some manual changes.
Addressing our requirements, also with an eye towards the near/mid future, asks for a framework that
- is well-controlled,
- is easy to extend or (re)configure,
- is cost-effective,
- allows for a high degree of parallelization,
- can manage logical dependencies (A and B need to be done before C starts, and E can run in parallel to all of this, etc.), and
- enables streaming of tokens and other objects
- allows in the future for more complex interactions.
And - oh, yeah! - the answers also needed to be produced in a timely manner matching user expectations, and do so at scale.
So the key question we had to address was ”How do we best implement this?”
Framework Options & Evaluation Approach
The options for us were essentially whether to implement this flow ourselves from the ground up by extending our existing flow, or to leverage an existing agentic framework - and if so, which one.
Given our priorities outlined above, we landed on LangGraph as our main candidate for our implementation framework, with implementation-from-scratch probably a relatively close second.
The initial drivers in favor of LangGraph were:
- A natural “their pictures look like our pictures”-situation, meaning: the flow we had laid out maps very well to LangGraph’s concepts of Nodes, Edges, and States
- Open Source framework with a strong community
- ‘Not brand-new’ (relative to the innovation time-scales in this space)
- High degree of control
- Native streaming support
- Interesting features for potential future Onyx functionality like Human-in-the-Loop or the ability to rerun the agent flow with some parameters altered
However, we certainly also had some concerns which favored an implementation from-scratch, including:
- Dependency on a third party and reduced end-to-end control
- Mutable state variables in LangGraph (‘Who changed this value?’)
- Less visibility into the call stack for debugging purposes
- Change of our existing flow
To decide, as one does for most projects of this type, we started with a prototype evaluation. Specifically, we quickly (~1 week/1 FTE, including learnings) implemented a stripped-down, stand-alone LangGraph implementation of our targeted flow, where we tried to test:
- approximate end-to-end functionality that delivers acceptable run times
- fan-out parallelization
- subgraph parallelization
- potential issues around state management
- streaming
The results were encouraging, and we proceeded to implement our actual flow in LangGraph within our application. As expected, in the process, we learned a number of additional lessons. Below we document what we have learned, and the conventions we intend to follow in the future, as our use of Agent flows will expand.
LangGraph Learnings - Our Best Practices Moving Forward
As the project quickly grew more complex, here are some of the observations we made and practices we adhered to.
Code Organization
Directory and File Structure
- The number of nodes can get rather large across a complex graph. We decided to use a one-node-per-file approach (modulo reuse of functions for different nodes).
- With many nodes, a clear directory structure and file naming strategy are advised. We created a directory for each subgraph and generally adopted an <action>_<object>.py naming convention. Adding a digit for the step number can also be helpful, while it would require some extra work when nodes are added or removed.
- We use subgraphs extensively for purposes of parallelization (see below) and reuse. Each subgraph directory has its own edges, states and models files as well as its graph builder.
- To visualize the graph, using a mermaid png of the overall graph proved to be very useful.
Typing & State Management
We use Pydantic across our code base, so it was great to see that LangGraph supports Pydantic models in addition to TypeDicts. Consequently, we use Pydantic models all through the LangGraph implementation as well. (Unfortunately, for graph outputs Pydantic is not yet supported).
As we have many nodes with their own actions and ‘outputs’ (state updates) within a subgraph, we generally look at the (sub)graph states as driven by the node updates. So rather than defining the keys directly within the subgraph state, we define Pydantic state models for the various node updates and then construct the graph state by inheriting from the various node update (and other) models.
Benefits**:
- Keys are naturally grouped
- One only has to update the node state model (and the node) when adding keys
- Overlap of keys is allowed
- Default values are allowed
Challenges:
- Following this approach certainly makes it a bit harder to see the complete set of keys in the overall graph state.
- One has to pick a good structure to avoid inheritance issues
Not surprisingly, being deliberate about setting default values (or not!) for state keys is very important. If not handled carefully, setting default values in inappropriate situations can lead to unintended behavior that may be more difficult to detect. For instance, consider the following problematic configuration:

Here, Main Graph Node A sets ‘my_key’ to ‘my_data’. This value is later intended to be used by an Inner Subgraph node. But in this example we (purposefully) missed adding that key to the Outer Subgraph. Not surprisingly, the value of this key for the inner subgraph would be an empty string.A similar situation would occur on the output side as well: if we updated ‘my_key’ in the Inner Subgraph Node C, then this would not update the ‘my_key’ state in the main graph.
Had we been instead careful and not set a default value for ‘my_key’ in the inner subgraph as shown here:

then* an error would be raised*, as ‘my_key’ does not have an input value for the Inner Subgraph. Then, the missed state in Outer SubGraph would be added to arrive at the proper configuration:

This is of course not really different from traditional nested functions, but in our experience, in the LangGraph context these issues are a bit harder to identify.
Our recommendations - no surprise - are to:
- define all keys in graph input states without defaults, except for documented exceptions, usually in the context of nested subgraphs
- define all keys that are updated in the graph as <type> | None = None, … with the exception of when the key is a list and we expect to add to a list from many nodes.
Graph Components & Considerations
Parallelism
**We have a lot of requirements for processes to be executed in parallel, and there are multiple types of parallelism.
Parallelism of Identical Flows:
Map-Reduce BranchesAn example of this type of parallelism in our flow is the validation of retrieved documents, i.e., the testing of each document in the list of retrieved documents for relevance to the question. Obviously, one wants to do these tests in parallel, and LangGraph’s Map-Reduce branches work quite well for us in these situations:

Above, the bold-face state key** is the one being updated during the fan-out, and italic keys refer to fan-out node-internal variables.
Parallelism of Distinct Flow Segments:
**Extensive Usage of *Subgraphs*! In the following situation, imagine B_1 and B_2 each take 5s to execute, whereas C takes 8s. In the scenario on the left, D starts actually 13s after A has completed, because B_2 only starts once B1 *and* C are completed. In the scenario on the right on the other hand, D starts 10s after A completed. Wrapping B_1 and B_2 into a subgraph ensures that from the parent graph’s perspective there is one node on the left and one node on the right, and B_2’s execution is not waiting for C’s completion. (Note: we always use *subgraphs as nodes* within the parent graph, vs *invoking a subgraph within a node* of the parent.)

Reusable Components: Subgraphs again!
We do have plenty of repeated flow segments that consist of multiple nodes. One example is the Extended Search Retrieval, where documents for a given (sub-)question are retrieved. At the core, the process consists of a Search, followed by a Relevance Validation of each retrieved doc with respect to the question, and concludes with a reranking of the validated documents. To make this repeated process efficient, we wrap it into a subgraph, which is then used either by the main graph or other subgraphs. One needs to be careful, as always, with the definition and sharing of the keys between the parent graph and the subgraph.
- Node StructureWe generally adopt the approach of ‘one-action-per-node’, though one could easily reduce node sprawl by putting more consecutive actions into one node.
- When a subgraph is used at multiple steps in the flow, on occasion we find it convenient to introduce a ‘formatting node’ at the end, whose role it is to convert data into a desired key update.
- StreamingIn-Node Streaming of Custom Events:To provide a good user experience we need to stream out a lot of events in-node. Examples include retrieved documents, LLM tokens of generated answers or sub-answers, etc. Many of these are custom events, as we need to distinguish individual sub-answers which could stream out simultaneously. Our application uses synchronous operations, and following LangGraph’s streaming documentation worked very well for us to facilitate the streaming of our custom events.
- LangGraph versionsAs this is a fast-moving field and a fast-moving project, it is important to stay current on the releases
Our Current Agent Search using LangGraph
Lastly, here is our current graph (x-ray level set to 1 to limit complexity, so a number of the nodes are actually subgraphs which may contain further subgraphs):

It is quite evident that this flow has a strong resemblance with the logical flow that we laid out at the beginning, with a few additions to facilitate the Basic Search flow if the Agent Search is not selected.
Outlook and Call To Action
We see this implementation as a first step, and we plan on expanding the flow to become substantially more agentic in the near future. LangGraph certainly has thus far been a good fit for our needs.
We invite you to check out agent-search on GitHub, book a demo, try out our cloud version for free, and join slack, discord #agent-search** channels to discuss our Enterprise AI Search more broadly, as well as Agents!
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み