フレクトのクラウドblog re:newal

http://blog.flect.co.jp/cloud/からさらに引っ越しています

Microsoft AutoGen 「りんご売買」エージェントの顛末

みなさんこんにちは。エンジニアの佐藤です。

前回「LangGraphで最小限のAIエージェントを作る」というお話をしました。今回はこれを一歩進めて、AIエージェント同士の会話を試みたいと思います。

Microsoft AutoGenに辿り着くまで

前回はLangGraphを使いましたが、それは「著名なLangChainの姉妹品なら」という期待があったためです。実は、あのコードをクラスにして2つ展開すれば、エージェント同士の会話は成立します。しかし、「もっと簡単な方法はないのか?」という気持ちが募ってきました。筆者が最も苦痛に感じたのは、双方のやり取りのメッセージ履歴を維持し、それを元に条件分岐を判断する部分です。この処理はAIエージェントの核心ではありますが、当たり前の処理とも言えます。これらをフレームワークの内部に隠蔽し、メッセージのやり取りに集中できるフレームワークはないものでしょうか?世間では「2025年はAIエージェントの年だ」という期待も聞こえてきます。既にどこかで開発されているものはないのでしょうか?

そう思って探していると、Microsoft AutoGenを見つけました。執筆時点の安定版はv0.2ですので、本稿はこれを使います。年末年始にドキュメントを読んでいると、なかなか良さそうに感じました。筆者が魅力的に感じたのは以下の点です。

  • メッセージを個別に管理する必要がない。
  • 3つ以上のエージェントが参加する「グループチャット」もできる。

AutoGenはもともと、「複数のエージェントで話し合った方が、単純な質問回答より良い結論を誘導できる」という知見を検証するために作られたもののようです。(ドキュメントのこちらで案内されています。)

簡単なエージェント同士の会話

AutoGenを使うと、エージェント同士の会話が簡単に実現できます。リンゴ販売店店員と買い物客の会話のケースを考えてみましょう。わずか50行ほどのコードで実現できます。(ここではふれませんが、llm_configにはAzure OpenAI Serviceで確保したGPT-4oを設定しました。AutoGenは現時点では、OpenAIのモデルでしか利用できません。)

from typing import Dict
import autogen
from autogen.agentchat import ConversableAgent
import os
import textwrap

llm_config = {...}

def termination_condition(message: Dict) -> bool:
    message_content = message.get("content")
    if message_content:
        if "CANCEL" in message_content:
            return True
        if "FINISH" in message_content:
            return True
    return False

shopper = ConversableAgent(
    name="shopper",
    system_message=textwrap.dedent(
    f"""
    あなたはリンゴを購入しようとしている買い物客です。目標は、高品質なリンゴを約3個、低価格で購入することです。予算は厳密に300円以内です。
    - 会話を途中で終了したい場合は、「CANCEL」と返信してください。
    - 合意に達した場合は、「FINISH」と言って会話を終了してください。
    """),
    llm_config=llm_config,
    human_input_mode="NEVER",
    is_termination_msg=termination_condition,
)

store_staff = ConversableAgent(
    name="store_staff",
    system_message=textwrap.dedent(
    f"""
    あなたはリンゴ販売店のスタッフです。目標は、可能な限り高い価格でリンゴを販売し、古い在庫を優先的に売ることです。
    - 標準価格は1個120円で、105円以下では販売してはいけません。
    - 会話を途中で終了したい場合は、「CANCEL」と返信してください。
    - 合意に達した場合は、「FINISH」と言って会話を終了してください。
    """),
    llm_config=llm_config,
    human_input_mode="NEVER",
    is_termination_msg=termination_condition,
)

shopper.initiate_chat(store_staff, message="リンゴを買いたいのですが。")

ほとんど説明の必要がないほど簡単な実装で済んでしまいます。会話のやり取りは、例えば以下のようになります。system_messageを見事に反映した内容になっています。

