1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

軽量ローカルLLMでも会話を止めない――発語支援AIロボット「あいくん」の「会話の呼吸」設計

1
Posted at

はじめに

発語に支援が必要な子どものための、ローカル完結型会話AIロボット「あいくん」を開発しています。

「あいくん」は、最も賢いAIを目指したものではありません。

うまく言えなくても否定せず、子どもの声を待ち、何度でも同じ調子で付き合える相手を、子どもの周りにもう一人増やすことを目指しています。

本記事では、次の2点を中心に紹介します。

  • 子どもの声や会話を外部の生成AIへ送らないためのローカルLLM構成
  • ローカルLLMの待ち時間を、音声会話として成立させる「会話の呼吸」の設計

本システムは、医療、診断、治療や専門的支援を代替するものではありません。大人の見守りの中で、声を出すきっかけを増やすための支援ツールです。

システム構成

現在の基本構成は次のとおりです。

PCまたはスマートフォンのマイク
        ↓
faster-whisperによる音声認識
        ↓
定型応答・登録知識・ローカルLLM
        ↓
安全判定と子ども向けの文章整形
        ↓
VOICEVOXによる音声合成
        ↓
PCまたは入力元端末から音声を再生

主な構成要素は以下です。

用途 採用技術
サーバー・管理API Python / FastAPI
音声認識 faster-whisper
会話AI Ollama上のローカルLLM
初期会話モデル qwen2.5:1.5b
RAG用埋め込み nomic-embed-text
音声合成 VOICEVOX
顔・操作画面 HTML / CSS / JavaScript
実行環境 Windows 11 PC

初期構成では、軽量なqwen2.5:1.5bと、RAG用のnomic-embed-textだけで起動できるようにしています。

より大きなモデルが必要な利用者は、後からgemma3:4bなどを追加できます。

管理画面の操作によって、モデルを勝手にダウンロードすることはありません。

なぜローカルLLMを採用したのか

ローカルLLMを選んだ最大の理由は、モデルの新しさや技術的な面白さではありません。

このシステムが扱う可能性のある情報には、次のようなものがあります。

  • 子どもの声
  • 日常の会話
  • 好きなもの
  • 練習している言葉
  • 気持ちカードで選んだ内容
  • 発語練習の記録

保護者や支援者にとって、これらの情報が外部の生成AIへ送られるかどうかは重要な問題です。

そこで、日常の音声認識、会話生成、音声合成をWindows PC内で処理する構成にしました。

子どもの声
    ↓
家庭・施設内のPCで音声認識
    ↓
PC内のローカルLLMで応答生成
    ↓
PC内のVOICEVOXで音声合成

日常会話のために、子どもの声や会話を外部の生成AIへ送信する必要はありません。

ただし、「ローカルだから情報漏洩しない」と断定することはできません。

初回導入やモデル取得には通信が必要になる場合があります。また、PC、記録ファイル、バックアップ、利用者権限などの管理も必要です。

正確には、日常会話を外部の生成AIへ送らない構成にすることで、外部送信の範囲を小さくしています。

軽量ローカルLLMを音声会話に使う難しさ

軽量ローカルLLMには、クラウド型の大規模モデルとは異なる制約があります。

  • 初回推論が遅くなりやすい
  • CPUやメモリの性能に左右される
  • 応答時間が一定ではない
  • 小さなモデルでは複雑な指示が不安定になる
  • LLMの生成後に音声合成の時間も必要になる

画面上のチャットであれば、「入力中」や「生成中」という表示を見ながら待てます。

しかし音声会話では、数秒間の無音が大きな問題になります。

子どもが話しかけたあと、相手が何も返さないと、次のことが分かりません。

  • 声が届いたのか
  • 聞き取れなかったのか
  • 考えているのか
  • システムが止まったのか

単純に推論を高速化するだけではなく、待ち時間そのものを音声会話として設計する必要がありました。

待ち時間を「会話の呼吸」に変える

「あいくん」では、LLMの回答を待つ前に短いリアクションを返します。

例えば、短い処理では次のような相づちを使います。

ACK_POOLS = {
    "chat_fast": [
        "うん",
        "うんうん",
        "そうなんだ",
    ],
}

時間がかかる可能性が高い処理では、待っている理由を明示します。

ACK_POOLS = {
    "chat_slow": [
        "ちょっと まってね。かんがえてるよ",
        "うんとね、かんがえるね",
    ],
}

質問を受け取った場合は、質問用の候補から選びます。

ACK_POOLS = {
    "question": [
        "うん、かんがえてみるね",
        "うん、いい しつもんだね!",
    ],
}

毎回同じ相づちを返すと機械的になるため、発話の種類や予想される処理時間に応じた候補からランダムに選びます。

LLM生成とリアクションを並列実行する

重要なのは、相づちを再生してからLLMを呼び出すのではないことです。

先にLLM生成を別スレッドで開始し、その生成中に相づちを再生します。

概略は次のようになります。

get_reply = generate_async(user_message)

