AIエージェントでROS2を動作させてみた〜1.ROS2とは〜

こんにちは、竹内 です。

ロボットAIエージェントを作るために、まずは基本になる ROS 2 を勉強してみました。

作ろうとしているのは「自然言語で指示すると自律的に動くロボット」です。LLM に「2メートル前進して」と伝えると、シミュレータの中のロボットが実際に走り出す——そんな構成を目指しています。

この手の話は「どの LLM を使うか」「プロンプトをどう書くか」に目が行きがちなんですが、実際に手を動かしてみて分かったのは、ハマるところはほとんど LLM の外側にあるということでした。

LLM が出した「前進せよ」という判断を、どうやってモータを回す信号まで届けるのか。その配管を担っているのが ROS 2 です。

今回はその第1回として、AIエージェントを載せる土台として ROS 2 の何を分かっていればいいのかを整理してみます。まずは、やりたいことの全体像から。

「2メートル前進して」 あなた AIエージェント LLM が命令を解釈し 速度の指令に変換する ここを自分で作る /cmd_vel ROS 2 のトピック ロボットが動く
図1:自然言語の指示が、ROS 2 のトピックを通ってロボットの車輪まで届く。この配管全部が ROS 2 の仕事

ROS 2 は「ロボットの OS」ではない

名前が Robot Operating System なので、私も最初は OS の一種かと思っていました。でも違うんです。Linux や macOS の上で動くソフトウェア群で、一言でいえば「分散プロセス間通信のミドルウェア+共通規約+ツール群」です。

ROS 2 が用意してくれるのは、大きく次の3つ。

  • 通信機構:プロセス間・マシン間でメッセージをやり取りする仕組み
  • 共通の型と名前geometry_msgs/msg/Twist のような標準メッセージ型と、/cmd_vel のような慣習的なトピック名
  • ツール群ros2 CLI、RViz(可視化)、rosbag(記録・再生)。通信を覗く口が最初から共通

「ロボットを動かすフレームワーク」というより、「ロボットを構成するたくさんのプログラムを、共通の作法で喋らせるための約束事」。そう捉え直したら、AIエージェントをどこに置けばいいのかもすんなり決まりました。

最小単位の「ノード」は、互いに独立している

ROS 2 の最小単位は ノード(node) で、その実体はひとつの OS プロセスです。ロボットの機能ごとにノードを立てて、それをメッセージで繋いで1台のロボットにしていきます。

カメラノード 認識ノード 制御ノード ドライバノード 別プロセス 別プロセス 別プロセス 別プロセス /image /detections /cmd_vel
図2:ノードは別プロセス。名前付きのトピックだけで繋がる

この「独立している」という性質が、ROS 2 の設計思想のほぼすべてだと思っています。効いてくるのは次の3点です。

  1. 1つ落ちても他は生き続ける。 認識ノードがクラッシュしても、制御ノードは動き続けます。
  2. 起動順を気にしなくていい。 送る側と受け取る側、どちらが先に立ち上がっても大丈夫。相手がまだ居なければ、メッセージがどこにも届かないだけでエラーにはなりません。
  3. 別マシンでも、別言語でもいい。 同じ LAN・同じ ROS_DOMAIN_ID なら、コードを1行も変えずにノードを別 PC へ移せます。Python(rclpy)のノードと C++(rclcpp)のノードが、同じトピックで普通に会話します。

ノード同士は関数呼び出しではなくメッセージだけで繋がるので、片方を丸ごと差し替えても、もう片方は気づきません。私のシステムでロボットを二輪車からドローンに変えても、上流のAIエージェントが無改造で済んだのは、まさにこの性質のおかげでした。

繋ぎ方は4種類。図で見るのが早い

ノード同士を繋ぐ手段は4種類あります。ここを選び間違えると後から構造ごと作り直しになるので、最初に押さえておく価値ありです。文章で説明すると長くなるので、ひとつずつ図にしてみました。

① トピック — 投げっぱなしで流し続ける

トピック(名前付きの通り道) LiDARノード 発行(Publish) /scan 障害物検知ノード 記録ノード 購読(Subscribe) 購読(Subscribe) 発行側は、誰が受け取っているかを知らない。返事も来ない
図3:トピック — 多対多。名前と型さえ合えば勝手に繋がる

いちばん使うのがこれです。センサの値やロボットへの速度指令のように、ずっと流れ続けるものはだいたいトピックになります。届いたかどうかは分かりませんが、その代わり速くて軽い。

② サービス — 一問一答

呼ぶ側 応える側 ① 要求「この設定に変えて」 ② 応答「変えました」 応答が返るまで待つ。途中でやめることはできない
図4:サービス — すぐ終わる一往復のやり取り向け

設定の変更や状態の問い合わせなど、すぐ返事が来るもの向けです。逆にいうと、数十秒かかる処理をサービスでやると待たされっぱなしで詰みます。

③ アクション — 時間がかかって、途中でやめたいもの

頼む側 やる側 ① 目標を送る ② 途中経過が届く ③ 結果が届く ④ 中断できる 「(x=5, y=3) まで行って」 「残り 3.2m」「残り 1.8m」…と繰り返し 「到達」または「失敗(経路なし)」 いつでも送れる
図5:アクション — 「途中経過」と「キャンセル」が最初から付いてくる

サービスとの決定的な違いが ②途中経過④中断です。

代表例が Nav2 の NavigateToPose。「あの座標まで行って」は数十秒かかるし、残り距離は知りたいし、障害物で詰まったらやめたい。まさにアクションの出番です。

④ パラメータ — ノードが持つ設定値

制御ノード max_speed use_sim_time 0.5 true ros2 param set / get 外から読み書き 再起動せずに 値を変えられる
図6:パラメータ — ノードごとの設定値。メッセージの通り道ではない