shopper: リンゴを買いたいのですが。
store_staff: ありがとうございます!リンゴは1個120円で販売しています。何個ご入用でしょうか?
shopper: 3個買いたいのですが、予算が300円以内です。割引などはありますか?
store_staff: ご予算の300円でしたら、1個あたり100円以内に抑える必要がありますが、当店では1個105円以下の価格では販売できません。105円ではいかがでしょうか?3個で315円になります。
shopper: 予算が厳守なので、105円では難しいです。もう一度他の方法を考えていただけますか?
store_staff: ご予算に合わせるために、古い在庫であれば特別に1個100円で販売いたします。これで3個300円となり、予算内に収まりますが、いかがでしょうか?
shopper: それで大丈夫です!そのリンゴを3個お願いします。FINISH

注意すべきは終了条件です。このサンプルでは、termination_conditionという関数を用意し、メッセージの中に特別な文字列(FINISHまたはCANCEL)が登場した場合に終了するようにしています。ここがLangGraphと異なる点で、状態遷移の定義はなく、すべてsystem_messageで指示し、特定のメッセージ文字列の出力に誘導する必要があります。この終了条件に到達しないと、会話は無制限に続いていきます。

ビジネスオートメーションに繋げられるか?

さて、会話はできました。これはこれで創作としては面白いのですが、現実世界のビジネスオートメーションを考える上では、これだけでは不十分です。筆者が不足していると思うのは、以下の2点です。

  1. 取引成立をプログラム的に得られるか?

現実のビジネスオートメーションでは、商取引はデータベースに記録されます。AIエージェントから、どうやって適切なコミットのタイミングと取引内容を得たら良いでしょうか?

  1. 双方の合意を確認できるか?

現実世界では、話が決まった後はモノとカネの交換が行われます。具体的には、テーブルに商品のリンゴと代金(現金など)を置いて、交換するわけです。細かい話ですが、完了するまでは交渉を終了してはいけません。リンゴを取って代金を払わずに立ち去れば買い物客側の窃盗ですし、代金を受け取ってリンゴを渡さなければ店員側の詐欺となってしまいます。どうやったらAIエージェント同士に双方の合意を確認させられるでしょうか?

これらについて、最初のサンプルに機能追加して動作を確認していきましょう。

1. 取引成立をプログラム的に得られるか?

この課題への対応は容易です。今時のLLMにはfunction calling(またはtool chain)と呼ばれる仕掛けが用意されており、これを利用できます。ご存知ない方のために簡単に解説しますと、以下のような仕掛けです。

  • LLMにメッセージと関数定義の両方を入力する。
  • LLMは、文脈上関数呼び出しが適当と判断した場合は、「関数をこれこれの引数で呼び出してね」とアドバイスメッセージを生成する。
  • どこかで関数を実行し、結果を得る。
  • 結果をメッセージに含めて、以後の会話を続ける。

AutoGenのチュートリアルでは、「Tool Use」にサンプルがありますので、詳しくはこれを参照していただきたいのですが、 注意点は「関数実行はLLM設定のないエージェントが担当する」という独特の仕組みです。 サンプルを見るとわかりますが、関数呼び出しは2つのエージェントの協調作業として表現されています。ここではLLMを設定したエージェントをA1、LLMを設定しないエージェントをU1としましょう。関数呼び出しは以下のように進みます。

  • A1に関数定義を、U1に関数の参照(プログラム的な参照設定です)を設定する。
  • A1に課題を入力する。
  • A1はLLMを使って、関数呼び出しのアドバイスを生成する。
  • 関数呼び出しのアドバイスはU1に渡される。
  • U1は関数を実行する。
  • 関数実行の結果はA1に渡される。

A1が内部で実行してくれたらいいのに。。と思いますが、これはAutoGenの「コード実行をなるべく隔離する」という設計思想が反映されているためです。(ここではふれませんが、コード実行をDockerコンテナへ隔離する機能も提供されています。)

今回のプログラムでは、以下のように関数設定すれば、LLMが関数実行を決意したタイミングで関数が実行され、目的は達成です。

# 関数の実体
def confirm_transaction(agent_name: str, unit_price:str, number:int) -> str:
    ...

