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に投げて
「うちのスタックちゃんをこうして」
といえばしてくれるかもしれない
※自己責任でどうぞ※
