第7章 順番が崩れるとき — 中断・エラーと user→assistant の交互
📖 この章のゴール:会話を途中で止めた/失敗したときに messages の交互(user→assistant→…)が崩れる 問題を理解し、履歴を整えてからAPIへ渡せるようになる。 ← 目次・はじめにへもどる
📱 ChatGPTではこう見える
ChatGPTで質問を送って、返事が出てくる途中で——うっかり タブを閉じた、わざと 「停止」ボタンで止めた、あるいは 電波が切れた。そんな経験はありませんか?
次にその会話をもう一度開くと、返事が途中で切れたままだったり、続きを送ろうとしても変になることがあります。
「送ったのに、返事が来ないまま終わった」 ——このとき、会話の中身(履歴)は 中途半端な形 で残っています。
この章では、その中途半端な履歴を そのままAIに渡してしまう とどうなるか、そして どう整えてから渡すか を学びます。これは背骨①(🧠記憶=履歴の健全性)の話です。
🤔 会話は「交互」が基本(🟢 基礎)
第4章で見たとおり、AIに渡す messages(メッセージズ=会話の配列)は、こんな形でした。
[
{ role: "user", content: "おはよう" },
{ role: "assistant", content: "おはようございます!" },
{ role: "user", content: "今日の予定は?" },
]
1行ずつ読むと:
role: "user":人間(あなた)の発言。role: "assistant":AIの返事。- 並びは user → assistant → user → assistant … の 交互(こうご=かわりばんこ) が基本。
これは 往復書簡(おうふくしょかん=手紙のやりとり) と同じです。こちらが1通出す → 相手が1通返す → またこちらが出す。もし相手の返事が来る前に もう1通たたみかけて 出したら、話が噛み合いませんよね。
会話を途中で止めると、ちょうどこれと同じことが起きます。
- userで終わったまま:あなたが送ったのにAIの返事が記録されず、
userが宙ぶらりん。 - assistantが未完成:返事が途中までしか届かず、
assistantのcontentが 書きかけ のまま保存される。
この 崩れた形 のまま次の送信でAIに渡すと、APIエラー になったり、話が噛み合わない変な応答 が返ってきます。「壊れた履歴をそのまま渡さない」——これが今回の合言葉です。
🛠 こう作る — 履歴を整えてから渡す(🟢 基礎)
考え方は3ステップです。① 状態を記録 → ② 開くときに整える → ③ 送信ボタンをロック。
ステップ1:発言に「状態」を持たせる
保存するとき、その発言が 完成済みか/まだ途中か の札(status)を付けます。
type ChatMessage = {
role: "user" | "assistant";
content: string;
status: "pending" | "done"; // pending=処理中, done=完了
};
1行ずつ読むと:
role:発言の主(人間かAIか)。content:発言の本文。status:この発言が終わったか の札。pending(ペンディング=処理中・宙ぶらりん)かdone(ダン=完了)。- AIに送信を始めたら
pending、返事が全部届いたらdoneに変える、という使い方をします。
ステップ2:会話を開くとき「整える」
会話を読み込んだら、AIへ渡す前に 崩れていないか点検 して直します。
function cleanHistory(messages: ChatMessage[]): ChatMessage[] {
const finished = messages.filter((m) => m.status === "done");
const last = finished[finished.length - 1];
if (last && last.role === "assistant") {
return finished; // きれいに assistant で終わっている
}
return finished; // user で終わっていてもOK(このあと続きを送る)
}
1行ずつ読むと:
messages.filter((m) => m.status === "done"):完了した発言だけ を残す(=未完了pendingは捨てる)。書きかけの返事をAIに見せない。const last = finished[...length - 1]:残ったうちの いちばん最後 の発言を見る。if (last.role === "assistant"):最後がAIの返事なら、交互がきれいに閉じている 状態。- 最後が
userのままでも返してよい:このあと「続きを送る/再送」でAIの返事を 追加する からです(次のステップ)。 - ポイントは
pendingをAPIへ送らない こと。整えたfinishedだけをopenai.chat.completions.createに渡します。
最後が
userで終わっている履歴は、「あなたが送ったのに返事が途切れた」状態。もう一度送り直す(再送) か、そのまま続けてAIに答えさせる ことで、交互を取り戻せます。
ステップ3:送信ボタンを「ロック」する(二重送信を防ぐ)
返事を待っているあいだにボタンを連打すると、同じお願いが 何回も飛びます(二重送信)。送信中はボタンを止めます。
let isSending = false; // 送信中かどうか
async function send(text: string) {
if (isSending) return; // 送信中なら、なにもしない
isSending = true;
try {
await postToServer(text); // サーバー(/api/chat)へ送る
} finally {
isSending = false; // 成功でも失敗でも、必ず解除
}
}
1行ずつ読むと:
let isSending = false:いま送信中かどうかの 見張り フラグ。if (isSending) return:すでに送信中なら 即やめる(2回目以降の連打を無視=二重送信ブロック)。isSending = true:送信開始の合図。ここからボタンは効かない。await postToServer(text):第3章で作った自分のサーバーの窓口へ送る。finally { isSending = false }:成功でも失敗でも必ず ロックを解除(finallyは最後に必ず通る)。これを忘れると、エラー時にボタンが 二度と押せなく なります。
🔧 応用:途中で“止める”ボタンの作り込み は本編では扱いません。返事の生成をこちらから打ち切る(
AbortController・stop・後始末)しくみは、詳しくは付録F にまとめています。ここでは「止めたあとの履歴を壊さない」考え方だけ押さえれば十分です。
⚠️ ハマりどころ
- 二重送信で同じ内容が二重保存:連打や通信の遅さで同じお願いが2回飛び、DBに同じ発言が2つ並ぶ。対策は 冪等性(べきとうせい)=同じ操作を2回しても結果は1回分にする こと。ステップ3のロックに加え、保存時に「同じ内容が直前にないか」を見るのも有効。
- 壊れた履歴をそのままAPIへ送る:
pending(書きかけ)の発言や、宙ぶらりんのuserを点検せず渡すと、APIエラー や 噛み合わない応答 に。送信前に必ずcleanHistoryを通す。 - pendingの放置:処理中に落ちた発言を
pendingのまま残すと、ゴミがたまり、次に開くたびに混乱します。開くときに捨てる(ステップ2)を徹底。 - リトライ(再送)で増殖:「失敗したからもう一度」を素直に作ると、実は1回目が成功していて 2回登録 されることが。再送も 冪等 に——「同じ送信は1回分」を守る設計に。
🤖 AIに頼むなら(Vibe codingのコツ)
🗣 プロンプト例: 「素のTS/JS のチャット画面で、送信中は送信ボタンをロックして二重送信を防いで。各メッセージに
status: 'pending' | 'done'を持たせて、会話を開くときにpendingを捨てて履歴を整える 関数を書いて。最後がuserで終わる履歴も安全に扱える ようにして(再送 or 続けて送信)。ロック解除はfinallyで必ず 行って」
出てきたコードの確認ポイント:
- 送信中は ボタンがロック され、連打しても1回しか飛ばないか(二重送信 対策)
- ロック解除が
finallyにあり、エラー時もちゃんと解除されるか - APIへ渡す前に
pending(書きかけ)を除外 しているか(壊れた履歴を渡さない) - 最後が
userの履歴をエラーにせず扱えるか(再送/続けて送信) - 再送・リトライが 冪等(2回押しても二重保存されない)か
📝 ことばメモ
- 交互(こうご):
userとassistantが かわりばんこ に並ぶこと。会話の基本の形 - ターン:1回分のやりとり(人が1つ送り、AIが1つ返す)の単位
- 未完了(pending/ペンディング):処理中・書きかけで、まだ完成していない発言の状態
- 冪等性(べきとうせい):同じ操作を 何回しても結果は1回分 になる性質。二重送信・再送の安全の鍵
- リトライ:失敗した送信を もう一度 やり直すこと。冪等でないと二重保存の原因に
➡️ 次の章へ
これで、途中で止まっても 履歴を整えてから渡す ことができるようになりました。会話の形は、もう簡単には崩れません。
ところが次は別の悩みが出てきます。会話を続けるほど 「これまでの会話ぜんぶ」が長く なり、やがてAIが一度に読める量の上限(トークン)を超えてしまうのです。
次の第8章では、その 「会話が伸びるとあふれる — トークンオーバーフロー」 に向き合います。