BC-250를 활용한 AI 프로젝트: UI 구축
OpenWebUI 연동 및 웹,문서 도구, KV 캐시 설정
Open WebUI Docker 구성과 llama.cpp 연동, 웹 검색·PDF RAG·KV 캐시 최적화
작성·검증 기준: 2026년 8월 5일
llama.cpp 빌드:llama.cpp-20260617
모델:Qwen3-8B-Q6_K.gguf
추론 장치: ASRock BC-250 / Vulkan
최종 컨텍스트: 25,600토큰
KV 캐시: Q8_0
Open WebUI: Docker 이미지ghcr.io/open-webui/open-webui:main
이번 작업의 목적은 BC-250에서 실행하는 로컬 Qwen3-8B 모델을 Open WebUI에 연결하고, 일반 대화뿐 아니라 웹 검색, URL 본문 분석, PDF 문서 검색까지 하나의 인터페이스에서 사용할 수 있도록 구성하는 것이다.
최종 구조는 다음과 같다.
브라우저
│
▼
Open WebUI Docker
├── llama.cpp OpenAI 호환 API → Qwen3-8B 추론
├── DDGS → 웹 검색
├── fetch_url → 웹페이지 본문 조회
└── Ollama Embedding → PDF·문서 RAGOpen WebUI는 Ollama뿐 아니라 OpenAI 호환 API를 제공하는 서버에도 연결할 수 있다. llama-server 역시 OpenAI 호환 API를 제공하므로 별도의 중계 프로그램 없이 연결할 수 있다.
1. Open WebUI Docker 구성
작업 디렉터리 생성
mkdir -p ~/open-webui/data
cd ~/open-webui비밀키 생성
Open WebUI는 세션과 일부 암호화 데이터에 WEBUI_SECRET_KEY를 사용한다.
이 값을 고정하지 않으면 컨테이너를 재생성할 때 기존 로그인 세션이 풀릴 수 있다.
printf 'WEBUI_SECRET_KEY=%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .env.env 파일은 외부에 공개하거나 Git 저장소에 올리지 않는다.
Open WebUI 데이터는 /app/backend/data에 저장되므로 이 경로를 영구 볼륨으로 연결해야 컨테이너를 재생성해도 설정과 채팅이 유지된다. 공식 문서에서도 영구 볼륨 연결과 고정된 WEBUI_SECRET_KEY 사용을 안내한다.
docker-compose.yml
services:
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
ports:
- "10000:8080"
environment:
WEBUI_SECRET_KEY: "${WEBUI_SECRET_KEY}"
# 내부망 또는 같은 호스트로 해석되는 URL을
# fetch_url로 읽어야 할 때만 사용
ENABLE_RAG_LOCAL_WEB_FETCH: "true"
# Python 기본 User-Agent를 차단하는 사이트 대응
USER_AGENT: >-
Mozilla/5.0 (X11; Linux x86_64)
AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/126.0 Safari/537.36
# 매우 긴 웹페이지 하나가 컨텍스트를 대부분 차지하는 것을 방지
WEB_FETCH_MAX_CONTENT_LENGTH: "25000"
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./data:/app/backend/data
restart: unless-stoppedYAML 작성 시 주의점
Compose의 environment는 목록 방식과 매핑 방식 중 하나로 통일해야 한다.
잘못된 예:
environment:
- OPENAI_API_KEY=apikey
- ENABLE_RAG_LOCAL_WEB_FETCH: "true"올바른 매핑 방식:
environment:
OPENAI_API_KEY: "apikey"
ENABLE_RAG_LOCAL_WEB_FETCH: "true"또는 올바른 목록 방식:
environment:
- OPENAI_API_KEY=apikey
- ENABLE_RAG_LOCAL_WEB_FETCH=true이 글에서는 가독성이 좋은 매핑 방식을 사용했다.
내부 URL 접근 옵션의 의미
ENABLE_RAG_LOCAL_WEB_FETCH=true는 Open WebUI의 URL 로더가 사설 IP 또는 로컬 주소로 해석되는 URL에도 접근할 수 있게 한다.
대표적으로 다음 범위가 포함된다.
127.0.0.0/8
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16이번 환경에서는 blog.sonny.co.kr이 Open WebUI와 같은 Docker 호스트 또는 내부 IP로 해석돼 fetch_url 호출 시 다음 오류가 발생했다.
{
"error": "The URL you provided is invalid. Please double-check and try again."
}ENABLE_RAG_LOCAL_WEB_FETCH=true를 적용하고 컨테이너를 재생성한 뒤에는 해당 URL의 본문을 정상적으로 읽을 수 있었다.
다만 이 옵션은 SSRF 보호 범위를 완화한다. 외부 사용자가 Open WebUI에 접근할 수 있는 환경에서 활성화하면 NAS, 라우터, 내부 관리 페이지 등에 대한 접근을 시도할 가능성이 생긴다.
개인 단독 사용 또는 신뢰된 사용자만 접근하는 환경에서만 활성화하는 편이 안전하다. Open WebUI 공식 문서에서도 이 설정의 기본값은 False이며, 신뢰할 수 있는 환경에서만 켜도록 경고한다.
USER_AGENT 설정
웹 검색 결과는 나오지만 페이지 본문이 비어 있거나 403 Forbidden이 발생하면 대상 사이트가 Python 기본 User-Agent를 차단한 것일 수 있다.
USER_AGENT: >-
Mozilla/5.0 (X11; Linux x86_64)
AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/126.0 Safari/537.36이 값은 Open WebUI의 웹 로더와 fetch_url 도구에 적용된다.
Open WebUI의 기본 User-Agent는 설정하지 않을 경우 Python 라이브러리 기본값을 사용할 수 있으며, 일부 사이트는 이를 차단한다. 공식 설정 문서에서도 브라우저 형태의 User-Agent 사용을 안내한다.
웹 본문 길이 제한
WEB_FETCH_MAX_CONTENT_LENGTH: "25000"은 URL에서 가져온 본문을 최대 25,000문자로 제한한다.
이는 토큰 수가 아니라 문자 수 기준이다. 페이지 내용이 이 값을 넘으면 뒤쪽이 잘릴 수 있지만, 매우 긴 웹페이지 하나가 모델의 컨텍스트 대부분을 차지하는 문제를 줄일 수 있다.
정확한 전문이 반드시 필요한 경우에는 이 값을 늘리거나 일시적으로 제거한다. 공식 문서상 이 값의 기본값은 제한 없음이다.
적용
sudo docker compose config
sudo docker compose pull
sudo docker compose up -d상태 확인:
sudo docker ps --filter name=open-webui
sudo docker logs --tail 100 open-webui환경변수 확인:
sudo docker exec open-webui \
printenv \
ENABLE_RAG_LOCAL_WEB_FETCH \
USER_AGENT \
WEB_FETCH_MAX_CONTENT_LENGTH이미지 태그 고정
이번 테스트에서는 다음 이미지를 사용했다.
image: ghcr.io/open-webui/open-webui:mainmain은 새 빌드가 계속 반영되는 롤링 태그다. 기능 테스트에는 편하지만 업데이트 후 설정 화면이나 동작이 갑자기 바뀔 수 있다.
장기 운영에서는 정상 동작이 확인된 릴리스 태그로 고정하는 편이 안전하다.
image: ghcr.io/open-webui/open-webui:vX.Y.Z업데이트 전에는 /app/backend/data 백업을 남기는 것이 좋다. 일부 데이터베이스 마이그레이션은 구버전 이미지로 단순 롤백해도 되돌아가지 않을 수 있다.
포트 충돌 주의
이번 구성 예시는 Open WebUI와 llama.cpp가 서로 다른 호스트에서 실행되는 구조다.
Open WebUI: 192.168.0.4:10000
llama.cpp: 192.168.0.7:10000서로 다른 IP이므로 양쪽 모두 TCP 10000번을 사용할 수 있다.
같은 호스트에서 두 서비스를 실행한다면 Open WebUI의 외부 포트를 다른 번호로 바꿔야 한다.
ports:
- "3000:8080"2. BC-250에서 llama.cpp 서버 실행
사용한 모델 파일:
~/models/Qwen3-8B-Q6_K.gguf사용한 실행 파일:
~/llama.cpp-20260617/build-static/bin/llama-server옵션 지원 확인
llama.cpp는 빌드 시점에 따라 옵션 이름과 지원 기능이 달라질 수 있다.
먼저 현재 바이너리에서 필요한 옵션이 존재하는지 확인한다.
~/llama.cpp-20260617/build-static/bin/llama-server \
--help 2>&1 |
grep -E -- \
'cache-ram|cache-idle-slots|ctx-checkpoints|context-shift|cache-type-[kv]|flash-attn|batch-size|ubatch-size'확인할 주요 항목:
--cache-ram
--no-cache-idle-slots
--ctx-checkpoints
--context-shift
-ctk / --cache-type-k
-ctv / --cache-type-v
-fa / --flash-attn
-b / --batch-size
-ub / --ubatch-sizellama.cpp 최신 공식 문서상 K/V KV 캐시는 기본적으로 F16을 사용하며 q8_0, q4_0 등의 자료형을 지원한다. Context Shift는 기본적으로 비활성화돼 있고 --cache-ram과 Context Checkpoint 관련 옵션도 제공된다.
최종 실행 명령
초기에는 16,384토큰으로 운영했지만 웹 검색 결과와 이전 대화가 빠르게 컨텍스트에서 밀리면서 후속 질문 품질이 떨어졌다.
32,768토큰은 Qwen3-8B의 기본 컨텍스트 범위 안이지만 BC-250의 16GB 공유 메모리에서는 여유가 적었다.
최종적으로 25,600토큰과 Q8 KV 캐시를 사용하는 절충안을 적용했다.
env -u LD_LIBRARY_PATH -u LD_PRELOAD \
~/llama.cpp-20260617/build-static/bin/llama-server \
-m ~/models/Qwen3-8B-Q6_K.gguf \
--device Vulkan0 \
-ngl 999 \
-c 25600 \
-np 1 \
-ctk q8_0 \
-ctv q8_0 \
-fa on \
-b 512 \
-ub 256 \
--context-shift \
--keep 4096 \
--cache-ram 0 \
--no-cache-idle-slots \
--ctx-checkpoints 2 \
--reasoning off \
--alias qwen3-8b-q6k-bc250 \
--host 0.0.0.0 \
--port 10000 \
--api-key '여기에-별도-API-KEY'실제 API 키는 블로그, 공개 저장소 또는 스크린샷에 그대로 노출하지 않는다.
25,600토큰으로 설정한 이유
Qwen3-8B의 기본 컨텍스트 길이는 32,768토큰이다.
따라서:
-c 25600은 모델의 기본 범위 안이며 YaRN이나 별도의 RoPE 확장이 필요하지 않다. Qwen 공식 모델 카드에는 Qwen3-8B가 32,768토큰을 기본 지원한다고 명시돼 있다.
처음 사용했던:
-c 16384는 모델 자체의 최대치가 아니라 실행할 때 임의로 제한한 값이다.
최종 설정:
-c 25600은 llama.cpp가 현재 슬롯에 25,600토큰 규모의 컨텍스트와 그에 필요한 KV 캐시를 확보하도록 한다.
25,600토큰에는 사용자 메시지만 들어가는 것이 아니다.
시스템 프롬프트
+ 채팅 템플릿
+ 도구 정의
+ 이전 대화
+ 웹 검색 결과
+ fetch_url 본문
+ PDF RAG 청크
+ 현재 질문
+ 생성할 답변
≤ 25,600토큰주요 옵션 설명
| 옵션 | 의미 |
|---|---|
env -u LD_LIBRARY_PATH -u LD_PRELOAD | 이번 프로세스에서 외부 라이브러리 강제 경로와 preload 설정 제거 |
-m | 사용할 GGUF 모델 파일 지정 |
--device Vulkan0 | BC-250의 Vulkan 장치 사용 |
-ngl 999 | 가능한 모델 레이어를 전부 Vulkan 장치로 오프로드 |
-c 25600 | 컨텍스트 크기 25,600토큰 |
-np 1 | 동시 처리 슬롯 1개 |
-ctk q8_0 | Key KV 캐시를 Q8_0으로 저장 |
-ctv q8_0 | Value KV 캐시를 Q8_0으로 저장 |
-fa on | Flash Attention 활성화 |
-b 512 | 논리 배치 크기 512 |
-ub 256 | 물리 마이크로배치 크기 256 |
--context-shift | 생성 중 컨텍스트가 가득 차면 오래된 일부를 이동 |
--keep 4096 | Context Shift 시 최초 프롬프트 앞 4,096토큰 유지 |
--cache-ram 0 | 별도 Prompt RAM Cache 비활성화 |
--no-cache-idle-slots | 유휴 슬롯을 Prompt RAM Cache에 저장하지 않음 |
--ctx-checkpoints 2 | 슬롯당 내부 컨텍스트 체크포인트 최대 2개 |
--reasoning off | 별도 reasoning/thinking 출력 비활성화 |
--alias | OpenAI 호환 API에 표시할 모델 이름 |
--host 0.0.0.0 | 모든 네트워크 인터페이스에서 접속 허용 |
--port 10000 | llama-server 수신 포트 |
--api-key | OpenAI 호환 API 인증키 |
Q6_K와 Q8 KV는 서로 다른 설정
Qwen3-8B-Q6_K.gguf
→ 모델 가중치의 양자화 형식
-ctk q8_0 / -ctv q8_0
→ 실행 중 생성되는 KV 캐시의 저장 형식모델 가중치는 Q6_K 품질을 유지하고 컨텍스트 증가로 커지는 KV 캐시만 Q8_0으로 줄였다.
KV 캐시가 필요한 이유
Transformer 모델은 다음 토큰을 생성할 때 이전 토큰의 Attention 계산 결과를 참조한다.
각 레이어에서 이전 토큰의 Key와 Value를 매번 다시 계산하면 생성 속도가 크게 떨어진다.
KV 캐시는 이전 토큰의 K/V 상태를 저장해 두고 새 토큰에 필요한 부분만 계산하도록 한다.
즉 KV 캐시는 단순한 선택 기능이 아니라 현재 대화를 효율적으로 처리하기 위한 핵심 데이터다.
컨텍스트 토큰 증가
→ 저장해야 할 K/V 상태 증가
→ KV 캐시 메모리 증가-ctk q8_0, -ctv q8_0은 KV 캐시를 끄는 옵션이 아니라 F16보다 작은 Q8 형식으로 저장하는 옵션이다.
BC-250에서 RAM 사용량이 증가하는 이유
BC-250은 CPU와 GPU가 같은 16GB GDDR6 메모리 풀을 공유한다. 이 때문에 호스트 캐시와 Vulkan 할당이 논리적으로 나뉘어 보여도 최종적으로 같은 물리 메모리 한도를 경쟁한다.
논리적으로는 다음과 같이 나뉘어 보일 수 있다.
모델 가중치 → Vulkan0
KV 캐시 → Vulkan0
일반 프로세스 → Host하지만 물리적으로는 같은 16GB 메모리 한도를 사용한다.
따라서 Vulkan 쪽 KV 버퍼가 커져도 운영체제의 가용 메모리가 줄고, 메모리 압박이 심해지면 Linux OOM Killer가 작동할 수 있다.
배치 크기를 낮춘 이유
-b 512
-ub 256은 긴 입력을 처리하는 프리필 단계의 순간 작업 메모리를 줄이기 위한 설정이다.
배치가 크면 긴 프롬프트를 빠르게 처리할 수 있지만 순간 메모리 사용량도 커진다.
BC-250에서는 처리 속도보다 OOM 방지가 우선이므로 비교적 보수적으로 설정했다.
Context Shift와 keep
--context-shift
--keep 4096--context-shift는 답변 생성 중 컨텍스트가 가득 찼을 때 오래된 토큰 일부를 밀어내고 생성을 이어가는 기능이다.
--keep 4096은 이때 최초 프롬프트 앞부분 4,096토큰을 유지한다.
보통 초기 부분에는 시스템 프롬프트와 도구 규칙이 포함되므로 장시간 대화에서 기본 지시를 유지하는 데 도움이 된다.
다만 이 옵션이 시스템 프롬프트만 정확히 골라 보존하는 것은 아니다. 토큰화된 최초 프롬프트의 앞부분을 그대로 유지한다.
또한 --context-shift는 처음부터 25,600토큰을 초과한 API 요청을 자동으로 잘라 받는 기능이 아니다.
입력 자체가 한도를 넘으면 Open WebUI에서 미리 압축하거나 줄여 보내야 한다.
체크포인트 2개
--ctx-checkpoints 2은 슬롯 내부 컨텍스트 체크포인트의 최대 수를 2개로 제한한다.
이는 모델의 장기 기억 기능이 아니다. 프롬프트 계산과 내부 상태 재사용을 위한 기능이다.
기본값보다 크게 낮춰 메모리 사용을 억제하면서도 최소한의 재사용 여지를 남겼다.
다시 OOM이 발생하면 가장 먼저 다음처럼 변경한다.
--ctx-checkpoints 025,600토큰과 Q8 KV는 그대로 유지한 채 체크포인트만 끌 수 있다.
API 확인
BC-250 자체에서:
curl -s \
-H 'Authorization: Bearer 여기에-별도-API-KEY' \
http://127.0.0.1:10000/v1/models외부 호스트에서:
curl -s \
-H 'Authorization: Bearer 여기에-별도-API-KEY' \
http://192.168.0.7:10000/v1/models다음 alias가 출력되면 정상이다.
qwen3-8b-q6k-bc250시작 로그에서 컨텍스트와 버퍼 확인:
env -u LD_LIBRARY_PATH -u LD_PRELOAD \
~/llama.cpp-20260617/build-static/bin/llama-server \
...기존 옵션... 2>&1 |
tee ~/llama-server.loggrep -Ei \
'n_ctx|context|KV buffer|compute buffer|model buffer' \
~/llama-server.log3. Open WebUI와 llama.cpp 연결
Open WebUI 관리자 화면에서 다음 경로로 이동한다.
관리자 설정
→ 연결
→ OpenAI
→ 새 연결 추가입력값:
API 기본 URL:
http://192.168.0.7:10000/v1
API Key:
llama-server에서 지정한 키
Provider:
llama.cpp저장 후 모델 목록에 다음 alias가 표시되는지 확인한다.
qwen3-8b-q6k-bc250Docker 컨테이너에서 연결 확인
Open WebUI 호스트에서:
sudo docker exec open-webui \
curl -s \
-H 'Authorization: Bearer 여기에-별도-API-KEY' \
http://192.168.0.7:10000/v1/models연결이 안 되면 다음을 확인한다.
ss -lntp | grep 10000
sudo firewall-cmd --list-ports
ping -c 3 192.168.0.7Open WebUI 로그에 다음 오류가 발생할 수 있다.
Cannot connect to host 192.168.0.7:10000
ConnectionRefusedError
Open WebUI: Server Connection Error이 오류는 Open WebUI 자체 장애가 아니라 llama-server 프로세스가 종료됐거나 포트가 열려 있지 않을 때도 발생한다.
실제 원인을 확인하려면 Open WebUI 로그뿐 아니라 llama-server 터미널과 커널 OOM 로그도 함께 봐야 한다.
4. 웹 검색 연결
DDGS 설정
관리자 화면:
관리자 설정
→ 도구
→ 웹 검색설정 예시:
웹 검색 활성화: ON
웹 검색 엔진: DDGS
DDGS 백엔드: DuckDuckGo
검색 결과 수: 5
검색 동시 요청 수: 1
웹 콘텐츠 불러오기 생략: ONDDGS는 여러 검색 공급자를 선택할 수 있는 메타 검색 백엔드다.
현재 Open WebUI 설정 화면에서는 다음 백엔드를 선택할 수 있다.
Auto
Bing
Brave
DuckDuckGo
Google
Grokipedia
Mojeek
Wikipedia
Yahoo
Yandex공식 Open WebUI 문서에도 해당 DDGS 백엔드 목록이 안내돼 있다.
다만 선택지에 존재한다고 해서 현재 설치된 DDGS 버전에서 모두 정상 동작하는 것은 아니다.
이번 컨테이너의 DDGS 버전:
ddgs 9.14.4직접 확인한 결과:
DuckDuckGo 정상
Brave 정상
Yandex 정상
Google 결과 없음
Mojeek 결과 없음
Bing 사용할 수 없는 백엔드 경고 후 auto로 폴백따라서 이 환경에서는 DuckDuckGo로 고정했다.
위 결과는 DDGS 버전과 시점에 따라 달라질 수 있으므로 절대적인 지원 목록이 아니라 해당 환경의 실측 결과로 봐야 한다.
DDGS 직접 테스트
sudo docker exec -i open-webui python - <<'PY'
from ddgs import DDGS
query = "Open WebUI GitHub releases"
results = list(
DDGS(timeout=15).text(
query,
backend="duckduckgo",
max_results=5,
) or []
)
print("results =", len(results))
for item in results:
print(item.get("title"))
print(item.get("href"))
PY결과 수와 제목, URL이 출력되면 DDGS 자체는 동작하는 것이다.
검색 스니펫과 페이지 본문은 다름
웹 콘텐츠 불러오기 생략을 켜면 Open WebUI는 검색엔진이 제공한 제목, URL, 짧은 스니펫만 사용한다.
search_web
→ 검색 제목·URL·스니펫
fetch_url
→ 특정 페이지의 실제 본문Open WebUI의 BYPASS_WEB_SEARCH_WEB_LOADER를 활성화하면 검색 결과 페이지의 본문을 가져오지 않고 검색엔진의 스니펫만 사용한다.
따라서 다음과 같은 작업은 검색만으로 끝내면 안 된다.
공식 사이트의 전체 목록
특정 페이지 상세 분석
정확한 수치나 계약 조건
긴 본문의 논리 구조
검색 스니펫만으로 확인하기 어려운 내용검색 결과가 5개 나왔다는 사실과 각 페이지의 전체 내용을 검증했다는 것은 별개다.
모델에 웹 도구 활성화
관리자 설정
→ AI
→ 모델
→ qwen3-8b-q6k-bc250 편집활성화할 항목:
기능
└─ 웹 검색
기본 기능
└─ 웹 검색
내장 도구
└─ 활성화
Function Calling
└─ NativeNative 모드에서는 Open WebUI가 모델에 search_web, fetch_url 등의 도구를 제공하고 모델이 직접 호출 여부를 판단한다.
정상 작동 시 채팅에 다음 표시가 나온다.
View Result from search_webURL 본문을 열면:
View Result from fetch_urlArena Model 주의
이번 테스트에서는 Arena Model을 선택했을 때 실제 로컬 모델에 설정한 웹 도구가 기대한 방식으로 작동하지 않았다.
웹 검색 기능을 검증할 때는 Arena가 아니라 실제 모델을 직접 선택했다.
qwen3-8b-q6k-bc250이는 모든 버전에서 동일하게 발생한다고 단정할 수 있는 일반 규칙이 아니라 이번 환경에서 확인한 현상이다.
로컬 8B 모델의 도구 사용 한계
Qwen3-8B는 단일 search_web 또는 fetch_url 호출은 수행할 수 있었지만 다음과 같은 다단계 작업에서는 실패가 나타났다.
검색 필요성 판단
→ 공식 출처 선별
→ 검색 결과 중 URL 선택
→ fetch_url로 원문 조회
→ 전체 목록 수집
→ 빠진 항목 재검색
→ 최종 검증예를 들어 공식 오버워치 영웅 전체 목록을 요구했을 때 검색 스니펫 일부만 보고 임의 설명을 붙이거나, 같은 항목을 반복하고, 전체 목록을 확보하지 못한 채 답변을 종료했다.
즉 웹 도구가 작동한다는 것과 모델이 검색 결과를 올바르게 조사·검증한다는 것은 별개의 문제다.
로컬 8B 모델용 시스템 프롬프트
규칙을 너무 길고 중복되게 작성하면 8B 모델이 핵심 지시를 놓치거나 도구 호출보다 실패 선언을 우선할 수 있었다.
최종적으로 다음 정도로 줄였다.
너는 정확성을 우선하는 모델이다.
- 확인하지 않은 사실을 사실처럼 말하지 않는다.
- 확정 사실, 추론, 확인 불가를 구분한다.
- 출처와 검색 결과를 만들지 않는다.
웹 도구 규칙:
1. 오늘, 현재, 최신, 실시간, 날씨, 뉴스, 가격, 버전, 일정처럼
시점에 따라 달라지는 질문에는 답변 전에 search_web를 호출한다.
2. 사용자가 URL을 제공하면 답변 전에 fetch_url을 호출한다.
3. 검색 결과만으로 부족하면 관련 URL을 fetch_url로 읽는다.
4. 사용자가 공식 출처만 요구하면 공식 도메인 이외의 결과는
최종 근거로 사용하지 않는다.
5. search_web 결과는 검색 스니펫이다.
전체 목록이나 상세 원문이 필요하면 fetch_url을 호출한다.
6. 사용자가 전체 목록을 요구하면 일부 예시만 제시하고
"이 외에도 많다"고 종료하지 않는다.
7. 모르는 고유명사나 약어는 뜻을 추측하지 말고
search_web로 확인한다.
8. 도구가 사용 가능한 상태에서는 도구를 호출하지 않고
"실시간 정보 제공 불가" 또는 "내용을 확인하지 못했다"고
바로 답하지 않는다.
9. 도구 호출이 실패하거나 결과가 비어 있는 경우에만
사용한 도구, 검색어 또는 URL과 오류를 설명한다.
10. 검색 결과가 0건이면 자료가 없다고 단정하지 않는다.
핵심어를 단순화하거나 영어로 바꿔 다시 검색한다.
11. 이전 답변의 반복을 요구받은 것이 아니면
같은 내용을 그대로 반복하지 않는다.프롬프트를 추가한다고 8B 모델의 다단계 조사 능력이 완전히 해결되는 것은 아니다.
중요한 작업은 질문에도 검색 대상, 공식 도메인, 조사 순서를 명시하는 편이 낫다.
예:
웹 검색을 사용하라.
1. 오버워치 공식 사이트의 영웅 목록 페이지만 찾아라.
2. 공식 Blizzard 도메인이 아닌 결과는 폐기하라.
3. 검색 결과의 공식 URL을 fetch_url로 읽어라.
4. 확인한 영웅을 역할별로 전부 나열하라.
5. 전체 목록을 확보하지 못했다면 완료했다고 말하지 마라.프록시 환경 옵션
신뢰할 수 있는 프록시 환경은 리버스 프록시 신뢰 설정이 아니다.
다음 환경변수에 지정된 외부 HTTP 프록시를 웹 로더가 사용할지 결정한다.
HTTP_PROXY
HTTPS_PROXY
NO_PROXY이번 컨테이너에서:
urllib.request.getproxies()결과가 {}였으므로 프록시 환경 설정은 동작에 영향을 주지 않았다.
5. PDF 및 문서 RAG 연결
Open WebUI의 문서 기능은 대략 다음 순서로 동작한다.
PDF 업로드
→ 본문 추출 또는 OCR
→ 텍스트 청크 분할
→ 임베딩 벡터 생성
→ 벡터 DB 저장
→ 질문과 관련된 청크 검색
→ 모델 프롬프트에 삽입PDF 파일이 업로드 목록에 보이는 것과 실제 본문 검색이 가능한 것은 별개다.
임베딩 모델 설치
Ollama가 설치된 호스트에서:
ollama pull qwen3-embedding:0.6b현재 Ollama 라이브러리의 qwen3-embedding:0.6b는 약 639MB의 텍스트 임베딩 모델이다.
API 테스트:
curl -s http://127.0.0.1:11434/api/embed \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3-embedding:0.6b",
"input": "웹사이트 제작 용역위탁 계약서"
}' |
head -c 300벡터 배열이 반환되면 Ollama 임베딩 API는 정상이다.
Open WebUI 문서 설정
관리자 설정
→ 도구
→ 문서설정 예시:
콘텐츠 추출 엔진: 기본값
PDF 이미지 추출/OCR: ON
PDF 로더 모드: 페이지
임베딩 엔진: Ollama
Ollama URL: http://host.docker.internal:11434
임베딩 모델: qwen3-embedding:0.6b
임베딩 배치 크기: 1
임베딩 동시 요청 수: 1
임베딩 검색 우회: OFF
전체 컨텍스트 모드: OFF
텍스트 분할기: 문자 또는 토큰
청크 크기: 1000
청크 중첩: 100
RAG Top K: 3~5Open WebUI는 RAG 임베딩 엔진으로 Ollama, OpenAI 등을 지원하며 RAG_TOP_K 기본값은 3이다.
Open WebUI Docker가 호스트의 Ollama에 접근하도록 Compose에 다음 항목을 추가했다.
extra_hosts:
- "host.docker.internal:host-gateway"초기 임베딩 설정 오류
처음에는 문서 임베딩 엔진이 OpenAI로 설정돼 있었지만 유효한 OpenAI 임베딩 연결과 모델이 준비돼 있지 않았다.
임베딩 엔진: OpenAI
유효한 OpenAI 임베딩 연결: 없음이 상태에서는 PDF 파일이 화면에 보이더라도 의미 검색용 벡터가 올바르게 생성되지 않아 다음과 같은 결과가 나올 수 있었다.
소스를 찾을 수 없습니다
관련된 파일이 검색되지 않았습니다임베딩 엔진을 Ollama로 변경하고 실제 임베딩 모델을 지정한 뒤 기존 문서를 다시 처리했다.
Knowledge Base 파일
→ 재색인
채팅에 직접 첨부한 PDF
→ 삭제 후 재업로드임베딩 모델을 바꾸면 새 모델의 벡터 공간과 기존 벡터가 호환되지 않으므로 기존 문서를 재색인해야 한다.
Open WebUI의 재색인은 Knowledge Base 문서만 처리하며, 채팅에 직접 첨부한 파일은 다시 업로드해야 새 임베딩 모델이 적용된다.
업로드 후 확인할 항목
1. 파일이 목록에 표시되는지 확인
2. 추출된 텍스트 미리보기 확인
3. 임베딩 작업 완료 확인
4. 본문에만 있는 고유 문구로 검색
5. 답변에 실제 파일 출처가 붙는지 확인스캔 PDF는 OCR이 필요하지만 OCR을 켰다고 항상 정상 추출되는 것은 아니다.
추출 미리보기가 비어 있거나 문장이 심하게 깨져 있다면 임베딩보다 먼저 콘텐츠 추출 엔진이나 OCR 결과를 확인해야 한다. Open WebUI 공식 문제 해결 문서도 추출된 텍스트 미리보기를 먼저 확인하도록 안내한다.
파일명 검색과 본문 검색은 다름
예를 들어 파일명이 다음과 같다고 하자.
웹사이트 제작 용역위탁 계약서.pdf파일명에 용역위탁 계약서가 포함돼 있어도 PDF 본문에서 해당 문자열이 추출됐다는 뜻은 아니다.
스캔 PDF라면 제목 부분의 OCR이 실패할 수 있고, 본문에는 제목이 아예 없을 수도 있다.
따라서 모델이 본문 검색 결과를 찾지 못했다고 해서 파일 자체가 없다고 단정하면 안 된다.
전체 컨텍스트 모드를 끄는 이유
전체 컨텍스트 모드는 문서 전체를 모델 프롬프트에 넣는다.
짧은 계약서 하나를 통째로 비교할 때는 유용하지만, 여러 PDF나 긴 문서를 동시에 사용하면 25.6K 컨텍스트를 빠르게 소모한다.
전체 컨텍스트 모드 ON
→ 문서 전체 삽입
→ 정확한 전체 비교에는 유리
→ 컨텍스트 소모 큼
전체 컨텍스트 모드 OFF
→ 관련 청크만 검색
→ 긴 문서 모음에 유리
→ 검색 품질은 임베딩과 청크 설정에 영향기본 운용은 OFF로 두고 필요한 청크만 가져오는 방식이 적절했다.
Open WebUI에서 전체 문서 모드를 사용하면 파일 전체가 매 메시지에 주입되며, 모델의 컨텍스트가 작으면 결국 일부 내용이 잘릴 수 있다.
PDF 테스트 프롬프트
첨부된 PDF에서 계약금액, 계약기간, 검수 조건,
하자보수 조건을 찾아라.
각 항목마다:
- 출처 파일명
- 원문 근거
- 확인된 페이지
를 표시해라.정확한 문자열 검색:
파일 본문에서 "용역위탁 계약서"라는 문자열을 검색해라.
검색 결과가 없더라도 파일 자체가 없다고 판단하지 마라.
파일명과 추출 본문을 구분해서 설명해라.6. 긴 대화와 컨텍스트 관리
Open WebUI가 모델에 전달하는 것은 현재 질문 하나가 아니다.
시스템 프롬프트
대화 기록
도구 정의
과거 도구 호출 결과
웹 검색 결과
fetch_url 본문
PDF RAG 청크
메모리 또는 필터가 삽입한 내용
현재 질문
답변 출력 여유대화가 길어질수록 이 합계가 커진다.
Open WebUI도 전체 채팅 기록, 파일, 도구 정의와 결과가 모두 모델의 컨텍스트를 차지한다고 설명한다.
16K 환경에서 나타난 문제
초기에는:
-c 16384를 사용했다.
일반 대화만 할 때는 큰 문제가 없었지만 웹 검색과 URL 본문, 도구 정의가 함께 들어오면 사용할 수 있는 대화 공간이 빠르게 줄었다.
그 결과 다음과 같은 현상이 나타났다.
이전 답변 반복
후속 질문의 대상 혼동
공식 출처 요구 누락
검색 결과를 읽고도 전체 목록 미완성
낯선 약어를 임의 해석
도구 호출 후 빈 응답모델 자체가 바뀐 것은 아니었지만 필요한 앞 문맥과 도구 결과가 충분히 남지 않아 모델이 갑자기 멍청해진 것처럼 보였다.
25.6K로 확장한 뒤
최종 설정:
-c 25600
-ctk q8_0
-ctv q8_0
--keep 4096을 적용한 뒤에는 웹페이지를 읽고 이어지는 후속 질문에서 이전 사건의 흐름을 더 잘 유지했다.
예:
URL 상세 분석
→ 글쓴이 대처 평가
→ 일반인도 가능한지 질문
→ 유사 사례와 차이 질문이전보다 대화의 주제를 유지하고 질문 간 관계를 연결하는 능력이 개선됐다.
다만 컨텍스트가 길어졌다고 검색 결과 선별 능력이나 법률 정확도가 자동으로 완벽해지는 것은 아니다.
문맥 유지와 사실 검증은 별개의 문제다.
Open WebUI Context Compaction
Open WebUI는 선택 기능으로 Context Compaction을 제공한다.
관리자 설정
→ Experience
→ 인터페이스25.6K 환경의 운용 시작값:
Context Compaction: ON
Token Threshold: 18000
Token Cap: 20000
Retained Messages: 40%
Context Compaction Model: Current Model이 값은 Open WebUI의 공식 고정 권장값이 아니라 이번 25.6K 환경에서 웹 검색 결과, PDF RAG와 출력 토큰을 위해 여유를 남기려는 운영값이다.
Open WebUI Context Compaction은 임계값을 넘으면 오래된 대화를 요약 체크포인트로 교체하고 최근 메시지는 원문으로 유지한다.
공식 기본 보존 비율은 40%이며 설정 범위는 10~50%다. 시스템 프롬프트와 현재 사용할 수 있는 도구 정의는 압축 대상이 아니라 요청마다 다시 조립된다.
동작 방식:
오래된 대화 일부
→ 요약 체크포인트로 교체
최근 약 40%
→ 원문 유지전체 채팅 원본은 UI와 데이터베이스에 남아 있지만 모델에 전달되는 과거 내용은 요약본으로 바뀔 수 있다.
Compaction의 한계
요약은 무손실 압축이 아니다.
다음 정보는 누락되거나 변형될 수 있다.
IP 주소
명령어 옵션
오류 로그
계약 금액
날짜
파일명
예외 조건
출처 URL기술 로그나 계약서처럼 정확한 값이 중요한 작업에서는 압축된 기억에만 의존하지 않는다.
새 채팅 생성
원문 파일 다시 첨부
중요한 설정은 별도 문서로 보관또한 한 번에 매우 긴 PDF 또는 도구 결과 하나가 들어오면 과거 대화를 압축해도 컨텍스트 한도를 넘을 수 있다.
Context Compaction은 강제적인 하드 제한이 아니라 조건이 맞을 때 오래된 메시지를 요약하는 기능이므로, 거대한 단일 메시지에서는 실행되지 않을 수도 있다.
llama.cpp와 Open WebUI의 역할 차이
Open WebUI Context Compaction
→ 요청을 보내기 전에 오래된 대화를 요약
llama.cpp --context-shift
→ 정상적으로 받은 요청의 생성 중 컨텍스트가 찼을 때 이동
llama.cpp --keep 4096
→ Context Shift 시 최초 앞부분 유지세 기능은 서로 대체 관계가 아니다.
7. BC-250에서 llama.cpp가 메모리 부족으로 죽는 문제
발생한 로그
대화가 약 9,752토큰까지 누적된 뒤 다음 로그를 남기고 llama-server가 종료됐다.
slot release:
stop processing: n_tokens = 9752, truncated = 0
srv get_availabl:
updating prompt cache
srv prompt_save:
saving prompt with length 9752,
total state size = 1371.487 MiB
죽었음당시 컨텍스트 한도는 16,384토큰이었다.
n_tokens = 9752
truncated = 0이므로 토큰 초과가 직접 원인은 아니었다.
Open WebUI 로그
llama-server가 종료된 뒤 Open WebUI에서는 다음 오류가 발생했다.
Cannot connect to host 192.168.0.7:10000
ConnectionRefusedError
Open WebUI: Server Connection ErrorOpen WebUI의 Server Connection Error는 원인이 아니라 결과였다.
백엔드 llama-server 프로세스가 먼저 죽었기 때문에 연결을 거부당한 것이다.
커널 OOM 로그
sudo journalctl -k -b --since "20 minutes ago" |
grep -Ei \
'oom|out of memory|killed process|llama|amdgpu|gpu reset'실제 결과:
llama-server invoked oom-killer
global_oom
Out of memory: Killed process 11692 (llama-server)
anon-rss:4985812kB이 로그로 Linux 커널의 OOM Killer가 llama-server를 강제 종료한 것이 확정됐다.
원인
죽기 직전 llama-server는 다음 작업을 수행하고 있었다.
updating prompt cache
prompt_save
state size = 1371.487 MiB이미 모델 가중치, Vulkan 작업 버퍼, KV 캐시, 운영체제와 기타 프로세스가 메모리를 사용하고 있는 상태에서 약 1.37GiB 규모의 상태를 저장하려는 시점에 전역 메모리가 부족해졌다.
모델 가중치
+ Vulkan 실행 버퍼
+ KV 캐시
+ Fedora 및 기타 프로세스
+ Prompt Cache 저장 작업
= 동일한 16GB 한도Prompt RAM Cache와 KV 캐시는 다름
KV 캐시
현재 대화 토큰의 Key·Value 상태다음 토큰을 빠르게 생성하기 위해 필요하다.
관련 옵션:
-c 25600
-ctk q8_0
-ctv q8_0Prompt RAM Cache
처리가 끝난 프롬프트 상태를 별도로 저장
→ 이후 비슷한 요청에서 재사용관련 옵션:
--cache-ram
--no-cache-idle-slots
--ctx-checkpoints따라서:
--cache-ram 0
≠ KV 캐시 비활성화현재 대화 처리에 필요한 KV 캐시는 계속 존재한다.
해결 방법
OOM을 일으킨 Prompt RAM Cache 경로를 비활성화했다.
--cache-ram 0
--no-cache-idle-slots그리고 컨텍스트 체크포인트는 완전히 끄지 않고 2개만 유지했다.
--ctx-checkpoints 2다시 OOM이 발생하면:
--ctx-checkpoints 0으로 내린다.
현재 llama.cpp 공식 옵션상 --cache-ram 0은 Prompt RAM Cache를 비활성화하며 Context Checkpoint 기본값은 32개다.
최종 메모리 배분
Qwen3-8B 모델 가중치: Q6_K 유지
컨텍스트: 25,600
KV 캐시: Q8_0
Prompt RAM Cache: OFF
Idle Slot 저장: OFF
Context Checkpoint: 2개
배치: 512 / 256
Flash Attention: ON이 구성은 모델 가중치 품질은 유지하면서, 별도 캐시 사본을 없애고 KV 캐시를 압축해 확보한 메모리를 더 긴 컨텍스트에 배분한 것이다.
다시 OOM이 발생할 경우 순서
1단계: 체크포인트 제거
--ctx-checkpoints 02단계: 컨텍스트 소폭 축소
25600 → 22528또는:
25600 → 204803단계: 배치 축소
-b 256
-ub 128프롬프트 처리 속도는 느려지지만 순간 메모리 사용량을 줄일 수 있다.
4단계: KV를 Q4로 축소
-ctk q4_0
-ctv q4_0메모리는 더 줄지만 Q8보다 KV 정밀도가 낮아진다.
현재 Q8에서 안정적이라면 굳이 Q4까지 내릴 필요는 없다.
5단계: 모델 가중치 축소
Q6_K → Q5_K_M 또는 Q4_K_M이는 컨텍스트가 아니라 모델 가중치 메모리를 줄이는 방법이다.
품질 손실 범위가 더 크므로 후순위로 둔다.
6단계: 로컬 swap
swap은 성능을 높이는 기능이 아니라 순간 메모리 피크로 프로세스가 즉시 죽는 것을 늦추는 안전망이다.
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile확인:
swapon --show
free -h재부팅 후에도 유지하려면:
echo '/swapfile none swap defaults 0 0' |
sudo tee -a /etc/fstabVulkan 장치 메모리가 일반 익명 메모리처럼 전부 swap으로 빠지는 것은 아니므로 swap만으로 컨텍스트 문제를 해결한다고 보면 안 된다.
네트워크 RAM 또는 네트워크 swap
다른 Open WebUI 호스트의 RAM을 BC-250 프로세스의 일반 RAM처럼 직접 붙이는 것은 불가능하다.
가능한 우회는 다음 정도다.
원격 호스트에서 llama-server 자체 실행
llama.cpp RPC로 원격 계산 장치 분산
NBD/iSCSI를 이용한 네트워크 swap하지만 네트워크 swap은 원격 RAM을 직접 쓰는 것이 아니라 네트워크 블록 장치를 swap으로 사용하는 구조다.
페이지 폴트마다 네트워크 지연이 발생하고 연결 장애 시 시스템이 불안정해질 수 있다.
현재 25.6K는 네트워크 swap 없이 Q8 KV와 캐시 제거로 동작하는 범위이므로 네트워크 swap보다 현재 구성을 유지하는 편이 낫다.
8. 답변 품질 변화와 남은 한계
개선된 부분
25.6K 적용 뒤 다음 부분은 분명히 개선됐다.
이전 URL 본문 유지
후속 질문의 대상 추적
대화 흐름 연결
같은 답변 반복 감소
시스템 프롬프트와 도구 규칙 유지
검색 결과와 최근 대화를 동시에 참조16K에서는 검색 결과와 이전 대화가 빠르게 밀려 모델이 갑자기 이상해진 것처럼 보였다.
컨텍스트를 늘리자 같은 Qwen3-8B 모델이 훨씬 안정적으로 답변했다.
즉 모델의 체감 성능은 모델 파일뿐 아니라 다음 자원 배분에 크게 좌우됐다.
모델 가중치 정밀도
컨텍스트 길이
KV 캐시 정밀도
배치 크기
프롬프트 캐시
동시 슬롯 수
도구 스키마와 시스템 프롬프트 길이아직 남은 문제
문맥 유지가 좋아져도 다음 한계는 남아 있다.
검색 스니펫을 원문처럼 해석
관련 없는 검색 결과를 근거로 사용
공식 출처와 비공식 출처 혼합
법률 표현 과장
검색에 없는 내용을 임의 생성
다단계 search → fetch → 검증 실패특히 8B 모델은 검색 도구 하나를 호출하는 것보다 여러 단계의 조사 계획을 안정적으로 수행하는 데 어려움이 있었다.
따라서 중요한 작업에서는 다음 원칙을 유지한다.
공식 도메인을 질문에 직접 명시
검색 후 fetch_url 호출을 요구
전체 목록이면 누락 여부 확인 요구
문서 답변은 파일명·페이지·원문 근거 요구
법률·계약 내용은 원문 재확인9. 최종 확인 항목
Open WebUI
sudo docker ps --filter name=open-webui
sudo docker logs --tail 100 open-webuillama.cpp API
curl -s \
-H 'Authorization: Bearer 여기에-별도-API-KEY' \
http://192.168.0.7:10000/v1/models웹 검색
웹 검색 도구를 사용해서
Open WebUI GitHub Releases를 검색하고
결과 제목과 URL을 출력해라.정상 표시:
View Result from search_webURL 본문 조회
https://blog.sonny.co.kr/ 무슨 블로그야정상 표시:
View Result from fetch_url공식 출처 전체 목록 테스트
웹 검색을 사용하라.
1. 공식 사이트의 목록 페이지만 찾아라.
2. 공식 도메인이 아닌 결과는 최종 근거로 사용하지 마라.
3. 검색 결과의 공식 URL을 fetch_url로 읽어라.
4. 전체 목록을 빠짐없이 정리하라.
5. 목록 전체를 확보하지 못하면 확인하지 못한 범위를 표시하라.정상적인 도구 흐름:
View Result from search_web
View Result from fetch_url
최종 답변PDF RAG
첨부된 계약서에서 계약금액, 계약기간,
검수 조건과 하자보수 조건을 찾아라.
각 항목마다 파일명, 페이지, 원문 근거를 표시해라.임베딩 API
curl -s http://127.0.0.1:11434/api/embed \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3-embedding:0.6b",
"input": "임베딩 정상 동작 확인"
}' |
head -c 300OOM 확인
sudo journalctl -k -b |
grep -Ei 'oom|out of memory|killed process|llama'정상화된 뒤에는 새로운 Killed process ... llama-server 기록이 추가되지 않아야 한다.
실시간 확인:
sudo journalctl -k -f |
grep --line-buffered -Ei \
'oom|out of memory|killed process|llama|amdgpu'메모리 확인:
watch -n 1 'free -h; echo; swapon --show'정리
최종 구성 상태:
Qwen3-8B 로컬 대화 정상 확인
llama.cpp OpenAI API 연동 정상 확인
Open WebUI Docker 운영 정상 확인
DDGS 웹 검색 정상 확인
fetch_url 웹페이지 조회 정상 확인
내부 웹페이지 조회 정상 확인
Ollama 로컬 임베딩 정상 확인
PDF OCR·RAG 재색인 후 질의 확인
prompt_save OOM 캐시 비활성화로 해결
최종 컨텍스트 25,600토큰
KV 캐시 Q8_0
초기 컨텍스트 유지 4,096토큰
컨텍스트 체크포인트 2개이번 작업에서 가장 혼동하기 쉬웠던 부분은 Open WebUI에 표시된 Server Connection Error였다.
실제 원인은 Open WebUI가 아니라 BC-250에서 실행 중이던 llama-server가 prompt_save 시점에 전역 OOM으로 종료된 것이었다.
n_tokens = 9752
truncated = 0
prompt_save state size = 1371.487 MiB
global_oom
Killed process llama-server따라서 토큰 초과가 아니라 메모리 피크 문제였고, 다음 설정이 핵심 해결책이 됐다.
--cache-ram 0
--no-cache-idle-slots여기에 KV 캐시를 Q8로 줄이고 컨텍스트를 25.6K로 재배분했다.
-c 25600
-ctk q8_0
-ctv q8_0
--keep 4096
--ctx-checkpoints 2결과적으로 모델 파일은 그대로였지만 문맥 유지와 후속 질문 품질이 눈에 띄게 좋아졌다.
로컬 AI는 단순히 큰 모델을 올리는 것으로 끝나지 않는다.
모델 정밀도
컨텍스트 길이
KV 캐시
배치 크기
프롬프트 캐시
도구 수
운영체제 여유 메모리사이에서 적절한 균형점을 잡는 것이 실제 체감 성능과 안정성을 결정한다.