技術約12分で読めます

音声チャットサーバーに長期記憶を入れたら週末の予定だけ思い出せなかった

いけさん目次

前回までで、検索した記憶に日付のスコアを足してプロンプトに入れる仕様は決まった。
ただ、あれは手書きした16件の記憶と10問の質問をPC上で試しただけで、会話から記憶を作る部分はまだ無かった。
そこで今回は、StackChanがつながっている音声チャットサーバーに長期記憶(ベクトル検索で関連記憶を取り出す仕組み)を組み込み、話した内容を後から聞き返せるかを音声で試してみた。

検証環境

項目内容
サーバーPC前回と同じ本番機(Windows 11 Home、AMD Ryzen 7 5800HS、RTX 3050 Ti Laptop 4GB、RAM16GB)
音声認識Qwen3-ASR-0.6B、CPU
チャットModelScope API-Inference、Qwen-Ambassador/Qwen3.8-Max、enable_thinking: false、Web検索あり
埋め込みQwen3-Embedding-0.6B、256次元、ローカルCPU、bf16(16ビット浮動小数点数)
ベクトル検索Qdrantローカルモード
音声合成Irodori-TTS、GPU

Omni-Flashに切り替えた記事のあと、サーバーは文字起こしを先にやる構成へ戻している。
Omni-Flashに音声を直接渡す構成だと、返答が始まる前に文字起こしのテキストが得られないので、記憶を検索する文が作れない。長期記憶はこの文字起こしを先にやる構成でだけ動かしている。

チャットのモデルをQwen3.8-Maxに替えた

入れ替える前に、StackChanの対話キャラクター「かな」のプロンプトとWeb検索ありの設定で、6つの質問を3.7-Plusと3.8-Maxに1回ずつ投げた。

質問3.7-Plus 初トークン(最初の1文字) / 完了3.8-Max 初トークン(最初の1文字) / 完了
ただいまー、今日めっちゃ疲れた2.3秒 / 2.6秒1.5秒 / 1.9秒
財布なくしちゃった…1.5秒 / 2.0秒1.7秒 / 2.3秒
明日の東京の天気は?(Web検索あり)2.9秒 / 3.5秒1.8秒 / 2.3秒
週末一緒にゲームしない?1.7秒 / 2.2秒1.9秒 / 2.5秒
また夜更かししちゃった1.9秒 / 2.2秒1.8秒 / 2.5秒
日本で一番高い山は?3.7秒 / 4.1秒2.1秒 / 2.8秒

初トークンまでは6問中4問で3.8-Maxの方が速く、天気は1.1秒、富士山は1.6秒縮んだ。Web検索が走ったのは天気だけで、プロンプトが3327トークンまで増えている。各1回なので、ばらつきもあるため参考値にとどめている。

返答の文量や傾向を見るため、特徴の出た3問の返答を並べた。

質問3.7-Plus3.8-Max
財布なくしちゃった…えっ、それは大変!まずは警察に届け出を出して、カード類はすぐに停止手続きをしてねえっ、それは焦るよね、大丈夫?まずは今日行った場所とかに電話して聞いてみてね。
週末一緒にゲームしない?いいね、何やろうか。今からわくわくしてきたよいいね、今日は日曜日だから今からでもやろうよ。何のゲームする予定?
また夜更かししちゃったもう、またなの。今日は早めに寝てね。また夜更かししちゃったの、身体に悪いからちゃんと早く寝てよね。明日は日曜日だからって油断しすぎだよ。

返答は3.8-Maxの方が少し長い。
夜更かしの返答で、3.8-Maxは日曜日なのに「明日は日曜日」と言った。ゲームの方では「今日は日曜日」と合っているので、明日の曜日を自分で数えて間違えたように思える。
システムプロンプトには今日の日時しか入れていなかったので、明日の日付と曜日も入れるようにした。

組み込んだ流れ

1往復の処理は、文字起こしの直後に記憶の検索を1回挟む形にした。
記憶を作る処理は返答とは別のスレッドで動かし、会話が5分途切れるか、8往復たまったところで、それまでの会話ログをまとめてQwenに渡し、相手に関する事実文を抜き出すようにした。

