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

로컬 LLM

이 페이지에 묻기

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

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

LOCAL · LLM

로컬 LLM

생성 수정

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

42개 문서 · 4일차 · 33 / 42

32차시 · AI 애플리케이션 통합

지도 위치 : 4일차 · ⑤애플리케이션 층 전체 — 부품을 한 앱으로 | 이전: 31차시 로컬 LLM 업무형 앱 구현 | 다음: 33차시 프로젝트 요구사항 분석 키워드 : LLM API, Streaming, Prompt, Structured Output, Function Calling 통합

이 차시를 다 읽으면

  1. 4일차의 기능들이 다섯 층 중 ⑤의 어디에 , 헥사고날의 어느 칸에 놓이는지 지도로 말할 수 있다.
  2. 문의봇 통합 구조도를 인바운드 · 유스케이스 · 아웃바운드 로 그릴 수 있다.
  3. 통합을 루프 엔지니어링 으로 볼 때 "완료"를 누가 판정하는지 말할 수 있다.
  4. 설정을 어디에 둘지 정하고, 프롬프트 · 응답 로그의 개인정보 마스킹 이 왜 필요한지 설명할 수 있다.
  5. 통합 시나리오 테스트 목록 (정상 · 긴 메일 · 형식 실패 · 도구 실패 · LLM 서버 다운 · 동시 10명)을 만들고 결과를 검수할 수 있다.

0. 이 차시의 판별 축

#축왼쪽 (0)오른쪽 (1)이 축으로 갈리는 것큰 축 (README 5장)
1계약인가 구현인가포트 (LLM · 주문 조회 · 대화 저장)어댑터 (vLLM · 주문 시스템 · Redis)무엇을 갈아 끼울 수 있는가⭐ ② 추상 ↔ 구상
2부품 시험인가 전체 시험인가단위 테스트 (가짜 LLM)통합 시나리오 (실제 LLM 서버)따로는 되는데 합치면 깨지는 것⑤ 하나 ↔ 여럿 (부품 하나 ↔ 합친 전체)
3로그에 원문을 남기는가원문 그대로마스킹디버깅 편의 ↔ 개인정보④ 로컬 ↔ 원격 (데이터가 밖으로 나가는가)
4한 명인가 여럿인가혼자 테스트동시 10명세션 섞임 · 대기열 · 지연⑤ 하나 ↔ 여럿

앞에서 이어집니다 — 25차시 6절에서 "이 뼈대에 26~30차시가 기능을 붙이지만 유스케이스와 어댑터만 늘어나고 폴더 구조는 그대로 이며 32차시에서 전체를 다시 그린다"고 했습니다. 그리고 README 7장의 문장 3 — "표준 API(포트)로 층을 나누면 엔진(어댑터)을 갈아 끼울 수 있다" — 를 이 차시에서 엔진 밖으로까지 넓혀 회수합니다.


1. 4일차 전체 지도

⑤ 애플리케이션   ├ 연결        LLM 클라이언트 (25)
                 ├ 받는 방식   스트리밍 · SSE (26)
                 ├ 기억        대화 기록 · 토큰 예산 (27)
                 ├ 지시        프롬프트 파일 · 평가 루프 (28)
                 ├ 출력 모양   구조화 출력 · Pydantic 검증 (29)
                 ├ 행동        도구 호출 · 권한 · HITL (30)
                 └ 업무        분류 · 추출 · 요약 · 초안 (31)
