LangGraph ではじめるAIエージェント操作 on Azure OpenAI

:::note warn

LangChainのLangGraphは、サイクリックな処理が可能なためループ状態になる可能性があります。また、処理内容によりトークン数が増大（コストが増大）する可能性があります。実行の際はご注意下さい。この記事の内容により事故や損失等が発生しても責任は負いかねますことを予めご了承下さい。コードを実行される場合は、langchain blogやgithub上のexampleにも是非当たって頂ければと思います。
:::


## はじめに
LangChainは、LLM（大規模言語モデル）を利用してアプリケーションを開発するための有名なフレームワークですが、今年2024年1月に最初の安定版であるLangChain v0.1.0がリリースされました。古い langchain パッケージは、1.langchain-core、2.langchain-community、3.langchainの個別パッケージに分割され、また、**LangChain Expression Language（LCEL）** により、より簡単にチェーンを構築できるとしています。**LCEL**については、先日、記事を書かせて頂きましたので一読頂けると幸いです。（本記事の最後にリンクを載せています。）

## LangGraphとは

LangChainドキュメントには、次のように書かれています。

「LangGraphは、LLMでステートフルなマルチアクターアプリケーションを構築するためのライブラリで、LangChainでの使用を意図しています。 LangChain式言語（LCEL）を拡張し、複数のチェーン(またはアクター)を複数の計算ステップで循環的に調整する機能を備えています。 」
　つまり、**LangGraphは、LangChainフレームワーク内でLCELを使い構築できるAgent Orchestratorです。**（"つまり"以下は、筆者の勝手な表現です。）

## LangGraphの特徴とは
LangGraphは、グラフ理論（グラフ理論についてはここでは割愛させて頂きます）に導かれており、**Node**と呼ばれるエージェントと、**Edge**と呼ばれる線で繋がれた構造をとることが特徴となっています。次の図は、langchain blogに載っているnodeとedgeの表現が含まれた図です。

![langgraph_agent06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537852/df85acd8-f4da-b80e-6d4d-7c9ebfebf909.jpeg)



引用元：https://blog.langchain.dev/langgraph-multi-agent-workflows/


## LangGraphでAIエージェントを動かしてみる

先ほどの図の内、左の図についてgithub上のexample（※1）を頼りに、langgraphのエージェントを動かしてみたいと思います。exampleでは、AIエージェント向け検索エンジン（Tavily）やLangSmith（LLM アプリケーションの開発、監視、およびテストツール）が使われていますが、ここではそれらは使わず、非常にシンプルな形で、エージェントに役割（プロンプトによる役割）を与え動かしてみます。

※1：https://github.com/langchain-ai/langgraph/blob/main/examples/multi_agent/agent_supervisor.ipynb?ref=blog.langchain.dev

▼全体のイメージ
営業スタッフ、営業マネージャー、営業ディレクターという階層毎のエージェントを作成。
AIドリブンな世界における商品販売戦略について、agent間で簡単なブレストしてもらう。

▼用意するエージェント
・スーパーバイザー
・エージェント1 : sales_staff
・エージェント2 : sales_manager
・エージェント3 : sales_director

## 環境
- Windows10
- Python v3.11.4
- 主なlangchainライブラリバージョン
 ・langchain-core==0.1.27
 ・langgraph==0.0.26
 ・langchain-community==0.0.24
 ・langchain==0.1.9
- APIキー等の環境変数は、試したコードと同じフォルダに".env"ファイルを作り、その中に記述しています。
- requirements.txt は、最後の方に載せています。

``` Python
AZURE_OPENAI_TYPE = "azure"
AZURE_OPENAI_KEY = "YOUR AZURE OPENAI KEY" 
azure_endpoint = "YOUR AZURE ENDPOINT URL"
AZURE_OPENAI_DEPLOYMENT_NAME = "YOUR AZURE DEPLOYMENT NAME"
AZURE_OPENAI_VERSION = "2024-02-15-preview"
```
※ AZURE_OPENAI_DEPLOYMENT_NAMEは、チャット用モデルのデプロイ名（筆者はgpt-4を利用）

## LangGraphを使ったコード
＜主な処理の流れ＞
1. 環境設定
2. ツール設定
3. エージェント設定
4. スーパーバイザー設定
5. グラフ設定
6. ワークフロー構築（スーパーバイザーと各エージェント（ノード）をエッジで連結）
7. スーパーバイザーを軸として各エージェント（ノード）と会話

``` Python
# main_supervisor.py
# ## 1.環境設定
import os
from dotenv import load_dotenv

load_dotenv("./.env")


# ## 2.ツール設定
from langchain_experimental.tools import PythonREPLTool

python_repl_tool = PythonREPLTool(verbose=True)


# ## 3.エージェント設定
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.messages import BaseMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import AzureChatOpenAI

llm = AzureChatOpenAI(
    api_version=os.getenv("AZURE_OPENAI_VERSION"),
    azure_endpoint=os.getenv("azure_endpoint"),
    api_key=os.getenv("AZURE_OPENAI_KEY"),
    azure_deployment=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"),
    temperature=0.7,
    streaming=True,
)


def create_agent(llm, tools: list, system_prompt: str):
    prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                system_prompt,
            ),
            MessagesPlaceholder(variable_name="messages"),
            MessagesPlaceholder(variable_name="agent_scratchpad"),
        ]
    )
    agent = create_openai_tools_agent(llm, tools, prompt)
    executor = AgentExecutor(agent=agent, tools=tools)
    return executor


def agent_node(state, agent, name):
    print(f"Executing {name} node!") 
    result = agent.invoke(state)
    return {"messages": [HumanMessage(content=result["output"], name=name)]}


# ## 4.スーパーバイザー設定
from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser

members = ["sales_staff", "sales_manager", "sales_director"]
system_prompt = (
    " You are a supervisor tasked with managing a conversation between the"
    " following workers:  {members}. Given the following user request,"
    " respond with the worker to act next. Each worker will perform a"
    " task and respond with their results and status. When finished,"
    " respond with FINISH."
)
# Our team supervisor is an LLM node. It just picks the next agent to process
# and decides when the work is completed
options = ["FINISH"] + members
# Using Azure openai function calling can make output parsing easier for us
function_def = {
    "name": "route",
    "description": "Select the next role.",
    "parameters": {
        "title": "routeSchema",
        "type": "object",
        "properties": {
            "next": {
                "title": "Next",
                "anyOf": [
                    {"enum": options},
                ],
            }
        },
        "required": ["next"],
    },
}
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        MessagesPlaceholder(variable_name="messages"),
        (
            "system",
            "Given the conversation above, who should act next?"
            " Or should we FINISH? Select one of: {options}",
        ),
    ]
).partial(options=str(options), members=", ".join(members))


supervisor_chain = (
    prompt
    | llm.bind_functions(functions=[function_def], function_call="route")
    | JsonOutputFunctionsParser()
)


# ## 5.グラフ設定
import operator
from typing import Annotated, Any, Dict, List, Optional, Sequence, TypedDict
import functools

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langgraph.graph import StateGraph, END


# エージェントの状態は、各ノードへの入力。
class AgentState(TypedDict):
    # このアノテーションは、新しいメッセージが、
    # 常に現在のステートに追加されることをグラフに伝える。

    messages: Annotated[Sequence[BaseMessage], operator.add]
    # 'next'は、次のルーティング先を示す。
    next: str


# セールススタッフ　役割: 日々の顧客対応や商品の提案、売上データの入力などを行う。
sales_staff = create_agent(
    llm,
    [python_repl_tool],
    system_prompt="(階層1) 顧客対応と製品、サービス提案を担当。顧客からの質問に答え、適切な製品、サービスを推薦し、商談データ、売上予定データをシステムに記録します。",
)
sales_staff_node = functools.partial(agent_node, agent=sales_staff, name="Sales_Staff")

# セールスマネージャ　役割: セールスチームの管理と指導、販売戦略の立案、目標達成のためのリソース配分などを行う。
sales_manager = create_agent(
    llm,
    [python_repl_tool],
    system_prompt="(階層2) チームの管理と指導を担当。販売目標の設定、販売戦略の策定、パフォーマンスの監視、そしてチームメンバーへのフィードバック提供を行います。",
)
sales_manager_node = functools.partial(
    agent_node, agent=sales_manager, name="Sales_Manager"
)

# セールスディレクタ　役割: 全体のセールス戦略と目標の設定、事業部門の収益性分析、市場動向の監視、そして高いレベルでの顧客との関係構築を行う。
sales_director = create_agent(
    llm,
    [python_repl_tool],
    system_prompt="(階層3) 全体的な販売戦略と目標を設定し、セールス部門の収益性を分析。市場動向を監視し、重要な顧客との関係を築き、事業の成長を促進する。",
)
sales_director_node = functools.partial(
    agent_node, agent=sales_director, name="Sales_Director"
)

# ワークフロー設定
workflow = StateGraph(AgentState)
workflow.add_node("sales_staff", sales_staff_node)
workflow.add_node("sales_manager", sales_manager_node)
workflow.add_node("sales_director", sales_director_node)

# スーパーバイザーのワークフロー設定
workflow.add_node("supervisor", supervisor_chain)


# ## 6.ワークフロー構築（スーパーバイザーと各エージェント（グラフ）をエッジで連結）
for member in members:
    # 各メンバーはタスク終了後、スーパーバイザーに報告
    workflow.add_edge(member, "supervisor")
# スーパーバイザーは、ノードにルーティングするグラフステート"next"を入力
conditional_map = {k: k for k in members}
conditional_map["FINISH"] = END
workflow.add_conditional_edges("supervisor", lambda x: x["next"], conditional_map)

# エントリーポイント（ここからスタート）
workflow.set_entry_point("supervisor")

# フローを動かすための準備
graph = workflow.compile()


# ## 7.スーパーバイザーを軸として各エージェント（ノード）と会話
for s in graph.stream(
    {
        "messages": [
            HumanMessage(
                content="このコードに事前に用意された各階層のagentを利用し、AIドリブンな世界における商品販売戦略について、agent間でブレストして下さい。" 
                "このコードに用意された各階層の1つのagentの発言は最大3回までです。発言は1回、100文字以内にして下さい。"             
            )
        ],
    },
    # グラフ内の最大ステップ数
    {"recursion_limit": 20},
):

    # 各エージェントの出力を個別に表示
    for key in ["sales_staff", "sales_manager", "sales_director"]:
        if key in s:
            # エージェント名でヘッダを出力
            # print(f"\n### {key} says:")
            messages = s[key]["messages"]
            for msg in messages:
                # エージェントからのメッセージ内容を出力
                print(msg.content)
                print("----\n")  # セクションの終わり



```
## 実行結果
```
python main_supervisor.py

Executing Sales_Director node!
了解しました。各階層のagentがAIドリブンな世界での商品販売戦略についてアイデアを出し合い、ブレインストーミングを行います。それぞれ
のagentが発言する際には、100文字以内で要点を伝えます。それでは、ブレインストーミングを始めましょう。

階層1のagentからスタートします。AIを活用した商品販売戦略について、どのようなアイデアがありますか？
----

Executing Sales_Staff node!
AIを使って顧客の購入履歴から次に欲しい商品を推薦し、個別のマーケティングを実施。
----

Executing Sales_Manager node!
階層2のagentです。AIを用いてチームの販売パフォーマンスをリアルタイムで分析し、戦略を即座に調整できるようにしましょう。
----

Executing Sales_Director node!
階層3のagentです。市場動向をAIで監視し、変化に素早く対応できる販売戦略を立案しましょう。重要な顧客との関係も深めます。
----

```

