ChatGPT API를 활용하여 생성된 텍스트 응답을 처리하고 파싱하는 과정은 응용 프로그램의 성능과 사용성을 결정하는 중요한 단계이다. 이 절에서는 응답 데이터를 효율적으로 처리하고, 파싱하는 방법에 대해 자세히 설명한다.

응답 데이터 구조 이해

OpenAI의 ChatGPT API는 호출에 대한 응답으로 JSON 형식의 데이터를 반환한다. 이 JSON 데이터는 여러 가지 정보를 담고 있으며, 이를 적절히 파싱하여 원하는 텍스트를 추출하는 것이 중요하다.

JSON 응답 구조 예시

아래는 ChatGPT API로부터 반환된 일반적인 응답의 구조를 보여준다:

{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1691234567,
  "model": "gpt-4",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "This is the generated text."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 10,
    "completion_tokens": 7,
    "total_tokens": 17
  }
}

이 JSON 객체에서 주요 관심 대상은 choices 배열 내의 message 객체이다. 이 객체에는 모델이 생성한 텍스트가 포함되어 있다.

텍스트 추출 및 파싱

기본 텍스트 추출

가장 기본적인 파싱 작업은 choices 배열의 첫 번째 요소에서 생성된 텍스트를 추출하는 것이다. 이를 위해 다음과 같은 파이썬 코드를 사용할 수 있다.

response = {
    "id": "chatcmpl-123",
    "object": "chat.completion",
    "created": 1691234567,
    "model": "gpt-4",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "This is the generated text."
            },
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 10,
        "completion_tokens": 7,
        "total_tokens": 17
    }
}

generated_text = response['choices'][0]['message']['content']
print(generated_text)

이 코드는 choices 배열의 첫 번째 항목에서 content 필드를 추출하여 생성된 텍스트를 출력한다.

응답에서 추가 정보 추출

응답에는 생성된 텍스트 외에도 다양한 유용한 정보가 포함되어 있다. 예를 들어, 사용된 토큰 수나 finish_reason 같은 정보는 API 사용량을 추적하거나 응답이 어떻게 종료되었는지 파악하는 데 도움이 된다.

usage_info = response['usage']
prompt_tokens = usage_info['prompt_tokens']
completion_tokens = usage_info['completion_tokens']
total_tokens = usage_info['total_tokens']

finish_reason = response['choices'][0]['finish_reason']

print(f"Prompt Tokens: {prompt_tokens}, Completion Tokens: {completion_tokens}, Total Tokens: {total_tokens}")
print(f"Finish Reason: {finish_reason}")

JSON 데이터의 복잡한 파싱

때로는 응답 데이터가 더 복잡할 수 있으며, 여러 choices 항목이 포함될 수 있다. 이 경우 각 항목을 순회하며 데이터를 처리해야 한다.

for choice in response['choices']:
    generated_text = choice['message']['content']
    print(generated_text)

이 코드는 모든 choices 항목을 순회하며 각각의 생성된 텍스트를 출력한다.

파싱된 데이터를 이용한 추가 처리

파싱된 텍스트는 다양한 용도로 활용될 수 있다. 예를 들어, 텍스트를 특정 패턴에 따라 분할하거나, 특정 키워드의 존재 여부를 확인하는 등의 작업을 수행할 수 있다.

정규 표현식을 이용한 텍스트 분석

정규 표현식(Regular Expressions, RegEx)을 사용하여 생성된 텍스트에서 특정 패턴을 추출하거나 분석할 수 있다.

import re

pattern = r'\d+'  # 숫자를 찾는 정규 표현식 패턴
matches = re.findall(pattern, generated_text)

print(f"Found numbers: {matches}")

이 예시는 생성된 텍스트에서 모든 숫자를 찾아 출력하는 코드이다.

API 응답의 안전성 검사

파싱하기 전에 응답 데이터가 예상된 구조를 가지고 있는지 확인하는 것은 매우 중요하다. API 응답의 구조가 변경되었거나 예상치 못한 오류가 발생할 수 있으므로, 안전성 검사를 통해 예외 처리를 해야 한다.

if 'choices' in response and len(response['choices']) > 0:
    generated_text = response['choices'][0]['message']['content']
else:
    generated_text = "No content generated."

print(generated_text)

이 코드는 choices 배열이 존재하고 비어 있지 않은지 확인한 후에 텍스트를 추출한다.

결측값 처리 및 기본값 설정

응답 데이터에서 특정 필드가 없거나 null 값일 경우를 대비해 기본값을 설정하는 방법도 필요하다.

generated_text = response.get('choices', [{}])[0].get('message', {}).get('content', 'No content generated.')
print(generated_text)

이 코드는 안전하게 값을 추출하며, 데이터가 없을 경우 "No content generated."라는 기본값을 반환한다.

다양한 응답 포맷 처리

때로는 OpenAI API의 응답이 복잡한 형식이거나, 여러 가지 시나리오에 따라 다르게 구성될 수 있다. 이러한 경우를 대비하여 다양한 응답 포맷을 처리할 수 있는 유연한 파싱 로직을 구현하는 것이 필요하다.

다중 선택 항목 처리

API 응답에서 choices 배열이 여러 개의 선택지를 포함하고 있을 때, 각 선택지를 모두 처리해야 할 수 있다. 예를 들어, 대화형 챗봇에서 여러 응답을 제시할 때 유용하다.

responses = []
for choice in response.get('choices', []):
    text = choice.get('message', {}).get('content', '')
    responses.append(text)

