SOFTMOA TECHNOLOGY

Ollama 스트리밍 응답 JSON 파싱 오류 해결 방법

소프트모아가 정리한 기술 기록입니다.

Contents
작성일 2026. 10. 11.

Ollama의 /api/generate 응답을 JSON 하나로 읽다가 ‘Extra data’ 오류가 나면 모델을 바꾸기 전에 스트리밍 설정부터 확인하세요. 이 API는 기본적으로 줄마다 JSON 객체를 보내므로, 한 줄씩 해석해 response를 붙이고 done이 true인지 확인해야 합니다.[1][6] 답변을 실시간으로 보여줄 필요가 없다면 요청 본문에 "stream": false를 지정해 일반 JSON 응답을 받는 방법도 있습니다.[1]

응답 형식과 답변 내용을 따로 확인하기

HTTP 응답의 Content-Type이 application/x-ndjson인지 먼저 확인합니다. 요청의 "stream": true는 Ollama가 조각을 보내도록 하는 설정입니다.[1][3] 클라이언트에서 전체 본문을 한 번에 JSON으로 읽는 코드가 남아 있다면 서버 설정만 바꿔서는 해결되지 않습니다. JSON 객체 두 개를 줄바꿈으로 이어 놓은 본문은 배열도, 단일 객체도 아닙니다.

여기서는 /api/generate의 텍스트 필드인 response만 모읍니다. done은 생성 종료 여부, done_reason은 멈춘 이유입니다.[3] 같은 줄에 마지막 텍스트와 done: true가 함께 올 수 있으므로, 종료 여부를 먼저 검사하고 반환하면 끝부분을 잃을 수 있습니다.[1] 종료 신호를 받았다는 사실과 답변이 업무에 쓸 만큼 정확하다는 판단도 구분해야 합니다.

마지막 조각까지 모으는 실행 예시

아래는 소프트모아의 연동 설계 예시입니다. Python 3.14.7에서 메모리 입력과 합성 HTTP 청크형 응답으로 검사했으며, 실제 모델 생성 결과를 사용하지 않았습니다. 파일을 collect_demo.py로 저장하고 python3 collect_demo.py로 실행하면 됩니다. 설치할 외부 패키지는 없습니다.

import json
from io import BytesIO

def collect(stream):
    parts, total = [], 0
    while True:
        raw = stream.readline(65537)
        if not raw:
            raise RuntimeError("완료 신호 없음")
        total += len(raw)
        if len(raw) > 65536 or total > 1048576:
            raise ValueError("예시 수신 한도 초과")
        if not raw.strip():
            continue
        item = json.loads(raw.decode("utf-8"))
        if not isinstance(item, dict):
            raise ValueError("JSON 객체 필요")
        if "error" in item:
            raise RuntimeError(str(item["error"]))
        text = item.get("response", "")
        if not isinstance(text, str):
            raise ValueError("response 문자열 필요")
        parts.append(text)
        if item.get("done") is True:
            return "".join(parts), item.get("done_reason")

if __name__ == "__main__":
    sample = ('{"response":"안녕","done":false}\n'
              '{"response":"하세요","done":true,"done_reason":"stop"}\n')
    print(collect(BytesIO(sample.encode("utf-8"))))

입력 첫 줄의 ‘안녕’과 종료 줄의 ‘하세요’를 합친 반환값은 ('안녕하세요', 'stop')입니다. 두 번째 줄의 response가 빈 문자열이어도 앞서 모은 답변은 남습니다. 반면 첫 줄만 있고 입력이 끝나면 ‘완료 신호 없음’ 예외가 발생합니다. 받은 문자열이 있다는 이유만으로 정상 완료를 반환하지 않는 것이 이 함수의 기준입니다.

readline은 줄 단위로 읽는 파일형 입력을 전제로 합니다. Python HTTPResponse도 이 메서드를 제공합니다.[8] 테스트에서는 한글의 UTF-8 바이트를 1바이트씩 HTTP 청크로 나눠도 같은 문자열로 복원되는지 확인했습니다. 네트워크 조각 하나가 JSON 한 줄이라고 가정해 조각마다 decode와 json.loads를 실행하는 방식은 이 예시와 다릅니다. 직접 받은 바이트 조각을 넘기려면 먼저 줄 경계를 복원하는 처리가 필요합니다.

HTTP 200인데도 실패로 끝내야 하는 경우

Ollama는 스트리밍 도중 문제가 생기면 error 필드가 있는 객체를 보낼 수 있습니다. 이미 응답이 시작됐으므로 HTTP 상태 코드는 바뀌지 않습니다.[2] 함수가 error를 발견하면 앞서 모은 답변을 성공 결과로 내보내지 않고 예외로 끝내는 이유입니다. 화면에 일부 텍스트를 먼저 보여줬다면 ‘생성 중단’ 표시를 남기고, 완료된 상담 기록이나 자동 승인 입력으로 확정하지 않아야 합니다.

  • 잘못된 JSON이나 UTF-8 입력은 건너뛰지 않습니다. 파싱 오류를 무시하면 빠진 문장까지 정상 답변으로 저장할 수 있습니다.
  • 최상위 값이 배열이거나 response가 숫자이면 형식 오류로 처리합니다. done이 문자열 "true"인 경우도 완료로 인정하지 않습니다.
  • 한 줄 65,536바이트, 총 수신 1,048,576바이트를 예시 한도로 정했습니다. 줄바꿈도 바이트 수에 포함하며, 이는 Ollama의 고정 제한이 아니라 이 코드의 수신 정책입니다.

실제 서비스에 연결할 때 남는 조건

이 함수는 답변을 모은 뒤 반환하는 수신 검사 예시이지, 화면에 매 조각을 즉시 출력하는 완성형 클라이언트가 아닙니다. HTTP 상태·Content-Type을 확인한 바이너리 응답 객체를 전달하고, 연결 오류나 읽기 시간 초과도 실패로 처리해야 합니다. 입력이 멈춘 채 연결만 유지되면 바이트 한도는 작동하지 않으므로 별도의 시간 제한이 필요합니다.

설정 변경 뒤에는 오류 문구가 사라졌는지만 보지 말고, 마지막 단어가 남는지, 종료 없이 끊긴 답변이 완료로 저장되지 않는지 확인하세요. 수신 형식 검사는 모델 답변의 사실성, 안전성, 도구 실행 권한까지 보장하지 않습니다. /api/generate가 아닌 다른 API를 연결할 때는 해당 응답 계약에 맞춰 텍스트 필드와 종료 조건을 다시 정해야 합니다.

확인한 공식 문서

소프트모아는 해당 시스템을 구축합니다. 문의하기

Share this

AI

Have a questions?

견적 및 기술문의

mobile : 010-7931-4813

Contact Form