7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AutoGen Coreでマルチエージェントが会話する仕組みを学んだ

Posted at

はじめに

AIエージェントと聞かない日がないくらい、巷ではAIエージェントの話題であふれていますね。便利なAIエージェントのサービスが続々と登場しています。

そんな中、自分でAIエージェントを作りたい!というケースもあると思います。今回は、AIエージェントフレームワークであるAutoGenのうち、AutoGen Coreを使ったAIエージェントの仕組みを学んだので、その内容をご紹介します。AIエージェント開発の参考になれば幸いです。

AutoGen Coreとは

概要

AutoGenはMicrosoftがリリースしたAIエージェント開発フレームワークです。2025年8月6日時点での最新版は0.7.1です。

AutoGenのフレームワークでは複数の機能群が提供されています。以下は主な機能です。

  • Core: AIエージェントを細かく柔軟に設定・制御できるAPIを提供。
  • AgentChat: CoreのAPIのラッパーとして、Coreと比較して扱いやすいAPIを提供。初心者のAutoGen入門にも有用。
  • Extensions: OpenAIやOllamaなど言語モデルとの連携を含めた拡張機能を提供

印象としては、Coreは細かい処理を自分でゴリゴリ書くスタイルです。これに対し、AgentChatは便利なエージェントの定義や会話遷移の枠組みがもともと用意されており、それらを組み合わせて処理を実現します。

AgentChatを触ってみたところ、細かい制御がしづらく、想定した会話フローが実現できないなどの難しさがありました。そのため、今回は細かくAIエージェントの動きを制御できるCoreを使用しました。なお、後ほど紹介しますが、Ollama上で言語モデルを動かして連携させたため、Extensionsも使いました。

題材: コード生成AIエージェント

いきなりゼロからAIエージェントを作って仕組みを学ぶのは厳しかったので、具体的な題材として、以下のAutoGenの公式ドキュメントで紹介されているコード生成AIエージェントのサンプルコードを使用して勉強しました。

なお、以降では「AIエージェント」と「エージェント」の用語の意味を区別します。単に「エージェント」と表記する場合、マルチエージェントシステムである「AIエージェント」の構成要素である、個々のエージェントを示します。

AIエージェントの概要

サンプルコードでは、以下の2つのエージェントが会話を繰り返し、コードを出力します。

  1. Coder: 与えられた仕様、およびReviewerからの指摘に基づいてコードを生成する。Reviewerから承認が得られた場合には、最終成果としてコードを出力する。
  2. Reviewer: Coderが生成したコードを受け取り、事前に定めた評価項目に従ってコードレビューを行う。生成されたコードの承認、書き直しの判断を行い、コードレビュー結果として出力する。

下図に、これらのエージェントがやり取りするフローを大まかに示します。

エージェントのやり取り

AIエージェント間の会話の仕組み

Agent Coreでは、エージェント同士がメッセージをやり取りすることで処理が進みます。サンプルコードでは、各処理に応じたメッセージ向けに、4つのクラスが定義されています。

  • CodeWritingTask: Coderに生成してもらうコードの仕様を表します。
  • CodeWritingResult: コードの仕様、最終レビュー結果、最終的に生成されたコードを表します。
  • CodeReviewTask: CoderがReviewerにレビュー依頼するために、コードの仕様、コード生成時のメモ、生成したコードなどを表します。
  • CodeReviewResult: ReviewerがCoderにレビュー結果を返すために、レビュー結果、承認/否認などを表します。

4つのクラスを先に示したエージェント間のやり取りの図にマッピングすると、以下のように描けます。
エージェントのやり取りとメッセージクラス

メッセージのやり取りは、Agent Runtimeというエージェントの実行環境上で行われます。サンプルコードでは、SingleThreadedAgentRuntimeというシンプルなAgent Runtimeが使われています。