flowchart TD
    A[録音を受け取る] --> B[無音判定]
    B --> C[Qwen3-ASRで文字起こし]
    C --> D[Qdrantで記憶を検索]
    D --> E[記憶と直近3往復を付けてQwen3.8-Maxへ]
    E --> F[1文ずつTTS]
    E -.会話ログをためる.-> G[5分無言か8往復で事実文を抽出]
    G --> H[重複を除いてQdrantへ保存]
項目値
短期の会話の流れ直近3往復。最後の発話から10分以内なら履歴としてプロンプトに入れる
記憶を作るタイミング会話が5分途切れたとき、または8往復たまったとき
重複の判定保存済みの記憶と類似度0.92以上なら保存しない
検索前回決めたとおり。素の類似度で上位8件、0.05 × exp(−経過日数/14) を足して0.45以上を上位3件まで
プロンプト形式前回の指示付き形式(B形式)。<retrieved_memory> で囲み、持ち出し禁止・新しい日付を優先・断定しない、の指示付き

短期の3往復と長期記憶は役割が異なり、10分以内の続きなら直前の発言履歴から答えられる。
長期記憶は、履歴から外れた話や日をまたいだ話題を参照するために使う。

事実文の抜き出し

抜き出しの指示は、相手の好み・習慣・予定・出来事などを対象にして、かな自身の発言、一般知識、天気やニュース、センサーの値、あいさつは除外した。
出力はJSONで、1件1文、30〜60字程度にしている。

主語は「あなた」に統一した。前回の10問テストで、記憶の主語が「ユーザー」だとかながそのまま「ユーザーくん」と呼んできたため、呼び名に使われても不自然でない言葉に揃えている。かなのシステムプロンプトでも、相手は名前で呼ばずに「あなた」と呼ぶ設定になっている。

主語を替えると検索スコアも変わるので、前回までの16件と10問で「ユーザー」と「あなた」を比べた。基準日は前回と同じ9月10日で、日付の加点も入れてある。

項目ユーザーあなた
10問の判定ヒット想定と矛盾の6問は通過、ヒットなし4問は全部弾く同じ
ヒット想定・矛盾側の1位のスコア0.475〜0.6480.524〜0.645
ヒットなし4問の最大値0.434(N1 今日の予定)0.446(N4 宝くじ)
宝くじの質問(N4)の最大値0.3890.446

10問の合否判定自体は変わらなかったが、ヒット想定の質問(H4のPython質問以外)のスコアが0.03前後上がったのと同様に、ヒットなしの宝くじ(N4)も0.389から0.446まで上がり、しきい値0.45まであと0.004に迫っていた。

「明日」「来週の土曜日」のような相対的な日付は、元の言い方を残したまま、具体的な日付を括弧で添えさせた。
例として「来週の土曜日(2026年10月3日)に」をそのまま指示に入れている。

埋め込みをbf16にした

埋め込みモデルはローカルCPUで常駐させるので、読み込みをfp32からbf16に替えた。
主語を「あなた」にした16件と10問で、fp32とbf16の挙動を比べてみた。

項目fp32bf16
モデル読み込みで増えたメモリ2381MB1212MB
16件と10問をまとめて埋め込む時間2.9秒15.6秒
質問1件の埋め込み0.18秒0.25秒
10問の判定全問同じ全問同じ

fp32とbf16のベクトル同士の類似度は最低でも0.9970で、スコアのずれは最大0.005だった。
まとめて埋め込むと5倍遅くなったが、会話中に埋め込むのは質問1件だけで、そちらの差は0.07秒にとどまった。メモリ使用量が約半分になるbf16を採用した。

以前TTSと競合して使えなくなっていたModelScopeの埋め込みAPIについて、復旧したときの自動切り替えも入れた。
ただし、保存済みのベクトルと整合性が取れないと困るので、APIが正常応答し、かつ同じ文をローカルとAPIで埋め込んだ類似度が0.99以上のときだけAPIに切り替えるようにした。今回のテスト中はローカルのまま動かしている。

音声での聞き返しテスト

会話はTTS(音声合成)で作成した音声を、StackChanの代わりにサーバーへ送った。
前半で5回話しかけ、自動保存を待たずにデバッグ用エンドポイントから即座に記憶を生成させた。その後、後半で5回聞き返している。
前半と後半の間は10分以内なので、短期の履歴も残ったままのテストにした。

