Contents

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·문서 RAG

Open 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-stopped

YAML 작성 시 주의점

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:main

main은 새 빌드가 계속 반영되는 롤링 태그다. 기능 테스트에는 편하지만 업데이트 후 설정 화면이나 동작이 갑자기 바뀔 수 있다.

장기 운영에서는 정상 동작이 확인된 릴리스 태그로 고정하는 편이 안전하다.

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-size

llama.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 Vulkan0BC-250의 Vulkan 장치 사용
-ngl 999가능한 모델 레이어를 전부 Vulkan 장치로 오프로드
-c 25600컨텍스트 크기 25,600토큰
-np 1동시 처리 슬롯 1개
-ctk q8_0Key KV 캐시를 Q8_0으로 저장
-ctv q8_0Value KV 캐시를 Q8_0으로 저장
-fa onFlash Attention 활성화
-b 512논리 배치 크기 512
-ub 256물리 마이크로배치 크기 256
--context-shift생성 중 컨텍스트가 가득 차면 오래된 일부를 이동
--keep 4096Context Shift 시 최초 프롬프트 앞 4,096토큰 유지
--cache-ram 0별도 Prompt RAM Cache 비활성화
--no-cache-idle-slots유휴 슬롯을 Prompt RAM Cache에 저장하지 않음
--ctx-checkpoints 2슬롯당 내부 컨텍스트 체크포인트 최대 2개
--reasoning off별도 reasoning/thinking 출력 비활성화
--aliasOpenAI 호환 API에 표시할 모델 이름
--host 0.0.0.0모든 네트워크 인터페이스에서 접속 허용
--port 10000llama-server 수신 포트
--api-keyOpenAI 호환 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 0

25,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.log
grep -Ei \
'n_ctx|context|KV buffer|compute buffer|model buffer' \
~/llama-server.log

3. 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-bc250

Docker 컨테이너에서 연결 확인

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.7

Open 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
웹 콘텐츠 불러오기 생략: ON

DDGS는 여러 검색 공급자를 선택할 수 있는 메타 검색 백엔드다.

현재 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
└─ Native

Native 모드에서는 Open WebUI가 모델에 search_web, fetch_url 등의 도구를 제공하고 모델이 직접 호출 여부를 판단한다.

정상 작동 시 채팅에 다음 표시가 나온다.

View Result from search_web

URL 본문을 열면:

View Result from fetch_url

Arena 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~5

Open 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 Error

Open 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_0

Prompt 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 0

2단계: 컨텍스트 소폭 축소

25600 → 22528

또는:

25600 → 20480

3단계: 배치 축소

-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/fstab

Vulkan 장치 메모리가 일반 익명 메모리처럼 전부 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-webui

llama.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_web

URL 본문 조회

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 300

OOM 확인

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 캐시
배치 크기
프롬프트 캐시
도구 수
운영체제 여유 메모리

사이에서 적절한 균형점을 잡는 것이 실제 체감 성능과 안정성을 결정한다.

Subscribe to Sonny_Blog

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe