8.3.6 対話システムとマルチターン管理
多くの人がチャットアプリを作るとき、最初に考えるのは次のようなことです。
historyを管理する- 履歴をまとめてモデルに渡す
これで最小限の demo は作れますが、本当に使える対話システムにはまだほど遠いです。
この節のポイントは、「マルチターン対話」をきちんと分解して理解することです。
学習目標
- 単一ターンのQ&Aとマルチターン対話システムの本質的な違いを理解する
- セッション状態、コンテキストウィンドウ、確認質問といった基本概念を理解する
- 最小限のマルチターン対話マネージャーを読めるようになる
- 対話システムの重要点は履歴を覚えることだけではなく、状態を管理することだと理解する
まずは全体図をつかもう
マルチターン対話を初学者が理解する順番として適しているのは、「履歴を全部入れること」ではなく、まず次をはっきり見ることです。
この節で本当に解決したいのは次の2つです。
- マルチターンシステムが単一ターンQ&Aより難しい理由
- 「履歴があること」と「状態があること」は別だという点
初学者向けのたとえ
マルチターン対話システムは、次のように考えるとわかりやすいです。
- 1人のカスタマーサポート担当が、ユーザーと会話し続けている
ユーザーは毎回、背景を最初から説明してくれません。
だから担当者は次を覚えておく必要があります。
- 今どの話題をしているか
- どの重要情報がもうわかっているか
- まだ足りない情報は何か
会話ログを横に全部置いているだけで、状態をきちんと整理していなければ、
その担当者はすぐに話を混乱させてしまいます。
なぜマルチターン対話は単一ターンQ&Aよりずっと難しいのか?
単一ターンQ&Aは「1問1答」に近い
たとえば、
- ユーザーが1回質問する
- システムが1回答える
このタイプは、長期的な状態がなくても動かせます。
マルチターン対話はどこが本当に難しいのか?
後続のターンでは、しばしば情報が省略されるからです。
- 「返金ポリシーは何ですか?」
- 「じゃあ、もう30%学習していたら返金できますか?」
2つ目の「じゃあ」は、1つ目の話題を引き継いでいることが前提です。
システムが前の文脈を覚えていなければ、理解が不完全になります。
つまり、マルチターン対話で本当に難しいのは「メッセージが増えること」ではなく、
コンテキスト依存と状態の継続です。
対話システムは少なくとも何を管理する必要があるのか?
最低限、ふつうは次を管理します。
- 会話履歴
- 現在のトピック
- ユーザーの確認情報
- 確認質問が必要かどうか
つまり、対話システムは「回答を生成する」だけでなく、次を管理する必要があります。
この対話が今どの状態にあるか。
最小の対話マネージャーの例
def new_session():
return {
"history": [],
"topic": None
}
def add_turn(session, role, content):
session["history"].append({"role": role, "content": content})
session = new_session()
add_turn(session, "user", "返金ポリシーは何ですか?")
add_turn(session, "assistant", "期間について知りたいですか、それとも条件ですか?")
print(session)
想定出力:
{'history': [{'role': 'user', 'content': '返金ポリシーは何ですか?'}, {'role': 'assistant', 'content': '期間について知りたいですか、それとも条件ですか?'}], 'topic': None}
このコードは小さいですが、すでに何を教えているのでしょうか?
このコードが教えているのは、
- 対話システムには本質的に状態がある
- その状態には少なくとも履歴と現在のトピックが含まれる
ということです。
これが、「モデルを1回呼ぶ」ことから「対話システムを作る」ことへの第一歩です。
もう1つ、最小限の「状態の流れ」の例
state = {
"topic": "refund",
"slots": {"progress": None},
}
user_message = "もう30%学習したけど、返金できる?"
if "30%" in user_message:
state["slots"]["progress"] = "30%"
print(state)
想定出力:
{'topic': 'refund', 'slots': {'progress': '30%'}}
この例は初学者にとても向いています。なぜなら、次のことが見えるからです。
- 対話システムが本当に保持するのは、ただの原文ではない
- 構造化された状態も保持している