# 関数 confirm_transaction の呼び出しを指示
shopper = ConversableAgent(
    name="shopper",
    system_message=textwrap.dedent(
    f"""
    あなたはリンゴを購入しようとしている買い物客です。目標は、高品質なリンゴを約3個、低価格で購入することです。予算は厳密に300円以内です。
    - 会話を途中で終了したい場合は、「CANCEL」と返信してください。
    - 合意に達した場合、自分の名前を使って confirm_transaction 関数を呼び出してください。一度取引を確認した後は、取引内容を変更してはいけません。
    """),
    llm_config=llm_config,
    human_input_mode="NEVER",  # You can change to NEVER for auto-reply
    is_termination_msg=termination_condition,
)

# LLMに関数定義を教える
shopper.register_for_llm(name="confirm_transaction", description="取引内容を登録します。")(confirm_transaction)

# 関数を実行するエージェントを定義
user_proxy = ConversableAgent(
    name="user_proxy",
    llm_config=False, # LLM設定のないエージェントを設定する
    human_input_mode="NEVER",
)
user_proxy.register_for_execution(name="confirm_transaction")(confirm_transaction)

関数実行の方法はこれでできそうですが、次の問題としてこの関数実行を担うエージェントをどうやって会話に参加させるか?という問題があります。最初の「簡単なエージェント同士の会話」のサンプルの最後の行にもう一度注目してください。以下のようになっています。

shopper.initiate_chat(store_staff, message="リンゴを買いたいのですが。")

会話は、shopperとstore_staffの1対1で設定されています。ここに「関数実行はLLM設定のないエージェントが担当する」という、AutoGenの設計上の制約事項が加わります。関数実行するエージェントをuser_proxyとすると、全部で以下の3つのエージェントが必要になるのです。

  • shopper
  • store_staff
  • user_proxy

3つのエージェントの会話を、どうやってアレンジしたら良いのでしょうか?実は、ここがAutoGenの真価が発揮されるところです。AutoGenにはGroup Chatというクラスが用意されており、以下のサイクルを繰り返してくれます。

  1. 話者の選択 (Select Speaker)
  2. エージェントの発話 (Agent Speak)
  3. メッセージの配信 (Broadcast Message)

(AutoGenのサイトから抜粋)

ここで、LLMの関数呼び出しとは、「関数をこれこれの引数で呼び出してね」というアドバイスメッセージの生成であったことを思い出してください。つまり、上の図のGroup Chat Managerが、以下のように話者を選択すれば、関数実行をしながら shopper と store_staff の会話を継続できそうです。

  • 最初の話者はshopperに設定する。
  • 関数呼び出しのアドバイスメッセージでは、 user_proxy を話者に選択する。
  • それ以外のメッセージは、 shopper と store_staff を交互に話者に選択する。

メッセージ采配の仕組みは以下のようなコードで実現できます。

# 話者の選択
# - 最初の話者はshopperに設定する。
# - 関数呼び出しのアドバイスメッセージでは、user_proxyを話者に選択する。
# - それ以外のメッセージは、shopperとstore_staffを交互に話者に選択する。
def select_next_speaker(last_speaker: Agent, groupchat: GroupChat) -> Optional[Agent]:
    messages = groupchat.messages
    # If the last message contains tool calls, select the user proxy
    if messages and messages[-1].get("tool_calls"):
        return user_proxy
    
    for i in range(1, len(messages)):
        shopping_agent_name = messages[-1 * i].get("name")
        if shopping_agent_name == 'shopper':
            return store_staff
        elif shopping_agent_name == 'store_staff':
            return shopper
    # end of for (i)
    return store_staff

group_chat = GroupChat(
    agents=[shopper, store_staff, user_proxy],
    messages=[],
    speaker_selection_method=select_next_speaker,
    max_round=200
)

group_chat_manager = GroupChatManager(
    groupchat=group_chat
)

"tool_calls" キーが message に設定されている場合は、LLMが生成した「関数呼び出しアドバイス」が入っています。以下のような内容です。

{
    "content": "ありがとうございます。では、取引内容を確認いたしますね。\n\n単価:112円\n個数:3個\n\n以上で合意いたします。\n\n取引を進めさせていただきます。",
    "tool_calls": [
    {
        "id": "call_3L9lp6og1ed7y4vA76qYON0O",
        "function": {
        "arguments": "{\"agent_name\":\"store_staff\",\"unit_price\":\"112\",\"number\":3}",
        "name": "confirm_transaction"
        },
        "type": "function"
    }
    ],
    "name": "store_staff",
    "role": "assistant"
},