テストは指示の調整を含めて複数回試した。下の表は、抜き出し指示を改善した後のテスト結果。

前半の会話5回

返答時の表情は顔を付けた記事の5種類から、返答の先頭にQwenが付けたタグ(joy, fun, smileなど)をそのまま反映させている。

話しかけた内容かなの表情返答
ただいま、今日は駅前の新しいラーメン屋に行ってきたよjoyおかえり、新しいラーメン屋いいね。どうだった、おいしかった?
味噌ラーメンがすごく美味しかった、また行きたいなjoyみそラーメンおいしかったんだね、また行きたいって思えるくらい最高だったんだ。今度よかったら私も一緒に連れてってよ。
そういえば来週の土曜日に友達と箱根に行くんだfun箱根いいね、温泉とか美味しいものたくさんあって最高じゃん。友達とめいっぱい楽しんできてね。
最近は仕事終わりにギターの練習してるよjoyえーすごい、毎日コツコツ練習してるんだね。どんな曲弾けるようになったか今度聴かせてよ。
さっき何ラーメン食べたって言ったっけ?smileみそラーメンだよ、すっごく美味しかったって言ってたよね。

5回目の「さっき何ラーメン」は、まだ長期記憶が空の状態で、直近3往復の履歴だけで答えている。

抜き出された記憶

5往復から抜き出された記憶は3件で、あいさつや返答側の話は入らなかった。

あなたは駅前の新しいラーメン屋でみそラーメンを食べ、とても気に入りまた行きたいと思っている。
あなたは来週の土曜日(2026年10月3日)に友達と箱根に行く予定だ。
あなたは最近、仕事終わりにギターの練習をしている。

テストをした9月27日は日曜日なので、来週の土曜日は10月3日で合っている。

後半の聞き返し5回

聞き返した内容検索でヒットした記憶返答
私の好きなラーメンって何だっけ?みそラーメンこの前駅前の新しいお店でみそラーメン食べてすごく気に入ってたよね。だからみそラーメンが大好きなんじゃないかなって思うよ。
週末の予定覚えてる?ギター週末の予定って教えてもらったことあったっけ。よかったらもう一回教えてほしいな。
最近私が練習してることって何?ギター仕事終わりにギターの練習してるって言ってたよね。上達してきたら聴かせてほしいな。
おすすめの映画ある?なし今ならブルーロックとか踊る大捜査線の新作がやっててめっちゃ盛り上がってるよ。アクション好きなら絶対楽しめるからぜひ観に行ってみてね。
日本で一番高い山は?なし富士山だよ。標高3776メートルで、やっぱり日本のシンボルだよね。

ラーメンとギターは記憶をもとに答えてくれた。
ラーメンについては、記憶に「気に入った」としか書いていないため「好きなラーメン」とは断定しないで、「大好きなんじゃないかな」と推測の形で返してきた。記憶に書かれていないことは断定しないというプロンプト指示が反映されている。

映画と富士山には記憶がヒットせず、無関係な昔の話を持ち出すこともなかった。

週末の予定だけは、箱根の記憶がヒットせず、代わりに関係のないギターの記憶が渡ってしまった。
かなはプロンプトの指示どおりギターの話は持ち出さないで、「教えてもらったことあったっけ」と聞き返してきた。
間違いを言っていないとはいえ、箱根に行く話をしたのは直前なので、思い出せないと会話がつながらない。

抽出指示による記憶文の違い

改善前の古い指示では相対的な日付を具体的な日付に置き換えさせていたため、箱根の記憶は次の文になっていた。

あなたは2026年10月3日に友達と箱根に行く予定がある。

古い指示のときも「週末の予定覚えてる?」には箱根ではなくギターの記憶が渡り、かなは「週末の予定は教えてもらってなかったからわからないな」と返していた。
そこで元の言い方を残して日付を括弧で添える指示に替えて試したが、結果は変わらず箱根はヒットしなかった。
ほかの4問はどちらの指示でも共通の記憶がヒットし、返答内容もほぼ同じだった。

各処理の実測時間

改善後のテストで各発話について、音声を送信してから文字起こしと記憶検索を経て最初のテキスト応答が確定するまで、1文目の音声ができるまで、全体の返答が終わるまでを測った。

