페이지 목차
이 페이지에서 검색
로컬 LLM
이 페이지에 묻기
로컬 LLM · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.
LOCAL · LLM
로컬 LLM
생성 수정
로컬 모델 실행과 GPU·메모리 이해부터 서빙·RAG·도구 호출·업무형 앱 구현까지 차례로 살펴봅니다.
42개 문서 · 5일차 · 38 / 42
37차시 · 애플리케이션 구현
지도 위치 : 5일차 · 프로젝트 5단계 · ⑤애플리케이션 층의 사용자 쪽 | 이전: 36차시 AI 기능 구현 | 다음: 38차시 Function Calling 및 서비스 통합 키워드 : 사용자 인터페이스, Streaming, 대화 처리 및 서비스 기능 구현
이 차시를 다 읽으면
- 상담원 화면을 입력 → 결과 카드 → 답변 초안 스트리밍 → 수정 · 확정 의 네 단계로 설계할 수 있다.
- 결과 카드는 한 번에 , 답변 초안은 스트리밍 으로 받는 이유를 축 ⑥과 ⑧로 설명할 수 있다.
- 브라우저 ↔ 앱 서버 ↔ 모델 서버의 두 구간 에서 SSE가 어떻게 흐르는지 말할 수 있다.
- LLM API가 무상태이므로 앱 서버가 상담원 · 문의 건별로 세션을 분리 해야 하는 이유를 말할 수 있다.
- 사람이 확정하는 흐름(HITL) 을 화면과 저장 구조에 넣을 수 있다.
- 두 브라우저 세션 · 긴 메일 · 중간에 끊기의 행동 시험 을 직접 할 수 있다.
0. 이 차시의 판별 축
| # | 축 | 왼쪽 (0) | 오른쪽 (1) | 이 축으로 갈리는 것 | 큰 축 (README 5장) |
|---|---|---|---|---|---|
| 1 | 기다렸다 받나, 흘려 받나 | 한 번에 받기 (결과 카드) | 스트리밍 (답변 초안) | 기능마다 받는 방식 | ⑥ 동기 ↔ 비동기 |
| 2 | 누가 기억하나 | 모델 서버 (무상태) | 앱 서버의 세션 저장소 (상태) | 대화 기록을 어디에 두나 | ⑦ 무상태 ↔ 상태 |
| 3 | 누구의 기록인가 | 전체가 하나를 공유 | 상담원 × 문의 건마다 분리 | 고객 정보가 섞이는가 | ⑤ 하나 ↔ 여럿 |
| 4 | 사람이 손을 대는가 | 상담원이 고쳐서 확정 | 자동 발송 | 최종 책임이 어디에 있나 | 사람이 손을 대는가 (⑦ 파생) |
앞에서 이어집니다 — 4일차 25차시 (API로 앱 만들기), 26차시 (스트리밍), 27차시 (대화 기록과 무상태)를 상담원 화면 하나에 모읍니다. README 5장의 두 축이 이 차시를 받칩니다. "비동기는 빠른 것이 아니다. 한 건의 시간은 같다"(축 ⑥, 26차시 1.2) — 스트리밍도 전체 생성 시간을 줄이지 않습니다. 그리고 "LLM API는 무상태다"(축 ⑦, 27차시 1절) — 모델 서버는 지난 요청을 기억하지 않으니 기억은 앱이 집니다.
1. 화면 흐름 — 상담원 화면 네 단계
1.1 화면
┌ ① 문의 입력 ─────────────────────────────────────────┐
│ [고객 문의 메일 붙여넣기 ...........................] [분석] │
├ ② 결과 카드 (한 번에) ────────────────────────────────┤
│ 분류: 배송지연 ▾ 주문번호: 20261128-0042 (원문 확인됨) │
│ 고객명: 고객A 연락처: 010-0000-0001 │
│ 요약: 주문 후 일주일째 배송이 없어 배송 현황 확인을 원함. │
│ ⚠ 수동 확인 필요 (검사에 걸린 경우에만 표시) │
├ ③ 답변 초안 (스트리밍) ───────────────────────────────┤
│ 고객님, 배송이 늦어져 불편을 드려 죄송합니다. 확인해 보니▌ [중지] │
├ ④ 수정 · 확정 ────────────────────────────────────────┤
│ [초안 직접 수정 가능] [더 공손하게] [다시 쓰기] [확정] │
└────────────────────────────────────────────────────┘
| 단계 | 앱 서버 호출 | 받는 방식 | 상담원이 하는 일 | 배운 곳 |
|---|---|---|---|---|
| ① 입력 | — | — | 메일 붙여넣기 | — |
| ② 결과 카드 | POST /api/triage | 한 번에 (JSON) | 분류 · 필드 확인, 틀리면 고침 | 36차시 |
| ③ 답변 초안 | POST /api/reply | 스트리밍 (SSE) | 읽기 시작, 필요하면 중지 | 26차시 |
| ④ 후속 요청 · 확정 | POST /api/reply (기록 포함) · POST /api/confirm | 스트리밍 · 한 번에 | 고치기, 확정 | 27차시, 4절 |
1.2 ⭐ 결과 카드는 왜 스트리밍하지 않나
| 결과 카드 (분류 · 추출) | 답변 초안 | |
|---|---|---|
| 출력 | 짧은 JSON | 몇 문단의 글 |
| 검사 | 다 와야 스키마 · 원문 포함 검사 를 할 수 있음 (36차시 3.2) | 사람이 읽으며 판단 |
| 반쯤 온 결과 | {"category": "배송 — 쓸모없고 오해만 삼 | 앞부분만으로도 읽을 수 있음 |
| 결론 | 한 번에 | 스트리밍 |
축 ⑥과 축 ⑧이 함께 답을 정합니다. 형식 검사가 필요한 출력은 다 받은 뒤에, 사람이 읽는 출력은 흘려서. "스트리밍이 더 좋은 기술"이 아닙니다.
2. 스트리밍 — SSE가 흐르는 길
2.1 정의와 두 구간
SSE (Server-Sent Events) — [HTTP 응답 방식]인데, [ 연결 하나를 열어 둔 채 서버가 조각을 data: 줄로 계속 보낸다 ]는 점에서 응답을 한 번에 보내고 닫는 일반 응답과, 양쪽이 모두 보낼 수 있는 WebSocket과 구별된다.
상담원 브라우저 ◀──SSE── 앱 서버 (FastAPI) ◀──SSE── vLLM (127.0.0.1:8000)
(fetch로 읽음) (받은 조각을 다시 흘려보냄) (35차시)
구간이 둘 입니다. 앞에서 배운 26차시에서는 Python이 모델 서버에서 직접 받았습니다. 이번에는 앱 서버가 중계 합니다. 브라우저가 모델 서버를 직접 부르지 않는 이유는 35차시 4.1절(모델 서버는 127.0.0.1)과 38차시(권한 검사는 앱 서버에서)에 있습니다.
2.2 앱 서버 코드
@app.post("/api/reply")
async def reply(req: ReplyRequest, request: Request):
messages = sessions.build_messages(req.session_id, req.instruction) # 3절
async def gen():
stream = await client.chat.completions.create(
model=settings.LLM_MODEL, messages=messages,
stream=True, temperature=0.4, max_tokens=500)
try:
async for chunk in stream:
if await request.is_disconnected():
break # 상담원이 떠나면 멈춤
delta = chunk.choices[0].delta.content or ""
if delta:
yield f"data: {json.dumps({'t': delta}, ensure_ascii=False)}\n\n"
if chunk.choices[0].finish_reason == "length":
yield 'data: {"warn": "초안이 길이 제한으로 잘렸습니다"}\n\n'
yield "data: [DONE]\n\n"
finally:
await stream.close() # 모델 서버 쪽 요청도 닫음
return StreamingResponse(gen(), media_type="text/event-stream")
🔍 읽는 포인트
| 줄 | 뜻 | 배운 곳 |
|---|---|---|
sessions.build_messages(req.session_id, …) | 기록을 세션별로 꺼내 매번 다시 보냄 | 27차시, 3절 |
stream=True / async for | 모델 서버에서 조각을 받음 | 26차시 |
is_disconnected() → break, finally: stream.close() | 상담원이 떠나면 모델 서버의 생성도 멈춤 | 5절 시험 ③ |
finish_reason == "length" | 잘렸다는 신호를 화면에 알림 | 20 · 29차시 |
media_type="text/event-stream" | SSE | 26차시 |
💡 브라우저의 EventSource는 GET만 됩니다. 문의 본문을 보내야 하므로 POST + fetch의 스트림 읽기를 씁니다(26차시).
2.3 ⚠️ 중간에서 모았다가 한 번에 보내는 경우
앱 서버 앞에 nginx 같은 리버스 프록시 (브라우저의 요청을 먼저 받아 뒤쪽 서버로 전달해 주는 중간 서버 — 주소 하나로 여러 서버를 묶거나 HTTPS를 처리할 때 둡니다)를 두면, 프록시가 응답을 모았다가 한 번에 보낼 수 있습니다. 화면에서는 "스트리밍이 안 된다"로 보입니다. proxy_buffering off로 끕니다. 확인은 curl -N으로 앱 서버를 직접 부른 결과와 프록시를 거친 결과를 비교합니다.
3. 대화 처리와 세션 분리
3.1 문의봇에서 "대화"란
상담원이 초안을 보고 "더 공손하게" , "배송 조회 결과를 한 줄 넣어 줘" 같은 후속 요청을 하는 것입니다. 모델 서버는 앞의 초안을 기억하지 않으므로(축 ⑦), 앱이 원문 · 결과 카드 · 이전 초안 · 후속 요청 을 매번 다시 보냅니다(8 · 27차시).
3.2 세션
세션 (문의봇) — [앱 서버의 상태 저장 단위]인데, [ 한 상담원이 한 문의 건을 처리하는 동안의 기록을 다른 기록과 섞이지 않게 묶는다 ]는 점에서 모델 서버 쪽 상태(KV 캐시 · 프리픽스 캐시)와 구별된다.
| 무엇 | 어디에 | 언제 지우나 |
|---|---|---|
| 세션 ID | 브라우저 (요청마다 함께 보냄) | 문의 건 종료 |
대화 기록 (messages) | 앱 서버의 세션 저장소, 세션 ID를 키로 | 확정 후 |
| 결과 카드 (triage) | 같은 세션에 | 확정 후 |
| 확정본 | 업무 DB (4절) | 업무 규칙대로 |
⚠️ 가장 위험한 실수 — 대화 기록을 앱 서버의 전역 변수 하나 에 두는 것. 상담원 10명이 같은 목록에 기록을 쌓으면, 상담원 A가 처리하던 고객의 이름과 연락처가 상담원 B의 초안에 나옵니다. 문의봇을 로컬로 만든 이유(개인정보)가 앱 안에서 무너지는 사고입니다.
💡 하루 프로젝트에서는 세션 저장소를 앱 서버 메모리의 사전(dict)으로 해도 됩니다. 단, 앱 서버를 재시작하면 사라지고, 앱 서버가 여러 대면 공유되지 않습니다. 운영으로 가면 "서버는 무상태로, 상태는 서버 밖 저장소로" 옮깁니다. 앱 서버가 기록을 들고 있지 않아야 몇 대로 늘리든, 재시작하든 기록이 남기 때문입니다. 서버 밖 저장소로는 Redis(메모리 기반의 빠른 키-값 저장소)나 DB를 씁니다. 33차시 범위 밖 목록에 적어 둡니다.
3.3 토큰 예산
max-model-len(35차시) ≥ 시스템 프롬프트 + 문의 원문 + 결과 카드 + 최근 N턴 + 출력 여유(max_tokens)
| 전략 (27차시) | 문의봇에서 |
|---|---|
| 최근 N턴만 유지 (슬라이딩 윈도) | 후속 요청은 최근 3턴 만 |
| 꼭 남길 것 | 문의 원문 과 결과 카드는 항상 포함 — 빼면 고객 사실이 사라짐 |
| 예산 초과 시 | 오래된 초안부터 뺌. 그래도 넘으면 화면에 "새 세션으로" 안내 |
⚠️ 기록이 길어질수록 매 요청의 입력이 길어져 TTFT가 늘어납니다 (Prefill, 2차시). 39차시 부하 테스트에서 "후속 요청 3턴째"를 별도 시나리오로 재는 이유입니다.
4. 사람이 확정하는 흐름 (HITL)
HITL (Human-in-the-Loop, 사람 확인 단계) — [시스템 설계 방식]인데, [ 시스템은 좋은 초안을 내고 최종 결정은 사람이 한다 ]는 점에서 결과를 그대로 내보내는 자동 처리와 구별된다. 앞에서 배운 도구 호출의 사람 확인(30차시 4.4) 을 이번에는 화면과 저장 구조로 만듭니다.
| 자동 처리 | 문의봇 (HITL) | |
|---|---|---|
| 분류 · 추출 | 그대로 저장 | 결과 카드에서 상담원이 고칠 수 있음 (드롭다운 · 입력칸) |
| 답변 | 자동 발송 | [확정]을 눌러야 확정본이 됨. 발송은 기존 업무 시스템에서 (범위 밖) |
| 틀렸을 때 | 고객에게 그대로 감 | 상담원이 고침 |
[확정]을 누르면 저장할 것
| 필드 | 왜 |
|---|---|
| 모델이 낸 원 초안 | 무엇을 고쳤는지 비교하려고 |
| 상담원의 확정본 | 실제 업무 결과 |
| 결과 카드의 원래 값 / 고친 값 | 분류 · 추출이 틀린 사례 = 다음 평가셋 후보 |
| 프롬프트 버전 · 모델 이름 | 어느 설정의 결과인지 (36차시 추적성) |
⭐ 이 저장이 있어야 33차시 FR-04의 의미 기준 — "수정 없이 확정한 비율" — 을 계산할 수 있습니다. 무엇이 나왔는지(원 초안 = 기록) 와 그 결과가 좋았는지(수정 여부 = 평가) 가 한 레코드에 연결돼 있어야, 어디를 고칠지 알 수 있고 다음 버전이 나아집니다.
⚠️ [확정]을 두 번 누르거나 네트워크 재시도로 요청이 두 번 가도, 확정본이 두 건 생기면 안 됩니다. 앞에서 배운 멱등성(25차시 5절, 30차시 4.5) — 같은 요청을 여러 번 해도 서버의 최종 상태 가 한 번 한 것과 같은 성질 — 이 필요한 자리입니다. 방법은 멱등성 키 입니다. 요청마다 "이 요청은 이 문의 건의 확정이다"라는 고유 키(여기서는 문의 건 ID)를 붙이고, 서버는 이미 처리한 키가 다시 오면 새로 저장하지 않고 앞의 결과를 돌려줍니다.
5. 행동 시험 — 직접 확인할 것
코드를 읽지 못해도 행동은 직접 볼 수 있습니다. 이 차시의 검수는 이것입니다.
| # | 시험 | 하는 법 | 기대 | 실패하면 의심할 곳 |
|---|---|---|---|---|
| ① | 두 브라우저 세션 | 일반 창과 시크릿 창(또는 다른 브라우저)에서 서로 다른 문의를 분석 → 각자 "고객명과 주문번호를 다시 말해 줘" | 각 창이 자기 문의의 고객만 말함 | 세션 키, 전역 변수 (3.2절) |
| ② | 긴 메일 | 평가셋의 가장 긴 메일, 그리고 max-model-len을 넘는 메일 | 앞의 것은 정상. 넘는 것은 경고 가 뜸 | 앱의 입력 길이 검사, finish_reason 처리 |
| ③ | 중간에 끊기 | 초안이 흘러나오는 중에 탭 닫기 또는 [중지] | 모델 서버의 실행 중 요청 수가 줄어듦 | is_disconnected, stream.close() (2.2절) |
| ④ | 모델 서버가 꺼졌을 때 | 35차시 서비스를 잠시 멈추고 [분석] | "모델 서버 응답 없음" 안내, 입력한 메일은 남아 있음 | 타임아웃 · 오류 처리 |
| ⑤ | 확정 두 번 | [확정] 빠르게 두 번 | 확정본 한 건 | 멱등성 키 (4절) |
💡 시험 ③의 확인 방법: 다른 터미널에서 curl -s http://127.0.0.1:8000/metrics -H "Authorization: Bearer $KEY" | grep running 을 반복해서 보며, 탭을 닫은 직후 실행 중 요청 수가 내려가는지 봅니다. 지표 이름은 버전에 따라 다르니 running, waiting이 들어간 줄을 찾습니다.
⚠️ 시험 ③이 실패하면 상담원이 창을 닫을 때마다 GPU가 아무도 읽지 않는 글 을 끝까지 만듭니다. 10명이 그러면 39차시의 처리량이 그만큼 깎입니다.
6. 실습 — 맡기고 검수하기
지시
문의봇 상담원 화면과 앱 서버 API를 구현해줘.
목표: 1.1절 화면 흐름(입력 → 결과 카드 → 답변 초안 스트리밍 → 수정·확정)을 FastAPI + 간단한 HTML/JS로.
맥락: 36차시의 classify_and_extract(), 프롬프트 파일(reply.v1.md), 35차시 vLLM(OpenAI 호환, .env).
제약:
- /api/triage 는 한 번에 JSON, /api/reply 는 SSE(text/event-stream), 브라우저는 fetch로 스트림 읽기.
- 대화 기록은 세션 ID별로 분리. 전역 리스트 하나에 쌓지 마.
- 클라이언트가 끊으면 모델 서버 쪽 스트림도 닫아.
- finish_reason이 length면 화면에 경고.
- 자동 발송 기능을 만들지 마. [확정]은 원 초안·확정본·고친 필드·프롬프트 버전을 저장하고, 같은 문의 건의 중복 확정은 막아.
- 모델 서버 주소를 브라우저 코드에 넣지 마. 브라우저는 앱 서버만 부른다.
- 추측하지 말고 실행 명령과 출력 원문을 붙여.
보고 형식:
1) [엔드포인트 | 메서드 | 입력 | 출력 | 한 번에/스트리밍] 표
2) 세션 저장 구조 설명 (키가 무엇인지)
3) curl -N 으로 /api/reply 를 부른 출력 원문 (data: 줄들과 [DONE])
4) 화면 스크린샷 (결과 카드, 스트리밍 중, 확정 후)
받을 증거
- 엔드포인트 표,
curl -N출력 원문 - 세션 저장 구조 (무엇을 키로 쓰는지)
- 화면 스크린샷 세 장
- 확정 저장 결과 (원 초안 · 확정본이 둘 다 있는 레코드 하나)
내가 직접 볼 것
| 볼 것 | 이걸로 아는 것 / 왜 내가 보는가 |
|---|---|
| 5절 행동 시험 ①~⑤를 내 손으로 | 코드를 못 읽어도 행동은 판정할 수 있습니다. AI의 "세션 분리했습니다"를 믿지 않고 두 창으로 확인 |
| 브라우저 개발자 도구의 네트워크 탭에 8000번 포트가 안 보이는가 | 브라우저가 모델 서버를 직접 부르면 35 · 38차시의 보안이 무너집니다 |
| 화면에 자동 발송 버튼이 없는가 | 33차시 범위 밖. AI는 "편의 기능"으로 넣기 쉽습니다 |
| 결과 카드에서 분류를 고칠 수 있는가 | HITL은 초안뿐 아니라 결과 카드에도 |
산출물 체크리스트 (37차시)
- 상담원 화면 (네 단계, 결과 카드 수정 가능, [중지] · [확정])
-
/api/triage(한 번에) ·/api/reply(SSE) ·/api/confirm(멱등) - 세션 분리 (세션 ID 키) + 토큰 예산 규칙 (최근 N턴, 원문 항상 포함)
- 끊김 시 생성 중단,
finish_reason: length경고 - 확정 레코드 (원 초안 · 확정본 · 고친 필드 · 프롬프트 버전)
- 행동 시험 결과표 (①~⑤, 내가 직접 한 결과)
부록 A. 스스로 확인하기
| # | 질문 | 답이 있는 곳 |
|---|---|---|
| 1 | 상담원 화면의 네 단계와 각각의 받는 방식은? | 1.1 |
| 2 | 결과 카드를 스트리밍하지 않는 이유를 두 축으로 말하면? | 1.2 |
| 3 | SSE를 유와 종차로 정의하면? 이번 프로젝트에서 구간이 둘인 이유는? | 2.1 |
| 4 | 상담원이 탭을 닫았을 때 모델 서버의 생성도 멈춰야 하는 이유는? | 2.2, 5 |
| 5 | 스트리밍이 화면에서 안 보일 때 의심할 중간 장치는? | 2.3 |
| 6 | 세션(문의봇)을 유와 종차로 정의하면? 전역 변수에 기록을 두면 생기는 일은? | 3.2 |
| 7 | 토큰 예산에서 문의 원문을 빼면 안 되는 이유는? | 3.3 |
| 8 | [확정] 때 원 초안과 확정본을 둘 다 저장하는 이유는? | 4 |
| 9 | 직접 할 행동 시험 다섯 가지는? | 5 |
부록 B. 자주 하는 실수
| 실수 | 바로잡기 |
|---|---|
| "스트리밍하면 응답이 빨라진다" | ❌ 전체 시간은 같음. 첫 글자까지가 빨라 보일 뿐 (README 5.1 축 ⑥) |
| "결과 카드도 스트리밍하면 더 좋다" | ❌ 다 와야 검사할 수 있음. 한 번에 |
| "대화 기록은 리스트 하나에 쌓으면 된다" | ❌ 상담원끼리 고객 정보가 섞입니다. 세션 ID별 |
| "모델 서버가 대화를 기억하니 마지막 말만 보내면 된다" | ❌ LLM API는 무상태 . 매번 다시 보냄 |
| "탭을 닫으면 알아서 멈춘다" | ⚠️ 앱이 모델 서버 쪽 스트림을 닫아야 멈춥니다. 시험 ③으로 확인 |
| "초안이 좋으니 자동 발송도 넣자" | ❌ 33차시 범위 밖, HITL 원칙. 사람이 확정 |
| "확정 버튼은 그냥 저장만" | ⚠️ 원 초안 · 고친 필드를 함께 저장해야 평가와 개선 이 됩니다 |
부록 C. NotebookLM 복습 프롬프트
이 37차시 노트만 근거로 6문제를 내줘. 비전공자 기준으로.
"결과 카드는 한 번에, 초안은 스트리밍인 이유", "세션 분리를 안 했을 때의 사고", "끊김 시 생성 중단", "HITL 확정 때 저장할 것"은 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.
LangTailor