모듈1: 모델 상호작용
Open Web UI 알아보기
OpenWeb UI
ChatGPT 와 같은 채팅 인터페이스로 제공 되는 생성형 AI로, 대중적으로 사용되는 웹 인터페이스와 비슷한 기능을 제공하고 있다.
- 파일 제공, 대화 관리, 프롬프트 템플릿, 개인화 설정
AWS 워크샵 세션에서는 공식 헬름 차트 를 사용하여 EKS 클러스터에 배포되어 있으며, 현재 사용되고 있는 Helm 차트를 확인할 수 있다.

현재 워크샵 세션에서 제공 해주고 있는 클러스터의 openwebui 네임스페이스 안에 어떤 파드가 존재하는지 확인 해봤다.
실습
- OpenWebUI 에 접속해서 계정을 생성하고, 새로운 채팅에 “How Can I help you today?” 입력 해보기
Open Web UI 계정 생성 및 채팅 시작하기

위 실습 과정에서 WebUI 콘솔 접근 URL 은 ALB 의 도메인으로 제공 된다.
웹으로 접근 후 기존 가이드는 “회원가입” 을 진행 하라고 하는데, 내 웹에서는 이상하게 계속 로그인 화면만 나오고 있다.
원인 파악을 위해 파드 목록을 검사하고, 백엔드 API 를 확인해서 회원가입 API 를 직접 날려야겠다고 판단했다.
백엔드 API 가 존재하는 파드 확인하기
k get po -A
...
openwebui openwebui-0 1/1 Running 0 2d3h
openwebui openwebui-redis-665bd69749-lsptq 1/1 Running 0 6h53m
파드 목록에서 oepnwebui-0 에 API 가 있을 것 같다고 판단이 들었고 한 번 내부로 들어가봤다.
k exec -it openwebui-0 -n openwebui -- /bin/bash
# Output
participant:~$ k exec -it openwebui-0 -n openwebui -- /bin/bash
Defaulted container "open-webui" out of: open-webui, copy-app-data (init)
root@openwebui-0:/app/backend#
이 파드에서 백엔드 API 가 동작하겠다고 짐작이 갔는데, 실제 어떤 포트를 사용하고 있는지 알아야 API 를 쏠텐데 라고 생각하며 ps, lsof 등 네트워크 명령어를 날려봤는데 모두 찾을 수 없다고 나오니 이미지에 관련 패키지를 제외 시켜둔 것 같다.
ps, top 등 우리가 편하게 썼던 커맨드 뒤에는 사실 리눅스의 파일 시스템으로 동작하는 것을 알고 있으니 클로드에게 /proc 을 읽고 현재 실행중인 파일을 찾을 수 있도록 요청했다.
for p in /proc/[0-9]*; do echo "$(basename $p) $(tr '\0' ' ' < $p/cmdline)"; done
root@openwebui-0:/app/backend# for p in /proc/[0-9]*; do echo "$(basename $p) $(tr '\0' ' ' < $p/cmdline)"; done
1 /usr/local/bin/python3 -m uvicorn open_webui.main:app --host 0.0.0.0 --port 8080 --forwarded-allow-ips * --workers 1
769 /bin/bash
8080 포트에서 떠있는데, uvicorn 으로 떠있고 파이썬이다. 이러면 Django, FastAPI 둘 중 하나라고 생각이 들었고 내부 패키지 구성을 살펴보고 어떻게 날려야 할지 생각해봤다.
root@openwebui-0:/app/backend/open_webui# ls
__init__.py alembic.ini constants.py env.py internal migrations retrieval socket storage test utils
__pycache__ config.py data functions.py main.py models routers static tasks.py tools
여기서 main.py 가 어떤 패키지를 임포트하고 있는지 확인하고, 이제 API 를 날리려고 했다. 그래서 한 번 “SignUp” 을 회원가입으로 많이 쓰니까, 파이썬 파일에서 사용중인 구문이 있는지 확인 해보려고 하는데 특이한 환경 변수들이 보인다.
root@openwebui-0:/app/backend/open_webui# grep -i "sign" ./*.py
./config.py:ENABLE_OAUTH_SIGNUP = PersistentConfig(
./config.py: "ENABLE_OAUTH_SIGNUP",
./config.py: "oauth.enable_signup",
./config.py: os.environ.get("ENABLE_OAUTH_SIGNUP", "False").lower() == "true",
./config.py:ENABLE_SIGNUP = PersistentConfig(
./config.py: "ENABLE_SIGNUP",
./config.py: "ui.enable_signup",
./config.py: else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
./constants.py: USER_SIGNUP = lambda username="": (
./constants.py: f"New user signed up: {username}" if username else "New user signed up"
./constants.py: EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
./constants.py: "Your session has expired or the token is invalid. Please sign in again."
./env.py:TRUSTED_SIGNATURE_KEY = os.environ.get("TRUSTED_SIGNATURE_KEY", "")
./env.py:ENABLE_INITIAL_ADMIN_SIGNUP = (
./env.py: os.environ.get("ENABLE_INITIAL_ADMIN_SIGNUP", "False").lower() == "true"
./env.py:ENABLE_SIGNUP_PASSWORD_CONFIRMATION = (
./env.py: os.environ.get("ENABLE_SIGNUP_PASSWORD_CONFIRMATION", "False").lower() == "true"
./env.py:WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.environ.get(
./env.py: "WEBUI_AUTH_SIGNOUT_REDIRECT_URL", None
./functions.py: # Get the signature of the function
./functions.py: sig = inspect.signature(function_module.pipe)
./main.py: ENABLE_SIGNUP,
./main.py: ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
./main.py: WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
./main.py: # Disable signup since we now have an admin
./main.py: app.state.config.ENABLE_SIGNUP = False
./main.py:app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
./main.py:app.state.WEBUI_AUTH_SIGNOUT_REDIRECT_URL = WEBUI_AUTH_SIGNOUT_REDIRECT_URL
./main.py: "enable_signup_password_confirmation": ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
./main.py: "enable_signup": app.state.config.ENABLE_SIGNUP,
./main.py:# 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user
./tasks.py: return {"status": True, "message": f"Stop signal sent for {task_id}"}
”# Disable signup since we now have an admin” 이라는 주석이 달려있다. 그렇다면 현재 어드민 계정이 생성 되어있다는 것인데, 저 문구에 어떤 코드가 있는지 확인 해보면 정답을 알 수 있다.
root@openwebui-0:/app/backend/open_webui# grep -n -B20 'Disable signup since we now have an admin' /app/backend/open_webui/main.py
602-
603-
604-@asynccontextmanager
605-async def lifespan(app: FastAPI):
606- # Store reference to main event loop for sync->async calls (e.g., embedding generation)
607- # This allows sync functions to schedule work on the main loop without blocking health checks
608- app.state.main_loop = asyncio.get_running_loop()
609-
610- app.state.instance_id = INSTANCE_ID
611- start_logger()
612-
613- if RESET_CONFIG_ON_START:
614- reset_config()
615-
616- if LICENSE_KEY:
617- get_license_data(app, LICENSE_KEY)
618-
619- # Create admin account from env vars if specified and no users exist
620- if WEBUI_ADMIN_EMAIL and WEBUI_ADMIN_PASSWORD:
621- if create_admin_user(WEBUI_ADMIN_EMAIL, WEBUI_ADMIN_PASSWORD, WEBUI_ADMIN_NAME):
622: # Disable signup since we now have an admin
root@openwebui-0:/app/backend/open_webui#
환경 변수로 받고 있었고,이 환경 변수를 주입하는 위치를 찾기 위해 Pod 가 생성될 때 YAML 파일을 확인하면 진짜 정답을 알 수 있다.
k get po -n openwebui openwebui-0 -o yaml
- env:
- name: WEBUI_URL
value: http://%!s(<nil>)
- name: ENABLE_OLLAMA_API
value: "False"
- name: OPENAI_API_BASE_URL
value: http://litellm.litellm:4000/v1
- name: OPENAI_API_KEY
value: 0p3n-w3bu!
- name: ENABLE_WEBSOCKET_SUPPORT
value: "True"
- name: WEBSOCKET_MANAGER
value: redis
- name: REDIS_URL
value: redis://openwebui-redis.openwebui.svc.cluster.local:6379/0
- name: WEBSOCKET_REDIS_URL
value: redis://openwebui-redis.openwebui.svc.cluster.local:6379/0
- name: OPENAI_API_KEY
value: sk-1234
- name: WEBUI_ADMIN_EMAIL
value: admin@example.com
- name: WEBUI_ADMIN_PASSWORD
value: Pass@123
image: ghcr.io/open-webui/open-webui:0.8.8
이렇게 어드민 계정을 찾았고, 로그인을 시도 했더니 정상적으로 들어가졌다.

대화를 이어가기 위해 “몸 크기에 비해 가장 큰 생물은 뭐야?” 라고 묻는 샘플 지문이 있었고, 이 지문에 대한 응답으로는 쇠똥구리가 나왔다.
그리고 제공 되는 웹이 워낙 잘 진행 되길래 자연스럽게 넘어갈 뻔 했는데 나는 현재 이 웹이 어떻게 인터랙티브 하게 동작하고 있는지 기능들을 점검 해야한다.
그 중 내 응답을 수정하고 다시 보내기, AI 응답을 수정해서 저장하기 등 해봤는데 충격적인건 답변을 오디오로 제공하는 기능이다.
이 기능은 미친듯한 콩글리쉬 라고 표현을 해야하는걸까, 영어를 읽어주는 AI 중 가장 충격적인 경험을 한 것 같다.
다음 실습을 위해 모델과 친해지도록 여러가지 심화적인 기능들을 이용해보고 프롬프트를 다양하게 입력 해보라고 가이드를 주셨다.
심심해서 과거 ChatGPT 를 고장냈던 “해마 이모지 만들어줘” 를 한 번 딥시크 친구에게 부탁해봤다.


이 친구도 고장난 것 같다.
그 외에 여러가지 모델을 한 번에 추가하여, 응답 형식을 비교해볼 수 있고 답변이 모두 완료 되면 알림도 오며 잘 만들어진 것 같다.
그리고 모델 추가 버튼을 누르면 여러 모델을 계속 선택할 수 있는데, 과연 동일한 모델을 여러개 넣으면 어떻게 동작할지 궁금했다.

동일한 모델 여러개가 답변을 생성하기 시작했고 가장 마지막에 추가한 모델 3개가 최대인 것 같다.
자체 호스팅 모델 서빙 구조
OpenWeb UI 에서 다양한 모델들을 활용해보고, 여러가지 프롬프팅도 해보면서 대화 세션에서 모델의 응답을 받아봤다.
실제 이 구조는 어떻게 동작하는 것일까? 그리고 현재, vLLM 은 어떻게 쿠버네티스 환경에서 떠있고 어떤 로그 데이터를 적재 하는지 살펴본다.
우선 셀프 호스팅 중인 vLLM 모델은 아래 두 개의 파드로 띄워져있고, 채팅 응답은 아래 로그 처럼 적재하고 있었다.
participant:/workshop/components/llm-model/vllm$ k get po -n vllm
NAME READY STATUS RESTARTS AGE
deepseek-r1-qwen3-8b-neuron-5d7b56c8b8-256gb 1/1 Running 0 2d4h
qwen3-8b-neuron-ff4cf7dd8-m676j 1/1 Running 0 35h
로그 살펴보기
(APIServer pid=1) INFO: 10.0.23.160:40980 - "POST /v1/chat/completions HTTP/1.1" 200 OK
INFO 08-01 18:07:28 [metrics.py:398] Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 1.9 tokens/s, Running: 0 reqs, Swapped: 0 reqs, Pending: 0 reqs, GPU KV cache usage: 0.0%, CPU KV cache usage: 0.0%.
INFO 08-01 18:07:38 [metrics.py:398] Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 0 reqs, Swapped: 0 reqs, Pending: 0 reqs, GPU KV cache usage: 0.0%, CPU KV cache usage: 0.0%.
INFO 08-02 08:40:44 [metrics.py:398] Avg prompt throughput: 2.4 tokens/s, Avg generation throughput: 4.6 tokens/s, Running: 1 reqs, Swapped: 0 reqs, Pending: 0 reqs, GPU KV cache usage: 0.0%, CPU KV cache usage: 33.3%.
(APIServer pid=1) INFO: 10.0.20.17:45120 - "POST /v1/chat/completions HTTP/1.1" 200 OK
INFO 08-02 08:40:59 [metrics.py:398] Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 5.6 tokens/s, Running: 0 reqs, Swapped: 0 reqs, Pending: 0 reqs, GPU KV cache usage: 0.0%, CPU KV cache usage: 0.0%.
INFO 08-02 08:41:09 [metrics.py:398] Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 0 reqs, Swapped: 0 reqs, Pending: 0 reqs, GPU KV cache usage: 0.0%, CPU KV cache usage: 0.0%.
INFO 08-02 08:41:34 [metrics.py:398] Avg prompt throughput: 2.4 tokens/s, Avg generation throughput: 7.2 tokens/s, Running: 1 reqs, Swapped: 0 reqs, Pending: 0 reqs, GPU KV cache usage: 0.0%, CPU KV cache usage: 33.3%.
(APIServer pid=1) INFO: 10.0.20.17:58640 - "POST /v1/chat/completions HTTP/1.1" 200 OK
INFO 08-02 08:41:45 [metrics.py:398] Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 2.6 tokens/s, Running: 0 reqs, Swapped: 0 reqs, Pending: 0 reqs, GPU KV cache usage: 0.0%, CPU KV cache usage: 0.0%.
INFO 08-02 08:41:55 [metrics.py:398] Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 0 reqs, Swapped: 0 reqs, Pending: 0 reqs, GPU KV cache usage: 0.0%, CPU KV cache usage: 0.0%.
(APIServer pid=1) INFO: 10.0.20.17:59486 - "POST /v1/chat/completions HTTP/1.1" 200 OK 로그에서 보고 있는 것
- 요청 수신됨: vLLM이 처리하는 프롬프트
- 모델 사고 중: 토큰 생성 및 처리 지표
- 성능 통계: 처리량, 지연시간 및 캐시 사용
- 실시간 업데이트: 생성되는 각 토큰 실시간
주목할 주요 지표
- Prompt throughput: ~1.3 tokens/s (질문을 읽는 속도 - 읽을 것이 많지 않아 낮음)
- Generation throughput: ~24 tokens/s (응답을 생성하는 속도)
- GPU KV cache usage: 메모리 활용률 표시
- 요청 처리: 시작부터 완료까지의 전체 요청 수명 주기
생성형 AI 가 답변하는 파이프라인 구조
Qwen 3 8B에서 받은 응답:
- 귀하의 메시지 → Open WebUI → LiteLLM → 이 vLLM Pod
- 보고 있는 Pod 가 AWS Neuron 하드웨어에서 요청을 처리했다
- 응답 이 같은 경로를 통해 브라우저로 돌아왔다
관리형 모델 서빙 구조
직접 파드에 띄워서 사용했던 모델은 성능의 한계가 존재 한다. 그렇기에 관리되는 서비스를 상용으로 사용하면 사용량 비례 하여 과금 체계이기 때문에 비싼 인프라를 사전에 구축할 필요가 사라지게 된다.
이 관리형 모델에서 사용하는 구조는 AWS Bedrock 을 연계 하고 지원하는 모델인 Claude 4.5 Sonnet 을 활용한다.
이번에도 역시 새 채팅을 열어 bedrock/claude-4-5-sonnet 모델을 선택한 뒤 실습 가이드에서 제공하는 프롬프트를 입력한다.

셀프 호스팅 모델을 지금은 쓰지 않고 있기 때문에 뒷담? 아닌 뒷담을 해보자면, 굉장히 답변이 느렸고 프롬프트가 약간이라도 명확하지 않는다면 초기 “해마 이모지” 처럼 장황한 답변을 내놓는다.
하지만, 관리형은 구조화된 응답과 구체적인 사례 그리고 역시 빠른 응답을 제공한다.
다음은 서로 다른 두 모델에게 수학적 계산식을 풀도록 프롬프트를 입력하면, 두 모델은 어떻게 사고하는지 그 차이점을 익힐 수 있다.
수학 문제 풀이 프롬프트
What is squareroot of 144 divided by 29 multiplied by pi?

현재 간단한 수학 문제에서는 두 모델 다 같은 방식으로 풀이를 진행 했다.
사고 과정을 보면 클로드는 굉장히 끝나서 그 사고 과정 전체를 볼 수는 없었지만 응답 후 자신의 풀이 방식을 보여줬고, 딥시크는 풀이와 응답을 내기 까지 그 과정에서 굉장히 많은 검증을 거치는 듯 했다.
RAG 를 사용한 답변 고도화 하기
클로드나 딥시크 모두 훈련된 데이터만 사용하여 사용자가 원하는 것과 다른 경우가 종종 존재한다.
이럴 경우에 사용자는 어떤 것을 원하는지 어떤 자료를 참고하면 되는지 등등 더 명시적으로 질문을 던질 수 있는데, 이 단계에서는 아래와 같은 지식을 참조시키도록 한다.
cat > super-secret.txt << 'EOF'
Super Secret Document
This document contains classified information about the secret project.
Only authorized personnel are allowed to access this data.
Project Codename: Nightfall
Objective: Develop an untraceable communication device.
Status: In progress
Remember, this information is top secret and must not be shared with anyone.
EOF
이 파일을 OpenWeb UI 에 지식으로 입력하고, 아래와 같은 프롬프트를 비교해본다.
RAG 검증을 위한 프롬프트
What is the objective of project nightfall?
우선 지식을 참조하지 않은 클로드의 답변이다.
I don't have any information about a "Project Nightfall." This could refer to:
1. **A fictional project** from a book, movie, game, or TV series
2. **A military or government operation** (historical or classified)
3. **A corporate/business initiative**
4. **A research project** in academia or industry
5. **A community or open-source project**
Could you provide more context about where you encountered this term? That would help me give you a more accurate answer about its specific objectives.
다음은, 지식을 참조한 클로드의 답변이다.
According to the document, the objective of Project Nightfall is to develop an untraceable communication device
RAG 를 사용하며 확인한 것
- 문서를 업로드하고 인덱싱할 수 있음
- AI 모델이 문서에서 관련 정보를 검색할 수 있음
- 응답이 데이터에 대해 컨텍스트에 맞게 정확하고 구체적으로 됨