print("Generated responses:")
for i, text in enumerate(responses, 1):
    print(f"Response {i}: {text}")

이 코드는 여러 choices 항목을 처리하여 각각의 생성된 텍스트를 리스트에 저장한 후, 이를 출력한다.

비동기 응답 처리

대규모 애플리케이션에서는 API 호출과 응답 처리의 비동기 처리가 필요할 수 있다. Python의 asyncio 라이브러리와 함께 OpenAI API를 비동기적으로 호출하고 응답을 처리하는 방법을 살펴보겠다.

비동기 호출 예제

import asyncio
import openai

async def fetch_response(prompt):
    response = await openai.ChatCompletion.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}]
    )
    return response['choices'][0]['message']['content']

async def main():
    prompts = ["Hello!", "How are you?", "Tell me a joke."]
    tasks = [fetch_response(prompt) for prompt in prompts]
    responses = await asyncio.gather(*tasks)

    for i, response in enumerate(responses, 1):
        print(f"Response {i}: {response}")

asyncio.run(main())

이 코드는 여러 프롬프트에 대해 비동기적으로 API 호출을 수행하고, 응답을 병렬로 처리한다.

대규모 데이터 처리

API 응답을 대규모 데이터로 처리할 때, 예를 들어 수천 개의 텍스트 생성 작업을 병렬로 수행해야 할 때, 데이터를 효율적으로 처리하는 방법을 고려해야 한다.

배치 처리

대규모 데이터에서 성능을 최적화하기 위해 응답을 배치로 처리할 수 있다.

batch_size = 5
prompts = ["Prompt 1", "Prompt 2", "Prompt 3", "Prompt 4", "Prompt 5", "Prompt 6"]
batches = [prompts[i:i + batch_size] for i in range(0, len(prompts), batch_size)]

for batch in batches:
    tasks = [fetch_response(prompt) for prompt in batch]
    responses = asyncio.run(asyncio.gather(*tasks))
    print("Batch responses:", responses)

이 코드는 프롬프트를 배치로 나누어 비동기적으로 처리한다. 대규모 데이터 세트를 효율적으로 처리할 때 유용한 기법이다.

텍스트 데이터의 후처리

생성된 텍스트를 받아 다양한 형태로 후처리할 수 있다. 예를 들어, 특정 포맷으로 변환하거나, 텍스트를 분석하여 추가적인 정보를 추출하는 작업을 수행할 수 있다.

텍스트 정리 및 포맷팅

생성된 텍스트에 불필요한 공백이나 특수 문자가 포함될 수 있다. 이를 정리하여 사용하기 적합한 형태로 변환한다.

cleaned_text = generated_text.strip().replace("\n", " ")
print(f"Cleaned Text: {cleaned_text}")

이 코드는 텍스트 양쪽의 공백을 제거하고, 줄 바꿈 문자를 공백으로 대체한다.

키워드 추출 및 요약

생성된 텍스트에서 중요한 키워드를 추출하거나 요약할 수 있다.

import re

def extract_keywords(text, top_n=3):
    words = re.findall(r'\b\w+\b', text.lower())
    freq = {}
    for word in words:
        freq[word] = freq.get(word, 0) + 1
    sorted_keywords = sorted(freq.items(), key=lambda item: item[1], reverse=True)
    return sorted_keywords[:top_n]

keywords = extract_keywords(generated_text)
print(f"Keywords: {keywords}")

이 코드는 텍스트에서 가장 빈도가 높은 키워드를 추출하여 출력한다.

로그 관리 및 디버깅

API 응답의 파싱 및 처리를 디버깅하기 위해 로그를 남기는 것은 매우 중요하다. 특히 비동기 처리와 같이 복잡한 응답을 처리할 때 로그는 문제를 추적하는 데 유용하다.

기본 로그 설정

import logging

logging.basicConfig(level=logging.INFO)

def log_response(response):
    logging.info(f"API Response: {response}")

log_response(response)

이 코드는 기본적인 로그 설정을 통해 API 응답을 기록한다. 로그를 통해 각 단계에서 발생하는 데이터를 추적할 수 있다.

종합적인 응답 처리 예제

위에서 언급한 다양한 방법을 결합하여, 보다 종합적이고 실용적인 응답 처리 시스템을 구축할 수 있다.

import logging

logging.basicConfig(level=logging.INFO)

async def handle_api_response(prompt):
    try:
        response = await openai.ChatCompletion.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}]
        )
        text = response['choices'][0]['message']['content']
        logging.info(f"Generated Text: {text.strip()}")

        # 추가적인 텍스트 후처리 및 분석
        cleaned_text = text.strip().replace("\n", " ")
        keywords = extract_keywords(cleaned_text)
        logging.info(f"Keywords: {keywords}")

        return cleaned_text, keywords
    except Exception as e:
        logging.error(f"Error processing API response: {str(e)}")
        return None, None

async def main():
    prompts = ["First prompt", "Second prompt", "Third prompt"]
    tasks = [handle_api_response(prompt) for prompt in prompts]
    results = await asyncio.gather(*tasks)

    for i, (cleaned_text, keywords) in enumerate(results, 1):
        print(f"Result {i}: Text - {cleaned_text}, Keywords - {keywords}")

asyncio.run(main())

이 코드는 API 호출에서부터 응답 파싱, 후처리, 키워드 추출, 그리고 로그 관리까지 포함하는 종합적인 처리 파이프라인을 제공한다.