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

로컬 LLM

이 페이지에 묻기

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

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

LOCAL · LLM

로컬 LLM

생성 수정

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

42개 문서 · 1일차 · 9 / 42

8차시 · Ollama 기반 AI 앱 구현

지도 위치 : 1일차 · ⑤애플리케이션 층 맛보기 + 1일차 정리 | 이전: 7차시 Ollama API 활용 | 다음: 9차시 LLM 메모리 구조와 GPU 자원 분석 키워드 : Python 기반 로컬 LLM 챗봇, 대화 이력 관리, 간단한 Web UI

이 차시를 다 읽으면

  1. "LLM이 대화를 기억한다"가 왜 틀린 말인지, 누가 무엇을 들고 있는지 로 설명할 수 있다.
  2. 대화가 길어지면 토큰 · 첫 응답 시간 · 컨텍스트 가 어떻게 되는지 말할 수 있다.
  3. UI → 앱 → Ollama 세 칸 구조에서 대화 이력이 사는 칸 을 짚을 수 있다.
  4. Streamlit 챗봇 코드에서 session_state와 messages= 줄 을 알아보고 무슨 일을 하는지 말할 수 있다.
  5. 기억 테스트 와 두 브라우저 세션 분리 테스트 로 앱을 검수할 수 있다.

0. 이 차시의 판별 축

#축왼쪽 (0)오른쪽 (1)이 축으로 갈리는 것큰 축 (README 5장)
1기억하는가무상태 (Ollama API · 모델)상태 (앱의 대화 이력)"모델이 기억한다" ↔ "앱이 다시 보낸다"⭐ ⑦ 무상태 ↔ 상태
2누구와 공유되는가전역 (모든 사용자 공유)격리 (세션마다 따로)전역 변수의 이력 ↔ session_state의 이력⑤ 하나 ↔ 여럿의 파생 — 전역 ↔ 격리 (3차시 0절 축 1과 같은 축)
3얼마나 쌓였는가짧은 대화긴 대화입력 토큰 · KV 캐시 · 첫 응답 시간① 메모리 (9 · 27차시)

7차시 2.2에서 "/api/chat에 이전 대화는 어디에도 저장되지 않는다"고 했습니다. 그런데 ChatGPT 같은 챗봇은 분명히 앞 대화를 기억하는 것처럼 보입니다. 이 차시의 질문은 "그렇다면 기억은 누가, 어디에 들고 있는가" 이고, 그 답을 가지고 1일차의 마지막 결과물인 대화형 챗봇 앱 을 만듭니다.


1. ⭐ LLM API는 무상태다 — 그럼 기억은 어디에?

1.1 무상태와 상태 — 정의, 그리고 세 칸 그림

무상태 (Stateless) — [통신 · 처리 방식]인데, [ 요청 하나하나를 그 요청 안에 담긴 정보만으로 처리하고, 이전 요청을 기억하지 않는다 ]는 점에서 상태 유지와 구별된다. 자판기처럼, 누가 몇 번째로 왔는지 모르고 지금 넣은 돈과 누른 버튼만 봅니다.

상태 유지 (Stateful) — [통신 · 처리 방식]인데, [ 이전에 있었던 일을 기억해 두고 다음 처리에 이어서 쓴다 ]는 점에서 무상태와 구별된다. 단골 손님의 지난 주문을 기억하는 직원이 그렇습니다.

⭐ 여기서 가장 흔한 오해를 먼저 짚습니다. "무상태 시스템"이라고 해서 아무도 상태를 갖지 않는 것이 아닙니다. 무상태인 것은 통신 구간 — 서버가 클라이언트의 지난 일을 기억해 주지 않는다 는 것 — 뿐이고, 기억이 필요하면 클라이언트 쪽이 들고 있다가 매번 같이 보냅니다.

웹 로그인이 같은 구조입니다. 많은 웹 서버는 "이 사람이 아까 로그인했다"를 기억하지 않고, 대신 로그인할 때 받은 신분증 같은 문자열(인증 토큰, 예: JWT) 을 클라이언트가 요청마다 들고 갑니다. 서버는 매번 그 신분증만 확인합니다.

