กลับไปที่บล็อก

การแก้ไขปัญหาคู่มือ

สตรีมล้มเหลวหลัง HTTP 200: จับข้อผิดพลาดกลางสตรีมใน Python

ทำไมสตรีมถึงพังหลังสถานะ 200, วิธีสังเกต event: error, และการลองใหม่ใน Python ที่เก็บข้อความบางส่วนไว้โดยไม่ทำคำตอบซ้ำ

โดย CheapTokens Teamเผยแพร่เมื่อ ใช้เวลาอ่าน 7 นาที

สตรีมที่มี HTTP 200 OK ซึ่งจบด้วย event: error และ overloaded_error แทน message_stop
ในหน้านี้
  1. 200 สัญญาอะไรจริง ๆ
  2. จับมันใน Python
  3. ลองใหม่โดยไม่พิมพ์คำตอบซ้ำ
  4. ไม่ใช่ทุกการหยุดก่อนกำหนดที่เป็นข้อผิดพลาด
  5. ก่อนปล่อย

UI แชทของคุณสตรีมคำตอบได้ครึ่งเดียวแล้วค้าง และล็อกบอกว่า 200 OK นั่นไม่ใช่ความขัดแย้ง: 200 บอกแค่ว่าสตรีมเริ่มแล้ว ไม่ได้บอกว่าจบแล้ว ถ้ามีอะไรพังภายหลัง เซิร์ฟเวอร์ทำได้เพียงส่ง error ภายใน สตรีม ดังนั้นการตรวจสอบสถานะโค้ดจึงไม่มีทางเห็นมัน วิธีแก้คือถือว่าสตรีมสำเร็จก็ต่อเมื่อมันจบด้วยสัญญาณสิ้นสุด และให้ลองใหม่จากบัฟเฟอร์ที่สะอาดเมื่อมันไม่จบ

ส่วนที่เหลือของหน้านี้แสดงให้เห็นว่าสตรีมที่พังหน้าตาเป็นอย่างไร, ตัวจัดการ Python สองตัว (Anthropic Messages และ OpenAI SDK), และการลองใหม่ที่ไม่พิมพ์ต้นคำตอบซ้ำสองครั้ง ตัวจัดการทั้งสองใช้ CheapTokens เป็น API endpoint: เอกสาร มี base URL และโมเดลตัวอย่างคือ DeepSeek-V4-Flash (id deepseek-ai/DeepSeek-V4-Flash-0731, context 380K, เอาต์พุตสูงสุด 32,768 โทเคนต่อคำตอบ, ราคาเดียวทุกชั่วโมง — ดูหน้ารุ่น)

200 สัญญาอะไรจริง ๆ#

ลองนึกถึงพนักงานส่งของที่เซ็นรับพัสดุที่หน้าประตู: ลายเซ็นพิสูจน์ว่าเขามาถึง ไม่ได้พิสูจน์ว่าพัสดุสมบูรณ์ HTTP ก็ทำงานแบบเดียวกัน เซิร์ฟเวอร์ส่งสถานะและเฮดเดอร์ก่อน จากนั้นบอดี้ค่อย ๆ มาทีละส่วน ถ้าแบ็กเอนด์ของโมเดลโอเวอร์โหลดกลางทาง สถานะก็ถูกส่งไปแล้ว

หน้าข้อผิดพลาดของ Anthropic API (ตรวจสอบเมื่อ 1 พฤศจิกายน 2026) บอกไว้ตรง ๆ ว่า: เมื่อคุณได้รับสตรีมมิ่งรีสปอนส์ผ่าน SSE ข้อผิดพลาดสามารถเกิดขึ้นได้หลังจาก API คืน 200 และการจัดการข้อผิดพลาดมาตรฐานใช้ไม่ได้ในกรณีนั้น เอกสารสตรีมมิ่งของพวกเขาให้รูปแบบของอีเวนต์นั้น:

text
event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}

