~ROS2でAIagentを動かす~マルチAIエージェントによる複数ロボットを協調作業させてみた

5つのエージェントが1つの仕事を分担する2分間。アームが部品を荷台へ積み、運搬車が畝間レーンの納品地点へ運び、地上ロボットが届いたかを確かめます

こんにちは、竹内 です。

上の動画が今回作ったものです。1つの畑に形の違うロボットを3台と、動かないアームを1本置いて、5つのエージェントに1つの仕事をさせています。

今回はマルチAIagentが複数ロボットを強調させることを目指していきます。

具体的なタスクとしては「作業台の青い部品を、畑の畝間レーンへ届ける」です。

この作業を分担して複数ロボットでアームロボットで掴んで、ドローンで上から画像を撮影したり、Lidar搭載のロボットでマップの作成を行っていきます!

5つのプロセスで1つの仕事をする

編成はプロセス5つで、管理者が1つ、移動担当が機体ごとに3つ、アームの把持担当が1つという内訳です。

それぞれ別の ROS 2 ノードとして立ち上がり、エージェント同士は rai_interfaces/msg/HRIMessage で会話します。

人が日本語で頼む 管理者(1プロセス) tool は委譲と zone_status だけ。機体は動かせない 移動担当 ground 速度指令 drive_distance 移動担当 air 速度指令 change_altitude 移動担当 cargo Nav2 navigate_to_pose 把持担当 pick_object / place_on_tray 差動二輪 ドローン(真下カメラ) 装軌ダンプ 6軸アーム(固定) エージェント同士の会話は DDS のトピック。外から ros2 topic echo で覗ける
図1:管理者は機体を直接動かさず、担当に頼むだけ

会話が DDS のトピックなので中身は外から覗けて、誰が誰に何を頼んだかはログを読まなくても追えます。

ros2 topic echo /cargo/motion/from_human   # 運搬車の担当が受けた依頼
ros2 topic echo /manipulation/to_human     # アームの担当が返した報告

ここでいう tool は、LLM が呼べる関数のことです。誰にどの tool を渡すかで、そのエージェントにできることが決まります

管理者に渡したのは機体を動かす tool ではなく、delegate_to_cargo_motion のような委譲の tool と、予約表を読む zone_status の2種類だけです。

移動担当の tool は機体ごとに違っていて、Nav2 が立っているのは運搬車だけです。運搬車には行き先を渡し、地上ロボットとドローンは速度指令で動かします。

複数のロボットを同時に動かす tool を足す

4台を同時に動かしたいところですが、最初のテイクはどう頼んでも1台ずつになりました。

原因は tool の呼び方でした。laggraphで構成したマルチAIagentは管理者は tool を1件呼ぶと、その結果が返るまで次に進めませんし、委譲の tool が返るのは頼んだ相手が動き終わったあとです。

つまり「地上ロボットに頼む」を呼んだ時点で、その1台が走り終わるまで残りの3台は待たされます。動いている機体は常に1台だけでした。

実測でも、「3台同時に」と明示した命令で運搬車、87秒後にドローン、地上ロボットは最後まで放置、という順になりました。

1ターンに複数の tool を並べて呼ぶこと自体はできますが、やるかどうかはモデル次第で、人格に「別の場所の仕事は同じターンで頼め」と書いても守られませんでした。

そこで複数のロボットを同時に動かす tool を足しました。delegate_together は受け取った仕事をスレッドに分けて同時に投げ、全部の報告が揃ってから返します。

管理者  delegate_together {"jobs": [
  {"agent": "ground_motion",  "task": "furrow_lane_7 を予約して (2.4, -7.0) へ入り…"},
  {"agent": "air_motion",     "task": "高度3mのまま同じ場所を上空から撮って報告…"},
  {"agent": "cargo_motion",   "task": "レーン入口には近づかず積み込み位置へ戻る…"},
  {"agent": "manipulation",   "task": "オレンジの部品を作業台の上へ戻す…"}]}

→ Tool delegate_together completed in 301.40 seconds.   # 4件の報告がまとめて返る

動画の後半はこの1回の呼び出しで、地上ロボットがレーンへ入り、ドローンが上から撮り、運搬車が作業台へ戻り、アームが次の部品を掴んでいます。

管理者がやるのはこの tool を1回呼ぶことだけで、同時に動かすかどうかはモデルの判断からこちら側のスレッドの話に移りました。

1台しか入れない場所は、予約表で捌く

レーンに入れるのは物理的に1台まででで

Nav2 の障害物回避も行いましたが避けるにはよけるスペースが要るので、一本道のレーンでは相手が見えても待避もすれ違いもできません。

