이 글은 제 보안 프로젝트 AgentShield의 트러블 슈팅 과정 기록입니다.
배경
로컬 GPU에서 qwen3.5:4b를 Ollama로 실행하고, LangGraph 기반 멀티 에이전트 판정 시스템이 이 모델을 호출하는 구조였다. Phase 1 스캔이 시작되자마자 아래 에러가 반복됐다.
Ollama API Error 500 (attempt 1/3): {"error":"EOF"}
Ollama API failed after 3 retries
Strict Auditor: Empty Ollama response, using default judgment
처음엔 VRAM 부족이나 네트워크 문제로 봤다. 하지만 단순한 curl 테스트는 정상 응답을 반환했다.
curl http://localhost:11434/api/generate -d '{
"model": "qwen3.5:4b",
"prompt": "Hello",
"stream": false
}'
# → {"response":"Hello! How can I help you today?", ...}
하드웨어는 멀쩡한데 왜 에이전트에서는 계속 EOF가 날까?
핵심 원인 분석
{"error":"EOF"}의 실제 의미
Ollama에서 EOF 에러는 단순 타임아웃이 아니다. 모델 runner 프로세스 자체가 crash했다는 신호다. 내부적으로 io.EOF — stdout 파이프가 끊겼다는 의미이며, 주로 다음 상황에서 발생한다.
- CUDA/Metal OOM (GPU 메모리 부족)
- KV cache 오버플로 (생성 토큰이 컨텍스트 윈도우를 초과)
- 모델 runner 예외 종료
Thinking 모델의 특수성
qwen3.5:4b는 Thinking 모델이다. 일반 모델과 달리, 답변 전에 <think>...</think> 블록에서 추론 과정을 먼저 생성한다.
/api/generate로 호출했을 때의 실제 응답 구조:
{
"response": "Hello! How can I help?",
"thinking": "Thinking Process:\n1. Analyze the Input...",
"done": true
}
문제는 thinking 토큰이 response 토큰 예산을 잠식한다는 것이다.
토큰 예산 계산
판정 에이전트의 기존 설정:
payload = {
"model": "qwen3.5:4b",
"prompt": prompt, # 시스템 프롬프트 + 공격 프롬프트 포함
"stream": False,
"options": {
"num_predict": 512, # ← 전체 생성 예산
}
}
실제 토큰 소비:
시스템 프롬프트: ~150 tokens
공격 프롬프트: ~300 tokens (1,200자 기준)
thinking: ~2,000+ tokens ← 이게 num_predict 512를 초과
실제 답변: 0 tokens 남음
num_predict=512를 thinking이 전부 소진하고, 답변을 생성할 토큰이 남지 않으면 Ollama runner가 즉시 종료(EOF)한다. 이 때문에 타임아웃(60초 대기)도 없이 즉각적으로 에러가 반환된 것이다.
왜 단순 curl 테스트는 성공했나?
curl ... -d '{"prompt": "Hello", ...}'
"Hello"는 약 3 토큰. Thinking이 200 토큰을 써도 num_predict=512 안에서 답변까지 여유가 있다. 반면 실제 판정 프롬프트는 수백 토큰짜리 시스템 프롬프트 + 공격 내용을 포함한다.
해결 방법
방법 1: /api/chat + think=False (권장)
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"stream": False,
"think": False, # ← thinking 토큰 완전 억제
"options": {
"num_predict": 512,
},
}
url = f"{ollama_base_url}/api/chat"
응답 파싱:
data = await resp.json()
content = data.get("message", {}).get("content", "").strip()
think=False를 지정하면 모델이 thinking 없이 직접 답변을 생성한다. 판정 에이전트처럼 구조화된 JSON 출력이 필요한 경우 특히 유효하다.
방법 2: /api/generate + thinking fallback
thinking 모드를 유지해야 한다면 response 필드가 비었을 때 thinking 필드를 fallback으로 사용한다.
raw_text = resp_json.get("response", "")
if not raw_text.strip() and resp_json.get("thinking"):
raw_text = resp_json["thinking"] # thinking 내용을 응답으로 활용
이미 llm_client.py에 이 패턴이 구현되어 있었고, 판정 노드만 누락되어 있었다.
방법 3: Testbed 챗봇 — config 기반 설정
Testbed(/api/chat 호출)에서는 config.py의 값을 payload에 반영한다:
payload = {
"model": OLLAMA_MODEL,
"messages": messages,
"stream": False,
"think": False,
"options": {
"num_ctx": config.OLLAMA_NUM_CTX, # 4096
"temperature": config.OLLAMA_TEMPERATURE, # 0.2
"num_predict": config.LLM_DEFAULT_NUM_PREDICT, # 1024
},
}
think=False와 적절한 num_predict 조합으로 EOF가 완전히 사라졌다.
디버깅 체크리스트
EOF 에러를 만났을 때 순서대로 확인:
# 1. 모델이 정상적으로 로드되는지
curl http://localhost:11434/api/tags | jq '.models[].name'
# 2. 짧은 프롬프트로 기본 동작 확인
curl http://localhost:11434/api/chat -d '{
"model": "qwen3.5:4b",
"messages": [{"role":"user","content":"hi"}],
"stream": false,
"think": false
}'
# 3. thinking 필드가 response를 잠식하는지 확인
curl http://localhost:11434/api/generate -d '{
"model": "qwen3.5:4b",
"prompt": "hi",
"stream": false
}' | python3 -c "import sys,json; d=json.load(sys.stdin); print('response:', len(d.get('response','')), 'thinking:', len(str(d.get('thinking',''))))"
핵심 요약
| 증상 | 원인 | 해결 |
|---|---|---|
| EOF, 즉각 실패(타임아웃 없음) | 모델 runner crash | /api/chat + think=False |
response: "" but thinking: "..." |
thinking이 토큰 예산 소진 | num_predict 증가 or think=False |
| 단순 테스트는 성공, 긴 프롬프트만 실패 | KV cache 오버플로 임계값 문제 | num_ctx 조정 |
Thinking 모델을 에이전트에 통합할 때는 반드시 think=False를 명시하거나 response/thinking 필드 분기 처리를 해야 한다.
'4. [팀] 프로젝트 및 공모전 > 4-5 AgentShield(보안 플랫폼)' 카테고리의 다른 글
| [AgentShield] 참고한 사이트 및 이유 총 정리 (1) (0) | 2026.04.26 |
|---|---|
| [트러블 슈팅] 로컬 단일 GPU에서 멀티 에이전트 LLM 파이프라인 실행하기: 직렬화와 VRAM 관리 (0) | 2026.04.22 |
| [개인 공부] AgentShield: 자동화 모의해킹 파이프라인 및 DPO 데이터 수집 아키텍처 정리 (0) | 2026.04.15 |
| [개인 공부 메모] AgentShield LLM Judge 오탐 문제 분석 및 보안 판정 동향 (0) | 2026.04.14 |
| [개인 공부] AgentShield 프로젝트 작업 일지 — 2026년 4월 14일 (0) | 2026.04.12 |
