📑

【LangGraph】ReActエージェント同士を会話させる お見合いに仲人が入る

に公開

概要

  • 前回はお見合いの男女が直接話すシチュエーションで、男性エージェントと女性エージェントを作成しました。
  • 今回は「仲人」を挟みます。こちらの記事にあるように、司会者とも取れます。
  • 流れとしては、仲人エージェント(partner_agent)が、最後に男性が話していたか女性が話していたかを判断して、相手方に話を振ります。
  • どうでも良い話ですが、著者は若かりし頃に仲人型のお見合いをした際、仲人の爺さんと大げんかして見合いが破綻になったことがあります。

成果物

サンプルコード
import uuid
from typing import Annotated, TypedDict, List, Dict, Any, Sequence, Union, cast, Literal, Optional
from dataclasses import dataclass

from langchain_core.tools import tool
from langchain_core.tools.base import InjectedToolCallId
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import ToolMessage, HumanMessage, AIMessage, BaseMessage

from langgraph.prebuilt import create_react_agent, InjectedState, interrupt
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.types import Command
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI


model = ChatOpenAI(model="gpt-4.1-mini", temperature=0.0)

@dataclass
class Evaluation:
    income_class: Optional[Literal['D', 'C', 'B', 'A']]
    academic_class: Optional[Literal['D', 'C', 'B', 'A']]

evaluation = Evaluation(income_class=None, academic_class=None)

# Stateの定義
class State(TypedDict):
    messages: Annotated[List[BaseMessage], add_messages]
    last_agent: str

######################## ツールとエージェントを作成 ########################

# エージェント定義
partner_agent = create_react_agent(
    model=model,
    tools=[],
    name="仲介役",
    prompt=(
        "あなたは結婚相談所の仲介役です。男性、女性それぞれが話をしやすいように、それぞれに話を振ってください。"
    )
)

# 男性のプロフィール、エージェント
male_profile = {
    "income": 600*10000,
    "birthday": "1990/04/09",
    "birthplace": "大阪",
    "academy": "京都大学",
    "job": "教師",
    "height": 170,
    "character": "明るくて楽観的",
    "hobby": "旅行、スポーツ観戦"
}

@tool
def get_male_profile(field: str, tool_call_id: Annotated[str, InjectedToolCallId]):
    """男性のプロフィールです。相手に聞かれた項目の情報を返します"""
    return male_profile

male_agent = create_react_agent(
    model=model,
    tools=[get_male_profile],
    name="男性",
    prompt=(
        "あなたは婚活中の男性(大野圭介)です。女性と会話する中で、質問されたら自然に情報を伝えてください。"
        "最初の自己紹介では、自分の名前と年齢と出身地と趣味を伝えてください。"
        "あなた自身のプロフィールはget_male_profileツールを使って調べてください。"
    )
)

# 女性は男性を学歴と年収で格付けする
@tool
def classify_income(income: int, tool_call_id: Annotated[str, InjectedToolCallId]):
    """ユーザーの年収を尋ね、年収をもとにA~Dの4段階に分類します"""
    if income < 300*10000:
        income_class = "D"
    elif income < 500*10000:
        income_class = "C"
    elif income < 700*10000:
        income_class = "B"
    else:
        income_class = "A"
    print(f"    この男の年収は{income_class}だな・・・")
    
    evaluation.income_class = income_class

@tool
def classify_academy(university: str, tool_call_id: Annotated[str, InjectedToolCallId]):
    """ユーザーの学歴を尋ね、最終出身大学の偏差値からA~Dの4段階に分類します"""
    if university == "京都大学":
        academic_class = "A"
    elif university == "大阪大学":
        academic_class = "B"
    elif university == "神戸大学":
        academic_class = "C"
    else:
        academic_class = "D"
    print(f"    この男の学歴は{academic_class}だな・・・")

    evaluation.academic_class = academic_class

female_agent = create_react_agent(
    model=model,
    tools=[classify_income, classify_academy],
    name="女性",
    prompt=(
        "あなたは婚活中の女性(佐藤さくら)です。会話を通じて、男性の年収、学歴、身長、性格などの情報を聞き出してください。"
        "聞き出した情報をもとに、男性が結婚相手として相応しいかどうかを判断します。"
    )
)



######################## ノード関数 ########################

def call_partner_agent(state: State) -> Command:
    response = partner_agent.invoke(state)
    print("partner_agent", response)

    next_agent = "female_agent" if state["last_agent"] == "male_agent" else "male_agent"

    return Command(
        goto=next_agent,
        update=response
    )


def call_male_agent(state: State) -> Command:
    response = male_agent.invoke(state)
    response["last_agent"] = "male_agent"

    return Command(
        goto="partner_agent",
        update=response,
    )

def call_female_agent(state: State) -> Command:
    response = female_agent.invoke(state)
    response["last_agent"] = "female_agent"

    return Command(
        goto="partner_agent",
        update=response
    )