overloaded_error ปกติสอดคล้องกับ HTTP 529 ในการเรียกแบบไม่สตรีม (โอเวอร์โหลดชั่วคราวจากทราฟฟิกสูง) หน้าเดียวกันยังระบุ 500 api_error (ลองใหม่ด้วย exponential backoff) และ 504 timeout_error (ใช้สตรีมมิ่งสำหรับคำขอที่ยาวนาน) บนสาย สตรีมที่พังมีหน้าตาแบบนี้:

สตรีมที่เริ่มด้วย 200 OK และจบด้วย event: error แทน message_stop
กายวิภาคของข้อผิดพลาดกลางสตรีม: อีเวนต์เนื้อหาหยุดและ event: error มาถึงในตำแหน่งที่ควรเป็น message_stop (รูปแบบจากเอกสารสตรีมมิ่งของ Anthropic)

เอกสารยังเตือนว่าอาจมีอีเวนต์ชนิดใหม่เพิ่มเข้ามา ดังนั้นโค้ดของคุณควรจัดการอีเวนต์ที่ไม่รู้จักอย่างราบรื่น นั่นคือเหตุผลที่ตัวจัดการด้านล่างไม่พยายามจดจำไบต์ข้อผิดพลาดทุกตัว พวกมันถามคำถามเดียว: สตรีมจบด้วยสัญญาณสิ้นสุดหรือไม่?

  • Anthropic Messages: อีเวนต์สุดท้ายคือ message_stop และ stop_reason ใน message_delta บอกว่าจบเพราะอะไร (end_turn, max_tokens, stop_sequence)
  • OpenAI Chat Completions: ชังก์สุดท้ายมี finish_reason ที่ไม่ว่าง (stop, length, …)

ไม่มีสัญญาณสิ้นสุด หรือมีข้อยกเว้นระหว่างทาง — คำตอบไม่สมบูรณ์ ไม่ว่าสถานะจะบอกอะไรก็ตาม

จับมันใน Python#

ลองใช้บอทซัพพอร์ตที่สตรีมคำตอบไปยังเบราว์เซอร์ ลูปเก็บข้อความลงบัฟเฟอร์และรายงานกลับสามอย่าง: ข้อความจนถึงตอนนี้, สตรีมจบหรือไม่, และข้อผิดพลาดถ้ามี เลือกแท็บสำหรับ SDK ที่คุณใช้

ตัวจัดการเดียว สอง SDK

Anthropic SDK

python
import os
import anthropic

