스트리밍
모델 출력을 단계적으로 받고 이벤트, 증분 데이터, 완료 및 연결 중단을 올바르게 처리합니다.
Streaming은 모델이 전체 답변을 생성하기 전에 클라이언트가 증분 콘텐츠를 계속 수신하도록 합니다. 채팅 화면, 긴 텍스트 생성 및 첫 결과를 빠르게 보여줘야 하는 상황에 적합하지만, 이벤트 경계, 콘텐츠 결합, 완료 상태와 연결 중단을 올바르게 처리해야 합니다.
Streaming은 반환 방식만 바꾸며 작업 자체는 바꾸지 않습니다
일반적으로 같은 작업에 비스트리밍 또는 스트리밍 반환을 선택할 수 있습니다. 비스트리밍 요청을 먼저 확인한 후 Streaming을 활성화하면 요청 매개변수 문제와 스트리밍 처리 문제를 구분하기 쉽습니다.
사용 전 확인 사항
- 모델 목록에서 대상 모델이 선택한 프로토콜과 Streaming을 지원하는지 확인합니다.
- 프로토콜에 맞는 엔드포인트와 이벤트 구조를 사용하며, 하나의 파서로 모든 프로토콜을 추측하여 처리하지 않습니다.
- 클라이언트, 리버스 프록시와 게이트웨이가 전체 응답을 버퍼링한 후 한 번에 반환하지 않는지 확인합니다.
전체 필드는 각각 Chat Completions, Responses, Anthropic Messages 및 Gemini Stream Generate Content 레퍼런스에서 관리합니다.
최소 스트리밍 요청 보내기
다음은 Chat Completions의 스트리밍 출력을 활성화하는 예시입니다:
curl --no-buffer 'https://api.agtcloud.cn/v1/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer your-api-key' \
--data '{
"model": "MODEL_ID",
"messages": [
{"role": "user", "content": "지수 백오프를 세 가지 요점으로 설명해 주세요."}
],
"stream": true
}'--no-buffer는 cURL이 수신한 데이터를 즉시 표시하도록 하며 서버 이벤트 구조를 바꾸지 않습니다.
네트워크 패킷이 아닌 이벤트 단위로 읽기
HTTP 연결이 한 번에 전달하는 바이트 수가 완전한 이벤트 하나와 일치하지는 않습니다. 하나의 JSON 객체가 여러 데이터 청크로 나뉘거나 여러 이벤트가 같은 청크로 도착할 수 있습니다.
SSE에서는 먼저 빈 줄을 기준으로 완전한 이벤트를 식별한 후 해당 이벤트의 data: 내용을 읽어야 합니다:
const response = await fetch('https://api.agtcloud.cn/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer your-api-key',
},
body: JSON.stringify({
model: 'MODEL_ID',
messages: [{ role: 'user', content: '지수 백오프가 무엇인지 설명해 주세요.' }],
stream: true,
}),
});
if (!response.ok || !response.body) {
throw new Error(`요청 실패: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n');
buffer = events.pop() ?? '';
for (const event of events) {
const data = event
.split('\n')
.filter((line) => line.startsWith('data:'))
.map((line) => line.slice(5).trimStart())
.join('\n');
if (!data || data === '[DONE]') continue;
const chunk = JSON.parse(data);
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}
}운영 코드는 요청 취소, 읽기 시간 초과, 하트비트, JSON이 아닌 이벤트 및 스트림 종료 시 처리되지 않은 버퍼도 다뤄야 합니다.
네 프로토콜의 스트리밍 구조
Chat은 일반적으로 Chat Completion Chunk를 반환합니다. 텍스트는 choices[].delta.content, 도구 매개변수는 choices[].delta.tool_calls에 나타날 수 있고, 종료 이유는 finish_reason에 있습니다. 이벤트 스트림은 [DONE]으로 끝날 수 있습니다.
프로토콜 엔드포인트가 존재한다고 해서 모든 모델이 위 콘텐츠 유형을 전부 반환하지는 않습니다. 애플리케이션이 실제로 사용하고 대상 모델에서 반환을 확인한 분기만 구현합니다.
텍스트 및 도구 매개변수 결합
- 텍스트 증분은 후보, 출력 항목 또는 Content Block의 인덱스별로 결합합니다.
- 도구 이름, 호출 ID와 매개변수는 여러 번에 나뉘어 도착할 수 있으므로 호출마다 독립된 버퍼를 유지합니다.
- 도구 매개변수는 전체 수신이 끝난 후에만 JSON으로 해석하며 청크별로 실행해서는 안 됩니다.
- 추론 요약, Thinking Block과 최종 답변에는 각각 다른 버퍼를 사용하며 하나의 텍스트로 섞어서는 안 됩니다.
- 프로토콜이 마지막에 Usage를 반환한다면 완료 이벤트를 받을 때까지 연결을 계속 읽습니다.
성공, 실패 및 중단 판단
| 현상 | 클라이언트 처리 |
|---|---|
| 프로토콜의 완료 이벤트 수신 | 전체 결과와 Usage를 저장하고 요청을 성공으로 표시합니다 |
| 오류 또는 실패 이벤트 수신 | 이미 표시한 콘텐츠를 저장하고 요청을 실패로 표시합니다 |
| 연결이 조기에 종료됨 | 미완료로 표시하고 기존 텍스트를 완전한 답변으로 취급하지 않습니다 |
| 사용자가 직접 중지함 | 읽기를 취소하고 화면에 “생성 중지됨”으로 표시합니다 |
| 새 이벤트가 장시간 없음 | 애플리케이션의 시간 초과 정책에 따라 취소하고 필요한 진단 정보를 보관합니다 |
스트리밍 요청에서 일부 콘텐츠를 사용자에게 보여준 후에는 자동으로 무한 재실행해서는 안 됩니다. 재시도로 다른 답변이 생성되거나 도구 및 기타 부수 효과가 있는 동작이 중복 실행될 수 있습니다.
운영 전 확인 사항
- 연결 종료까지 기다리지 않고 화면에 텍스트가 점진적으로 표시됩니다.
- 중국어 및 멀티바이트 문자가 데이터 청크 분할로 깨지지 않습니다.
- 도구 매개변수가 호출 ID별로 올바르게 결합되고 실행 전에 검증됩니다.
- 사용자 취소, 네트워크 단절, 시간 초과 및 서버 오류에 명확한 상태가 있습니다.
- 로그에 API 키, 민감한 Prompt 전체 또는 마스킹하지 않은 도구 매개변수를 기록하지 않습니다.
다음으로 Tool Calling과 오류 및 문제 해결을 참고합니다.