処理区間実測値
音声送信〜最初のテキスト確定まで4.3〜8.5秒
1文目の音声合成完了7.1〜11.3秒
全文の返答完了9.3〜13.8秒
5往復分の記憶保存(非同期)5.2秒

表の1行目は、文字起こし、記憶の検索、ModelScopeが返答を返し始めるまでの待ちがすべて完了してからの所要時間になっている。
1問目だと8.5秒のうち、文字起こしが5.01秒、記憶の検索が1.42秒で、残りの約2秒がModelScopeの応答開始待ちだった。

記憶の検索は文字起こしのあとに1回入るだけで、質問文の埋め込みと検索を合わせて0.2〜0.4秒ほど。
記憶の生成・保存は返答とは別スレッドで動くため、5.2秒かかっても会話自体は止まらなかった。

なお初回の試行では、記憶作成の直後の1問目だけ文字起こし〜テキスト確定まで125.9秒かかる事象が発生した。記憶作成自体は13.8秒で終了しており、2問目以降は5秒前後に戻っていた。改善後のテストでは同じ位置でも4.9秒で処理され、文字起こしが2.28〜5.01秒、記憶検索は0.16〜1.42秒で推移した。初回の遅れがAPI側の詰まりだったのか内部処理の競合だったのかはログ不足のため特定できていない。

箱根の記憶が使われなかった理由

直近3往復の短期履歴を確認すると、後半2問目の時点で残っていたのは「ギター」「さっき何ラーメン」「好きなラーメン」のやり取りで、箱根の話題はすでに履歴から外れていた。
そのため、箱根の話は長期記憶から取得するしかなかった。

長期記憶については、テスト前に日付の表現を変えた4パターンの比較データがある。
箱根の記憶文を4通り用意し、日付加算スコア0.05を足した値を5つの質問で比べた。埋め込みはbf16で、質問の疑問符はすべて全角「?」で入力していた。

書き方週末の予定覚えてる?箱根の話覚えてる?来週何するんだっけ?今度の旅行どこだっけ?今週末ひま?
A 日付だけ「2026年10月3日に」0.4160.5260.5110.4920.450
B 日付と曜日「2026年10月3日(土)に」0.4140.5220.5050.4960.458
C 元の言い方と日付「来週の土曜日(2026年10月3日)に」0.4530.4710.5640.5010.516
D 週末と明記「週末の2026年10月3日(土)に箱根へ旅行に」0.4760.4840.5520.5300.508

Aは古い指示で保存された文で、「週末の予定覚えてる?」は0.416となりしきい値0.45を下回っていた。
そこで指示文には、「週末の予定覚えてる?」で0.453を出して0.45を超えたCの表現を採用した。
しかし、改善後のテストでもこの記憶は使われなかった。

実際の後半2問目の文字起こしログを確認すると、「週末の予定覚えてる?」と末尾の疑問符が半角になっていた。
そこで、保存された文と実際の文字起こしテキストで類似度を再計算した。

記憶半角「?」(実際の文字起こし)全角「?」疑問符なし
箱根(テストで保存された文)0.4220.4510.480
箱根(比較で使ったCの文)0.4240.4530.480
ギター0.4660.4880.527
ラーメン0.2380.2750.378

半角疑問符だと箱根は0.422となってしきい値0.45を割り込み、逆にギターは0.466でしきい値を超えていた。ギターの0.466はサーバーログの値とも一致している。
同じ後半でも、3問目の「最近私が練習してることって何?」は全角で文字起こしされていて、Qwen3-ASRは疑問符の全角と半角が発話ごとに揺れる。
書き方の比較は全角の質問でしかやっていなかったので、比較では通っていたCの文も、実際の文字起こしでは通らなかった。

疑問符を付けなければ箱根は0.480でヒットし、全角でも0.451と、しきい値0.45との差はわずか0.001しかない。記号1文字で±0.03動く程度の差で判定が分かれる境界線上に、箱根の記憶スコアがもともと位置していた。
検索した記憶をプロンプトに入れた10問テストでも、0.40〜0.50の範囲はわずかな表記の差で合否が逆転する現象が見られており、今回の箱根もその境界領域のシビアさを示す結果となった。