client = anthropic.Anthropic(
    base_url='https://api.cheaptokens.dev',  # no /v1 for the Anthropic SDK
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'

def ask_once(prompt, on_text=lambda t: None):
    parts, finished = [], False
    try:
        with client.messages.stream(
            model=MODEL,
            max_tokens=1024,
            messages=[{'role': 'user', 'content': prompt}],
        ) as stream:
            for event in stream:
                if event.type == 'content_block_delta' and event.delta.type == 'text_delta':
                    parts.append(event.delta.text)
                    on_text(event.delta.text)
                elif event.type == 'message_stop':
                    finished = True
    except anthropic.APIError as err:
        return ''.join(parts), False, err
    return ''.join(parts), finished, None

OpenAI SDK

python
import os
import openai

client = openai.OpenAI(
    base_url='https://api.cheaptokens.dev/v1',
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'

def ask_once(prompt, on_text=lambda t: None):
    parts, finish = [], None
    try:
        stream = client.chat.completions.create(
            model=MODEL,
            max_tokens=1024,
            stream=True,
            messages=[{'role': 'user', 'content': prompt}],
        )
        for chunk in stream:
            if not chunk.choices:
                continue
            choice = chunk.choices[0]
            if choice.delta.content:
                parts.append(choice.delta.content)
                on_text(choice.delta.content)
            if choice.finish_reason:
                finish = choice.finish_reason
    except openai.APIError as err:
        return ''.join(parts), False, err
    return ''.join(parts), finish is not None, None

มีสองรายละเอียดที่ควรดูอีกครั้ง อย่างแรก ตัวจัดการทั้งสองจับคลาส base ของข้อผิดพลาดใน SDK (anthropic.APIError, openai.APIError) ดังนั้นจึงไม่สำคัญว่า SDK จะโยนคลาสย่อยใดสำหรับความล้มเหลวหนึ่ง ๆ อย่างที่สอง แฟล็ก finished แยกจากข้อยกเว้น: สตรีมอาจจบก่อนกำหนดโดยไม่มีข้อยกเว้นเลยก็ได้ และกรณีนั้นต้องนับเป็นความล้มเหลวด้วย

README ของ openai-python เสริมสิ่งที่ควรคาดหวังฝั่ง OpenAI (ตรวจสอบเมื่อ 1 พฤศจิกายน 2026): read timeout ในสตรีมจะโยน APITimeoutError, ความล้มเหลว HTTPX อื่น ๆ โยน APIConnectionError พร้อมข้อยกเว้นดั้งเดิมใน __cause__ ถ้าสตรีมค้างแทนที่จะล้มเหลว ดูtimeouts และ retries ใน OpenAI Python SDK

ลองใหม่โดยไม่พิมพ์คำตอบซ้ำ#

นี่คือบั๊กที่พลาดง่ายมาก ความพยายามครั้งแรกพิมพ์ "นโยบายการคืนเงินคือ…" แล้วตาย การลองใหม่เริ่มต้นใหม่และพิมพ์ "นโยบายการคืนเงินคือ…" อีกครั้ง — ตอนนี้ผู้ใช้เห็นต้นข้อความสองครั้ง หรือประโยคที่ต่อกับจุดเริ่มต้นของตัวเอง สาเหตุปกติคือบัฟเฟอร์ที่ใช้ร่วมกัน: answer += text อยู่นอกความพยายาม ดังนั้นครั้งที่สองจึงต่อท้ายครั้งแรก

วิธีแก้ไม่น่าตื่นเต้น ความพยายามหนึ่งครั้งเป็นเจ้าของบัฟเฟอร์หนึ่งอัน ข้อความจากความพยายามที่ล้มเหลวถูกทิ้ง (หรือเก็บไว้แค่ในล็อก) และการลองใหม่เริ่มจากศูนย์:

python
import time

def ask_with_retry(prompt, attempts=3, on_text=lambda t: None, on_restart=lambda: None):
    for attempt in range(attempts):
        text, finished, err = ask_once(prompt, on_text)
        if finished:
            return text
        status = getattr(err, 'status_code', None)
        if status is not None and 400 <= status < 500 and status not in (408, 409, 429):
            raise err  # a 400/401/404 will not fix itself
        print(f'attempt {attempt + 1} failed after {len(text)} chars: {err!r}')
        if attempt == attempts - 1:
            break
        on_restart()  # clear the message box in your UI
        time.sleep(2 ** attempt)  # 1 s, 2 s
    raise RuntimeError('stream failed after all attempts')

on_restart คือส่วนที่หยุดการซ้ำ ใน UI เว็บ มันล้างกล่องข้อความก่อนที่ความพยายามถัดไปจะสตรีมเข้ามา ในเทอร์มินัลธรรมดาคุณลบที่พิมพ์ไปแล้วไม่ได้ ดังนั้นพิมพ์เฉพาะเมื่อ finished เป็นจริง หรือพิมพ์บรรทัด --- retrying --- ที่มองเห็นได้ การลองใหม่จากศูนย์ส่งพรอมป์ทั้งหมดซ้ำ ดังนั้นอินพุตจะถูกคิดเงินอีกครั้งสำหรับความพยายามใหม่ สำหรับคำตอบแชทส่วนใหญ่ นั่นเป็นราคาที่สมเหตุสมผลเพื่อคำตอบที่สะอาด

เรียงลำดับ

เรียงลำดับขั้นตอนการลองใหม่ตามลำดับที่โค้ดควรทำ

  1. หยุดอ่านสตรีมที่พังและเก็บข้อความบางส่วนไว้ในล็อก
  2. ตรวจสอบข้อผิดพลาด: 4xx อื่นที่ไม่ใช่ 408, 409 และ 429 หมายถึงหยุด ไม่ใช่ลองใหม่
  3. ล้างกล่องข้อความและเริ่มบัฟเฟอร์ใหม่
  4. รอด้วย exponential backoff (1 วินาที แล้ว 2 วินาที)
  5. ส่งคำขอทั้งหมดอีกครั้งและแสดงเฉพาะคำตอบใหม่

SDK ของ Anthropic ลองใหม่ความล้มเหลวชั่วคราวอยู่แล้ว — connection error, rate limit, 5xx — ด้วย exponential backoff, ค่าเริ่มต้น 2 ครั้ง, เคารพ retry-after และ max_retries เปลี่ยนค่านั้น นั่นครอบคลุมตัวคำขอ สตรีมที่ตายหลัง 200 เป็นอีกเรื่อง: เอกสารบอกว่าการจัดการข้อผิดพลาดมาตรฐานใช้ไม่ได้ที่นั่น ดังนั้นลูปด้านบนจึงเป็นของคุณเอง สำหรับ 429 โดยเฉพาะ มีคำแนะนำแยก: 429 Too Many Requests: retries with backoff

ไม่ใช่ทุกการหยุดก่อนกำหนดที่เป็นข้อผิดพลาด#

ก่อนลองใหม่ ให้ตรวจสอบว่าคำตอบจบเพราะอะไร สตรีมที่จบด้วย stop_reason: max_tokens (Anthropic) หรือ finish_reason: length (OpenAI) จบแล้วจริง ๆ — มันแค่ชนขีดจำกัดที่คุณตั้งไว้ การลองใหม่จะได้ข้อความที่ถูกตัดเหมือนเดิมและบิลเท่าเดิม เพิ่มขีดจำกัดแทน; คู่มือคำตอบที่ถูกตัด อธิบายไว้

มีกลุ่มที่สาม: เครือข่าย การเชื่อมต่อที่ไม่ได้ใช้งานอาจถูกตัดระหว่างทาง ซึ่งแสดงเป็น connection error หรือสตรีมที่หยุดโดยไม่มีอีเวนต์ใด ๆ จัดการเหมือนข้อผิดพลาดกลางสตรีม และดูข้อผิดพลาดการเชื่อมต่อที่พบบ่อย ถ้ามันเกิดขึ้นซ้ำ สำหรับการสร้างที่ยาวนาน Anthropic แนะนำให้สตรีมมิ่งแทน max_tokens แบบไม่สตรีมขนาดใหญ่ ดังนั้นคุณอยู่ในด้านที่ถูกต้องแล้ว

ก่อนปล่อย#

บันทึก request id ของทุกความพยายามที่ล้มเหลว บอดี้ข้อผิดพลาดของ Anthropic มีฟิลด์ request_id และเฮดเดอร์รีสปอนส์ request-id และ Python SDK เปิดเผย message._request_id; ด้วย id นั้น ความล้มเหลวจึงตรวจสอบย้อนกลับได้แทนที่จะเป็นแค่เรื่องเล่า

เช็กลิสต์

การจัดการข้อผิดพลาดสตรีมมิ่ง ทำให้ถูกต้อง

ถ้าอยากเห็นตัวจัดการทำงาน ให้ชี้ไปที่คีย์ของคุณเอง: สร้าง API key ในเอกสาร, ตั้งค่า CHEAPTOKENS_API_KEY, และเรียก ask_with_retry('Explain SSE in two sentences') สำหรับพื้นฐานของสตรีมมิ่งใน Python เริ่มที่สตรีมมิ่งรีสปอนส์ใน Python; ฝั่ง Anthropic ครอบคลุมในAnthropic Python SDK กับ CheapTokens

โฟลว์การลองใหม่: สตรีมเริ่ม, ข้อผิดพลาดมาถึง, ข้อความบางส่วนถูกทิ้ง, ส่งคำขออีกครั้ง
ลูปการลองใหม่ในห้าขั้นตอน: เริ่ม, อีเวนต์ข้อผิดพลาด, ทิ้งบัฟเฟอร์บางส่วน, back off, ถามอีกครั้ง