say(
    pick_ack("chat_fast"),
    expression="happy",
    cache=True,
)

face.set(
    state="thinking",
    text="",
)

reply = get_reply()

処理の流れは次のとおりです。

子どもの発話
    ├── LLMの回答生成を開始
    │
    └── 合成済みの短い相づちを再生
              ↓
        顔を「考え中」に変更
              ↓
        LLMの回答を受け取る
              ↓
        VOICEVOXで回答を合成
              ↓
        回答文の表示と音声再生

相づちの再生時間を、そのままLLMの生成時間として利用できます。

これは単なる時間稼ぎではありません。

入力を受け取ったことと、現在も処理が続いていることを、音声と顔で伝えるための会話制御です。

定型音声を起動時に事前合成する

相づちを使うたびにVOICEVOXへ音声合成を依頼すると、相づち自体が遅くなります。

そこで、短い定型音声は起動時に事前合成してキャッシュします。

for texts in ACK_POOLS.values():
    for text in texts:
        cached_wav(text)

キャッシュは、話者IDと文章の組み合わせで管理します。

cache_key = (speaker_id, text)

顔の種類によってVOICEVOXの話者を変えられるため、文章だけではなく話者IDもキーに含めています。

事前合成の対象には、次のような短い音声も含めています。

"もういっかい、きいてもいい?"
"うーんとね…"
"うん、きいてるよ!"

これにより、聞き取り失敗や処理待ちに対して、すぐ反応できます。

実際に使用するモデルをウォームアップする

起動時には、現在選択されているLLMへ短い入力を送り、初回応答の遅延を軽減します。

llm.chat(
    system_prompt,
    "こんにちは",
    model=current_model,
    max_tokens=8,
)

ここで重要なのは、「推奨モデル」ではなく、実際に使用するモデルをウォームアップすることです。

管理画面上の設定と実際のウォームアップ対象が異なると、最初の本番会話でコールドスタートが発生します。

4秒を超えた場合の追加リアクション

想定より回答生成が長引いた場合は、4秒後に一度だけ追加の反応を返します。

timer = threading.Timer(
    4.0,
    lambda: say("うーんとね…", cache=True),
)

timer.start()

回答が速く返った場合はタイマーをキャンセルします。

try:
    reply = llm.chat(...)
finally:
    timer.cancel()

この反応は、架空の進捗率を見せるものではありません。

「入力を受け付けている」「現在も処理中である」という、実際の状態を伝えるためのフィードバックです。

顔の状態も会話の一部にする

音声だけでは状態が分からないため、顔UIを次の状態に分けています。

待機中
聞いている
考えている
話している
停止

例として、次のように状態を切り替えます。

face.set(state="listening")
face.set(state="thinking")
face.set(state="speaking", text=reply)
face.set(state="idle")

話している最中は口を動かし、音声と同じ文章を画面にも表示します。

音声合成やスピーカーで障害が起きた場合も、応答本文は画面に残します。

try:
    wav = tts.synthesize(reply)
except Exception:
    face.set(
        state="idle",
        text=reply,
        notice="おとが でなかったよ",
    )

重要な内容を音声だけに依存させないためです。

音声と文字をなるべく同時に出す

回答文だけを先に画面へ表示し、数秒後に音声が始まると、視覚と聴覚のタイミングがずれます。

そのため通常の回答では、先にVOICEVOXで音声を合成し、合成完了後に文字表示と音声再生を開始します。

回答生成
    ↓
VOICEVOXで音声合成
    ↓
顔を「話している」に変更
    ↓
文字表示と音声再生を開始

一方、先行リアクションは事前合成済みなので、発話直後に再生できます。

通常回答と先行リアクションで、音声合成の扱いを分けています。

LLMにすべてを任せない

会話レベル1~3では、自由生成を使用しません。

レベル 応答方式
Lv1 定型の褒め言葉
Lv2 褒め言葉と2択の問いかけ
Lv3 登録済み知識から回答
Lv4~6 軽量ローカルLLMによる短い自由会話
Lv7以上 追加モデルを使用する高度な会話

初期構成のqwen2.5:1.5bでは、Lv4~6の短い自由会話を利用できます。

追加モデルが導入されていない場合に、管理画面の設定だけを変更して動かない状態にしないよう、モデルの導入状況も確認します。

また、管理画面からモデルを暗黙にダウンロードすることはありません。

LLM出力に対する後処理

ローカルLLMの出力は、そのまま読み上げません。

次の処理を通してからVOICEVOXへ渡します。

  • 子ども向けの短い文数に制限
  • 漢字を読みやすい表記へ変換
  • TTSが読みづらい絵文字を除去
  • 括弧書きなどを整理
  • 同じ文章の繰り返しを抑制
  • 外国語や壊れた文章を拒否
  • 危険発話と自傷関連の安全判定
  • 不適切語の最終フィルタ
  • 生成失敗時は定型応答へフォールバック

概略は次のようになります。

reply = filter_reply(raw_reply)
reply = to_kana_display(reply)
reply = strip_emoji(reply)
reply = compact_sentences(reply)
reply = strip_repetition(reply)

