본문으로 건너뛰기
페이지 목차
이 페이지에서 검색

로컬 LLM

이 페이지에 묻기

로컬 LLM · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.

생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.

LOCAL · LLM

로컬 LLM

생성 수정

로컬 모델 실행과 GPU·메모리 이해부터 서빙·RAG·도구 호출·업무형 앱 구현까지 차례로 살펴봅니다.

42개 문서 · 5일차 · 38 / 42

37차시 · 애플리케이션 구현

지도 위치 : 5일차 · 프로젝트 5단계 · ⑤애플리케이션 층의 사용자 쪽 | 이전: 36차시 AI 기능 구현 | 다음: 38차시 Function Calling 및 서비스 통합 키워드 : 사용자 인터페이스, Streaming, 대화 처리 및 서비스 기능 구현

이 차시를 다 읽으면

  1. 상담원 화면을 입력 → 결과 카드 → 답변 초안 스트리밍 → 수정 · 확정 의 네 단계로 설계할 수 있다.
  2. 결과 카드는 한 번에 , 답변 초안은 스트리밍 으로 받는 이유를 축 ⑥과 ⑧로 설명할 수 있다.
  3. 브라우저 ↔ 앱 서버 ↔ 모델 서버의 두 구간 에서 SSE가 어떻게 흐르는지 말할 수 있다.
  4. LLM API가 무상태이므로 앱 서버가 상담원 · 문의 건별로 세션을 분리 해야 하는 이유를 말할 수 있다.
  5. 사람이 확정하는 흐름(HITL) 을 화면과 저장 구조에 넣을 수 있다.
  6. 두 브라우저 세션 · 긴 메일 · 중간에 끊기의 행동 시험 을 직접 할 수 있다.

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"SSE26차시

💡 브라우저의 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
3SSE를 유와 종차로 정의하면? 이번 프로젝트에서 구간이 둘인 이유는?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 확정 때 저장할 것"은 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.