API DocsChatsWebSocket

WebSocket / Socket.IO

RAG 검색 및 툴 실행 중 실시간 상태 이벤트

개요

백엔드는 채팅 작업 중 실시간 상태 이벤트를 전송하기 위해 Socket.IO(일반 WebSocket이 아님)를 사용합니다. 이 이벤트는 RAG 검색 및 에이전트 툴 실행 중 클라이언트에 진행 상황 업데이트를 푸시하는 데 사용되며, SSE를 통해 최종 채팅 완성 응답이 도착하기 전에 사용자에게 실시간 상태를 표시할 수 있습니다.

기본 URL:

연결

Socket.IO 연결 수립

JWT 토큰 또는 API 키를 인증으로 사용하여 socket.io-client 라이브러리로 Socket.IO 서버에 연결합니다:

javascript
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 검색 또는 툴 실행 중 진행 상황 업데이트.

SOCKET

처리 중 실시간 진행 상황 이벤트.

이벤트 페이로드

json
{
"action": "retrieving",
"description": "Searching knowledge base for relevant documents...",
"done": false
}

페이로드 필드

필드타입설명
actionstring작업 유형: retrieving, processing, executing_tool
descriptionstring사용자에게 표시할 사람이 읽을 수 있는 상태 메시지
doneboolean이 단계가 완료된 경우 true, 진행 중이면 false

사용 예시

javascript
socket.on('status', (data) => {
console.log(data.action, data.description, data.done);
// Update UI with progress: "Searching knowledge base..."
});

source

응답과 관련된 검색된 지식 청크.

SOCKET

RAG 중 검색된 지식 베이스 소스.

이벤트 페이로드

json
[
{
  "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_idstring소스 문서의 UUID
chunk_idstring검색된 특정 청크의 UUID
titlestring소스의 제목 또는 제목
contentstring문서의 관련 발췌
scorenumber관련성 점수 (0-1, 높을수록 더 관련성 높음)
metadataobject추가 메타데이터 (소스 URL, 날짜 등)

사용 예시

javascript
socket.on('source', (sources) => {
sources.forEach((source) => {
  console.log(`[${source.score.toFixed(2)}] ${source.title}`);
  // Render citation/source UI
});
});

follow-up

응답 후 생성된 후속 질문 제안.

SOCKET

응답 후 제안된 후속 질문.

이벤트 페이로드

json
[
"How do I enable single sign-on?",
"What are the system requirements?",
"How do I integrate with external APIs?"
]

페이로드 필드

각 항목이 사용자가 다음에 물어볼 수 있는 후속 질문인 문자열 배열.

사용 예시

javascript
socket.on('follow-up', (questions) => {
const followUpUI = document.getElementById('follow-ups');
followUpUI.innerHTML = questions
  .map(q => `<button onclick="sendMessage('${q}')">
    ${q}
  </button>`)
  .join('');
});

권장 패턴

설정: 요청 전에 연결 및 리스너 설정

이벤트가 즉시 발생할 수 있으므로 채팅 완성 요청을 보내기 전에 항상 이벤트 리스너를 설정하세요:

javascript
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 필드와 일치해야 합니다. 서버는 이를 사용하여 올바른 클라이언트로 이벤트를 라우팅합니다:

javascript
// 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 });

오류 처리

연결 실패

javascript
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 연결이 끊깁니다. 새 토큰으로 재연결하세요:

javascript
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를 사용한 채팅 완성 중 이벤트가 흐르는 방식을 보여주는 전체 예시입니다:

javascript
// 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는 네트워크 장애 시 자동으로 재연결을 처리합니다
  • 토큰 만료: 연결 안정성을 유지하기 위해 만료 전에 토큰을 갱신하세요