if not is_clean_japanese(reply):
    return fallback_reply

if is_unsafe(reply):
    return fallback_reply

ローカルLLMを採用することと、LLMを無条件に信用することは別の話です。

安全判定はショートカットより先に行う

音声UIには、挨拶、終了、音楽、読み聞かせなどのショートカットがあります。

しかし、例えば次のような混合発話が入力される可能性があります。

おはよう しにたい
ばいばい しにたい
おんがく しにたい

この場合、挨拶や終了、音楽を先に処理してはいけません。

音声認識直後に安全判定を行い、危険な発話や自傷に関する発話を、すべてのショートカットより優先します。

音声認識
    ↓
安全判定
    ↓
挨拶・終了・活動などの判定
    ↓
通常会話

生成後の回答にも防御的な安全判定を残しています。

問い返しすぎない

発語を促す目的で、AIから質問を返す機能があります。

しかし、毎回答えを求められる会話は疲れます。

自由会話では、問い返しを付ける割合を約4割に抑えています。

if random.random() >= 0.4:
    return reply

return f"{reply} {varied_followup(profile)}"

問い返す場合も、2択だけに偏らないようにしています。

OPEN_FOLLOWUPS = [
    "どうして そう おもったの?",
    "もっと きかせて!",
    "それから どう なったの?",
    "どんな きもちだった?",
    "なにが たのしかった?",
]

残りの約6割は、短い返事だけで会話を終えます。

会話を無理に続けることより、子どもを質問攻めにしないことを優先しています。

停止とキャンセル

音声、読み聞かせ、音楽は、顔UIと管理画面の「とめる」から停止できます。

停止後に古いLLM結果や音声が再生されないよう、再生処理には世代番号を持たせています。

再生世代 10で回答生成を開始
    ↓
利用者が停止
    ↓
再生世代を11へ更新
    ↓
世代10の回答が完成
    ↓
現在の世代と異なるため再生しない

停止操作は冪等にし、複数回押しても安全な設計にしています。

失敗時のフォールバック

音声AIでは、LLMだけでなく複数の箇所で障害が発生します。

  • マイクが見つからない
  • 音声認識に失敗する
  • LLMがタイムアウトする
  • VOICEVOXが応答しない
  • スピーカーが利用できない
  • スマートフォンとの接続が切れる

そのため、失敗時には次のような代替経路を用意しています。

障害 フォールバック
音声認識失敗 責めずに、もう一度話すよう案内
LLM生成失敗 定型の褒め言葉へ切り替え
TTS失敗 回答本文を画面へ残す
音声出力失敗 サーバーを終了させず次の会話へ戻る
スマートフォン切断 端末側のマイク資源を解放
停止操作 古い生成結果と再生を無効化

「失敗しないAI」ではなく、「失敗しても会話全体が壊れないAI」を目指しています。

現在の利用条件

現在、正式に想定している構成は次のとおりです。

  • Windows 11 PC 1台
  • マイクとスピーカー1組
  • 顔画面1つ
  • 同時に会話する子ども1人
  • 大人の見守りの中で利用

複数人、複数マイク、複数会話の同時利用には対応していません。

施設で使用する場合は、カームルームなどの個別環境を想定しています。

まとめ

軽量ローカルLLMを音声UIへ組み込む場合、モデルの応答速度だけを改善しても、自然な体験にはなりません。

今回の実装では、次の方法を組み合わせました。

  • 実際に使用するモデルのウォームアップ
  • 定型音声の事前合成
  • LLM生成と先行リアクションの並列実行
  • 処理が長引いた場合の一度だけの追加反応
  • 顔と文字による状態表示
  • 音声障害時の視覚フォールバック
  • 停止と世代番号によるキャンセル
  • 生成失敗時の定型応答
  • 問い返し頻度の制御
  • 自由生成に依存しすぎない会話レベル設計

目指しているのは、最も賢いAIではありません。

子どもの声を受け取り、無言で置き去りにせず、急かさず、何度でも同じ調子で付き合える相手です。

AIの待ち時間、失敗、不確実さを隠さず、人が理解できる形へ変える。

それが、このシステムで考えている「会話の呼吸」です。

公開資料:

▼概要
https://www.docswell.com/s/KanameShiga/KE197D-2026-07-15-224601/1
▼支援者向け資料
https://www.docswell.com/s/KanameShiga/KY8964-2026-07-15-225726/1
▼技術者向け資料
https://www.docswell.com/s/KanameShiga/ZN7X2D-2026-07-15-230034/1
▼あいくんの概要・使い方
https://www.docswell.com/s/KanameShiga/KVJ9EL-2026-07-15-230334/1

紹介動画

実際の顔UI、会話、まねっこ、管理画面との連携を動画で紹介しています。

発語支援AIロボット あいくん|紹介動画(本編)

音声クレジット

  • あいくんの声:VOICEVOX:ずんだもん
  • 紹介動画のナレーション:VOICEVOX:青山龍星
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?