下図にAgent Runtimeを使ったメッセージのやり取りのイメージを示します。サンプルコードでは、Subscriptionの形式でメッセージのやり取りを行っています。例えば、CoderがCodeReviewTaskをAgent Runtimeに送信した際、Agent RuntimeをSubscribeしているReviewerは、自身が処理できるCodeReviewTaskを見つけて処理します。下図では、最初のReviewerからの応答でapproved=Falseのため、Coderがもう一度コードを生成し、次のReviewerからの応答はapproved=Trueのため、CoderはCodeWritingResultを送って全体の処理が終了します。

エージェント間のメッセージ遷移

※軽い処理ではSingleThreadedAgentRuntimeで十分ですが、重い並列処理を行う場合などは、別のAgent Runtimeを選択した方が良いようです。

各エージェントのプロンプト

各エージェントは明確に役割が異なっており、それぞれのプロンプトも異なっています。

Coderのプロンプト

まず、Coderのシステムプロンプトは以下のとおり定義されています。

> You are a proficient coder. You write code to solve problems.
> Work with the reviewer to improve your code.
> Always put all finished code in a single Markdown code block.
> For example:
> ```python
> def hello_world():
>     print("Hello, World!")
> ```
> 
> Respond using the following format:
> 
> Thoughts: <Your comments>
> Code: <Your code>

上記のシステムプロンプトに対し、プロンプトは具体的な仕様です。サンプルコードでは以下の仕様が定義されており、これがそのままCoderがコード生成時に使うプロンプトとなります。

> Write a function to find the sum of all even numbers in a list.

Reviewerのプロンプト

一方、Reviewerのシステムプロンプトは以下のとおり定義されています。

> You are a code reviewer. You focus on correctness, efficiency and safety of the code.
> Respond using the following JSON format:
> {
>     "correctness": "<Your comments>",
>     "efficiency": "<Your comments>",
>     "safety": "<Your comments>",
>     "approval": "<APPROVE or REVISE>",
>     "suggested_changes": "<Your comments>"
> }

Reviewerが毎回のレビューで使用するプロンプトは以下です。詳細は割愛しますが、各エージェントは過去のメッセージの履歴にもアクセスできるため、前回Reviewer自身が送信したレビューコメントと、Coderから提示された最新のコードを見比べることで、レビューコメントに適切に対処したかわかるようになっています。

> The problem statement is: <仕様>
> The code is:
> ```
> <生成されたコード>
> ```
> 
> Previous feedback:
> <前回のレビューコメント>
> 
> Please review the code. If previous feedback was provided, see if it was addressed.

システムプロンプトの特徴

上記のシステムプロンプトのポイントとして、コードおよびレビュー結果を出力する際のフォーマットが指示されていることが挙げられます。これらの要素は、実装時に課題につながるので、後ほどご紹介します。

  • Coderは、コードを生成するのはもちろんですが、生成したコードをマークダウンブロックに入れて回答する必要がある
  • Reviewerは、レビュー結果に5つの項目を入れて、JSONとして回答する必要がある

動かしてみた

サンプルコードを動かすために一部変更した部分と、実行結果を紹介します。

AutoGen Coreのインストール

サンプルコードを動かす前に、AutoGen Coreをインストールしてください。

pip install autogen-core

Ollamaとの連携

サンプルコードではOpenAIのモデルを使う設定になっていますが、今回は、ローカル環境でモデルを動かせるOllamaを使って、AutoGenのエージェントからモデルを使う設定にしました。

OllamaとAutoGenを連携する場合は、AutoGenのExtensionsのインストールが必要です。

pip install autogen-ext[ollama]

コード内では、以下のモジュールをインポートします。

from autogen_ext.models.ollama import OllamaChatCompletionClient

そして、以下のようにモデルのClientを定義します。

    model_client = OllamaChatCompletionClient(
        model="llama3.1:8b"
    )

AutoGenからモデルにアクセスする前に、使用するモデルをローカル環境にpullしておく必要があります。上記の例ではllama3.1:8bを使用するので、ollama pull llama3.1:8bを事前に実行します。

※Ollama自体のインストール・設定方法は割愛します。

Visual Studio Codeから実行する場合の設定

