# 응답 데이터 처리 및 파싱

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

#### 응답 데이터 구조 이해

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

**JSON 응답 구조 예시**

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

```json
{
  "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` 배열의 첫 번째 요소에서 생성된 텍스트를 추출하는 것이다. 이를 위해 다음과 같은 파이썬 코드를 사용할 수 있다.

```python
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 사용량을 추적하거나 응답이 어떻게 종료되었는지 파악하는 데 도움이 된다.

```python
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` 항목이 포함될 수 있다. 이 경우 각 항목을 순회하며 데이터를 처리해야 한다.

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

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

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

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

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

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

```python
import re

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

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

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

#### API 응답의 안전성 검사

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

```python
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` 값일 경우를 대비해 기본값을 설정하는 방법도 필요하다.

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

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

#### 다양한 응답 포맷 처리

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

**다중 선택 항목 처리**

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

```python
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를 비동기적으로 호출하고 응답을 처리하는 방법을 살펴보겠다.

**비동기 호출 예제**

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

**배치 처리**

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

```python
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)
```

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

#### 텍스트 데이터의 후처리

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

**텍스트 정리 및 포맷팅**

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

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

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

**키워드 추출 및 요약**

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

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

**기본 로그 설정**

```python
import logging

logging.basicConfig(level=logging.INFO)

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

log_response(response)
```

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

#### 종합적인 응답 처리 예제

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

```python
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 호출에서부터 응답 파싱, 후처리, 키워드 추출, 그리고 로그 관리까지 포함하는 종합적인 처리 파이프라인을 제공한다.