# グラフの構築
builder = StateGraph(State)
builder.add_node("partner_agent", call_partner_agent)
builder.add_node("female_agent", call_female_agent)
builder.add_node("male_agent", call_male_agent)

builder.set_entry_point("partner_agent")
graph = builder.compile(checkpointer=MemorySaver())

if __name__ == "__main__":
    # 初期状態の定義
    initial_state: State = {
        "messages": [],
        "last_agent": "partner_agent",
    }
    
    # グラフを実行
    for values in graph.stream(
        initial_state, 
        config=RunnableConfig({"configurable": {"thread_id": uuid.uuid4()}}),
        stream_mode="values"
    ):
        print("\n==== 現在の状態 ====")
        messages = values.get("messages", [])
        if len(messages) > 0:
            print(messages[-1].name, messages[-1].content)

    print("=== 評価結果 ===")
    print(evaluation.income_class, evaluation.academic_class)

実行結果

==== 現在の状態 ====
仲介役 かしこまりました。では、まずお二人にお話を伺いながら進めていきますね。

男性の方、まずは自己紹介をお願いできますか?どんなことに興味があるかや、結婚に対してどんなイメージを持っているかなど、教えていただけると嬉しいです。

女性の方も、自己紹介とともに、どんな方と出会いたいか、結婚生活で大切にしたいことなどをお聞かせください。

==== 現在の状態 ====
男性 はじめまして、大野圭介と申します。年齢は32歳で、出身は東京です。趣味は映画鑑賞とジョギングです。よろしくお願いします。

==== 現在の状態 ====
仲介役 ありがとうございます、大野さん。映画鑑賞とジョギング、アクティブで素敵ですね。どんなジャンルの映画がお好きですか?

女性の方も、よろしければ自己紹介と、どんな方と出会いたいか教えていただけますか?

==== 現在の状態 ====
女性 はじめまして、佐藤さくらです。29歳で、趣味は読書とカフェ巡りです。結婚相手には誠実で優しい方を希望しています。よろしくお願いします。

==== 現在の状態 ====
仲介役 佐藤さん、ありがとうございます。読書とカフェ巡り、落ち着いた時間を大切にされているんですね。どんなジャンルの本がお好きですか?

大野さん、佐藤さんは誠実で優しい方を希望されているとのことですが、ご自身の性格や、相手に求める具体的なポイントなどありますか?

==== 現在の状態 ====
男性 こんにちは、佐藤さん。僕は誠実さを大切にしていて、周りからは穏やかで話しやすいと言われることが多いです。相手にはお互いに尊重し合える関係を築ける方だと嬉しいですね。佐藤さんは読書がお好きとのことですが、どんな本をよく読みますか?

==== 現在の状態 ====
仲介役 佐藤さん、どんなジャンルの本がお好きか教えていただけますか?また、カフェ巡りでお気に入りの場所や、そこでの過ごし方についてもお聞かせください。

==== 現在の状態 ====
女性 佐藤さくらです。読書はミステリーや恋愛小説が好きです。カフェではゆっくり本を読んだり、友達とおしゃべりを楽しんだりしています。大野さんは、普段どんなお仕事をされているんですか?また、年収や学歴、身長なども教えていただけると嬉しいです。

女性エージェントには特にプロフィールを入れてないんですが、趣味は読書とカフェ巡りと答えてきました。gpt-4.1はマッチングアプリ女をよく学習してらっしゃる。

コード解説

state

  • 今回は「最後に男性が話していたか、女性が話していたか」という状態を管理する必要がありますので、Stateに"last_agent"を配置します。
    (あくまでmale_agentかfemale_agentを見分けるためで、本当の意味で「最後にアクティブだったエージェント」ではありません。messages:BaseMessageからそのメッセージを発したエージェント名を取得するには、BaseMessage.nameで取得できます)
# Stateの定義
class State(TypedDict):
    messages: Annotated[List[BaseMessage], add_messages]
    last_agent: str

ノード構成

仲人エージェント

  • 仲人は英語でmatchmakerというらしいですが、ここではpartner_agentという名前をつけています。なお結婚相談所のパートナーエージェントとは関係ありません。
  • 男性と女性の交互に話を振りたいので、state["last_agent"]を読み込んで、次に男性に進むか女性に進むか判断します
def call_partner_agent(state: State) -> Command:
    response = partner_agent.invoke({"messages": state["messages"]})

    next_agent = "female_agent" if state["last_agent"] == "male_agent" else "male_agent"

    return Command(
        goto=next_agent,
        update=response
    )

男性および女性エージェント

  • 前の記事と同様、エージェントに会話履歴を渡し、Commandを返します。
  • ReActエージェントに会話履歴を投げて返ってきたresponseは"messages"プロパティのみが格納されたdictです
  • last_agentが入ったstateを投げたとしても、responseにはlast_agentは入っていません。なので、Command(update=response)に入れる前にlast_agentを追加してやります。
def call_male_agent(state: State) -> Command:
    response = male_agent.invoke(state)
    response["last_agent"] = "male_agent"

    return Command(
        goto="partner_agent",
        update=response,
    )

Discussion