🕸️

無料でLangGraphに入門してみよう!【Quick Start】

2024/11/16に公開

はじめに

こんにちは、iharuです。
LangGraphのQuick Startが面白かったので、日本語&無料APIのみで記事として書き直してみました!
https://langchain-ai.github.io/langgraph/tutorials/introduction/

今回はツールを組み込んだグラフまで作成したいと思います。

事前準備

適当なディレクトリを作成します。(今回はlanggraphとします)
.envというファイルとquickstartというフォルダを作成します。

 langgraph
 ├── quickstart
 └── .env

お好きなPython環境と次のライブラリをインストールしておきます。

パッケージ名 バージョン
langgraph ^0.2.0
langchain ^0.3.0
langchain-community ^0.3.0
langchain-groq ^0.2.0

今回はLLMのAPIとして、Groqという無料で利用できるサービスを使いたいと思います。
(OpenAIやBedrockのAPIを使用している方はlangchain-groqの代わりにlangchain-openai, langchain-awsを使用してください)
またツールのAPIとして、TavilySearchというこちらも無料で利用できるサービスを使いたいと思います。

Groq APIキーの取得

こちらからGroqのアカウントを作成します。
https://console.groq.com/login

続いて、こちらからCreate API Keyを押してAPIキーを取得します。
https://console.groq.com/keys

APIキーが取得出来たら、先ほど作成した.envファイルに貼り付けます。

.env
GROQ_API_KEY = ***

TavilySearch APIキーの取得

こちらからTavilySearchのアカウントを作成します。
https://app.tavily.com/home

続いて、API KeysにあるAPIキーを取得します。

APIキーが取得出来たら、先ほど作成した.envファイルに貼り付けます。

.env
TAVILY_API_KEY = ***

実践

LLMインスタンスの作成

llm.pyにGroqのLLMインスタンスを作成する関数を作成します。

langgraph/quickstart/components/llm.py
from dotenv import load_dotenv
from langchain_groq import ChatGroq

load_dotenv()


def get_llm():
    return ChatGroq()


if __name__ == "__main__":
    llm = get_llm()
    print(llm.invoke("What is LangGraph?"))

このファイルを実行すると次のような回答が返ってきます。

content='I'm not aware of a specific technology or tool called "LangGraph." The term "langgraph" could potentially refer to a linguistic graph, which is a type of data structure used in natural language processing and computational linguistics. Linguistic graphs are used to represent various aspects of language, such as syntax trees, semantic networks, or word embeddings.\n\nSyntax trees are hierarchical structures that represent the grammatical structure of a sentence. Semantic networks are used to represent the meaning of words and their relationships to other words in a conceptual space. Word embeddings are vector representations of words that capture their semantic and syntactic properties.\n\nIt's also possible that "LangGraph" could refer to a proprietary technology or tool developed by a specific organization. To provide a more accurate answer, I would need more context or information about where you encountered the term "LangGraph."' additional_kwargs={} response_metadata={'token_usage': {'completion_tokens': 189, 'prompt_tokens': 13, 'total_tokens': 202, 'completion_time': 0.300618712, 'prompt_time': 0.002309784, 'queue_time': 0.012407025, 'total_time': 0.302928496}, 'model_name': 'mixtral-8x7b-32768', 'system_fingerprint': 'fp_c5f20b5bb1', 'finish_reason': 'stop', 'logprobs': None} id='run-329f90e3-1e13-452f-bfcc-4d9887dc98be-0' usage_metadata={'input_tokens': 13, 'output_tokens': 189, 'total_tokens': 202}

とりあえずLLMが動くことがわかりましたが、GroqはLangGraphについては知らないようです。
続いて、LangGraphの形式にLLMを組み込んでいきます。

シンプルなグラフの作成

llmを使ってチャット回答をするchatbotノードをつくることを目標にします。

LangGraphの形式で動かすためには以下のコンポーネントが必要となります。