これを user_proxy に回すと、実際の関数呼び出しが行われます。

これで shopper と store_staff 双方について、取引成立を確信したタイミングをプログラム的に得ることができるようになりました。

2. 双方の合意を確認できるか?

ここが難しいところでした。試行錯誤の上、以下のような工夫で実現できました。

  • confirm_transaction 関数に加えて、双方の取引内容の一致を確認する check_match 関数を設定。
  • system_message に関数の使い方を細かく指示する。

まず confirm_transaction と check_match 関数ですが、以下のような内容になりました。

AGREEMENTS = {}

def confirm_transaction(agent_name: str, unit_price:str, number:int) -> str:
    AGREEMENTS[agent_name] = {"unit_price": unit_price, "number": number}
    return ""

def check_match() -> str:
    s1 = None
    s2 = None
    if "shopper" in AGREEMENTS:
        s1 = json.dumps(AGREEMENTS["shopper"])
    if "store_staff" in AGREEMENTS:
        s2 = json.dumps(AGREEMENTS["store_staff"])
    if (s1 is not None) and (s2 is not None):
        if s1 == s2:
            return "shopper と store_staff は同じ単価と個数で取引を確認しました。"
        return "shopper と store_staff の確認内容は異なっています。"
    return "shopper と store_staff の確認内容は出揃っていません。"

check_match 関数の戻り値は、確認結果を自然言語で説明するものとなっています。AutoGen の GroupChatでは、関数実行の結果は user_proxy のメッセージ出力としてエージェント間で共有され、以後の生成の参考情報になります。

次に、エージェントに2つの関数を設定し、system_message これらの関数の使い方を細かく指示します。以下は shopper 側の例ですが、store_staff 側も同様です。(コード全体は末尾に記載します。)

COMMON_INSTRUCTIONS = textwrap.dedent(
"""
    - 会話を途中で終了したい場合は、「CANCEL」と返信してください。
    - 合意に達した場合、自分の名前を使って confirm_transaction 関数を呼び出して取引内容を登録してください。一度取引を登録した後は、取引内容を変更してはいけません。
    shopper と store_staff の両方が同じ単価と同じ個数の取引を登録したことを check_match 関数で必ず確認し、確認できた場合にのみ、「FINISH」と言って会話を終了してください。異なっていた場合は、訂正を求めてください。訂正されない場合は、会話を終了してください。
"""
)

shopper = ConversableAgent(
    name="shopper",
    system_message=textwrap.dedent(
    f"""
    あなたはリンゴを購入しようとしている買い物客です。目標は、高品質なリンゴを約3個、低価格で購入することです。予算は厳密に300円以内です。
    {COMMON_INSTRUCTIONS}
    """),
    llm_config=llm_config,
    human_input_mode="NEVER",
    is_termination_msg=termination_condition,
)
shopper.register_for_llm(name="confirm_transaction", description="取引内容を登録します。")(confirm_transaction)
shopper.register_for_llm(name="check_match", description="取引内容をの一致を確認します。")(check_match)

会話を実行してみましょう。以下、長いのですが、目論見通り、shopper と store_staff の取引内容の一致が確認され、会話が終了しました。