<details><summary> requirements.txt </summary>

``` rb
aiohttp==3.9.3
aiosignal==1.3.1
annotated-types==0.6.0
anyio==4.3.0
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
arrow==1.3.0
asttokens==2.4.1
async-lru==2.0.4
attrs==23.2.0
Babel==2.14.0
beautifulsoup4==4.12.3
black==24.2.0
bleach==6.1.0
certifi==2024.2.2
cffi==1.16.0
charset-normalizer==3.3.2
click==8.1.7
colorama==0.4.6
comm==0.2.1
dataclasses-json==0.6.4
debugpy==1.8.1
decorator==5.1.1
defusedxml==0.7.1
distro==1.9.0
executing==2.0.1
fastjsonschema==2.19.1
fqdn==1.5.1
frozenlist==1.4.1
greenlet==3.0.3
h11==0.14.0
httpcore==1.0.4
httpx==0.27.0
idna==3.6
ipykernel==6.29.3
ipython==8.22.1
ipywidgets==8.1.2
isoduration==20.11.0
jedi==0.19.1
Jinja2==3.1.3
json5==0.9.17
jsonpatch==1.33
jsonpointer==2.4
jsonschema==4.21.1
jsonschema-specifications==2023.12.1
jupyter==1.0.0
jupyter-console==6.6.3
jupyter-events==0.9.0
jupyter-lsp==2.2.3
jupyter_client==8.6.0
jupyter_core==5.7.1
jupyter_server==2.12.5
jupyter_server_terminals==0.5.2
jupyterlab==4.1.2
jupyterlab_pygments==0.3.0
jupyterlab_server==2.25.3
jupyterlab_widgets==3.0.10
langchain==0.1.9
langchain-community==0.0.24
langchain-core==0.1.27
langchain-experimental==0.0.52
langchain-openai==0.0.7
langgraph==0.0.26
langsmith==0.1.9
MarkupSafe==2.1.5
marshmallow==3.21.0
matplotlib-inline==0.1.6
mistune==3.0.2
multidict==6.0.5
mypy-extensions==1.0.0
nbclient==0.9.0
nbconvert==7.16.1
nbformat==5.9.2
nest-asyncio==1.6.0
notebook==7.1.1
notebook_shim==0.2.4
numpy==1.26.4
openai==1.12.0
orjson==3.9.15
overrides==7.7.0
packaging==23.2
pandas==2.2.1
pandocfilters==1.5.1
parso==0.8.3
pathspec==0.12.1
platformdirs==4.2.0
prometheus_client==0.20.0
prompt-toolkit==3.0.43
psutil==5.9.8
pure-eval==0.2.2
pycparser==2.21
pydantic==2.6.2
pydantic_core==2.16.3
Pygments==2.17.2
python-dateutil==2.8.2
python-dotenv==1.0.1
python-json-logger==2.0.7
pytz==2024.1
pywin32==306
pywinpty==2.0.13
PyYAML==6.0.1
pyzmq==25.1.2
qtconsole==5.5.1
QtPy==2.4.1
referencing==0.33.0
regex==2023.12.25
requests==2.31.0
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rpds-py==0.18.0
Send2Trash==1.8.2
six==1.16.0
sniffio==1.3.1
soupsieve==2.5
SQLAlchemy==2.0.27
stack-data==0.6.3
tenacity==8.2.3
terminado==0.18.0
tiktoken==0.6.0
tinycss2==1.2.1
tornado==6.4
tqdm==4.66.2
traitlets==5.14.1
types-python-dateutil==2.8.19.20240106
typing-inspect==0.9.0
typing_extensions==4.10.0
tzdata==2024.1
uri-template==1.3.0
urllib3==2.2.1
wcwidth==0.2.13
webcolors==1.13
webencodings==0.5.1
websocket-client==1.7.0
widgetsnbextension==4.0.10
yarl==1.9.4
```
</details>

## さいごに
LangGraphを使った非常にシンプルなAIエージェントによる会話でしたが、設定したAIエージェントが呼び出され、会話をすることが確認できました。AIエージェントの役割やプロンプトをもっとしっかり設計できれば、自動化や課題解決に有効だと感じました。


https://qiita.com/hiromitsu_iwasaki/items/7d471ba05b0ecc582489


https://qiita.com/hiromitsu_iwasaki/items/824e6ac033b2c350ce35