AI 챗봇 API
순라이프 AI 공지 챗봇 — 메시지 전송(NDJSON 스트리밍), 중단, 대화 기록, 답변 평가
개요
순라이프 AI는 학교 공지·학사일정 등을 근거로 답변하는 챗봇입니다. 자체 호스팅된 vLLM 서버(OpenAI 호환 /chat/completions API, stream=True)를 사용하며, 공지 검색·시맨틱 검색·학사일정 조회 등의 **도구 호출(tool calling)**을 통해 최신 정보를 찾아 답변합니다.
챗봇도 다른 포털 API와 동일하게 로그인이 필요합니다(sch_session 쿠키). 별도 공개 엔드포인트가 아닙니다.
엔드포인트 목록
| 메서드 | 경로 | 설명 |
|---|---|---|
POST | /api/chat/ | 메시지 전송 — NDJSON 스트리밍 응답 |
POST | /api/chat/stop/ | 진행 중인 답변 생성 중단 |
POST | /api/chat/clear/ | 대화 컨텍스트 초기화 (새 대화) |
GET | /api/chat/health/ | DB·LLM·시맨틱 검색 서버 상태 확인 |
GET | /api/chat/history/ | 저장된 대화 목록 조회 |
POST | /api/chat/history/ | 로컬(브라우저) 대화 기록을 서버로 이전 |
DELETE | /api/chat/history/ | 저장된 대화 기록 전체 삭제 |
GET | /api/chat/history/{id}/ | 특정 대화 상세 조회 |
DELETE | /api/chat/history/{id}/ | 특정 대화 삭제 |
POST | /api/chat/feedback/ | 답변 평가(좋아요/싫어요) 제출 |
모든 엔드포인트는 sch_session 쿠키 인증이 필요합니다.
대화 기록 저장 방식 — 로컬 저장 vs 클라우드 동기화
기본값은 로컬 저장입니다. 사용자가 설정에서 명시적으로 동의(클라우드 동기화 켬)하지 않으면 학번과 대화 내용은 서버 DB에 남지 않습니다.
| 모드 | 저장 위치 | 서버에 보내는 값 |
|---|---|---|
| 로컬 저장 (기본값) | 브라우저 localStorage (sch-chat-history 키, 최대 30개 대화) | save_history: false + 최근 대화 맥락(local_turns)을 매 요청마다 함께 전송 |
| 클라우드 동기화 (동의 시) | 서버 DB (ChatConversation, 학번 기준) | save_history: true + conversation_id |
클라우드 동기화를 켜면 로컬 대화가 POST /api/chat/history/로 서버에 이전되고, 끄면 서버 대화가 다시 로컬로 내려온 뒤 DELETE /api/chat/history/로 서버 기록이 삭제됩니다.
저장 설정과 무관하게, 익명화된 문답 로그(ChatLog)는 답변 품질 검토를 위해 항상 남습니다. 학번 등 식별 정보는 포함하지 않으며, 같은 대화의 턴을 묶기 위한 키는 비밀키 HMAC으로 파생되어 원래 사용자로 역추적할 수 없습니다.
POST /api/chat/
메시지 1건을 전송하고 답변을 NDJSON(줄바꿈으로 구분된 JSON) 스트림으로 받습니다.
요청 본문
클라우드 동기화 모드 (save_history: true)
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
message | string | O | 사용자 메시지 (최대 4000자) |
high_quality | boolean | 아니오 | 고성능 모드 여부 (아래 참조) |
save_history | boolean | O | true |
conversation_id | number | null | 아니오 | 이어서 대화할 서버 저장 대화 id. null/생략 시 새 대화 |
{
"message": "이번 학기 기숙사 규정 뭐가 바뀌었어?",
"high_quality": false,
"save_history": true,
"conversation_id": null
}로컬 저장 모드 (save_history: false, 기본값)
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
message | string | O | 사용자 메시지 (최대 4000자) |
high_quality | boolean | 아니오 | 고성능 모드 여부 |
save_history | boolean | O | false |
local_key | string | 아니오 | 브라우저에서 생성한 대화 식별 키 (예: "loc-<uuid>") |
local_turns | object[] | 아니오 | 최근 대화 맥락 — [{"question": "...", "answer": "..."}] (최근 대화만, 항목당 텍스트 길이 제한 있음) |
{
"message": "이번 학기 기숙사 규정 뭐가 바뀌었어?",
"high_quality": false,
"save_history": false,
"local_key": "loc-8f3a2b1c",
"local_turns": [
{ "question": "안녕", "answer": "안녕하세요! 순라이프 AI입니다." }
]
}서버는 사용자당 답변을 한 번에 하나만 처리합니다. 이전 답변이 아직 생성 중일 때 새 요청을 보내면 곧바로 {"type":"error","message":"이전 답변이 아직 생성 중이에요."}가 반환됩니다.
응답 — NDJSON 스트림
Content-Type: application/x-ndjson. 줄 단위로 아래 이벤트 타입이 순서대로 도착합니다.
| 이벤트 | 필드 | 설명 |
|---|---|---|
meta | conversation_id | 새 서버 저장 대화가 처음 생성된 시점에 1회 전송 — 이후 요청부터 이 값을 conversation_id로 사용 |
trace | message | 진행 상황 안내 문구 (예: "공지사항을 찾는 중이에요.") — 내부 도구명은 노출되지 않음 |
delta | text | 최종 답변 텍스트 조각 (6자 단위, 초당 약 500자 — 타자기 효과) |
ping | — | 15초 이상 이벤트가 없을 때 보내는 keepalive (프록시 idle timeout 방지) |
done | stopped(선택) | 턴 종료. 사용자가 중단한 경우 stopped: true |
error | message | 오류 발생 |
delta는 LLM 토큰을 실시간으로 그대로 흘리는 것이 아닙니다. 답변을 전부 생성하고 검증까지 마친 뒤 확정된 텍스트를 잘게 나눠 스트리밍하는 방식입니다.
스트림 예시
{"type":"trace","message":"질문을 살펴보는 중이에요."}
{"type":"meta","conversation_id":42}
{"type":"trace","message":"공지사항을 찾는 중이에요."}
{"type":"delta","text":"이번 학"}
{"type":"delta","text":"기부터 "}
{"type":"ping"}
{"type":"delta","text":"바뀐 규정은..."}
{"type":"done"}에러 응답
| 에러 코드 | HTTP | 설명 |
|---|---|---|
UNAUTHORIZED | 401 | 세션 없음 또는 만료 |
EMPTY_MESSAGE | 400 | message가 빈 문자열 |
MESSAGE_TOO_LONG | 400 | message가 4000자 초과 |
INVALID_CONVERSATION_ID | 400 | conversation_id가 정수로 변환 불가 |
스트림 도중 발생하는 오류(대화를 찾지 못함, LLM 호출 실패 등)는 200 응답 안에서 {"type":"error", ...} 이벤트로 전달됩니다.
고성능 모드 (high_quality)
첨부문서(공지 첨부 PDF/HWP/DOCX 등)를 근거로 답변할 때, 일부 페이지만 보고 답하다 보면 중간에 있는 내용을 놓칠 수 있습니다. 고성능 모드는 첨부 전체 페이지를 배치로 전사(轉寫)해 근거 누락을 줄입니다. 그만큼 응답 시간이 길어집니다.
| 항목 | 일반 모드 | 고성능 모드 |
|---|---|---|
| 첨부 페이지 처리 | 처음 3페이지 + 마지막 페이지만 | 전체 페이지 |
| 이미지 처리 한도 | 최대 8장 | 최대 32장 (8장씩 배치 전사) |
"규정 변경"을 묻는 질문은 high_quality 값과 무관하게 서버가 자동으로 전체 페이지 조회 모드로 전환합니다. 또한 특정 문구(기숙사 규정 변경 질문)를 그대로 보내면 서버가 고성능 모드를 강제로 켭니다 — 프론트엔드도 동일한 문구를 감지해 "고성능 모드로 자동 전환했어요" 안내를 보여줍니다.
POST /api/chat/stop/
진행 중인 답변 생성을 즉시 중단합니다. 이미 생성된 부분까지는 스트림으로 전달되었을 수 있으나, 해당 턴은 대화 기록에 저장되지 않습니다.
요청
Body 없음.
응답
204 No Content
에러 응답
| 에러 코드 | HTTP | 설명 |
|---|---|---|
UNAUTHORIZED | 401 | 세션 없음 또는 만료 |
클라이언트가 fetch 연결을 그냥 끊어도(페이지 이탈, 브라우저 탭 닫기 등) 서버가 이를 감지해 동일하게 중단 처리합니다 — 반드시 이 엔드포인트를 호출해야만 중단되는 것은 아닙니다.
POST /api/chat/clear/
서버 메모리에 유지 중인 대화 컨텍스트를 초기화합니다(새 대화 시작). 진행 중인 답변 생성이 있다면 함께 중단됩니다.
요청
Body 없음.
응답
204 No Content
에러 응답
| 에러 코드 | HTTP | 설명 |
|---|---|---|
UNAUTHORIZED | 401 | 세션 없음 또는 만료 |
GET /api/chat/history/
내 대화 기록 목록을 최신순으로 반환합니다. (클라우드 동기화로 서버에 저장된 대화만 해당 — 로컬 저장 모드의 대화는 브라우저에만 있어 이 API로 조회되지 않습니다.)
쿼리 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
full | string | 아니오 | "1"이면 각 대화의 turns(전체 문답 내용) 포함 |
응답 — 200 OK
{
"conversations": [
{
"id": 42,
"title": "이번 학기 기숙사 규정 뭐가 바뀌었어?",
"updated_at": "2026-07-20T15:30:00+09:00"
}
]
}full=1이면 각 항목에 turns 배열([{question, answer, trace}])이 추가됩니다.
에러 응답
| 에러 코드 | HTTP | 설명 |
|---|---|---|
UNAUTHORIZED | 401 | 세션 없음 또는 만료 |
POST /api/chat/history/
브라우저(로컬 저장)에 있던 대화 기록을 서버로 이전합니다. 클라우드 동기화를 켤 때 호출됩니다.
멱등(idempotent) 처리됩니다. 각 로컬 대화의 local_id를 기준으로 이미 이전된 대화는 다시 만들지 않고 기존 서버 id를 그대로 반환합니다 — 이전 도중 실패 후 재시도해도 중복 저장되지 않습니다.
요청 본문
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
conversations | object[] | O | 이전할 로컬 대화 목록 |
conversations[].local_id | string | 아니오 | 브라우저의 로컬 대화 id — 멱등 처리 키 |
conversations[].title | string | 아니오 | 대화 제목. 생략 시 첫 질문으로 자동 생성 |
conversations[].turns | object[] | O | [{question, answer}] 형식의 문답 목록 |
{
"conversations": [
{
"local_id": "loc-8f3a2b1c",
"title": "기숙사 규정 문의",
"turns": [
{ "question": "이번 학기 기숙사 규정 뭐가 바뀌었어?", "answer": "..." }
]
}
]
}응답 — 200 OK
{
"mapping": {
"loc-8f3a2b1c": 42
}
}mapping은 로컬 대화 id → 서버에 생성/재사용된 대화 id의 매핑입니다.
에러 응답
| 에러 코드 | HTTP | 설명 |
|---|---|---|
UNAUTHORIZED | 401 | 세션 없음 또는 만료 |
INVALID_PAYLOAD | 400 | conversations가 배열이 아님 |
DELETE /api/chat/history/
내 대화 기록을 서버에서 전부 삭제합니다. 클라우드 동기화를 끌 때 호출됩니다.
응답
204 No Content
에러 응답
| 에러 코드 | HTTP | 설명 |
|---|---|---|
UNAUTHORIZED | 401 | 세션 없음 또는 만료 |
GET /api/chat/history/{id}/
특정 대화의 전체 내용을 조회합니다.
응답 — 200 OK
{
"id": 42,
"title": "이번 학기 기숙사 규정 뭐가 바뀌었어?",
"turns": [
{ "question": "...", "answer": "...", "trace": ["..."] }
],
"updated_at": "2026-07-20T15:30:00+09:00"
}에러 응답
| 에러 코드 | HTTP | 설명 |
|---|---|---|
UNAUTHORIZED | 401 | 세션 없음 또는 만료 |
NOT_FOUND | 404 | 해당 id의 대화가 없거나 내 대화가 아님 |
DELETE /api/chat/history/{id}/
특정 대화를 삭제합니다.
응답
204 No Content
에러 응답
| 에러 코드 | HTTP | 설명 |
|---|---|---|
UNAUTHORIZED | 401 | 세션 없음 또는 만료 |
POST /api/chat/feedback/
답변에 대한 좋아요/싫어요 평가를 제출합니다. 추후 응답 품질 개선(RLHF 학습 데이터)에 활용됩니다.
로그인한 사용자만 제출할 수 있지만, 저장되는 데이터에는 학번 등 식별 정보가 포함되지 않습니다.
요청 본문
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
rating | string | O | "up" 또는 "down" |
question | string | O | 평가 대상 질문 |
answer | string | O | 평가 대상 답변 |
comment | string | 아니오 | 사용자 의견 (최대 2000자) |
context | object[] | 아니오 | 직전 문맥 — [{question, answer}] 최대 3쌍 |
high_quality | boolean | 아니오 | 고성능 모드로 생성된 답변인지 여부 |
{
"rating": "up",
"question": "이번 학기 기숙사 규정 뭐가 바뀌었어?",
"answer": "...",
"comment": "표로 정리해줘서 좋았어요",
"context": [],
"high_quality": true
}응답
201 Created
에러 응답
| 에러 코드 | HTTP | 설명 |
|---|---|---|
UNAUTHORIZED | 401 | 세션 없음 또는 만료 |
INVALID_RATING | 400 | rating이 "up"/"down"이 아님 |
EMPTY_TURN | 400 | question 또는 answer가 빈 문자열 |
GET /api/chat/health/
챗봇 관련 인프라(DB, LLM, 시맨틱 검색 서버) 상태를 확인합니다. 로그인 여부와 무관하게 호출 가능합니다.
응답 — 200 OK
{
"ok": true,
"db": "connected",
"model": "soonlife-qwen",
"embed_search": "connected (12480건 인덱싱됨)"
}| 필드 | 타입 | 설명 |
|---|---|---|
ok | boolean | DB 연결 정상 여부 |
db | string | "connected" 또는 에러 메시지 |
model | string | 현재 사용 중인 vLLM 모델명 |
embed_search | string | 시맨틱 검색 서버 상태 — "not_configured" / "connected (N건 인덱싱됨)" / "unhealthy" / "unreachable (...)" |
기숙사 규정 변경 질문 — 고정 답변
특정 조건에 맞는 질문(기숙사 관련 단어 + "바뀌다/변경/개정" 등 변경 관련 단어가 모두 포함되고, 보조 판별 모델이 "2026-2학기 기숙사 규정 변경 문의"로 확인한 경우)은 LLM 생성 과정을 완전히 건너뛰고 사람이 직접 검증한 고정 답변을 즉시 반환합니다. 모델이 첨부 문서의 비교표를 잘못 옮겨 적는 문제를 막기 위한 안전장치입니다.
이 경로로 응답되는 경우 도구 호출, 답변 검증 단계가 모두 생략되며, trace 이벤트도 발생하지 않고 곧바로 delta/done이 이어집니다.
챗봇 설정 (참고 — 서버 환경변수)
배포 환경에 따라 아래 값들이 챗봇 동작에 영향을 줍니다. (API 사용자 입장에서 직접 제어하는 값은 아니며, 서버 운영 참고용입니다.)
| 환경변수 | 기본값 | 설명 |
|---|---|---|
VLLM_BASE_URL | http://localhost:8000/v1 | vLLM 서버 주소 (OpenAI 호환 API) |
VLLM_MODEL | soonlife-qwen | 사용할 모델명 |
VLLM_CONTEXT_LEN | 0 (자동 감지) | 컨텍스트 길이 |
AGENT_MAX_TURNS | 10 | 한 답변 내 도구 호출 루프 최대 반복 횟수 |
VLLM_TIMEOUT | 180 | vLLM 요청 타임아웃(초) |
VLLM_TEMPERATURE | 0.3 | 샘플링 온도 |
VLLM_MAX_TOKENS | 4096 | 응답 최대 토큰 |
ENABLE_THINKING | true | 모델 reasoning(사고) 모드 사용 여부 |
EMBED_SEARCH_URL | "" (미설정) | 시맨틱 검색 서버 주소 — 미설정 시 관련 도구는 비활성화 |
EMBED_SEARCH_TIMEOUT | 20 | 시맨틱 검색 요청 타임아웃(초) |