로컬 AI 실측 노트

로컬 LLM 한국어 JSON 추출 6GB 실측: 형식은 format이, 날짜는 코드가

2026-09-30

로컬 LLM으로 문자·주문·리뷰·문의 글에서 정보를 뽑아 JSON으로 받으면, 그 결과를 바로 프로그램에 넣을 수 있습니다. 그런데 모델이 가끔 설명을 붙이거나 마크다운 코드 블록 표시(백틱 세 개)로 감싸서 보내면 프로그램이 읽지 못합니다. Ollama에는 이걸 막는 format 옵션이 있습니다. 이 옵션이 형식만 지켜 주는지, 답의 정확도까지 바꾸는지 GTX 1660 SUPER 6GB에서 재 봤습니다.

지어낸 한국어 글 24개(약속 문자·주문·상품 리뷰·고객 문의 각 6개)에서 정해진 칸을 뽑게 하고, 같은 질문을 세 가지 방법으로 보냈습니다. ① 질문에 "JSON으로만 답하라"고만 쓰기 ② format: "json" ③ format에 JSON 스키마(칸 이름과 자료형)를 넣기. 모델은 6GB에 전부 올라가는 Qwen3 4B-instruct, Gemma 3 4B, Qwen3 8B(num_gpu 99, 생각 끄기)입니다.

결론부터 정리하면 이렇습니다.

방법별로 바로 읽을 수 있는 JSON이 나온 비율

답을 아무 손질 없이 Python json.loads에 넣었을 때 읽힌 개수입니다(24개 중).

손질 없이 읽힌 JSON 개수. 질문만 보냈을 때 Gemma 3 4B 0개, Qwen3 8B 기본 설정 19개, 나머지는 모두 24개
질문만 보냈을 때(회색) Gemma 3 4B는 24개 모두 코드 블록으로 감쌌고, format을 쓰면(파랑·주황) 세 모델 모두 24개 전부 바로 읽혔습니다.
모델 질문만 format: "json" format: 스키마
Qwen3 4B-instruct 24 / 24 24 / 24 24 / 24
Gemma 3 4B 0 / 24 (24개 모두 코드 블록) 24 / 24 24 / 24
Qwen3 8B 24 / 24 24 / 24 24 / 24
Qwen3 8B (모델 기본 설정) 19 / 24 (주문 글 5개를 코드 블록으로) 24 / 24 24 / 24

위 세 줄은 temperature 0입니다. 모델 기본 설정(temperature 등을 보내지 않고 모델에 들어 있는 값을 그대로 쓰는 경우, seed 1)으로도 한 번씩 돌렸는데, Qwen3 4B-instruct와 Gemma 3 4B는 결과가 위와 같았고 Qwen3 8B만 달라서 따로 적었습니다.

정확도: format을 바꿔도 틀린 곳이 그대로

글마다 정답을 정해 두고 칸별로 맞았는지 셌습니다(약속 4칸·주문 4칸·리뷰 3칸·문의 4칸, 모두 90칸). 장소·상품 이름은 핵심 단어가 들어 있으면 맞은 것으로 쳤습니다.

모델 맞힌 칸 (90칸) 모든 칸을 맞힌 글 (24개) 약속 주문 리뷰 문의
Qwen3 4B-instruct 85 19 22/24 24/24 18/18 21/24
Gemma 3 4B 86 20 22/24 24/24 17/18 23/24
Qwen3 8B 86 21 23/24 24/24 16/18 23/24

temperature 0 기준이고, 세 방법(질문만·json·스키마)에서 숫자가 모두 같았습니다. 모델 기본 설정에서도 Qwen3 4B-instruct와 Gemma 3 4B는 같았고, Qwen3 8B만 약속 한 칸을 더 틀리고 리뷰 한 칸을 덜 틀려서 맞힌 칸은 같은 86칸, 모든 칸을 맞힌 글은 20개였습니다.

틀린 곳은 이랬습니다.