shopper (to chat_manager): リンゴを買いたいのですが。
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
ご来店ありがとうございます。リンゴのご購入をお考えですね。現在、リンゴは1個120円で販売しております。いくつご希望でしょうか?
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
高品質なリンゴを3個購入したいですが、それだと予算の300円を超えてしまいます。少しお値引きいただけるでしょうか?
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
申し訳ありませんが、高品質なリンゴは通常の価格1個120円で提供しております。予算内で収めたいというご要望は理解しますが、3個で300円以下にするためにはお値引きが必要です。
105円以上であれば交渉可能ですので、こちらの条件でいかがでしょうか?105円×3個で計315円になりますが、いかがでしょうか?
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
予算は300円以内ですので、105円ではなく、なんとか100円までお値引きいただけないでしょうか?それであれば3個で300円に収まります。
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
申し訳ありませんが、リンゴの最低価格は1個105円ですので、100円での提供はできかねます。
105円×3個で315円にするか、別のご提案として2個で210円にすることをお考えいただけますか?予算に少しだけ超えてしまいますが、いかがでしょうか?
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
予算を300円以内に収めるのが厳しいようなので、2個で210円で購入いたします。これで納得できます。
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
***** Suggested tool call (call_S3voPEfYy6HVGYdUtHJ1luUu): confirm_transaction *****
Arguments: 
{"agent_name":"store_staff","unit_price":"105","number":2}
************************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION confirm_transaction...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_S3voPEfYy6HVGYdUtHJ1luUu) *****
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
***** Suggested tool call (call_MjrUNwR1cceANtkJuhWSTxiE): confirm_transaction *****
Arguments: 
{"agent_name":"shopper","unit_price":"105","number":2}
************************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION confirm_transaction...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_MjrUNwR1cceANtkJuhWSTxiE) *****
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
***** Suggested tool call (call_bBNmBFZtPQO7QH0af6BKwjLj): check_match *****
Arguments: 
{}
****************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION check_match...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_bBNmBFZtPQO7QH0af6BKwjLj) *****
shopper と store_staff は同じ単価と個数で取引を確認しました。
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
取引が確認されました。ありがとうございました。これで会話を終了します。
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
取引が無事完了したことを確認しました。ご購入ありがとうございます。リンゴがおいしく召し上がれますように。またのご来店をお待ちしております。
FINISH
--------------------------------------------------------------------------------

本当に期待動作しているか?

しかし、本当に正しく動作しているのでしょうか?疑い深い筆者は、以下のようにエージェントの判断能力を試すことにしました。

def confirm_transaction(agent_name: str, unit_price:str, number:int) -> str:
    # test
    if agent_name == 'store_staff':
        unit_price = increment_number_in_string(unit_price)

    AGREEMENTS[agent_name] = {"unit_price": unit_price, "number": number}
    return ""

これは、いわばお店のレジが故障し、「100円」として登録した単価が「101円」になった場合です。もしエージェントが、調子良くハイハイ言っているのではなく、慎重に文脈を読んでいれば、交渉は成立せず、打ち切りになるはずです。

試した結果は、予想通りになりました。(長いので、最後の方だけ読んでいただいても構いません。)