コンポーネント 説明
State アプリケーションの現在の状態を表すデータ構造
Nodes 現在のStateを入力として受け取り、ロジックに基づき更新されたStateを返す関数
Edges 現在のStateに基づいて次に実行するNodeを決定する関数

LangGraphの一連の流れは以下のようになります。

  • 最初のノードSTARTからStateが出発
  • 最後のノードENDStateが到着するまで以下を繰り返す
    • 各ノードでStateを更新
    • Stateに基づきエッジを通り別のノードへ遷移

State

Stateはグラフのコンポーネントで共通のデータ構造となります。

今回はmessagesという会話履歴のリストを持たせることにします。
add_messagesはLangGraphの組み込み関数でmessagesに色々な情報を持たせて会話履歴を追加するものになります。

src/langgraph/quickstart/components/state.py
from typing import Annotated
from typing_extensions import TypedDict

from langgraph.graph.message import add_messages


class State(TypedDict):
    messages: Annotated[list, add_messages]

Node

続いて、llmからchatbotノードを返す関数を作成します。
このchatbotノードは、後ほどtoolノードを追加した際にエージェントとしての役割を持ちます。
ノードに入れるものはState -> Stateの関数だったので、get_chatbot_nodeの戻り値は関数chatbotになっています。

langgraph/quickstart/components/chatbot.py
from .state import State


def get_chatbot_node(llm):
    def chatbot(state: State):
        return {"messages": [llm.invoke(state["messages"])]}

    return chatbot

Graph

最後に今まで作ったコンポーネントを組み合わせてグラフを作っていきます。
まずはStateGraph(State)Stateをもつグラフインスタンスを作成します。
このグラフインスタンスにadd_nodeadd_edgeでノードとエッジを追加していきます。
最後にcompileでグラフをコンパイルします。(ここのオプション変数は後ほどmemoryを追加するときに使います)

langgraph/quickstart/graph.py
from langgraph.graph import StateGraph, START, END

from components.chatbot import get_chatbot_node
from components.llm import get_llm
from components.state import State


graph_builder = StateGraph(State)

llm = get_llm()
chatbot = get_chatbot_node(llm)

graph_builder.add_node("chatbot", chatbot)

graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)

graph = graph_builder.compile()

グラフを実行する前に、グラフ図を確認するためのdraw_graphという関数とグラフの実行結果を確認するためのstream_graphという関数を定義しておきます。

langgraph/quickstart/utils/draw.py
from langgraph.graph.state import CompiledStateGraph


def draw_graph(graph: CompiledStateGraph):
    try:
        graph_image = graph.get_graph().draw_mermaid_png()

        file_path = f"output.png"

        with open(file_path, "wb") as f:
            f.write(graph_image)

        print(f"画像が '{file_path}' に保存されました.")

    except Exception as e:
        print("画像の保存中にエラーが発生しました:", e)
        pass


これを先ほどのgraph.pyで実行してみましょう。

langgraph/quickstart/graph.py
if __name__ == "__main__":
    from utils.draw import draw_graph

    draw_graph(graph)

以下の画像が出力されます。

draw_mermaid_png()の代わりにdraw_mermaid()を使うとmermaidのコードを出力することもできます。

mermaidの出力結果
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
        __start__([<p>__start__</p>]):::first
        chatbot(chatbot)
        __end__([<p>__end__</p>]):::last
        __start__ --> chatbot;
        chatbot --> __end__;
        classDef default fill:#f2f0ff,line-height:1.2
        classDef first fill-opacity:0
        classDef last fill:#bfb6fc

次に対話式でグラフを実行する関数を作成します。

langgraph/quickstart/utils/stream.py
from langgraph.graph.state import CompiledStateGraph


def stream_graph(graph: CompiledStateGraph):
    def stream_graph_updates(user_input: str):
        for event in graph.stream({"messages": [("user", user_input)]}):
            for value in event.values():
                print("Assistant:", value["messages"][-1].content)

    while True:
        user_input = input("User: ")
        if user_input.lower() in ["quit", "exit", "q"]:
            print("Goodbye!")
            break

        stream_graph_updates(user_input)