LLM 앱을 이 그림에 겹치면 이렇습니다.

   [ UI — 브라우저 ]                  ← 상태 있음: 화면에 보이는 말풍선
          │
   [ 앱 — Python (Streamlit) ]       ← ⭐ 상태 있음: messages 배열 (대화 이력)
          │
          │  ◀────── 이 구간만 무상태 (Ollama /api/chat)
          │           Ollama는 이전 요청을 기억하지 않는다
          ▼
   [ Ollama + 모델 ]                 ← 대화 기억 없음. 받은 배열을 읽고 다음 토큰을 만들 뿐
칸대화를 기억하는가무엇을 들고 있나
UI보여 주기만말풍선 (앱이 그려 준 것)
앱⭕ 기억함messages 배열 — system, user, assistant, user, ...
Ollama API❌없음
모델 (가중치)❌학습 때 정해진 숫자. 대화로 바뀌지 않음 (2차시 1절 추론의 정의)

⭐ 대화 기억 = 앱이 매번 이력 전체를 다시 보내는 것 입니다. 웹 로그인에서 서버가 안 기억해 주니까 클라이언트가 신분증(인증 토큰)을 매번 들고 가듯 , 여기서는 앱이 대화 이력을 매번 들고 갑니다. 같은 구조입니다.

1.2 턴마다 배열이 자란다

1턴 요청: [system, user1]
1턴 응답:                            → assistant1
2턴 요청: [system, user1, assistant1, user2]
3턴 요청: [system, user1, assistant1, user2, assistant2, user3]
           └──────────── 매번 처음부터 다시 보냄 ────────────┘

대화 이력 (Conversation History) — [애플리케이션 상태]인데, [ 지난 턴의 질문과 답을 역할별로 쌓아 두었다가 매 요청에 통째로 다시 보낸다 ]는 점에서 모델 안에 저장되는 기억과 구별된다 — 그런 기억은 없습니다.

1.3 왜 굳이 무상태로 만드는가 — 장단점

무상태는 "기능이 모자란 것"이 아니라 일부러 고른 설계 입니다. 두 쪽의 장단점을 나란히 놓습니다.

무상태상태 유지
이 앱에서Ollama 호출 한 번 — 배열을 받으면 답을 내고 끝. 바깥의 기억에 의존하지 않음앱의 세션 — 대화 내내 이력이 쌓이고 흐름이 이어짐
장점⭐ 확장이 쉬움 — 어느 서버가 받아도 요청 안에 필요한 게 다 있으므로, 서버를 몇 대로 늘려도 아무 서버에나 보내면 됨. 서버가 죽었다 살아나도 잃을 기억이 없음사용자가 "아까 말한 거"를 쓸 수 있음
단점기억을 못 함 — 필요한 것은 매번 밖에서 받아야 함 → 요청이 길어짐 (2절)기억을 어디에 · 누구 것으로 두는지 관리해야 함 → 누구의 이력인지 섞이면 사고 (3.3절)

그래서 실무의 흔한 짝은 "엔진은 무상태, 앱이 상태를 든다" 입니다. 엔진(Ollama · vLLM)은 여러 대로 늘리기 쉽게 두고, 기억은 앱 한 곳에서 책임집니다.

⚠️ 반례 — "서버 쪽에도 뭔가 남지 않나요?"

서버 쪽에 남는 것대화 기억인가
KV 캐시 · 앞부분 재사용 (2 · 21차시)❌ 계산 결과의 재사용 . 같은 배열이 다시 오면 덜 계산할 뿐, 안 보내면 모름
keep_alive로 메모리에 올라가 있는 모델 (5차시 3.2)❌ 모델이 올라가 있을 뿐, 대화 내용은 없음

2. 이력이 자라면 생기는 일

2.1 비용이 같이 자란다

무엇이왜어디서 보나
입력 토큰매 턴 이력 전체를 다시 보냄7차시 3.2의 prompt_eval_count
첫 응답 시간 (TTFT)입력이 길수록 Prefill이 오래 걸림 (2차시 4절)체감, 14차시 측정
KV 캐시 (VRAM)토큰마다 쌓임 (2차시 2.2)nvidia-smi, 9차시 계산
컨텍스트 초과시스템 프롬프트 + 이력 + 질문 + 답 > num_ctx⚠️ 조용히 잘림 (6차시 4.1)
 num_ctx 8192 (예)
 1턴   [sys][u1]                                                 여유 많음
 10턴  [sys][u1][a1][u2][a2] ... [u10]                           여유 줄어듦
 30턴  [sys]···[u1][a1]··· 잘려 나감 ···[u29][a29][u30]           ⚠️ 첫 턴의 "제 이름은 홍길동" 이 사라짐