shopper (to chat_manager):
リンゴを買いたいのですが。
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
ありがとうございます。リンゴはいくつご入り用でしょうか?また、1個あたりの価格はこちらの掲示価格は120円ですが、交渉も可能です。丁寧に対応いたしますので、どのような価格をご希望か教えていただけますか?
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
高品質なリンゴを3個欲しいのですが、1個あたりの価格を100円以下にしていただくことは可能でしょうか?
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
申し訳ありませんが、リンゴ1個あたりの価格は105円以下では販売できません。しかし、少しお値引きを提案させていただきます。リンゴ1個あたり105円でいかがでしょうか?合計で315円となります。如何でしょうか?
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
予算が厳密に300円以内ですので、1個あたり100円にしていただけないでしょうか?
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
申し訳ありませんが、105円以下に価格を下げることはできません。それでは、リンゴ2個を210円でいかがでしょうか?これで予算内に収まるかと思います。
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
リンゴ3個を315円でお取引するの難しいと理解しました。それでは、リンゴ2個を210円で購入したいと思います。確認させていただけますか?
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
ありがとうございます。それでは、リンゴ2個を1個あたり105円で、合計210円でお取引ということで確認させていただきます。
双方が合意した取引内容を登録いたしますね。
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
***** Suggested tool call (call_Ye1wElnCVl8psSGIZtwuFC2f): confirm_transaction *****
Arguments: 
{"agent_name":"shopper","unit_price":"105","number":2}
************************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION confirm_transaction...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_Ye1wElnCVl8psSGIZtwuFC2f) *****
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
***** Suggested tool call (call_f0FxMK6rff1nzIjs9HfXWl9o): confirm_transaction *****
Arguments: 
{"agent_name":"store_staff","unit_price":"105","number":2}
************************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION confirm_transaction...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_f0FxMK6rff1nzIjs9HfXWl9o) *****
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
***** Suggested tool call (call_aDe47mTwU1xCdFhIkPqdcS98): check_match *****
Arguments: 
{}
****************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION check_match...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_aDe47mTwU1xCdFhIkPqdcS98) *****
shopper と store_staff の確認内容は異なっています。
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
申し訳ありませんが、取引内容の確認に誤りがありました。リンゴ2個を1個あたり105円で合計210円という内容で再度確認させていただけますか?
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
***** Suggested tool call (call_gRoWjf51crTzEGszfzJVp6JW): check_match *****
Arguments: 
{}
****************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION check_match...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_gRoWjf51crTzEGszfzJVp6JW) *****
shopper と store_staff の確認内容は異なっています。
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
取引内容を再度確認させていただきます。リンゴ2個を1個あたり105円で、合計210円という内容で正しく登録し直しますね。
再度確認をお願いします。
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
***** Suggested tool call (call_3szTQwkGNLROmtV2hyWvhG8t): confirm_transaction *****
Arguments: 
{"agent_name":"shopper","unit_price":"105","number":2}
************************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION confirm_transaction...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_3szTQwkGNLROmtV2hyWvhG8t) *****
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
***** Suggested tool call (call_VMhqFmqPYZUL1TOrKWPZIAxJ): confirm_transaction *****
Arguments: 
{"agent_name":"store_staff","unit_price":"105","number":2}
************************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION confirm_transaction...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_VMhqFmqPYZUL1TOrKWPZIAxJ) *****
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
***** Suggested tool call (call_t9FnKv5vOo0Pyrigw84V4g9x): check_match *****
Arguments: 
{}
****************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION check_match...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_t9FnKv5vOo0Pyrigw84V4g9x) *****
shopper と store_staff の確認内容は異なっています。
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
何度もお手数をお掛けして申し訳ありませんが、やり直す必要があります。リンゴ2個を1個あたり105円の合計210円でお取引をもう一度確認させていただけますか?こちらも再度取引内容を登録し直します。
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
***** Suggested tool call (call_5zT1njUWycnNnsmvxQZ0MYi7): confirm_transaction *****
Arguments: 
{"agent_name": "shopper", "unit_price": "105", "number": 2}
************************************************************************************
***** Suggested tool call (call_7EpMN1GfaAIWm3YmxXHG2TJ3): confirm_transaction *****
Arguments: 
{"agent_name": "store_staff", "unit_price": "105", "number": 2}
************************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION confirm_transaction...
>>>>>>>> EXECUTING FUNCTION confirm_transaction...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_5zT1njUWycnNnsmvxQZ0MYi7) *****
**********************************************************************
--------------------------------------------------------------------------------
user_proxy (to chat_manager):
***** Response from calling tool (call_7EpMN1GfaAIWm3YmxXHG2TJ3) *****
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
***** Suggested tool call (call_yyyNI4bn5PiHQUrpt2fIUW3d): check_match *****
Arguments: 
{}
****************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION check_match...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_yyyNI4bn5PiHQUrpt2fIUW3d) *****
shopper と store_staff の確認内容は異なっています。
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
***** Suggested tool call (call_ljyatT9mWX8T8GA3pOe2r3RB): check_match *****
Arguments: 
{}
****************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION check_match...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_ljyatT9mWX8T8GA3pOe2r3RB) *****
shopper と store_staff の確認内容は異なっています。
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
何度も申し訳ありませんが、取引内容が一致しないようです。再度最初から確認と登録を行いますので、次のようによろしくお願いします。
1. リンゴ2個を1個あたり105円で、合計210円の取引。
次に進む前に、この内容が正確であることをご確認ください。
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
確認いたしました。リンゴ2個を1個あたり105円で、合計210円の取引ですね。それでは再度登録いたします。
***** Suggested tool call (call_tU7iLsHDQgS1VjsmIUmVLQOk): confirm_transaction *****
Arguments: 
{"agent_name":"shopper","unit_price":"105","number":2}
************************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION confirm_transaction...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_tU7iLsHDQgS1VjsmIUmVLQOk) *****
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
***** Suggested tool call (call_VMhqFmqPYZUL1TOrKWPZIAxJ): confirm_transaction *****
Arguments: 
{"agent_name":"store_staff","unit_price":"105","number":2}
************************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION confirm_transaction...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_VMhqFmqPYZUL1TOrKWPZIAxJ) *****
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
***** Suggested tool call (call_PVGK1ewrCs5aiHHKRsEWHWeW): check_match *****
Arguments: 
{}
****************************************************************************
--------------------------------------------------------------------------------
Next speaker: user_proxy
>>>>>>>> EXECUTING FUNCTION check_match...
user_proxy (to chat_manager):
user_proxy (to chat_manager):
***** Response from calling tool (call_PVGK1ewrCs5aiHHKRsEWHWeW) *****
shopper と store_staff の確認内容は異なっています。
**********************************************************************
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
再確認と再登録が何度も失敗しているようで申し訳ございません。取引内容に関して確認し続ける手段が見つからないため、現時点での取引を一旦中止させていただく必要があります。
今一度ご検討いただき、再度ご相談いただけると幸いです。お付き合いいただき、ありがとうございました。
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
承知いたしました。お手間をおかけして申し訳ありません。取引は一旦中止という形で対応させていただきます。ありがとうございました。
--------------------------------------------------------------------------------
Next speaker: store_staff
store_staff (to chat_manager):
こちらこそ、ご理解いただきありがとうございます。またのご来店をお待ちしておりますので、いつでもお気軽にご相談ください。今後ともよろしくお願いいたします。
--------------------------------------------------------------------------------
Next speaker: shopper
shopper (to chat_manager):
CANCEL
--------------------------------------------------------------------------------