Visual Studio Codeなどの環境でサンプルコードを実行する場合、以下のような設定が追加で必要な場合があります。

If you are using VSCode or other Editor remember to import asyncio and wrap the code with async def main() -> None: and run the code with asyncio.run(main()) function.
(引用元: https://microsoft.github.io/autogen/stable//user-guide/core-user-guide/quickstart.html)

例えば、main関数を以下のように定義すれば実行できました。

async def main() -> None:
    runtime = SingleThreadedAgentRuntime()

    model_client = OllamaChatCompletionClient(
        model="llama3.1:8b"
    )
     省略...

if __name__ == "__main__":
    asyncio.run(main())

実行結果: 成功例

上記のような変更を加えて、サンプルコードを動かしてみると、以下のような結果が出力されました。簡単な仕様だからですが、きちんと生成されているようですね。ちなみにこの結果は、CoderがCodeWritingResultのメッセージをPublishしたあと、Coder自身が標準出力にprintしたものです。

Code Writing Result:
--------------------------------------------------------------------------------
Task:
Write a function to find the sum of all even numbers in a list.
--------------------------------------------------------------------------------
Code:
def sum_even_numbers(lst):
    return sum(num for num in lst if num % 2 == 0)
--------------------------------------------------------------------------------
Review:
Code review:
correctness: The function seems to be correctly implemented and should produce 
the expected results. It checks each number in the list to see if it's even 
(i.e., if the remainder when divided by 2 is 0), and sums up all those numbers.
efficiency: This code has a good time complexity of O(n), where n is the length 
of the list, because it uses a generator expression. This means it only needs 
to iterate over each number in the list once, rather than creating a new list 
with all the even numbers and then summing that.       
safety: This code appears to be safe as it doesn't have any obvious error sources. 
It will work correctly for most inputs, but could potentially handle empty lists 
(which would cause a ZeroDivisionError), or non-numeric values in the input list.
approval: APPROVE
suggested_changes: One minor suggestion would be to add a docstring to explain 
what this function does and how it works. This can make the code easier to 
understand for other developers who might need to read it.
--------------------------------------------------------------------------------

実行結果: 失敗例

サンプルコードを動かすと、以下のような課題に直面することがあります。サンプルコードで指定されている簡単な仕様を難しい仕様に変えた場合や、プロンプトの指示に追従する能力が低いモデルを使った場合に発生しやすくなります。言語モデルの特性的に、想定通りに動いてくれるとは限らず、エラーハンドリングが必要になることがわかります。

1. 生成されたコードを抽出できない

Coder側のエラーで、Code block not foundというエラーが出ます。先述した通り、システムプロンプトでは出力のフォーマットを指定しており、Coderはマークダウンブロックにコードを入れたうえで応答する必要があります。しかし、生成されたコードがマークダウンブロックに入っていなかった場合などには、コード抽出用の正規表現が当てはまらないので、エラーとなります。

2. レビュー結果を整形できない

Reviewer側のエラーで、JSONDecodeErrorが出ます。エラー内容は名前の通りです。Reviewer側の応答はJSONとすることが、システムプロンプトで指示されてはいますが、モデルが必ずしもJSONで応答してくれるとは限りません。実際、サンプルコードのコメント行にもこの点の注意は述べられており、対策としてguidanceという、指定したフォーマットでモデルに回答させるためのライブラリを使うことがコメントされています。

3. レビューにおける承認/否認結果を抽出できない

Reviewer側のエラーで、KeyError: 'approval'というエラーが出ます。JSON形式で出力できていた場合、先述したエラーには該当しませんが、承認/否認を表すapprovalという項目がJSONに含まれていなかった場合にエラーとなります。

4. レビューの無限ループに陥る

サンプルコードのReviewerは、生成されたコードを見ただけで承認か否認について判断します。実は既に品質として十分なコードができていても、承認しない可能性があり、その場合には永遠にレビューが終わらない無限ループに入ってしまいます。逆に、コードの品質は不十分ですが、承認してしまうケースもあり得ます。この点は、コードレビューに限らず、モデルが判断をするようなLLM as a Judgeというユースケースで課題として研究が進められており、すぐに解があるものではありません。

無限ループを防ぐには、モデルだけに判断を任せるだけでなく、例えばコード実行した際にエラーが発生しないか、すべての自動テストを通るか、などの明確な基準を設けるのも有効と考えられます。

エージェントの会話終了後にメッセージを処理する

エージェント間のメッセージのやり取りをあとで使いたいというケースはいくつか考えられます。例えば今回のサンプルコードであれば、生成したコードをprintするだけでなくて、ファイルに出力したいというケースが考えられます。また、上記の通りレビューが無限ループに陥ってしまった場合もそうですが、エージェント間でどのようなメッセージがやり取りされたのか分析したいケースもあります。

AutoGenでは標準でログ出力がサポートされていますが、ログをパースして各メッセージを確認するのは少しツラいものがあります。今回は、ClosureAgentという機能を使って、Agent Runtimeでの会話終了後にメッセージを取り出し、処理する方法を紹介します。

実装例: 生成されたコードを取り出してファイル出力

ClosureAgentを使う場合、以下のモジュールをインポートします。

from autogen_core import ClosureAgent, ClosureContext

main関数の冒頭に以下のようにqueueを追加して、各メッセージを格納できるようにします。

async def main() -> None:
    queue = asyncio.Queue[CodeWritingTask | CodeWritingResult | CodeReviewTask | CodeReviewResult]()

    async def output_result(_ctx: ClosureContext, message: CodeWritingTask | CodeWritingResult | CodeReviewTask | CodeReviewResult, ctx: MessageContext) -> None:
        await queue.put(message)

    runtime = SingleThreadedAgentRuntime()

    await ClosureAgent.register_closure(
        runtime, "output_result", output_result, subscriptions=lambda: [DefaultSubscription()]
    )

    model_client = OllamaChatCompletionClient(
        model="llama3.1:8b"
    )
     省略...

そして、Agent RuntimeとModel Clientの利用終了後に、以下のような処理でCodeWritingResultの内容をファイルに出力します。await model_client.close()の行までは、サンプルコードに含まれていた行です。

    # Keep processing messages until idle.
    await runtime.stop_when_idle()

    # Close the model client.
    await model_client.close()

    items = []  
    while not queue.empty():  
        item = await queue.get()
        items.append(item)

    for item in items:  
        if isinstance(item, CodeWritingResult): # メッセージがCodeWritingResultの場合
            output_data = {
                'session_id': item.session_id,
                'task': item.task,
                'code': item.code,
                'review': item.review,
            }
            with open(file='test.jsonl', mode='a', encoding='utf-8') as file: 
                file.write(json.dumps(output_data)+'\n')
        else: # メッセージのクラスが異なる場合
            print('This message is not the class: CodeWritingResult.') 

queueの中にメッセージが入っており、一つずつ処理できます。ここでは最終結果を見たいだけなのでCodeWritingResultしか扱っていませんが、条件を変えれば、他のメッセージクラスを含めて、やり取りされたメッセージの内容をいろいろと確認・分析できます。

まとめ

今回は、AutoGen Coreを使ってエージェント間が会話する仕組みを、AutoGenのドキュメントで公開されているコード生成AIエージェントのサンプルコードから学んでみました。

AutoGen Coreは細かい処理を書いてエージェントを制御できるのですが、その分、コードも複雑になりがちです。サンプルコードを最初に見たときも内容をすぐには読み解けませんでしたが、AutoGen CoreにおけるAgent Runtime上でのメッセージのやり取りの仕組みを理解すれば、各エージェントの動きがわかり、コード全体の流れもわかりやすくなると感じました。今回のサンプルコードで学んだ仕組みをベースに、他の用途のAIエージェントも作ってみたいと思います。

今回の記事が、AutoGen Coreを使ってAIエージェントを作りたい場合の参考になれば幸いです。

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?