見出し画像

LangGraph の概要

以下の記事が面白かったので、かるくまとめました。

LangGraph

1. LangGraph

LangGraph」は、LLMでステートフルな「マルチアクターアプリケーション」を構築するためのライブラリです。「LCEL」(LangChain Expression Language) を拡張して、複数チェーン (またはアクター) を複数ステップにわたって循環的に協調動作させることができます。

LangChain」の大きな価値の1つに、カスタムチェーンを簡単に作成できることがあります。このための機能として「LCEL」を提供してきましたが、サイクルを簡単に導入する方法がありませんでした。「LangGraph」によって、LLMアプリケーションにサイクルを簡単に導入できるようになりました。

2. エージェントとステートマシン

2-1. エージェント

エージェントの最も単純で同時に最も野心的な形式は、次の2つのステップを持つループです。

(1) LLMを呼び出して、「どのようなアクションを実行するか」または「ユーザーにどのような応答を返すか」を決定
(2) 与えられたアクションを実行し、ステップ1に戻る

このループは、最終的な応答が生成されるまで繰り返されます。これは本質的に「LangChain」の「AgentExecutor」を動かすループであり、「AutoGPT」のようなプロジェクトを顕著にさせたのと同じロジックです。意思決定と推論能力のほとんどすべてをLLMにオフロードするため、最も野心的です。

2-2. ステートマシン

エージェントを実運用に投入するためにコミュニティや企業と取り組んできたことで見てきたことの1つは、多くの場合、より多くの制御が必要であるということです。

エージェントに最初に常に特定のツールを呼び出すように強制したい場合があります。ツールがどのように呼び出されるかをもっと制御したい場合があります。エージェントの状態に応じて異なるプロンプトを使用したい場合があります。

これらのより制御されたフローのことを「LangChain」では「ステートマシン」と呼んでいます。

これらの「ステートマシン」は、ループできる力を持っています。単純なチェーンよりもあいまいな入力を処理することができます。しかし、そのループがどのように構築されるかという点では、まだ人間の指導の要素があります。

「LangGraph」は、グラフとして指定することで、これらの「ステートマシン」を作成する方法になります。

3. LangGraphの要素

「LangGraph」の要素は、次のとおりです。

3-1. StateGraph

StateGraph」はグラフを表すクラスです。クラス初期化時には、「状態定義」を渡します。 「状態定義」は、時間の経過とともに更新される状態オブジェクトで、この状態はグラフ内の「Node」によって更新されます。状態の属性の更新方法 (オーバーライド or 追加) も指定します。

疑似コードの例は、次のとおりです。

from langgraph.graph import StateGraph
from typing import TypedDict, List, Annotated
import Operator


class State(TypedDict):
    input: str
    all_actions: Annotated[List[str], operator.add]


graph = StateGraph(State)

3-2. Node

「StateGraph」を作成した後、graph.add_node(name, value) で「Node」を追加します。 nameはEdge追加時にNodeを参照するために使用する文字列、valueは呼び出される「関数」または「LCEL Runnable」です。

疑似コードの例は、次のとおりです。

graph.add_node("model", model)
graph.add_node("tools", tool_executor)

END」はグラフ終端を表す特別なNodeです。サイクルが最終的に終了できることが重要です。

from langgraph.graph import END

3-3. Edge

「Node」を追加した後、「Edge」を追加してグラフを作成できます。
「Edge」にはいくつかの種類があります。

・The Starting Edge
グラフ開始点を特定のNodeに接続するEdgeです。入力がグラフに渡された時、そのNodeが最初に呼び出されるようになります。

疑似コードの例は、次のとおりです。

graph.set_entry_point("model")

・Normal Edge
1つのNodeが常に次に呼び出されるEdgeです。例としては、ツールを呼び出した後に常にモデルを呼び出す必要がある基本的なエージェントランタイムが挙げられます。

graph.add_edge("tools", "model")

・Conditional Edge
最初にどのNodeに移動するかを決定するために関数 (多くの場合 LLM) が使用される場所です。
このEdgeを作成するには、次の3つを渡す必要があります。