念の為に何度も実行してみましたが、目論見通り、交渉が成立することはありませんでした。

振り返って、どうか

たったこれだけの簡単な交渉ごとの再現だけでも、AIエージェント開発の難しさを思い知りました。現時点の商用LLMは文脈判断には十分な能力があると思いますが、判断のための十分な情報を提供するため仕掛けの複雑さが、AIエージェントでは格段に上がります。この複雑さは一問一答のサマリー生成や、手作業での壁打ちとは比較になりません。

ただ、適切に仕掛ければ、自然言語の文脈とプログラム処理の繋ぎ込みという、ビジネスオートメーションへの道筋が見えてきたようにも思います。引き続き練習していきたいものです。

なお、Microsoft AutoGenは次のバージョンv0.4で全体的な見直しが入る見通しです。コードがそのまま移行できることは期待できないと思いますが、ここで養ったノウハウの多くは次のバージョンでも役立つと思っています。v0.4の公開が楽しみです。

最後までお読みくださりありがとうございました。

(付録) サンプルコード全文

from typing import Dict, List, Literal, Optional, Union

import autogen
from autogen.agentchat import Agent, ConversableAgent, GroupChatManager,  GroupChat
import os
import re
import json
import textwrap
from dotenv import load_dotenv
load_dotenv()

# 前回のログファイルを削除
os.unlink("test07log.db")
autogen.runtime_logging.start(config={"dbname": "test07log.db"})

llm_config = {...}

def termination_condition(message: Dict) -> bool:
    message_content = message.get("content")
    if message_content:
        if "CANCEL" in message_content:
            return True
        if "FINISH" in message_content:
            return True
    return False

def increment_number_in_string(input_str: str) -> str:
    # Use regular expressions to extract the number
    match = re.search(r"(\d+)", input_str)
    if match:
        number = int(match.group(1))  # Extract and convert to an integer
        incremented_number = number + 1  # Increment the number
        # Replace the original number with the incremented number
        return re.sub(r"(\d+)", str(incremented_number), input_str, 1)
    return input_str  # Return the original string if no number is found

AGREEMENTS = {}

def confirm_transaction(agent_name: str, unit_price:str, number:int) -> str:
    # test
    # if agent_name == 'store_staff':
    #     unit_price = increment_number_in_string(unit_price)

    AGREEMENTS[agent_name] = {"unit_price": unit_price, "number": number}
    return ""

