見出し画像

AIに身体をつける第二段階―― Stack-chanとHermes Agentを「同じ会話」に接続するまで


前回の記事が「Hermes AgentとStack-chanを接続し、喋らせるところまで」今回はその続編として、技術記録兼手順書としてまとめる形で残す

ウルトラながいす ご注意を
ど素人が一歩ずつなので進行は遅めです
chatGPTさんに頼みつつ記事化しております



【まえがき】

前回、M5Stack CoreS3で動くStack-chanとHermes Agentを接続し、AIの返答をStack-chanから発声させるところまで到達した。

構成は概ねこうだった。

Stack-chan
    ↓
CoreS3
    ↓
Hermes Gateway
    ↓
Hermes Agent
    ↓
回答
    ↓
CoreS3
    ↓
VOICEVOX
    ↓
発声+首振り

最初は、

とりあえずKITTの返答をロボットから喋らせられれば第一段階成功

くらいに考えていた。

実際、これは成功した。

CoreS3からHermes Gatewayへ問い合わせを投げると、Hermes Agentが返答し、それをStack-chanがVOICEVOXで読み上げる。

サーボも動く。

さらに試してみると、Hermes Agentのファイル操作やブラウザ操作などのツールまでCoreS3経由から起動できることも分かった。

ここまで来た時点で、

Stack-chanは単なる外部スピーカーではなく、Hermes Agentへの物理インターフェースになり始めている

と考えられるようになった。

しかし、その翌日。

もう少し細かくログを追ってみたところ、重要な問題に気付いた。

Stack-chanから一回話しかけるたびに、Hermes上で別のセッションが作られている。

今回はその問題を追いかけ、最終的に

Stack-chan専用の固定会話セッションを作り、Hermes DesktopとCoreS3の両方から同じ会話を継続する

ところまで到達した記録である。


1.最初の違和感――なぜ会話が別々の窓に増える?

Stack-chanへの接続確認を何度か行っていたところ、Hermes Desktopの左側に、

  • OpenAI公式サイト検索

  • 利用可能ツール確認

  • 天気

  • 名前確認

  • 接続テスト

などが別々のセッションとして残っていることに気付いた。

最初は、

Stack-chan用の一つの会話が表示上だけ分かれているのかな?

程度に思っていた。

しかし、各セッションにはそれぞれ別のIDが付いていた。

例えば、

api-98c9ec10310432b2

や、

api-5276b7173741a9a6

などである。

OpenAIの公式サイトを検索しようとしたセッションと、利用可能ツールを確認したセッションでIDが違う。

これは単なる表示名ではなく、

Hermes側では別の会話セッションとして扱われている可能性が高い

ということになる。

そこで、この挙動を意図的に調べることにした。


2.A・B・Cを送ってみる

まず条件をなるべく単純にした。

HermesOneなど別クライアントは一旦停止。

Hermes Gatewayを起動。

Stack-chan側からPowerShellを使って、内容の違う短い質問を三回送る。

使用したのはこのようなコマンド。

curl.exe "http://192.168.11.5/chat?text=セッション確認Aです。Aとだけ答えてください"

次に、

curl.exe "http://192.168.11.5/chat?text=セッション確認Bです。Bとだけ答えてください"

さらに、

curl.exe "http://192.168.11.5/chat?text=セッション確認Cです。Cとだけ答えてください"

結果は非常に分かりやすかった。

A → api-f73f282303f12ec7

B → api-b786b21ecc5c35b9

C → api-4f049f181e419255

全部違う。

しかもHermes Desktop上でも、毎回新しい会話窓が作られた。

ここでかなり確信が持てた。

現在のStack-chanからHermesへの通信は、

Stack-chan
   ↓
質問A
   ↓
新規セッション

Stack-chan
   ↓
質問B
   ↓
また新規セッション

Stack-chan
   ↓
質問C
   ↓
さらに新規セッション

という動きをしている。

つまり、

発話するたびに別の頭へ話しかけているような状態

だった。


3.なぜ毎回新しいセッションになるのか

前回、Stack-chanからHermesへ送っているリクエストのMonitorログを確認していた。

送信JSONは概ね、

{
  "model": "hermes-agent",
  "messages": [
    {
      "role": "system",
      "content": "You are an AI robot named Stack-chan. Please speak in Japanese."
    },
    {
      "role": "user",
      "content": "こんにちは"
    }
  ],
  "stream": false
}

という形である。

Hermes GatewayのOpenAI互換API、

/v1/chat/completions

へPOSTしている。

しかし、このリクエストには、

どのHermesセッションを継続するのか

という情報が入っていなかった。

Hermes側には、

X-Hermes-Session-Id

というHTTPヘッダーがあり、これを指定すると同じSession IDで会話を継続できる。