(1) 上流ノード : このノードの出力を見て、次に何をするかを決定
(2) 関数 : 次にどのノードを呼び出すかを決定するために呼び出される。文字列を返す必要がある
(3) マッピング : このマッピングは、(2) の関数の出力を別のノードにマッピングするために使用される。キーは、(2) の関数が返す可能性のある値である必要がある。値は、その値が返された場合に移動するノードの名前である必要がある。

例としては、モデルが呼び出された後、グラフを終了してユーザーに戻るか、ユーザーの決定に応じてツールを呼び出すことが考えられます。

疑似コードの例は、次のとおりです。

graph.add_conditional_edge(
    "model",
    should_continue,
    {
        "end": END,
        "continue": "tools"
    }
)

3-4. Compile

グラフを定義したら、それを実行可能ファイルにコンパイルできます。 これは、これまでに作成したグラフ定義を単純に受け取り、実行可能ファイルを返します。このRunnableは、LangChain ランナブル (.invoke、.stream、.astream_log など) とすべて同じメソッドを公開し、Chainと同じ方法で呼び出すことができます。

app = graph.compile()

4. Agent Executor

4-1. Agent Executor

「 AgentExecutor」を「LangGraph」で再作成しました。 これにより、「AgentExecutor」の内部をより簡単に変更できるようになりました。

デフォルトのグラフ状態には、LangChainでよく知られている概念、つまり input、chat_history、intermediate_steps (および最新のエージェントの結果を表す agent_outcome) が含まれています。

from typing import TypedDict, Annotated, List, Union
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.messages import BaseMessage
import operator


class AgentState(TypedDict):
   input: str
   chat_history: list[BaseMessage]
   agent_outcome: Union[AgentAction, AgentFinish, None]
   intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]

詳しくは、ノートブックを参照。

4-2. Chat Agent Executor

メッセージリストを操作する「チャットモデル」が増えています。 このモデルは多くの場合、「Function Calling」などの機能を備えており、エージェントのような体験を可能にします。このようなモデルを操作する場合、多くの場合、エージェントの状態をメッセージのリストとして表現するのが直感的です。

そのため、この状態で動作する「エージェントランタイム」を作成しました。入力はメッセージリストであり、ノードは時間の経過とともにこのメッセージリストに単に追加されます。

from typing import TypedDict, Annotated, Sequence
import operator
from langchain_core.messages import BaseMessage


class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]

詳しくは、ノートブックを参照。

4-3. AgentExecutorのロジック変更

「LangGraph」の大きな利点の1つは、「AgentExecutor」のロジックをより自然で変更可能な方法で公開できることです。
変更例は、次のとおりです。

・ツールの強制呼び出し
常にエージェントに最初にツールを呼び出してもらいます。
(Agent Executor および Chat Agent Executor)

・Human-in-the-loop
ツールを呼び出す前に人間参加型のステップを追加します。
(Agent Executor および Chat Agent Executor)

・エージェントのステップの管理
エージェントが実行する可能性のある中間ステップを処理する方法に関するカスタムロジックを追加します。
(Agent Executor および Chat Agent Executor)

・特定の形式で出力を返す
「Function Calling」を使用して、エージェントが特定の形式で出力を返すようにします。
(Chat Agent Executor のみ)

・ツールの出力を動的に直接返す
場合によっては、ツールの出力を直接返したい場合があります。LangChain の return_direct パラメータを使用する簡単な方法を提供します。ただし、これにより、ツールの出力が常に直接返されるようになります。場合によっては、応答を直接返すかどうかを LLM に選択させたい場合があります。
(Chat Agent Executor のみ)

5. 今後の取り組み

近い将来実装を検討しているものは、次のとおりです。

・より高度なエージェントランタイム  (LLM Compiler、plan-and-solveなど)
・ステートフルツール (ツールによる一部の状態変更を許可)
・より制御された人間参加型のワークフロー
・マルチエージェントのワークフロー

関連



この記事が気に入ったらサポートをしてみませんか?