"User: "でユーザーインプットを受け付け、stream_graph_updatesでユーザーインプットを実行し、途中結果を"Assistant:"に続けて返すようなスクリプトになっています。
ユーザーインプットが"quit", "exit", "q"なら終了します。

これを先ほどのgraph.pyで実行してみましょう。

langgraph/quickstart/graph.py
if __name__ == "__main__":
    from utils.stream import stream_graph

    stream_graph(graph)

User: Hello
Assistant: Hello! It's nice to meet you. Is there something you would like to talk about or ask me? I'm here to help with any questions you might have about writing, grammar, or language-related topics. Just let me know how I can assist you.

対話形式で会話ができることが確認できました!

Toolを組み込む

toolsノードを追加してツールを利用して回答するグラフをつくることを目標にします。

Tool

今回はTavilySearchというLLM用の検索ツールを使用します。
まずはTavilySearchツールを返す関数を作成します。ToolNodeでtoolのリストをラップするとノードとして利用できるようになります。

langgraph/quickstart/components/tools.py
from dotenv import load_dotenv

from langgraph.prebuilt import ToolNode
from langchain_community.tools.tavily_search import TavilySearchResults

load_dotenv()

tavily_tool = TavilySearchResults(max_results=2)

tools = [tavily_tool]


def get_tool_node():
    return ToolNode(tools=tools)

tavily_toolの部分だけ実行してみます。

langgraph/quickstart/components/tools.py
if __name__ == "__main__":
    print(tavily_tool.invoke("What's a 'node' in LangGraph?"))

LangGraphについて、webで検索して最新の情報を教えてくれます。

[{'url': 'https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141', 'content': 'Nodes: Nodes are the building blocks of your LangGraph. Each node represents a function or a computation step. You define nodes to perform specific tasks, such as processing input, making'}, {'url': 'https://www.datacamp.com/tutorial/langgraph-tutorial', 'content': "In LangGraph, each node represents an LLM agent, and the edges are the communication channels between these agents. This structure allows for clear and manageable workflows, where each agent performs specific tasks and passes information to other agents as needed. State management. One of LangGraph's standout features is its automatic state"}]

conditional edge

続いて、chatbotからToolやENDへ遷移するための条件付きエッジを作成します。
条件付きエッジは複数のノードへ条件により遷移するエッジとなり、条件分岐のロジックを書くroute_tool関数を作成します。

route.py
from components.state import State
from langgraph.graph import END


def route_tools(
    state: State,
):
    if isinstance(state, list):
        ai_message = state[-1]
    elif messages := state.get("messages", []):
        ai_message = messages[-1]
    else:
        raise ValueError(f"No messages found in input state to tool_edge: {state}")
    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
        return "tools"
    return END

このコードでは、Stateを受け取りリストなら一番最後のもの、そうでなければmessagesにアクセスしてそれをAIの最後のメッセージとします。AIの最後のメッセージにtool_callsが含まれており、tool_callsの要素が1つ以上あるならツールノードに遷移するため"tools"を返します。
それ以外なら終了するためにENDを返します。

これも加えてグラフを作っていきます。まず、llmtoolの情報を渡すためにllm.bind_tools(tools)とします。(情報しか渡していないのでこれだけではツールの実行はできません)
次に、graph_builder.add_node("tools", tool_node)でツールノードを追加します。
最後にadd_conditional_edgesに先ほどのroute_toolを渡すことで条件付きエッジが追加されます。

langgraph/quickstart/graph.py
from langgraph.graph import StateGraph, START, END


from components.chatbot import get_chatbot_node
from components.llm import get_llm
from components.state import State
+ from components.tools import get_tool_node, tools


graph_builder = StateGraph(State)

llm = get_llm()
+ llm_with_tools = llm.bind_tools(tools)

+ chatbot = get_chatbot_node(llm_with_tools)
+ tool_node = get_tool_node()

graph_builder.add_node("chatbot", chatbot)
+ graph_builder.add_node("tools", tool_node)

graph_builder.add_edge(START, "chatbot")