④ API           OpenAI 호환 /v1/chat/completions (20)
③ 추론 엔진      vLLM (운영) / Ollama (개발)
② 모델 파일      Qwen3-8B (양자화 선택은 2일차)
① 하드웨어       RTX 4090 24GB
차시기능헥사고날의 칸핵심 축
25LLM 클라이언트LLMPort + OpenAICompatAdapter (아웃바운드)②
26스트리밍SSE 라우터 (인바운드) + 어댑터의 stream()⑥
27대화 기록ConversationStorePort (아웃바운드) + 유스케이스의 메시지 조립⑦
28프롬프트prompts/*.md — 유스케이스가 읽는 파일결정론 ↔ 확률론
29구조화 출력LLM 출력 스키마(어댑터 쪽 Pydantic) → DTO⑧
30도구 호출OrderPort (아웃바운드) + 유스케이스의 루프 · 권한 확인③, HITL
31업무 기능유스케이스 — 분류 · 추출 · 요약 · 초안⑧

⭐ 일곱 줄 중 새 폴더를 만든 줄이 없습니다. 모두 25차시의 트리 안에 들어갔습니다.


2. ⭐ 통합 구조도 — 헥사고날 배치

   인바운드 어댑터                애플리케이션 코어                      아웃바운드 어댑터
┌────────────────────────┐  ┌──────────────────────────────────┐  ┌──────────────────────────┐
│ FastAPI 라우터          │  │ use_cases/                        │  │ OpenAICompatAdapter       │─▶ vLLM / Ollama
│  POST /inquiries/{id}/  │─▶│  ProcessInquiry                   │─▶│   (LLMPort)               │
│    analyze      (JSON)  │  │    분류 · 추출 · 요약              │  │ OrderApiAdapter           │─▶ 주문 시스템
│  POST /inquiries/{id}/  │  │  DraftReply                       │  │   (OrderPort)             │
│    draft/stream (SSE)   │  │    대화 조립 · 도구 루프 · 초안     │  │ RedisConversationStore    │─▶ Redis
│ schemas/ (문지기)        │  │ ports/output/ (세 개의 포트)       │  │   (ConversationStorePort) │
│ 인증 · 인가              │  │ prompts/*.md                      │  │                           │
└────────────────────────┘  └──────────────────────────────────┘  └──────────────────────────┘
바꾸고 싶은 것고치는 곳유스케이스
LLM 엔진 Ollama → vLLM.env의 LLM_BASE_URL · LLM_MODEL그대로
가짜 주문 DB → 실제 주문 시스템 APIOrderApiAdapter 하나그대로
대화 저장 dict → Redis의존성 조립소 한 줄그대로
분류 프롬프트 v4 → v5prompts/classify.md그대로
웹 화면 → 사내 메신저 봇인바운드 어댑터를 하나 추가그대로

⭐ README 7장의 문장 3은 엔진 이야기였습니다. 이제 주문 시스템 · 저장소 · 화면 까지 같은 원리로 갈아 끼웁니다. 헥사고날 배치는 좌우 대칭 입니다(25차시 2.1의 인바운드 · 아웃바운드) — 왼쪽(들어오는 쪽)과 오른쪽(나가는 쪽)이 같은 방식으로 교체됩니다.

⚠️ 안쪽 코드(app/)에서 바깥 기술로 바로 가는 길 이 있으면 그 자리에 포트(문)를 세워야 합니다. 이 노트에서는 이 점검 원칙을 Port Gate 라고 부릅니다. 지켜졌는지는 한 줄로 검사합니다. grep -rnE "fastapi|openai|redis|httpx" app/ 결과가 비어 있어야 합니다.

2.1 요청 한 건이 지나는 길

상담원 클릭
 └ [인바운드] 인증 · 인가 → 스키마 검증(본문 길이 상한)
 └ ProcessInquiry  분류+추출 (LLM, JSON 스키마, t=0) → Pydantic 검증 → 원문 대조 → 요약
 └ DraftReply      대화 기록 조립(예산) → LLM(tools) → get_order(권한 확인) → LLM → 초안 스트리밍
 └ [규칙]          금지 표현 · 다른 고객 정보 검사
 └ 상담원 화면     수정 → 발송 버튼 (사람)

3. 통합 = 루프 엔지니어링

부품이 하나씩 통과해도 합치면 깨지는 곳 이 있습니다.

합쳤을 때 생기는 일어디와 어디가 만났나
도구 결과가 대화 기록을 채워 예산 초과 — 도구 스키마 자체도 매 요청 토큰을 먹음27 × 30
오래된 턴을 버리다 tool_calls ↔ tool 짝이 깨짐27 × 30
도구 호출 단계까지 스트리밍하려다 화면에 호출 JSON이 찍힘 → 도구 단계는 한 번에 받고 최종 답만 스트리밍26 × 30
구조화 출력과 도구를 같은 요청에 쓰려 함 → 엔진에 따라 제약이 있을 수 있음, 문서 확인. 단계를 나누면 피할 수 있음(31차시 6절)29 × 30

그래서 "완료"를 사람의 느낌이나 AI의 "다 됐습니다"가 아니라 정해진 검사들이 판정하게 합니다.

루프 엔지니어링 — [개발 진행 방식]인데, [ "변경 → 검사 → 고침"을 반복하되, 끝났는지를 AI의 "완료했습니다" 선언이 아니라 테스트 · 평가 · 벤치마크 같은 정해진 검사가 판정하게 한다 ]는 점에서 한 번 만들고 눈으로 확인하는 방식과 구별된다. 코드를 AI에게 맡기는 이 과정에서는 특히 중요합니다.

코드 · 프롬프트 변경 ─▶ 단위 테스트 (FakeLLM, 수 초) ─▶ 평가셋 (실제 LLM, 31차시) ─▶ 통합 시나리오 (5절)
                   ─▶ 성능 측정 (22차시) ─▶ 전부 통과해야 배포
단계잡는 것
단위 테스트코드 흐름 오류 (GPU 없이도 돌아감)
평가셋품질 회귀 — 프롬프트 · 모델 · 양자화 변경이 점수를 떨어뜨렸나
통합 시나리오부품 사이의 이음새, 장애 상황
성능 측정동시 사용에서 지연 · 처리량

💡 CI(지속적 통합) 는 코드가 바뀔 때마다 이 검사들을 자동으로 돌리는 장치입니다. 이 줄을 CI에 올리면 사람이 잊어도 검사가 매번 돕니다. 평가셋과 통합 시나리오는 GPU가 필요하므로, GPU가 있는 서버에서 도는 별도 단계로 둡니다.


4. 설정과 로그

4.1 설정은 어디에

값어디에이유
엔진 주소 · 모델 이름 · 키환경변수 (25차시 3절)환경마다 다름, 비밀
기능별 temperature · max_tokens프롬프트 파일의 front matter (28차시 1.1)프롬프트와 한 묶음 으로 바뀜
컨텍스트 길이 · 출력 여유환경변수⚠️ 엔진의 --max-model-len / num_ctx와 같은 값 이어야 27차시 예산이 맞음
재시도 횟수 · 타임아웃 · MAX_STEPS환경변수운영 중 조정
도구 권한코드 (인가)설정 파일이나 프롬프트로 열 수 없어야 함 (30차시 4.3)

4.2 ⚠️ 로그 — 로컬 LLM을 고른 이유가 로그에서 무너진다

디버깅하려면 프롬프트와 응답을 봐야 합니다. 그런데 그 안에는 고객 이름 · 연락처 · 주소 · 주문번호 가 그대로 있습니다. 1차시에서 로컬을 고른 첫째 이유 — 데이터가 밖으로 나가지 않는다 — 가 있었는데, 이 로그를 외부 로그 수집 서비스로 보내면 LLM은 로컬인데 데이터는 밖으로 나갑니다.

항목남기나방식
요청 ID · 상담원 ID · 문의 IDO그대로 (30차시 4.6의 요청 ID 로그)
프롬프트 id · version, 모델 이름, temperatureO그대로
토큰 수 · TTFT · E2E · finish_reasonO그대로 (14차시 지표)
도구 이름 · 검증 결과 · 권한 결과O그대로
문의 원문 · 응답 본문 · 도구 인자와 결과△마스킹 후, 보관 기간 짧게, 볼 수 있는 사람 제한
PATTERNS = [(re.compile(r"01[016789]-?\d{3,4}-?\d{4}"), "<PHONE>"),
            (re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+"), "<EMAIL>"),
            (re.compile(r"\d{8}-\d{3}"), "<ORDER>")]

def mask(text: str, known_names: list[str]) -> str:
    for pat, tag in PATTERNS:
        text = pat.sub(tag, text)
    for name in known_names:                    # 29차시 추출 결과로 알게 된 고객명
        text = text.replace(name, "<NAME>")
    return text

🔍 읽는 포인트

  • 전화번호 · 이메일 · 주문번호는 모양이 정해져 있어 정규식으로 잡힙니다.
  • ⭐ 이름 · 주소는 정규식으로 잘 안 잡힙니다. 그래서 29차시에서 추출한 고객명 을 그 요청의 로그에서 지웁니다. 구조화 출력이 마스킹의 재료가 됩니다.
  • ⚠️ 마스킹은 줄여 줄 뿐 완벽하지 않습니다. 원문 로그는 기본으로 끄고, 디버깅할 때만 짧게 켭니다.

⚠️ 앱 로그만이 아닙니다. vLLM · Ollama 서버 자체도 설정에 따라 요청 내용을 로그에 남길 수 있습니다. 관련 옵션 이름은 버전에 따라 바뀌어 왔으니 공식 문서와 실제 로그 파일로 확인 합니다.


5. 통합 시나리오 테스트

#시나리오준비기대 결과앞 차시
1정상 문의평가셋의 평범한 문의분석 JSON 200, 초안 스트리밍, 도구 1회25~31
2긴 메일컨텍스트 길이에 가까운 메일앞부분의 주문번호가 추출됨, 또는 문지기가 명시적으로 거절 . ⚠️ 조용히 잘리면 실패13, 27
3형식 실패가짜 LLM 어댑터가 깨진 JSON을 돌려주게 함1회 재시도 → "수동 분류 필요" 표시, 500 아님29
4도구 실패주문 시스템을 끔초안이 조회 실패를 숨기지 않음 , 500 아님30
5LLM 서버 다운sudo systemctl stop ollama 또는 vLLM 종료503 + 화면 안내, 서버를 다시 켜면 재시작 없이 복구25
6동시 10명상담원 10계정이 동시에 분석 + 초안세션 섞임 0건, 에러 0건, TTFT p95 기록22, 27
7인젝션28차시 5절의 문의분류 정상, 약속 표현 0, 남의 주문 조회는 권한 없음28, 30
8클라이언트 끊김초안 스트리밍 도중 탭 닫기LLM 서버 쪽 생성 중단26

🔍 읽는 포인트

  • 3번은 진짜 LLM으로는 재현이 어렵습니다(스키마 강제를 켜면 거의 안 깨지므로). 가짜 어댑터로 고장을 일부러 넣습니다 — 포트가 있어서 가능한 일입니다(25차시 2.4).
  • 6번의 목표는 숫자로 미리 정해 둡니다. 예: "동시 10명에서 TTFT p95 < 1초" (예시 목표, 실측 아님). 숫자는 33차시 요구사항에서 정합니다.

6. 4일차 정리 → 5일차 예고

6.1 4일차를 세 문장으로

문장회수한 곳
앱은 LLM 서버를 포트 뒤에 둔다. 엔진도, 주문 시스템도, 저장소도 어댑터다25, 32
모델은 기억하지도, 형식을 지키지도, 실행하지도 않는다. 기억은 앱이(27), 형식은 엔진의 강제와 앱의 검증이(29), 실행은 앱이(30) — 모델이 하는 일은 1차시 1.2의 단 하나의 동작 뿐이다1, 27, 29, 30
형식은 기계가, 뜻은 평가셋과 사람이 본다.28, 29, 31

6.2 5일차에서 다시 만나는 곳

5일차4일차에서 가져가는 것
33 요구사항 분석31차시 평가셋 = "충분한 품질"의 기준선, 5절의 시나리오 목록
34 모델 선정 · 최적화같은 평가셋으로 모델 · 양자화 비교
35 Serving 구축25차시 .env의 LLM_BASE_URL이 가리킬 서버
36 AI 기능 구현28~31차시
37 애플리케이션 구현26 · 27차시 (스트리밍 · 대화)
38 Function Calling · 서비스 통합30 · 32차시 (도구 · HITL · 통합 시나리오)
39 성능 테스트 · 최적화5절의 6번 시나리오 + 22차시 측정

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

지시

25~31차시에서 만든 문의봇을 하나의 앱으로 통합하고 통합 시나리오 8개를 돌려줘.
- 구조: 노트 2절의 헥사고날 배치를 지킬 것. app/ 아래에서 fastapi·openai·redis·httpx import 금지
- 포트 세 개(LLMPort, OrderPort, ConversationStorePort)와 각 어댑터, 테스트용 가짜 어댑터
- 로그: 요청 ID, 프롬프트 version, 토큰 수, TTFT, finish_reason, 도구 결과는 항상 / 원문·응답은 mask() 후에만
- 시나리오 1~8은 노트 5절 표 그대로. 3번은 가짜 LLM 어댑터로 깨진 JSON 주입. 6번은 asyncio로 계정 10개 동시 실행
맥락: vLLM(또는 Ollama), FastAPI, openai 라이브러리 공식 문서 기준. 추측하지 말고 실행 출력 원문을 붙여.
보고 형식: [# | 시나리오 | 기대 결과 | 실제 결과 | 통과 여부 | 증거 위치(로그 줄·파일)] 표

받을 증거

  • tree와 grep -rnE "fastapi|openai|redis|httpx" app/ 출력 ( 비어 있어야 함 )
  • 시나리오 8개의 결과 표와 각각의 로그 · 응답 원문
  • 마스킹 전후 로그 한 줄씩 (테스트용 가명 데이터로)
  • 6번의 TTFT 중앙값 · p95 측정 출력 원문

내가 직접 볼 것

볼 것이걸로 아는 것
grep 결과가 비었는가포트가 진짜 문인가 — 갈아 끼우기가 가능한가
5번 후 LLM 서버만 다시 켰을 때 앱 재시작 없이 되살아나는가장애에서 스스로 회복하는가
6번 로그에서 다른 계정의 고객 이름 이 한 번이라도 보이는가동시 사용에서만 보이는 세션 사고
로그 파일을 열어 전화번호 · 이름이 그대로 남은 줄 이 있는가마스킹이 실제로 적용됐는가 — 로컬 LLM을 고른 이유를 지키는가
"통과"라고 적힌 칸마다 증거 위치가 실제로 있는가AI의 "통과했습니다"를 증거로만 믿기

부록 A. 스스로 확인하기

#질문답이 있는 곳
125~31차시의 기능은 각각 ⑤ 안의 어떤 자리이고 헥사고날의 어느 칸인가?1
2주문 시스템을 실제 API로 바꿀 때 고치는 곳은?2
3Port Gate가 지켜졌는지 확인하는 한 줄 검사는?2
4부품이 각각 통과해도 합치면 깨지는 예 두 가지는?3
5통합에서 "완료"를 판정하는 것은 무엇이고, 왜 루프 엔지니어링인가?3
6컨텍스트 길이를 앱과 엔진에 같은 값으로 둬야 하는 이유는?4.1
7로그가 로컬 LLM을 고른 이유를 무너뜨리는 경로와 대응은?4.2
8형식 실패 시나리오를 진짜 LLM이 아니라 가짜 어댑터로 재현하는 이유는?5
94일차를 세 문장으로 줄이면?6.1

부록 B. 자주 하는 실수

실수바로잡기
기능마다 새 폴더 · 새 구조를 만듦⚠️ 포트와 어댑터만 늘어남. 트리는 그대로
유스케이스에서 Redis · 주문 API를 직접 호출❌ Port Gate 위반. 갈아 끼우기가 불가능해짐
부품별 테스트만 통과하고 "통합 완료"⚠️ 이음새(예산 초과, 메시지 짝 깨짐)는 통합 시나리오 로만 보임
앱의 컨텍스트 길이 설정과 엔진 설정이 다름⚠️ 토큰 예산이 엉터리가 됨
프롬프트 · 응답 원문을 그대로 외부 로그 서비스로❌ 데이터가 밖으로 나감. 마스킹 + 보관 기간 + 접근 제한
엔진 서버 로그는 신경 쓰지 않음⚠️ 엔진도 요청 내용을 남길 수 있음. 직접 확인
혼자 테스트하고 동시 사용 테스트 생략❌ 세션 섞임 · 지연은 동시 10명 에서만 보임

부록 C. NotebookLM 복습 프롬프트

이 32차시 노트만 근거로 6문제를 내줘. 비전공자 기준으로.
"4일차 기능들의 헥사고날 배치", "합치면 깨지는 이음새", "로그 마스킹이 로컬 LLM을 고른 이유와 연결되는 지점", "통합 시나리오 8개"는 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.