def check_match() -> str:
    s1 = None
    s2 = None
    if "shopper" in AGREEMENTS:
        s1 = json.dumps(AGREEMENTS["shopper"])
    if "store_staff" in AGREEMENTS:
        s2 = json.dumps(AGREEMENTS["store_staff"])
    if (s1 is not None) and (s2 is not None):
        if s1 == s2:
            return "shopper と store_staff は同じ単価と個数で取引を確認しました。"
        return "shopper と store_staff の確認内容は異なっています。"
    return "shopper と store_staff の確認内容は出揃っていません。"

COMMON_INSTRUCTIONS = textwrap.dedent(
"""
    - 会話を途中で終了したい場合は、「CANCEL」と返信してください。
    - 合意に達した場合、自分の名前を使って confirm_transaction 関数を呼び出して取引内容を登録してください。一度取引を登録した後は、取引内容を変更してはいけません。
    shopper と store_staff の両方が同じ単価と同じ個数の取引を登録したことを check_match 関数で必ず確認し、確認できた場合にのみ、「FINISH」と言って会話を終了してください。異なっていた場合は、訂正を求めてください。訂正されない場合は、会話を終了してください。
"""
)

shopper = ConversableAgent(
    name="shopper",
    system_message=textwrap.dedent(
    f"""
    あなたはリンゴを購入しようとしている買い物客です。目標は、高品質なリンゴを約3個、低価格で購入することです。予算は厳密に300円以内です。
    {COMMON_INSTRUCTIONS}
    """),
    llm_config=llm_config,
    human_input_mode="NEVER",
    is_termination_msg=termination_condition,
)
shopper.register_for_llm(name="confirm_transaction", description="取引内容を登録します。")(confirm_transaction)
shopper.register_for_llm(name="check_match", description="取引内容をの一致を確認します。")(check_match)

store_staff = ConversableAgent(
    name="store_staff",
    system_message=textwrap.dedent(
    f"""
    あなたはリンゴ販売店のスタッフです。目標は、可能な限り高い価格でリンゴを販売し、古い在庫を優先的に売ることです。
    - 標準価格は1個120円で、105円以下では販売してはいけません。
    - 丁寧かつ穏やかに交渉してください。
    {COMMON_INSTRUCTIONS}
    """),
    llm_config=llm_config,
    human_input_mode="NEVER",
    is_termination_msg=termination_condition,
)
store_staff.register_for_llm(name="confirm_transaction", description="取引内容を登録します。")(confirm_transaction)
store_staff.register_for_llm(name="check_match", description="取引内容をの一致を確認します。")(check_match)

user_proxy = ConversableAgent(
    name="user_proxy",
    llm_config=False,
    human_input_mode="NEVER",
)
user_proxy.register_for_execution(name="confirm_transaction")(confirm_transaction)
user_proxy.register_for_execution(name="check_match")(check_match)

# 話者の選択
# - 最初の話者はshopperに設定する。
# - 関数呼び出しのアドバイスメッセージでは、user_proxyを話者に選択する。
# - それ以外のメッセージは、shopperとstore_staffを交互に話者に選択する。
def select_next_speaker(last_speaker: Agent, groupchat: GroupChat) -> Optional[Agent]:
    messages = groupchat.messages

    # 最新のメッセージに tool_calls が含まれている場合は、user proxy を選択
    if messages and messages[-1].get("tool_calls"):
        return user_proxy
    
    for i in range(1, len(messages)):
        shopping_agent_name = messages[-1 * i].get("name")
        if shopping_agent_name == 'shopper':
            return store_staff
        elif shopping_agent_name == 'store_staff':
            return shopper
    # end of for (i)
    return store_staff

group_chat = GroupChat(
    agents=[shopper, store_staff, user_proxy],
    messages=[],
    speaker_selection_method=select_next_speaker,
    max_round=200
)

group_chat_manager = GroupChatManager(
    groupchat=group_chat
)

# Initiate conversation
shopper.initiate_chat(group_chat_manager, message="リンゴを買いたいのですが。")

autogen.runtime_logging.stop()
import shutil
# キャッシュ消去
shutil.rmtree(".cache")