글 (요지) 정답 Qwen3 4B-instruct Gemma 3 4B Qwen3 8B
"모레 정오에 … 면접 스터디" 2026-10-01 09-30 ✗ 09-29 ✗ 09-30 ✗
"다음 주 월요일 저녁 7시 반에 …" 2026-10-05 10-04 ✗ (일요일) 09-29 ✗ 10-05 ✓ (기본 설정에서는 10-11 ✗)
"공일공 삼삼삼삼 칠칠칠칠로 연락 주세요" 010-3333-7777 010-0000-0000 ✗ 010-3337-7777 ✗ 010-0000-0000 ✗
"매장 영업시간이 궁금해요" (문의 종류) 기타 배송문의 ✗ ✓ ✓
"같은 주문이 두 번 결제됐어요 … 환불해 주세요" 환불 교환 ✗ ✓ ✓
"배송이 일주일이나 걸렸고 …" (배송 언급) true ✓ false ✗ ✓
"처음엔 별로였는데 … 좋네요. 재구매 의사 있습니다. 택배는 평범하게 왔어요." 긍정 / 배송 언급 true ✓ ✓ 중립 ✗ / false ✗ (기본 설정에서는 긍정 ✓ / false ✗)

상대 날짜 고치기: 달력 · 생각 모드 · 코드 계산

약속 글 6개를 세 가지 방법으로 다시 돌렸습니다(temperature 0, 스키마 사용). ②는 아래 '생각 모드' 절에서 24개 전체를 돌린 결과 중 약속 글 6개만 뽑은 것입니다. 표의 숫자는 날짜 칸 6개 중 맞힌 개수입니다.

방법 Qwen3 4B-instruct Gemma 3 4B Qwen3 8B
그대로 (위 결과) 4 / 6 4 / 6 5 / 6
① 질문에 2주 치 달력 붙이기 5 / 6 4 / 6 4 / 6
② 생각 켜기 4 / 6 (생각 전용 qwen3:4b) (생각 기능 없음) 4 / 6
③ 날짜 표현만 옮겨 적게 하고 코드로 계산 6 / 6 6 / 6 6 / 6

① 달력 붙이기: 오늘 날짜 문장 뒤에 9/29(화), 9/30(수), 10/1(목), … 10/12(월)을 붙였습니다. "모레"는 세 모델 모두 여전히 틀렸고, 오히려 Qwen3 8B는 맞히던 "다음 주 월요일"을 10-12로, Gemma 3 4B는 "모레"를 있지도 않은 2026-09-31로 답했습니다. 질문은 약 125토큰 길어졌습니다.

② 생각 켜기: 생각 내용(한글·영문 글자 중 93~95%가 영문)을 보면 틀린 이유가 드러납니다.

③ 표현만 옮기고 코드로 계산: 질문과 스키마의 date 칸을 date_text("메시지에 적힌 날짜 표현을 바꾸지 말고 그대로", 예시 "다음 주 수요일"·"3월 2일")로 바꾸고, 오늘 날짜 문장은 뺐습니다. 세 모델 모두 6개 표현을 그대로 옮겼고("모레", "다음 주 월요일", "10월 15일(목)" 등, Qwen3 4B-instruct는 "모레 정오"처럼 시각까지 붙인 것 하나), 아래 함수로 바꾸니 18개 모두 맞았습니다. 시각·장소·인원 칸도 모두 맞았고, 답 길이는 약속 글 원래 답과 거의 같았습니다(약 50토큰).

import datetime as dt, re

WD = "월화수목금토일"
REL = {"오늘": 0, "내일모레": 2, "내일": 1, "모레": 2, "글피": 3}   # 긴 말부터 확인

def resolve(expr: str, today: dt.date):
    s = re.sub(r"\s+", "", expr)
    for word, days in REL.items():
        if s.startswith(word):
            return today + dt.timedelta(days=days)
    m = re.search(r"(\d{1,2})월(\d{1,2})일", s)
    if m:
        d = dt.date(today.year, int(m.group(1)), int(m.group(2)))
        return d if d >= today else d.replace(year=today.year + 1)
    m = re.search(r"(이번주|다음주|다다음주)(.)요일", s)
    if m and m.group(2) in WD:
        monday = today - dt.timedelta(days=today.weekday())      # 주는 월요일부터
        weeks = {"이번주": 0, "다음주": 1, "다다음주": 2}[m.group(1)]
        return monday + dt.timedelta(days=7 * weeks + WD.index(m.group(2)))
    return None          # 모르는 표현은 사람이 확인