逆にこれが無ければ、API呼び出しごとに新しいセッションが作られる。

今回のA/B/C実験は、その挙動をほぼそのまま観察していたことになる。


4.Stack-chan側のどこを変更すればいい?

ここからコードを追った。

AI_StackChan_ExのプロジェクトをVS Codeで開き、

customEndpoint

を検索する。

すると、

firmware/src/llm/ChatGPT/ChatGPT.cpp

に、OpenAI互換エンドポイントへ送信する処理が見つかった。

その中に、

ret = post_json(customEndpoint.c_str(), json_string.c_str(), ca);

という記述がある。

つまり、

ChatGPT.cpp
   ↓
customEndpoint
   ↓
post_json()
   ↓
Hermes Gateway

という流れ。

さらに、

post_json(

を検索すると、実際のHTTP送信処理が見つかった。

該当部分は、

String ChatGPT::post_json(const char* url, const char* json_string, const char* root_ca) {

で始まる関数。

中では、

HTTPClient http;

を使って、

http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", String("Bearer ") + param.api_key);

としてから、

http.POST(...)

を実行していた。

つまり、

Session IDを追加するならここ

である。


5.ただし無条件で追加してはいけない

ここで少し注意が必要だった。

この post_json() はHermes専用ではない。

通常のOpenAI APIへの接続でも使われている。

そのため、

http.addHeader("X-Hermes-Session-Id", "stackchan-body");

を無条件で追加すると、通常のOpenAI接続にもHermes専用ヘッダーを送ってしまう。

そこで今回は、

Hermes Gatewayのポート8642へ接続する場合だけ追加する

という最小変更にした。

実装はWorkモードへ依頼した。

今回のようなコード編集、差分確認、Buildまでを人間が全部手作業でやるより、

設計と切り分けはこちら
実装とBuildはWork

と分けた方が安全だと、ようやく学んだ。

実際の追加は概ね、

http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", String("Bearer ") + param.api_key);

if (strstr(url, ":8642/") != nullptr) {
  http.addHeader("X-Hermes-Session-Id", "stackchan-body");
}

int httpCode = http.POST((uint8_t*)json_string, strlen(json_string));

というもの。

ポイントは、

:8642/

を含む場合だけ、

X-Hermes-Session-Id: stackchan-body

を付けるところ。

これにより、

Hermes Gateway
→ Session固定

通常OpenAI
→ 今まで通り

という形を保つ。

今回のSession IDは、

stackchan-body

とした。

意味としてはそのまま、

Stack-chanという身体のための会話セッション

である。


6.Buildは成功

Workに依頼した条件はかなり限定した。

変更対象は、

firmware/src/llm/ChatGPT/ChatGPT.cpp

のみ。

さらに、

  • YAMLは変更しない

  • SG90設定を触らない

  • GPIOを触らない

  • PWMを触らない

  • サーボ設定を触らない

  • Buildのみ

  • Uploadはしない

という条件を付けた。

結果、

Build成功
exit code 0

となった。

既存のArduinoJson等の警告は残っているが、今回の変更によるエラーはなし。

そこで初めて実機へUploadした。


7.再びA・B・Cテスト

Upload後、Gatewayを起動。

再び同じA/B/Cを送る。

今度の結果は、

A → stackchan-body
B → stackchan-body
C → stackchan-body

だった。

しかもHermes Desktop上でも、

A・B・Cが一つの会話窓の中へ順番に並んだ。

変更前は、

A → api-f73f...
B → api-b786...
C → api-4f049...

だったものが、

変更後は、

A ─┐
B ─┼→ stackchan-body
C ─┘

になった。

ここでSession ID固定そのものは成功した。


8.ただ同じIDなだけでは意味がない

しかし、IDが同じになっただけでは十分ではない。

本当に確認したいのは、

会話の文脈が継続しているか

である。

そこで新しいテストを行った。

Stack-chan側から、

このセッションだけのテスト合言葉は黒い鍵です。覚えてください

と送る。

Hermesは、

了解しました。このセッションの合言葉は「黒い鍵」ですね。記憶しました。

と返答。

次に別のHTTPリクエストとして、

今教えた、このセッションだけのテスト合言葉は何ですか?

と送る。

返答は、

このセッションのテスト合言葉は「黒い鍵」です。

だった。

これで、

HTTPリクエスト1
「黒い鍵を覚えて」

      ↓

stackchan-body
      ↓
会話履歴保持

      ↓

HTTPリクエスト2
「さっきの合言葉は?」

      ↓

「黒い鍵」

という流れが成立した。

つまり、

CoreS3から独立して送った複数のHTTPリクエストを、Hermesが一つの連続した会話として扱える

ことが実証できた。


9.さらにHermes Desktopから同じセッションへ入る

次の実験はもっと面白かった。

Hermes Desktop上で、

stackchan-body

を直接開く。

そこには先ほどの、

A
B
C
黒い鍵

の会話が残っている。

ここへDesktopから直接、

セッション確認Dです。Dとだけ答えてください

と入力してみた。

返答は、

D

だった。

つまり、

CoreS3 / Stack-chan
       │
       │
       ▼
  stackchan-body
       ▲
       │
Hermes Desktop

という構造が成立した。

これはかなり大きい。

Stack-chanとHermes Desktopが、同じHermes会話セッションを共有できるようになった。


10.ここまでで何ができたのか

今回の成果を整理すると、

まず最初は、

CoreS3
 ↓
質問
 ↓
Hermes
 ↓
毎回新しいapiセッション

だった。

つまりStack-chanは喋れるが、会話の連続性がなかった。

それを、

CoreS3
 ↓
X-Hermes-Session-Id
 ↓
stackchan-body
 ↓
Hermes Agent

へ変更。

結果、

Stack-chan
   ↓
stackchan-body
   ↑
Hermes Desktop

という共有会話が成立した。

さらに、

A
 ↓
B
 ↓
C
 ↓
黒い鍵
 ↓
D

と、

CoreS3とDesktopの両方から同じ会話を続けられる

ところまで確認した。


11.第一段階と第二段階

ここで一度、身体接続実験の段階を整理してみる。

第一段階

Hermes Agentの返答をStack-chanから発声させる。

CoreS3
 ↓
Hermes Gateway
 ↓
Hermes Agent
 ↓
回答
 ↓
CoreS3
 ↓
VOICEVOX
 ↓
発声+首振り

これは前回突破。


第二段階

Stack-chan専用の継続Sessionを作り、Desktopと身体から同じ会話へ入る。

                 stackchan-body
                /              \
      CoreS3 / Stack-chan     Hermes Desktop

今回ここを突破した。

単に喋るだけではなく、

身体側にも「続きの会話」ができるようになった

という違いが大きい。


12.今回分かったもう一つのこと

今回の実験中、Hermes Agent本人に、

今のメッセージがDesktopから来たのか、Stack-chanから来たのか分かりますか?

と聞いてみた。

答えは、

分からない

だった。

Hermes Agentから見ると、

Desktop入力

も、

Gateway経由のCoreS3入力

も、最終的には、

role: user
content: "..."

として入ってくる。

つまりKITT自身には、

今の言葉はキーボードから来た
今の言葉は身体から来た

という入力経路の感覚はまだない。

これは不具合というより、現在の設計上そうなっている。

将来的には、

source: desktop
input_mode: text

や、

source: stackchan
input_mode: voice

のような情報を付ければ、

KITT自身が「どこから入力されたか」を認識する

構造も作れるかもしれない。

ただ、今はまだそこまでやらない。


13.そして、まだ喋れない方向がある

今回、

Stack-chan
 ↓
stackchan-body
 ↓
Hermes
 ↓
返答
 ↓
Stack-chan発声

は完成した。

さらに、

Hermes Desktop
 ↓
stackchan-body
 ↓
Hermes
 ↓
返答

も完成した。

しかし、

Hermes Desktop
 ↓
stackchan-body
 ↓
返答
 ↓
Stack-chan発声

はまだできない。

理由は単純である。

Stack-chanから質問した場合は、

CoreS3
 ↓
HTTP request
 ↓
Hermes
 ↓
HTTP response
 ↓
CoreS3

という往復なので、CoreS3は返答を受け取れる。

だがDesktopから質問した場合、

Desktop
 ↓
Hermes
 ↓
Desktopへ返答

で終わる。

CoreS3には、

新しいassistantの返答が生成された

という通知が来ない。

つまり次の問題は、

Hermesから身体へ返答を配送する経路

である。


14.第三段階候補――KITTから身体を使う

次にやりたいのは、

Hermes Desktop
      ↓
stackchan-body
      ↓
KITT
      ↓
返答
      ↓
CoreS3
      ↓
VOICEVOX
      ↓
Stack-chanが喋る

という逆方向の経路。

ここまで行けば、

「身体から話しかけた時だけ喋る」のではなく、KITT側から身体を使える

ようになる。

さらに発展させれば、

KITT
 ├─ speech: "こんにちは"
 ├─ motion: "nod"
 ├─ expression: "happy"
 └─ look: "left"
        ↓
      CoreS3

のように、

音声だけでなく身体動作そのものをHermes Agentから指定する

こともできる。

ここまで来るとStack-chanは、

単なる外部スピーカーでも、

単なるHTTPクライアントでもなく、

Hermes Agentの物理I/O

にかなり近くなる。


15.ここから先は、一度設計を考える

第三段階にはいくつか方法が考えられる。

例えば、

  • CoreS3がHermes Sessionを定期的にpollingする

  • Hermes側からCoreS3へHTTP POSTする

  • WebSocketのような常時接続を使う

  • Hermesの既存Messaging/Gateway機構を利用する

  • KITTへStack-chan専用ツールを渡し、必要な時だけ喋らせる

など。

ただし、ここは今までより設計上の選択肢が多い。

だから次は、

何が実装できるか

より先に、

どの方向がKITTという構造に一番合うか

を考えたい。

個人的には、

Hermesの全回答を無条件でStack-chanへミラーする

よりも、

KITTが必要だと思った時だけ身体を使う

構造の方が面白いと思っている。

例えば、

通常返信
→ Desktopだけ

speak("ちょっと聞いて")
→ Stack-chanが喋る

nod()
→ 頷く

look_left()
→ 左を見る

とする。

そうすればStack-chanは単なる音声出力装置ではなく、

KITTが選択して使う身体

になる。


16.今回の変更内容まとめ

後から自分で再現できるように、今回の技術変更だけ簡潔に残しておく。

対象プロジェクト:

AI_StackChan_Ex

対象ファイル:

firmware/src/llm/ChatGPT/ChatGPT.cpp

対象関数:

String ChatGPT::post_json(...)

追加した処理:

if (strstr(url, ":8642/") != nullptr) {
  http.addHeader("X-Hermes-Session-Id", "stackchan-body");
}

追加位置は、

http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", String("Bearer ") + param.api_key);

の直後。

その後、

http.POST(...)

を行う。

Hermes Gateway:

http://192.168.11.2:8642/v1/chat/completions

固定Session ID:

stackchan-body

確認方法:

curl.exe "http://192.168.11.5/chat?text=セッション確認Aです。Aとだけ答えてください"

A/B/Cを送って、Hermes Desktop上ですべて、

stackchan-body

へ入れば成功。

さらに、

黒い鍵を覚えて

の後に別リクエストで、

さっきの合言葉は?

と聞き、

黒い鍵

と答えれば会話継続も成功。


【現時点の構成】

2026年8月13日時点。

                ┌──────────────┐
                │ Hermes Agent │
                └──────┬───────┘
                       │
                stackchan-body
                  ▲         ▲
                  │         │
          Hermes Desktop   CoreS3
                             │
                             ▼
                          Stack-chan
                          ├─ 発声
                          └─ 首振り

入力はDesktopからでも身体からでも同じSessionへ入る。

ただし出力はまだ、

CoreS3から質問した時

だけ身体へ戻る。

第三段階では、

Desktopからの回答も身体へ配送する

経路を作る予定。


【あとがき】

最初は、

Stack-chanからKITTの声が出れば面白い

くらいだった。

ところが実際に作ってみると、

「声が出るか」

より、

「その声は昨日の会話を覚えているのか」

の方が重要になった。

そして調べてみると、Stack-chanは一回話すたびに別セッションへ飛んでいた。

なるほど。

それでは身体というより、

毎回別のAIへ電話をかけているようなものだ。

そこで今回、

stackchan-body

という一つの会話を与えた。

するとAの次にBを覚え、

Bの次にCを知り、

「黒い鍵」を覚え、

さらにDesktopから入ったDまで同じ流れの中で扱えるようになった。

ここでようやく、

身体側にも一つの時間の流れができた

ように見える。

もちろん、これはまだ会話Sessionという技術的な仕組みにすぎない。

だがAIに身体を接続していく過程では、

スピーカーやサーボより先に、

「同じ会話の続きを生きているか」

という問題が出てくるらしい。

第一段階は、

声が出た。

第二段階は、

会話が続いた。

次の第三段階では、

KITT自身が身体を使って話す

ところへ進んでみたい。

まだ先は長い。

でも、昨日まで毎回別のセッションから返事をしていたStack-chanが、今日はちゃんと一つの stackchan-body の中で、

「黒い鍵」

を覚えていた。

ひとまず第二段階突破、として記録しておこうと思う。

Stack-chanとHermes Desktopが、同じHermes会話セッションを共有できるようになった。
これものすごい大変でしたw
好き勝手にセッション作りまくるスタックチゃンの暴れ馬っぷりを
まず何をやっているのかということから調べ
ようやくここまで
まあほとんど

chatGPT様Work様のおかげ

なんですがまあできたのでヨシ
このシリーズ割と見る方多いみたいで
何かの役に立てば幸いです
この記事をchatGPTに投げて
「うちのスタックちゃんをこうして」
といえばしてくれるかもしれない
※自己責任でどうぞ※


いいなと思ったら応援しよう!