速度の上限や use_sim_time のような設定値です。データを流す通り道ではないので、ここにセンサ値を入れたりしないよう注意です。

迷ったときの目安。流れ続けるならトピック、一問一答ならサービス、時間がかかって中断したいならアクション、外から値を変えたいだけならパラメータ。

通信の中身は DDS。そして QoS でハマる

ROS 2 の通信は自前実装ではなく、DDS(Data Distribution Service) という産業標準の Pub/Sub ミドルウェアの上に乗っています。ROS 2 はその上に薄い抽象層をかぶせて、DDS 実装を差し替えられるようにしているんですね。

アプリ(自作ノード) rclpy / rclcpp rcl rmw DDS 実装 UDP マルチキャスト AIエージェントもここに置く 言語ごとのクライアントライブラリ 共通の C 実装 差し替え可能な抽象層 CycloneDDS / FastDDS / … 同一 LAN 上で自動発見 ROS 2 本体 DDS(産業標準)
図7:自分が書くのは最上層だけ。下は既製品が面倒を見てくれる

各ノードは起動時に「自分はこのトピックをこの型で発行/購読します」とネットワークに広告して、条件が合う相手と勝手に繋がります。設定ファイルに相手の IP を書く必要はありません。ここは本当に楽です。

ただし ROS_DOMAIN_ID(0〜101、既定 0)だけは要注意。同じ LAN で複数人が ROS 2 を動かすとき、これを分けておかないと他人のノードと勝手に繋がります

そして、繋がる条件は名前だけではありません。型が一致しないと繋がりませんし、QoS(Quality of Service) という通信の性質設定も揃っている必要があります。

QoS では「再送してでも確実に届けるか(RELIABLE)/落ちても構わないか(BEST_EFFORT)」などをトピックごとに指定します。

⚠️ 最大の罠:QoS が合っていないと、名前も型も正しいのに繋がりません。

典型例はセンサ系です。カメラや LiDAR は BEST_EFFORT(落ちてもいいから低遅延)で発行されるのに、購読側を既定の RELIABLE のままにすると一切データが来ません。しかもエラーは出ず、ただ黙るんです。これ、原因に辿り着くまでけっこう時間を溶かしました……。

「トピック名は合っているのにデータが来ない」ときは、型か QoS を疑って ros2 topic info <名前> -v で両側を突き合わせるのが早いです。

この作り、何が嬉しいのか

「関数呼び出しではなく、名前付きのメッセージで繋ぐ」ことの見返りは3つありました。

  • 差し替えが効く。 シミュレータを実機ドライバに交換しても、/cmd_vel を出す側は無改造。私のシステムでロボット4種(二輪車/ドローン/メカナム/クローラ)を上流そのままで切り替えられるのも同じ理屈です。
  • 覗ける・記録できる・再生できる。 通信の口が共通なので、どのトピックでも同じコマンドで中身を見られます。自前でプロセス間通信を書いていたら、この道具は全部自作でした。
  • 割り込める。 手でメッセージを流せば、上流のノードを止めずに動作確認できます。「LLM が悪いのか、ロボット側が悪いのか」を切り分けるとき、これがとにかく効きました。

実際によく使っているのはこのあたりです。

ros2 node list                 # 動いているノード
ros2 topic list -t             # トピックと型
ros2 topic info /cmd_vel -v    # 発行者/購読者と QoS(詰まったらまずこれ)
ros2 topic echo /odom          # 中身を覗く

# 上流を止めずに、手で操縦してみる
ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.3}}'

AIエージェントは、ROS 2 から見れば「ただのノード1個」

ここまで分かると、冒頭の問い——LLM をどうやってロボットに繋ぐのか——の答えが出ます。

私が作ったAIエージェントのノードは、/agent/instruction(自然言語のテキスト)を購読して /cmd_vel(速度指令)を出す、ただそれだけのノードです。

中で HTTPS を叩いて LLM と会話していることは、他のノードからは見えませんし、知る必要もありません。

AI をノードの内側に閉じ込めて、外との境界は標準の型で保つ。これが設計の肝でした。おかげでロボットもシミュレータも LLM も、それぞれ独立して差し替えられます。

実際、LLM を使わないルールベース実装に切り替えても、下流は一切変わりませんでした。

そして「AIエージェントへの命令」も、ROS 2 から見ればただのメッセージ1本です。UI も LLM も無しで、この一行だけで配線を試せます。

ros2 topic pub --once /agent/instruction std_msgs/String \
  "{data: '少し前進してから左に90度回って'}"

まとめ

  • ROS 2 は OS ではなく、分散プロセス間通信のミドルウェア+共通規約+ツール群
  • ノードは互いに独立したプロセスで、名前と型が合うメッセージだけで繋がる。だから差し替えが効いて、覗けて、割り込める。
  • 繋ぎ方は4種類。流れ続けるならトピック/一問一答ならサービス/時間がかかって中断したいならアクション/値を変えたいだけならパラメータ
  • ハマるのはだいたい型の不一致・QoS の非互換・ROS_DOMAIN_ID の衝突。どれもエラーではなく「沈黙」で出てくるのが厄介。
  • AIエージェントは ROS 2 から見ればただのノード1個。知能はノードの内側に閉じ込める。

次回は、この上に乗せるシミュレータ(Gazebo)の話です。今回のエージェントは「言われた秒数だけ速度を出す」だけなので、目の前に壁があっても平気で突っ込んでいきます。

そこから「地図を持って、自分の位置を知って、障害物を避けて目的地へ行く」段階へどう進むのか——というところに入っていきます。お楽しみに!

新規CTA