today = dt.date(2026, 9, 29)
print(resolve("모레 정오", today), resolve("다음 주 월요일", today))   # 2026-10-01 2026-10-05

"다음 주"를 어느 요일부터 셀지는 서비스마다 정하기 나름이라, 이렇게 코드에 규칙을 적어 두는 쪽이 모델에게 맡기는 것보다 결과가 한결같습니다.

생각 모드를 켜면: 정확도는 비슷하고 훨씬 느리다

24개 전체를 생각을 켜고 다시 돌렸습니다(temperature 0, 생성 한도 3,500토큰).

모델 생각 맞힌 칸 (90칸) 모든 칸을 맞힌 글 답 하나 평균 시간 (최대)
Qwen3 4B-instruct 없음 85 19 0.74초
Qwen3 4B 생각 전용 (qwen3:4b) 켜기 · 스키마 88 22 15.5초 (42.9초)
Qwen3 8B 끄기 86 21 1.35초
Qwen3 8B 켜기 · 스키마 87 21 9.8초 (21.8초)
Qwen3 8B 켜기 · 질문만 88 22 9.9초 (21.7초)

속도와 메모리

모델 답 하나 평균 시간 답 쓰는 속도 답 길이 그래픽 메모리 최고 (6,144MiB 중)
Qwen3 4B-instruct 0.74초 약 78 tok/s 약 40토큰 3,441MiB
Gemma 3 4B 1.22초 약 73 tok/s 약 45토큰 4,043MiB
Qwen3 8B (num_gpu 99) 1.35초 약 46 tok/s 약 41토큰 5,745MiB

시간·속도·답 길이는 temperature 0에서 format: "json"과 스키마를 쓸 때의 값입니다. 그래픽 메모리는 두 번 돌린 것 중 최고값이고, 화면 표시에 쓰는 약 316MiB를 포함합니다. 질문만 보낸 경우도 답 쓰는 속도는 2% 안에서 같았고, Gemma 3 4B는 코드 블록 표시만큼 답이 5토큰 길어졌습니다. 모델마다 짧은 질문으로 한 번 불러온 뒤, 세 방법을 질문만 → json → 스키마 순서로 이어서 쟀습니다.

쓰는 법: format에 스키마 넣기

Ollama API 문서와 구조화 출력 안내 기준입니다. format에는 "json" 또는 JSON 스키마를 넣을 수 있고, 문서는 질문에도 JSON으로 답하라고 적으라고 권합니다. 이번 측정에서는 한 걸음 더 나가 질문에 칸 이름과 형식까지 적었습니다.

import json, requests

schema = {
    "type": "object",
    "properties": {
        "item": {"type": "string"},
        "qty": {"type": "integer"},
        "unit_price": {"type": "integer"},
        "total": {"type": "integer"},
    },
    "required": ["item", "qty", "unit_price", "total"],
}
prompt = ("아래 주문 메시지에서 주문 정보를 뽑아 JSON 객체 하나로만 답하세요.\n"
          "키와 형식:\n- item: 상품 이름 문자열\n- qty: 수량, 정수\n"
          "- unit_price: 한 개 가격(원), 정수\n- total: 총액(원), 정수\n"
          "설명이나 다른 글은 쓰지 마세요.\n\n"
          "메시지: 무선 마우스 2개 주문할게요. 개당 15,900원이죠?")
r = requests.post("http://localhost:11434/api/generate", json={
    "model": "qwen3:4b-instruct",
    "prompt": prompt,
    "format": schema,            # 또는 "json"
    "think": False,              # 생각 기능이 있다고 표시되는 모델이면 끄기
    "stream": False,
    "options": {"temperature": 0},
})
order = json.loads(r.json()["response"])
print(json.dumps(order, ensure_ascii=False))
# 이번 측정의 답: {"item": "무선 마우스", "qty": 2, "unit_price": 15900, "total": 31800}

측정 환경과 방법

한계