⚠️ 이 상태에서 모델은 "기억 안 난다"고 말하지 않고 그럴듯한 이름을 지어낼 수 있습니다. LLM은 입력이 모자라거나 틀려도 무언가를 만들어 냅니다 — 다음 토큰을 예측하는 기계이기 때문입니다(1차시 1.2). 그래서 답이 나왔다는 것은 답이 맞았다는 뜻이 아닙니다. 6차시 4.1의 "조용히 잘린다"와 같은 함정입니다.

2.2 전략 — 오늘은 이름만

전략하는 일대가
최근 N턴만 (슬라이딩 윈도)오래된 턴을 버림. 시스템 프롬프트는 항상 유지오래된 내용을 잊음
요약오래된 부분을 짧게 요약해 한 메시지로요약 호출 비용, 요약 누락
토큰 예산system + 이력 + 질문 + 답 여유 를 미리 나눠 둠계산 코드 필요

앞으로 다시 만납니다 — 27차시(대화형 AI 서비스)에서 세 전략을 구현하고 토큰 예산을 숫자로 짭니다. 오늘 앱은 아무 전략 없이 만들어서, 2.1의 문제를 직접 겪어 보는 것이 목적입니다.


3. 앱 만들기 — UI → 앱 → Ollama

3.1 세 칸 구조

 [브라우저]  ──HTTP──▶  [Streamlit 앱 :8501]  ──HTTP──▶  [Ollama :11434]  ──▶ GPU
  말풍선 UI              session_state.messages        127.0.0.1에만 (5차시 4.2)
                         ⭐ 대화 이력이 사는 곳
칸누가 만드나이 차시에서
UIStreamlit이 자동으로 그림st.chat_input, st.chat_message
앱우리 (AI에게 맡김)이력 관리 + Ollama 호출
엔진Ollama5 · 7차시 그대로

⭐ 5차시 4.2의 "Ollama는 127.0.0.1에 두고, 앞에 앱을 세운다"가 이 그림입니다. 사용자는 앱만 만나고 Ollama를 직접 만나지 않습니다.

Streamlit — [Python 웹 UI 프레임워크]인데, [ Python 스크립트 하나로 화면을 만들고, 사용자 동작마다 스크립트 전체를 처음부터 다시 실행한다 ]는 점에서 HTML · JS를 따로 짜는 프론트엔드 프레임워크와 구별된다. 같은 자리에 Gradio (ChatInterface가 이력을 대신 관리)도 많이 씁니다.

3.2 코드

import ollama
import streamlit as st

SYSTEM = "당신은 쇼핑몰 고객센터 상담원을 돕는 도우미입니다. 모르는 주문 정보는 지어내지 마세요."
st.title("문의봇 (로컬)")

if "messages" not in st.session_state:                       # ① 이 브라우저 세션의 첫 실행일 때만
    st.session_state.messages = [{"role": "system", "content": SYSTEM}]

for m in st.session_state.messages[1:]:                      # ② 지금까지의 대화를 다시 그림
    st.chat_message(m["role"]).write(m["content"])

if question := st.chat_input("문의 내용을 입력하세요"):
    st.session_state.messages.append({"role": "user", "content": question})        # ③
    st.chat_message("user").write(question)
    with st.chat_message("assistant"):
        stream = ollama.chat(model="qwen3:8b",
                             messages=st.session_state.messages,                  # ④ ⭐
                             stream=True, options={"num_ctx": 8192})
        answer = st.write_stream(c["message"]["content"] for c in stream)        # ⑤
    st.session_state.messages.append({"role": "assistant", "content": answer})    # ⑥
streamlit run app.py      # 브라우저에서 http://localhost:8501

🔍 읽는 포인트

번호알아볼 것0절 축
①session_state에 messages가 없을 때만 만듦 → 처음 한 번만 system을 넣음축 2 오른쪽 (세션마다)
②Streamlit은 매번 스크립트를 처음부터 다시 실행 → 화면도 이력으로 다시 그림
③ ⑥질문과 답 둘 다 이력에 추가. ⑥을 빼먹으면 모델은 자기가 뭐라 했는지 모름축 1
④⭐ 이력 전체 를 Ollama에 보냄. 1.2절의 "매번 처음부터"가 이 한 줄축 1, 축 3
⑤7차시의 스트리밍 — 조각을 화면에 흘려 쓰고, 다 모인 글을 answer로 돌려줌⑥ 동기 ↔ 비동기