Nav2は静的障害物に対する。回避はできますが、時系列で障害物が動く場合などはそのままではうまくいきません。

なので、名前をつけた区間を一台ずつ予約するツールを設定して、そこに入れるかどうかを設定することにしました。

tool は reserve_zonerelease_zonezone_status の3つだけです。取れなかった側には、誰が持っているかと「入るな」が返ります。

同時に頼まれても、勝つのは必ず1台 地上ロボットの担当 reserve_zone(furrow_lane_7) 運搬車の担当 reserve_zone(furrow_lane_7) 予約表 furrow_lane_7 同時に来ても、持てるのは1台だけ Reserved furrow_lane_7. 入っていい。出たら release_zone held by ground_motion, 12 s ago Do not drive in anyway. 管理者は zone_status で盤面を読むだけ。自分では取らない
図2:狭い一本道は、入る前に1台だけが取る

管理者に渡したのは読む方の zone_status だけで、取る tool はありません。自分では区画に入らないので、握ったまま忘れる詰み方が増えるからです。

協調は、tool が使えるかの判断から始まる

協調動作は全てのツールがそのまま使えるわけではない状況によってはその動作ができないケースが生まれます。

なので、tool が使えるかどうかは、実行エージェント自身に確かめさせます。条件を満たしていなければ、何がどれだけ足りないかが返るような設計にしました。

足りない数字を読んだエージェントが、それを埋められる相手に頼みます。協調が始まるのはここからです。

例えば、アームが荷台に荷物を乗せる場合は、荷台がアームが届く範囲の外ならアームは1ミリも動きません。代わりに返ってくるのは「何メートル寄れ」という数字です。

radius = math.hypot(x - base[0], y - base[1])
if radius > _TRAY_MAX_R:            # 0.70 m
    return (f'The cargo bed of {target_fleet} is {radius:.2f} m from the arm '
            f'base, too far to load safely. Ask {target_fleet} to come '
            f'{radius - _TRAY_MAX_R + 0.05:.2f} m closer, then try again. '
            f'Nothing was attempted.')

この tool を渡した状態で仕事を頼むと、こうなりました。管理者への指示は「アームが遠いと言ったら、その距離を運転側に伝えて」の一文だけです。

把持担当  place_on_tray {"fleet":"cargo"}
       → The cargo bed of cargo is 0.92 m from the arm base, too far to load
         safely. Ask cargo to come 0.27 m closer ... Nothing was attempted.

管理者   → 移動担当(cargo)「東向きのまま 0.27 m 前進して、荷台をアームに近づけて」
移動担当  drive_distance {"distance":0.27} → Moved forward 0.288 m

把持担当  place_on_tray {"fleet":"cargo"}
       → Cargo bed of cargo is at (1.369, -2.500), 0.63 m from the arm base.
         Placed part_cube at x=1.371, y=-2.501, z=0.297, as asked.
① tool が断る 荷台は 0.92 m、遠すぎる アームは動かさない ② 数字を返す 「0.27 m 寄って」 0.92 − 0.70 + 余裕 0.05 ③ 管理者が中継 運搬車の担当へ委譲 0.288 m 前進した ④ 積める 0.63 m = 範囲内 z=0.297 に着地 手順表は書いていない。断り文に数字が入っているだけ 距離を返さず「無理です」とだけ答える tool にすると、ここで止まる 管理者は次に何を頼めばいいのか分からず、同じ依頼を繰り返す
図3:断り方に数字を入れておくと、それだけで交渉になる

断る tool に距離を持たせておくと、手順を書かなくても「届かない→寄る→積む」が出てきます。

動画のテイクでは交渉は起きていません。運搬車が Nav2 で積み込み位置へ入り、place_on_tray が測った距離は 0.70 m、範囲のちょうど端でした。

仕事がうまくいっているか他のロボットに確かめさせる

次に、仕事がうまくいっているかを他のロボットに確かめさせました

実際に作業をしていくと、荷台から荷物が落ちたり、逆にうまく降ろせていないといったことが起きていました。

カメラの無いこの機体には分かりません。確かめるのは、あとから入る地上ロボットとドローンの仕事になります。

まとめ

4台を1つの仕事で噛み合わせるのに、手順表は書きませんでした。用意したのは次の3つです。

  • 届かない tool は、何メートル足りないかを返して何もしない
  • 1台しか入れない場所は、名前をつけて予約表で捌く
  • 同時に動かすのは、モデルに守らせず tool 側で並列にする

この3つで、届かない距離を測る担当と、その距離だけ動く担当が噛み合いました。誰がどう動くかは、こちらでは決めていません

新規CTA