WebSocket / Socket.IO
RAG 검색 및 툴 실행 중 실시간 상태 이벤트
개요
백엔드는 채팅 작업 중 실시간 상태 이벤트를 전송하기 위해 Socket.IO(일반 WebSocket이 아님)를 사용합니다. 이 이벤트는 RAG 검색 및 에이전트 툴 실행 중 클라이언트에 진행 상황 업데이트를 푸시하는 데 사용되며, SSE를 통해 최종 채팅 완성 응답이 도착하기 전에 사용자에게 실시간 상태를 표시할 수 있습니다.
기본 URL:
연결
Socket.IO 연결 수립
JWT 토큰 또는 API 키를 인증으로 사용하여 socket.io-client 라이브러리로 Socket.IO 서버에 연결합니다:
import { io } from 'socket.io-client';
const socket = io('https://<backend-host>', {
auth: { token: '<jwt_or_api_key>' },
transports: ['websocket'],
});파라미터:
auth.token: JWT 토큰(로그인에서 받은) 또는 API 키(장기 대안)transports: WebSocket 전송을 보장하려면['websocket']사용 (폴링으로 폴백 방지)
이벤트
서버에서 전송되는 이벤트
서버는 채팅 작업 중 다음 실시간 이벤트를 전송합니다:
status
RAG 검색 또는 툴 실행 중 진행 상황 업데이트.
처리 중 실시간 진행 상황 이벤트.
이벤트 페이로드
{
"action": "retrieving",
"description": "Searching knowledge base for relevant documents...",
"done": false
}페이로드 필드
| 필드 | 타입 | 설명 |
|---|---|---|
action | string | 작업 유형: retrieving, processing, executing_tool 등 |
description | string | 사용자에게 표시할 사람이 읽을 수 있는 상태 메시지 |
done | boolean | 이 단계가 완료된 경우 true, 진행 중이면 false |
사용 예시
socket.on('status', (data) => {
console.log(data.action, data.description, data.done);
// Update UI with progress: "Searching knowledge base..."
});source
응답과 관련된 검색된 지식 청크.
RAG 중 검색된 지식 베이스 소스.
이벤트 페이로드
[
{
"document_id": "doc-uuid",
"chunk_id": "chunk-uuid",
"title": "Product Overview",
"content": "Our product provides AI-powered...",
"score": 0.92,
"metadata": {
"source_url": "https://docs.example.com/overview",
"created_at": 1700000000
}
}
]페이로드 필드
| 필드 | 타입 | 설명 |
|---|---|---|
document_id | string | 소스 문서의 UUID |
chunk_id | string | 검색된 특정 청크의 UUID |
title | string | 소스의 제목 또는 제목 |
content | string | 문서의 관련 발췌 |
score | number | 관련성 점수 (0-1, 높을수록 더 관련성 높음) |
metadata | object | 추가 메타데이터 (소스 URL, 날짜 등) |
사용 예시
socket.on('source', (sources) => {
sources.forEach((source) => {
console.log(`[${source.score.toFixed(2)}] ${source.title}`);
// Render citation/source UI
});
});follow-up
응답 후 생성된 후속 질문 제안.
응답 후 제안된 후속 질문.
이벤트 페이로드
[
"How do I enable single sign-on?",
"What are the system requirements?",
"How do I integrate with external APIs?"
]페이로드 필드
각 항목이 사용자가 다음에 물어볼 수 있는 후속 질문인 문자열 배열.
사용 예시
socket.on('follow-up', (questions) => {
const followUpUI = document.getElementById('follow-ups');
followUpUI.innerHTML = questions
.map(q => `<button onclick="sendMessage('${q}')">
${q}
</button>`)
.join('');
});권장 패턴
설정: 요청 전에 연결 및 리스너 설정
이벤트가 즉시 발생할 수 있으므로 채팅 완성 요청을 보내기 전에 항상 이벤트 리스너를 설정하세요:
const sessionId = crypto.randomUUID();
// Set up listeners FIRST
socket.on('status', (data) => {
updateProgressUI(data.description);
});
socket.on('source', (sources) => {
displayCitations(sources);
});
socket.on('follow-up', (questions) => {
displayFollowUpButtons(questions);
});
// Then send the chat request
const response = await fetch('/api/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'my-model',
messages: [...],
stream: true,
session_id: sessionId, // Must match Socket.IO session
}),
});세션 동기화
Socket.IO 연결 설정 시 사용하는 session_id는 채팅 완성 요청 본문의 session_id 필드와 일치해야 합니다. 서버는 이를 사용하여 올바른 클라이언트로 이벤트를 라우팅합니다:
// Client 1
const sessionId1 = 'session-uuid-1';
socket.emit('set_session', { session_id: sessionId1 });
// Then send chat request with same session_id
fetch('/api/chat/completions', {
body: JSON.stringify({
model: 'gpt-4',
messages: [...],
session_id: sessionId1, // ← Must match
}),
});
// Client 2 won't receive Client 1's events because session_ids differ
const sessionId2 = 'session-uuid-2';
socket.emit('set_session', { session_id: sessionId2 });오류 처리
연결 실패
socket.on('connect_error', (error) => {
console.error('Connection failed:', error);
// Fallback: poll or retry without real-time events
});
socket.on('disconnect', (reason) => {
if (reason === 'io server disconnect') {
socket.connect();
}
});만료된 토큰
JWT 토큰이 만료되면 Socket.IO 연결이 끊깁니다. 새 토큰으로 재연결하세요:
socket.on('disconnect', () => {
// Get new token
const newToken = await refreshToken();
// Reconnect with new credentials
socket = io('https://<backend-host>', {
auth: { token: newToken },
transports: ['websocket'],
});
});이벤트 흐름 예시
RAG를 사용한 채팅 완성 중 이벤트가 흐르는 방식을 보여주는 전체 예시입니다:
// 1. User sends message
const userMessage = "How do I enable SSO?";
// 2. Server emits status updates
socket.on('status', (data) => {
// "Searching knowledge base for relevant documents..."
// "Analyzing retrieved documents..."
// "Generating response..."
});
// 3. Server emits retrieved sources
socket.on('source', (sources) => {
// [{title: "SSO Setup", score: 0.95, content: "..."}]
});
// 4. Chat completion streams via SSE
const response = await fetch('/api/chat/completions', {
method: 'POST',
body: JSON.stringify({
model: 'gpt-4',
messages: [{ role: 'user', content: userMessage }],
stream: true,
session_id: sessionId,
}),
});
// 5. Read SSE stream
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split('\n')) {
if (!line.startsWith('data: ')) continue;
const payload = line.slice(6).trim();
if (payload === '[DONE]') return;
const chunk = JSON.parse(payload);
// Process: chunk.choices[0].delta.content
}
}
// 6. Server emits follow-up suggestions
socket.on('follow-up', (questions) => {
// ["What about SAML?", "How do I troubleshoot OIDC?"]
});성능 고려 사항
- 세션 ID: 각 채팅 요청에 고유한 세션 ID를 사용하여 이벤트 라우팅이 올바르게 되도록 하세요
- 이벤트 버퍼링: 초기 이벤트를 놓치지 않으려면 요청을 보내기 전에 연결하세요
- 정리: 메모리 누수를 방지하기 위해 더 이상 필요하지 않을 때 리스너를 제거하세요
- 재연결: Socket.IO는 네트워크 장애 시 자동으로 재연결을 처리합니다
- 토큰 만료: 연결 안정성을 유지하기 위해 만료 전에 토큰을 갱신하세요