💡 Qwen3의 생각 모드는 설치된 버전에 맞는 방법으로 끕니다(4차시 3.3, 7차시 4.3).

3.3 ⚠️ 이력을 어디에 두는가 — 전역이면 사고

Streamlit은 사용자 동작마다 스크립트를 다시 실행하므로 보통 변수는 매번 사라집니다 (무상태). 그래서 상태를 남기려면 어딘가에 둬야 하는데, 두는 곳에 따라 결과가 갈립니다.

이력을 두는 곳누가 보나결과
스크립트 안 보통 변수아무도 (매번 사라짐)기억 못 하는 챗봇
⭕ st.session_state그 브라우저 세션만사용자마다 따로 (격리)
❌ 모듈 전역 변수 · 캐시접속한 모든 사람⚠️ A 상담원의 고객 정보가 B 상담원 화면 · 프롬프트에 섞임 = 개인정보 사고

3차시에서 패키지를 두고 본 전역 ↔ 격리 (3차시 0절 축 1, 3.1)가 여기서는 사용자 데이터를 두고 다시 나오고, 이번에는 보안 문제가 됩니다. AI가 "간단하게 전역 리스트로 했습니다"라고 하면 반드시 되돌립니다.

⚠️ session_state는 메모리에만 있습니다(README 5.1 축 ① — 메모리는 휘발, 꺼지면 사라짐). 새로고침하거나 앱을 재시작하면 이력이 사라집니다. 영구 저장(DB)과 세션 ID는 27차시에서 다룹니다.

⚠️ Streamlit은 설정에 따라 같은 네트워크의 다른 PC에서도 접속되는 주소 를 엽니다(실행 화면의 Network URL). 실습에는 편하지만 이 앱에는 로그인이 없습니다 — 즉 인증("너는 누구냐", 5차시 4.2)이 없습니다. 운영 전에는 인증을 붙입니다(32차시).


4. 검수 — 기억 테스트와 세션 분리 테스트 ⭐

테스트하는 법기대 결과실패하면
기억"제 이름은 홍길동이고 주문번호는 20261128-001이에요" → 몇 턴 뒤 "제 이름이랑 주문번호 기억해요?"정확히 답함③ · ⑥ 누락, 또는 ④에 이력이 아닌 마지막 질문만 보냄
세션 분리브라우저 두 개 (또는 일반 창 + 시크릿 창)로 접속, A에서 이름을 말하고 B에서 "제 이름이 뭐예요?"B는 모름❌ 이력이 전역에 있음 → 즉시 수정
새로고침A에서 새로고침 후 이름을 물음모름 (메모리 휘발)— (정상. 영구 저장은 27차시)
긴 대화같은 세션에서 20턴 이상 이어 감턴마다 prompt_eval_count 증가2.1의 비용이 눈에 보임

💡 긴 대화 테스트에서 prompt_eval_count가 기대보다 덜 늘어 보일 수 있습니다. 엔진이 앞부분 계산을 재사용하는 경우입니다(1.3 반례 표, 21차시). 그래도 보낸 배열 자체는 매번 길어진다 는 사실은 같습니다.


5. 1일차 정리 — 다섯 층을 한 번 훑었다

README 4장의 다섯 층 지도에 오늘 한 것을 겹칩니다.

층1일차에서 만난 곳깊이 들어가는 곳
⑤ 애플리케이션6차시 프롬프트 · 평가셋, 7차시 스트리밍, 8차시 대화 이력4일차 (25~32차시)
④ API7차시 Ollama 고유 API, /v1 예고20차시 OpenAI 호환
③ 추론 엔진4차시 Transformers, 5차시 Ollama3일차 vLLM (17~24차시)
② 모델 파일2차시 구조, 4차시 safetensors · config, 5차시 양자화 태그, 6차시 세 계열2일차 양자화 · GGUF (10~12차시)
① 하드웨어3차시 사슬 · nvidia-smi, 5차시 PROCESSOR 열2일차 메모리 · 성능 (9, 14~16차시)

README 4장의 순서 그대로 — 넓게 → 깊게 → 다시 넓게 중 넓게 를 한 번 훑었습니다. 내일부터 깊게 들어갑니다.