履歴は元の材料で、state はシステムが今維持している「現在の理解」です。図で topic、slots、last_tool_result、summary を分けているのは、すべてのコンテキストを雑に prompt に詰め込まないためです。
対話システムは回答するだけでなく、確認質問もできる必要がある
なぜ確認質問が重要なのか?
ユーザー入力は、途中までしか書かれていないことが多いからです。
たとえば、
- 「天気を調べて」
このとき、システムが勝手に都市を推測すると、体験は悪くなりがちです。
よりよい方法は、
足りない情報を先に補うこと。
最小の確認質問の例
def dialog_step(session, user_message):
add_turn(session, "user", user_message)
if "天気" in user_message and "北京" not in user_message and "上海" not in user_message:
reply = "どの都市の天気を調べたいですか?"
add_turn(session, "assistant", reply)
return reply
reply = f"システムは次を処理しています:{user_message}"
add_turn(session, "assistant", reply)
return reply
session = new_session()
print(dialog_step(session, "天気を調べて"))
print(session["history"])
想定出力:
どの都市の天気を調べたいですか?
[{'role': 'user', 'content': '天気を調べて'}, {'role': 'assistant', 'content': 'どの都市の天気を調べたいですか?'}]
ここには、とても大事な能力が表れています。
対話システムは答えるだけでなく、情報の不足を管理する必要があります。
なぜ確認質問は「システムが安定している」ことの表れなのか?
初心者はよく、次のように考えがちです。
- できるだけ確認質問が少ない方が賢い
でも実際のプロダクトでは、むしろ逆のことがよくあります。
- 先に確認して条件をそろえる
- その方が、適当に推測するよりずっと信頼できる
なぜ「履歴を全部そのままモデルに入れる」だけでは足りないのか?
履歴が長くなると何が起きるか?
- token コストが増える
- 応答が遅くなる
- 関係ない情報がどんどん増える
だから実際のシステムは選別する
たとえば、
- 直近 N ターンだけ残す
- 現在のトピックは別に状態として持つ
- もっと前の履歴は要約する
つまり、マルチターン管理は「履歴があるかどうか」ではなく、
どの履歴を有用な形で残すか。
という問題です。
実践:直近ターンを残し、古い履歴を圧縮する
次の小さな練習は、実際のシステムでよく使う考え方をまねています。直近の数ターンは元のメッセージとして残し、それより古い履歴は短い要約にします。
def compact_history(history, keep_last=2):
older = history[:-keep_last]
recent = history[-keep_last:]
if older:
summary = " | ".join(f"{turn['role']}: {turn['content']}" for turn in older)
else:
summary = None
return {
"summary": summary,
"recent": recent
}
history = [
{"role": "user", "content": "返金ポリシーは何ですか?"},
{"role": "assistant", "content": "購入後 7 日以内、かつ学習進度が 20% 未満なら返金可能です。"},
{"role": "user", "content": "もし 30% 学習済みなら?"},
{"role": "assistant", "content": "その場合、通常は返金条件を満たしません。"},
]
memory_view = compact_history(history, keep_last=2)
print(memory_view)
想定出力:
{'summary': 'user: 返金ポリシーは何ですか? | assistant: 購入後 7 日以内、かつ学習進度が 20% 未満なら返金可能です。', 'recent': [{'role': 'user', 'content': 'もし 30% 学習済みなら?'}, {'role': 'assistant', 'content': 'その場合、通常は返金条件を満たしません。'}]}