+ graph_builder.add_conditional_edges(
+     "chatbot",
+     route_tools,
+     {"tools": "tools", END: END},
+ )
+ graph_builder.add_edge("tools", "chatbot")

graph = graph_builder.compile()

グラフを実行する前に、グラフ図を確認します。

langgraph/quickstart/graph.py
if __name__ == "__main__":
    from utils.draw import draw_graph

    draw_graph(graph)

以下の画像が出力されます。

chatbotからtoolとENDに点線の矢印が伸びていますが、これが条件付きエッジです。

グラフを実行しましょう。

langgraph/quickstart/graph.py
if __name__ == "__main__":
    from utils.stream import stream_graph

    stream_graph(graph)

User: What is LangGraph?
Assistant:
Assistant: [{"url": "https://www.langchain.com/langgraph", "content": "LangGraph is a framework for building stateful, multi-actor agents with LLMs that can handle complex scenarios and collaborate with humans. Learn how to use LangGraph with Python or JavaScript, deploy it with LangGraph Cloud, and see examples from real-world use cases."}, {"url": "https://www.datacamp.com/tutorial/langgraph-tutorial", "content": "LangGraph is a library within the LangChain ecosystem that simplifies the development of complex, multi-agent large language model (LLM) applications. Learn how to use LangGraph to create stateful, flexible, and scalable systems with nodes, edges, and state management."}]
Assistant: LangGraph is a framework for building stateful, multi-actor agents with large language models (LLMs) that can handle complex scenarios and collaborate with humans. It can be used with Python or JavaScript and can be deployed with LangGraph Cloud. LangGraph simplifies the development of complex, multi-agent LLM applications, allowing for the creation of stateful, flexible, and scalable systems with nodes, edges, and state management. You can learn more about LangGraph at https://www.langchain.com/langgraph and https://www.datacamp.com/tutorial/langgraph-tutorial.

1回目のAssistantでは何も出力されていません。これはchatbotがツールを選択したときには空文字を返すようになっているためです。
2回目のAssistantでは、先ほどのTavilySearchツールと同じような内容が返ってきており、ツールを使用したことがわかります。
最後のAssistantでは、ツールの結果をもとに回答が生成されていることが確認できました!

なお、ツールを使うかはLLMが判断をするのですが、Groqでは通常の会話でもツールを使ってしまうことがありました。
内部的なモデルにも依存しますが、ツールの説明を追加したりプロンプトを追加することで適切な判断をしてもらう必要がありそうです。

通常の会話でもツールを使ってしまう例

User: Hello
Assistant:
Assistant: [{"url": "https://www.openphone.com/blog/how-to-professionally-answer-the-phone/", "content": "2. Say the greeting with a smile (because warmth comes through the voice) Smile. Now say something. It might not sound to you like there's a big difference, but your body language can have a genuine impact on your tone of voice. "A smile is more than an expression," writes Kaan Turnali for Forbes."}, {"url": "https://smith.ai/blog/receptionist-greeting-scripts-15-professional-ways-to-answer-the-phone", "content": "14. Be an active listener. What you hear is more important than what you say—make sure that you know how to listen to what the callers need so that you can assist them properly. 15. Avoid slang and filler words. Don't just answer the phone with "yeah.". That sounds short and rude."}]
Assistant: Hello! It's nice to speak with you. I'm here to help answer your questions. Based on the information provided, it's important to remember to smile while answering the phone as it can genuinely impact the tone of voice, making it sound warm and welcoming. Additionally, it's recommended to avoid using slang and filler words, ensuring a professional and respectful greeting.

おわりに

今回はLangGraphのチュートリアルを実践してみました。
エッジの付け方の自由度が高いので、より発展的なグラフを作ることもできます。

https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_agentic_rag/
https://langchain-ai.github.io/langgraph/tutorials/multi_agent/multi-agent-collaboration/

LangGraphのチュートリアルでは色々なパターンが解説されているので、これらもキャッチアップして自分なりのグラフを作っていきたいですね!

Discussion