앞으로 다시 만납니다 — 오늘 2.1의 표에 "KV 캐시 (VRAM) — 토큰마다 쌓임"이라고만 적었습니다. 9차시 에서 config.json(4차시)의 숫자로 그 크기를 직접 계산하고, 8B 모델 · 8K 컨텍스트 · 상담원 10명이 24GB에 들어가는지 답합니다. 1일차의 질문이 "어떻게 돌리나"였다면, 2일차의 질문은 "내 GPU에 어떻게 맞춰 넣나" 입니다.


6. 실습 — 맡기고 검수하기

지시

목표: Ollama(qwen3:8b)를 쓰는 Streamlit 챗봇(app.py)을 만들어줘. 대화 이력을 유지해야 해.
맥락: Streamlit 공식 문서의 chat 요소(st.chat_input, st.chat_message, st.write_stream)와
      session_state 문서, ollama-python README를 기준으로 해.
제약: - 대화 이력은 반드시 st.session_state에만 둬. 모듈 전역 변수·공유 캐시 금지.
      - Ollama는 localhost:11434로만 호출 (OLLAMA_HOST 변경 금지).
      - num_ctx 8192, 생각 모드 끔.
      - 매 요청마다 [턴 번호 | 보낸 메시지 수 | prompt_eval_count | eval_count | TPS]를
        터미널 로그로 한 줄씩 남겨.
보고 형식: app.py 전체, 실행 명령, 그리고 4절의 테스트 네 가지의 화면 캡처와 로그 원문.
추측하지 말고 실행 출력 원문을 붙여.

받을 증거

  • 기억 · 세션 분리 · 새로고침 테스트의 화면 캡처
  • 20턴 대화의 로그 원문 (턴마다 한 줄)

내가 직접 볼 것

볼 것이걸로 아는 것
코드에서 messages= 뒤가 st.session_state.messages 인가이력 전체를 보내는가 (축 1)
코드에 session_state 밖의 전역 리스트 가 없는가세션이 격리됐는가 (축 2)
세션 분리 테스트에서 B가 A의 이름을 모르는가개인정보가 섞이지 않는가
로그의 "보낸 메시지 수"와 prompt_eval_count가 턴마다 커지는가무상태의 대가 (축 3)

부록 A. 스스로 확인하기

#질문답이 있는 곳
1무상태와 상태 유지는 무엇으로 갈리는가? UI · 앱 · Ollama · 모델 중 대화를 기억하는 칸은?1.1
2웹 로그인의 인증 토큰(JWT)과 같은 구조라는 것은 무슨 뜻인가? 무엇과 무엇이 대응하나?1.1
33턴째 요청에 들어가는 메시지는?1.2
4엔진을 무상태로 두는 이유는? KV 캐시가 서버에 남는데 왜 "대화 기억"이 아닌가?1.3
5대화가 길어지면 커지는 네 가지는?2.1
6컨텍스트를 넘으면 모델은 어떻게 반응하기 쉬운가?2.1
7코드의 ④ 줄과 ⑥ 줄은 각각 무엇이고, ⑥을 빼면?3.2
8이력을 전역 변수에 두면 무슨 사고가 나는가?3.3
9세션 분리 테스트는 어떻게 하고, 무엇이 기대 결과인가?4
10다섯 층 각각을 1일차의 어느 차시에서 만났나?5

부록 B. 자주 하는 실수

실수바로잡기
"모델이 대화를 기억한다"❌ 앱이 매번 이력 전체를 다시 보낸다
"새 질문만 보내면 서버가 앞 내용을 이어 준다"❌ API는 무상태 . 배열에 없으면 모름
"assistant 답은 화면에만 띄우면 된다"❌ 이력에도 추가 해야 다음 턴에 모델이 앎
"이력은 전역 리스트에 두면 간단하다"❌ 모든 사용자가 공유 → 개인정보 사고 . 세션별로
"대화가 길어도 비용은 같다"❌ 입력 토큰 · TTFT · KV 캐시가 턴마다 증가
"긴 대화에서도 답이 나오니 다 기억하는 것"⚠️ num_ctx를 넘으면 앞부분이 조용히 잘리고 지어낼 수 있음
"새로고침해도 대화가 남아야 정상"⚠️ session_state는 메모리. 영구 저장은 별도 (27차시)

부록 C. NotebookLM 복습 프롬프트

이 8차시 노트만 근거로 7문제를 내줘. 비전공자 기준으로.
"LLM API는 무상태 — 기억은 앱이 든다", "messages 배열이 턴마다 자라는 대가", "전역 변수 이력의 개인정보 사고", "세션 분리 테스트"는 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.