第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が未完成:返事が途中までしか届かず、assistantcontent書きかけ のまま保存される。

この 崩れた形 のまま次の送信で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回押しても二重保存されない)か

📝 ことばメモ

  • 交互(こうご)userassistantかわりばんこ に並ぶこと。会話の基本の形
  • ターン:1回分のやりとり(人が1つ送り、AIが1つ返す)の単位
  • 未完了(pending/ペンディング):処理中・書きかけで、まだ完成していない発言の状態
  • 冪等性(べきとうせい):同じ操作を 何回しても結果は1回分 になる性質。二重送信・再送の安全の鍵
  • リトライ:失敗した送信を もう一度 やり直すこと。冪等でないと二重保存の原因に

➡️ 次の章へ

これで、途中で止まっても 履歴を整えてから渡す ことができるようになりました。会話の形は、もう簡単には崩れません。

ところが次は別の悩みが出てきます。会話を続けるほど 「これまでの会話ぜんぶ」が長く なり、やがてAIが一度に読める量の上限(トークン)を超えてしまうのです。

次の第8章では、その 「会話が伸びるとあふれる — トークンオーバーフロー」 に向き合います。

→ 第8章 会話が伸びると…トークンあふれへ

← 目次・はじめにへもどる