これはまだ学習用の例ですが、大事な実装習慣を教えてくれます。prompt を無限に伸ばさず、直近の会話は正確に残し、古い文脈は意図的に要約します。
少しだけ本格的なマルチターン例
def dialog_reply(session, user_message):
add_turn(session, "user", user_message)
if "返金" in user_message:
session["topic"] = "refund"
reply = "返金ポリシーは、購入後 7 日以内かつ学習進度が 20% 未満なら返金可能です。期間を知りたいですか、それとも自分が条件を満たすかを確認したいですか?"
elif "30%" in user_message and session["topic"] == "refund":
reply = "学習進度が 30% なら、通常は返金条件を満たしません。"
else:
reply = "現在のトピックについて、引き続きお手伝いできます。"
add_turn(session, "assistant", reply)
return reply
session = new_session()
print(dialog_reply(session, "返金ポリシーは何ですか?"))
print(dialog_reply(session, "じゃあ、もしもう30%学習していたら?"))
print(session)
想定出力:
返金ポリシーは、購入後 7 日以内かつ学習進度が 20% 未満なら返金可能です。期間を知りたいですか、それとも自分が条件を満たすかを確認したいですか?
学習進度が 30% なら、通常は返金条件を満たしません。
{'history': [{'role': 'user', 'content': '返金ポリシーは何ですか?'}, {'role': 'assistant', 'content': '返金ポリシーは、購入後 7 日以内かつ学習進度が 20% 未満なら返金可能です。期間を知りたいですか、それとも自分が条件を満たすかを確認したいですか?'}, {'role': 'user', 'content': 'じゃあ、もしもう30%学習していたら?'}, {'role': 'assistant', 'content': '学習進度が 30% なら、通常は返金条件を満たしません。'}], 'topic': 'refund'}
この例は、普通のQ&Aと比べて何が増えたのでしょうか?
増えた重要点は、モデルが強くなったことではなく、
- トピック追跡
- コンテキストの継承
です。
つまり、
対話システムの核心は、まず状態設計にあることが多いです。
初学者が最初に覚えるとよい状態表
| 状態の種類 | 何を記録するか |
|---|---|
| history | これまでに言ったこと |
| topic | 今何について話しているか |
| slot | 現在のタスクでまだ不足している重要情報 |
| tool state | ツールを呼んだか、結果を受け取ったか |
この表は初学者にとても向いています。なぜなら、マルチターン対話の複雑さを、いくつかのわかりやすい箱に分けて見られるからです。
対話システムによくある状態の種類
topic state
今、何について話しているのか。
slot state
どの重要情報がすでにわかっていて、何がまだ足りないのか。
たとえば天気システムなら、
- 都市が既知 / 未知
- 日付が既知 / 未知
tool state
どのツールをすでに呼んだか、どの結果を受け取ったか。
これは Agent 型の対話で特に重要です。
もし目標が「知識ベース駆動の教材生成アシスタント」なら、どのスロットを管理すべきか?
このタイプのプロジェクトは、普通のチャットと大きく違います。
- ユーザーが要件を一度で全部言わないことが多い
たとえば、最初に次のように言うかもしれません。
- 「割引の文章題の Word 教材を作って」
そのあとで、さらに次を補足します。
- 「小学高学年向け」
- 「練習問題を3問入れて」
- 「授業での説明っぽい雰囲気で」
だから、最初の段階では、スロットを次のように決めるとよいです。
| スロット | 何を記録するか |
|---|---|
topic | 教材のテーマ |
audience | 対象者 / 学年 |
doc_format | Word / PPT |
style | 授業説明 / 箇条書き / 配布資料風 |
exercise_count | 練習問題の数 |
最小の状態オブジェクトは、まず次のように書けます。
state = {
"topic": "割引の文章題",
"audience": None,
"doc_format": "word",
"style": None,
"exercise_count": None,
}
print(state)
想定出力:
{'topic': '割引の文章題', 'audience': None, 'doc_format': 'word', 'style': None, 'exercise_count': None}
この例の一番大きな価値は、
- マルチターン対話が、実際のプロジェクトでどんな情報を補っているか
を初学者に先に理解してもらえることです。
もっと実際のプロジェクトに近い最小確認質問の例
def next_question(state):
if not state["audience"]:
return "この教材は、主にどの学年や人向けですか?"
if not state["style"]:
return "授業説明のような形がよいですか、それとも箇条書きの資料がよいですか?"
if not state["exercise_count"]:
return "最後に、練習問題は何問付けたいですか?"
return "情報はかなりそろいました。教材の構成を作り始められます。"
state = {
"topic": "割引の文章題",
"audience": None,
"doc_format": "word",
"style": None,
"exercise_count": None,
}
print(next_question(state))
state["audience"] = "小学高学年"
print(next_question(state))
想定出力:
この教材は、主にどの学年や人向けですか?
授業説明のような形がよいですか、それとも箇条書きの資料がよいですか?
これによって、初学者はとても大事な感覚をつかめます。
- 対話システムの目的は「たくさん雑談すること」ではない
- 生成に必要なパラメータを少しずつ埋めることにある
初学者が最初に対話システムを作るときの、いちばん安全な順番
おすすめの順番は次の通りです。
- まず単一ターンQ&Aを作る
- 次にトピック状態を追加する
- 次に確認質問ロジックを追加する
- 最後にツール状態や、より複雑な記憶を追加する
最初からすべての状態を一気に実装しようとすると、たいてい混乱します。
これをプロジェクトとして見せるなら、何を見せるのが一番よいか
一番伝わりやすいのは、長いチャットのスクリーンショットではなく、次のようなものです。
- 文脈を引き継いだ1往復の対話
- トピック状態がどう変わるか
- スロットがどう埋まるか
- いつシステムが確認質問を選ぶか
- いつシステムがそのまま回答を続けるか
こうすると、見る人にはっきり伝わります。
- あなたが作っているのはマルチターンシステムだ
- 履歴をただつなげてモデルに渡しているだけではない
なぜマルチターン対話は特に「話がそれやすい」のか?
それは、次の影響を受けやすいからです。
- 直前のトピックの残り
- 長すぎる履歴
- ユーザーの表現不足
- 状態の明示的な記録がないこと
そのため、次のことがよくわかります。
対話システムでは、「見た目の返答の良さ」より「状態管理」の方がずっと重要なことが多いです。
初学者がよくハマる落とし穴
history だけを管理して、構造化状態を管理しない
システムの制御がどんどん難しくなります。
1回でわからないと、すぐ推測する
多くの場合、雑に答えるより確認質問をした方がよいです。
履歴を無限に積み続ける
コストもノイズも増えます。
まとめ
この節で最も大事なのは、「会話できる関数」を作ることではなく、次を理解することです。
対話システムの核心は、マルチターンのテキストを生成することではなく、マルチターンの状態を管理することにある。
この違いを本当に理解できれば、次に学ぶインテリジェントアシスタント、Agent 対話、記憶システムも、かなり理解しやすくなります。
この節で必ず持ち帰りたいこと
- 対話システムの本質は、まず状態管理である
- 履歴、トピック、スロット、ツール状態はどれも重要になりうる
- 最初はシンプルな状態管理をきちんと作り、その後で少しずつ拡張する方が、一気に大きな記憶システムを作るより安定している
練習
- この節の例に、「証明書」トピックの状態を追加してみましょう。
- 天気検索タスクのために、都市や日付を含む
slot stateを設計してみましょう。 - 考えてみましょう:「確認質問」が「適当に推測する」よりよい対話戦略であるのはなぜでしょうか?
- 自分の言葉で説明してみましょう。なぜマルチターン対話の核心は、履歴をつなげることではなく状態管理だと言えるのでしょうか?