페이지 목차
이 페이지에서 검색
로컬 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
이 차시를 다 읽으면
- "LLM이 대화를 기억한다"가 왜 틀린 말인지, 누가 무엇을 들고 있는지 로 설명할 수 있다.
- 대화가 길어지면 토큰 · 첫 응답 시간 · 컨텍스트 가 어떻게 되는지 말할 수 있다.
- UI → 앱 → Ollama 세 칸 구조에서 대화 이력이 사는 칸 을 짚을 수 있다.
- Streamlit 챗봇 코드에서
session_state와messages=줄 을 알아보고 무슨 일을 하는지 말할 수 있다. - 기억 테스트 와 두 브라우저 세션 분리 테스트 로 앱을 검수할 수 있다.
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)
⭐ 대화 이력이 사는 곳
| 칸 | 누가 만드나 | 이 차시에서 |
|---|---|---|
| UI | Streamlit이 자동으로 그림 | st.chat_input, st.chat_message |
| 앱 | 우리 (AI에게 맡김) | 이력 관리 + Ollama 호출 |
| 엔진 | Ollama | 5 · 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차시) |
| ④ API | 7차시 Ollama 고유 API, /v1 예고 | 20차시 OpenAI 호환 |
| ③ 추론 엔진 | 4차시 Transformers, 5차시 Ollama | 3일차 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 |
| 3 | 3턴째 요청에 들어가는 메시지는? | 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 배열이 턴마다 자라는 대가", "전역 변수 이력의 개인정보 사고", "세션 분리 테스트"는 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.
LangTailor