ChatGPT API Token Limit 해결하기 (+ 요금 줄이기)

노관옥·2023년 3월 18일
8
post-thumbnail
post-custom-banner

OpenAI Chat Completion API

회사에서 OpenAI API를 사용한 챗봇 서비스를 개발하다가

기존 text-davinci-003 모델에서 gpt-3.5-turbo 로 바꾸는 과정에서 겪은 어려움과 토큰 제한을 회피할 수 있는 방법을 공유합니다.

Chat completions API

문서 링크

ChatGPT처럼 AI와 대화 형식으로 구현하기 위해선 질문을 할 때 마다 전에 나눴던 대화(Dialog)를 보내야 합니다.

제가 이런 질문을 했고 다음 질문을 보낸다고 가정하면,

API Request의 messages 안에 내용이 쌓이는 방식으로 구현할 수 있습니다.

    messages: list[dict] = [
        {
            'role': 'user',
            'content': '퇴근을 빨리하려면?',
        },
        {
            'role': 'assistant',
            'content': '퇴근을 빨리 하기 위해서는 다음과 같은 방법들을 고려해 볼 수 있습니다:\n\n일을 빠르게 처리하기: 퇴근 시간이 다가올수록 일이 더 바쁘고 복잡해질 수 있습니다. 따라서, 퇴근 시간 전에 할 일을 미리 끝내는 것이 중요합니다. 빠르게 일을 처리하기 위해서는, 우선순위를 정하고, 일의 목록을 작성하고, 일정에 따라 진행하면서 빠르게 처리하는 것이 좋습니다.\n\n다른 사람에게 도움을 요청하기: 일이 많은 경우, 다른 동료들에게 도움을 요청할 수 있습니다. 다른 사람의 도움으로 일을 빨리 처리할 수 있고, 퇴근 시간을 빨리 다가오게 만들 수 있습니다.\n\n퇴근 시간에 일을 하지 않기: 퇴근 시간이 다가오면, 일을 마무리하고 퇴근 준비를 시작하는 것이 좋습니다. 퇴근 시간에 일을 하게 되면, 퇴근 시간이 늦어질 수 있습니다. 따라서, 일을 마치고 퇴근 준비를 빨리 마치는 것이 좋습니다.\n\n일의 분량을 조절하기: 일의 분량이 너무 많다면, 일부를 다음 날로 미루는 것이 좋습니다. 또는, 긴급한 일을 우선 처리하고, 나머지 일을 다음 날로 미루는 것도 좋은 방법입니다.\n\n일을 계획하고 조직하기: 일을 계획하고 조직하는 것은 시간을 절약하고 효율적으로 일을 처리하는 데 도움이 됩니다. 하루 일의 목록을 작성하고, 우선순위를 정하고, 일정에 따라 일을 처리하는 것이 좋습니다.\n\n이러한 방법들을 고려하여, 퇴근을 빨리 할 수 있습니다. 그러나, 일의 중요성과 긴급성에 따라 퇴근 시간을 늦출 필요가 있을 수도 있습니다.',
        },
        {
            'role': 'user',
            'content': '... 다음 질문 ...',
        }
    ]

이렇게 되면 질문을 하면 할 수록 messages안의 데이터는 커지겠죠?

실제로 최근 서비스중 AI 여행플래너에서 실험을 한 결과 질문을 너무 많이 하면 문제가 발생하는 것을 확인했는데 아마 저와 비슷한 고민을 했을 수 있을 것 같습니다.

(여행플래너인데 코딩질문해서 죄송합니다)

token?

GPT 모델에서 텍스트를 처리할 때 사용하는 단위입니다.

GPT는 토큰 단위로 돈을 받고 있습니다.

gpt-3.5-turbo 모델 기준 1000 토큰당 0.002달러를 받고 있죠

한 요청당 4097이 최대입니다.

토큰은 이 링크에서 테스트가 가능합니다. 테스트 해보시면 대충 어느정도 돈이 빠져나가겠구나 싶을겁니다.

아무리 많이 보내도 요청당 한국 돈으로 10.72원 이상은 안나온다는 의미입니다.

테스트

실제로 한 맥락의 대화를 얼마나 유지할 수 있는지 확인해보겠습니다.

제가 사용한 실습 데이터입니다.

정말 다양한 케이스가 존재할 수 있지만, 9번만에 토큰을 다 써버렸습니다.

여기서 더 질문하면 이미 최대치라고 대답을 안해줍니다.

{
    "error": {
        "message": "This model's maximum context length is 4097 tokens. However, your messages resulted in 4112 tokens. Please reduce the length of the messages.",
        "type": "invalid_request_error",
        "param": "messages",
        "code": "context_length_exceeded"
    }
}

해결 방법

1. 내용을 자른다

질문을 보낼 때 messages를 모두 보내는 것이 아닌 오래된 질문을 하나씩 빼서 보내는 겁니다.

예를 들면 최근 2개의 대화만 보내는 방법이 있죠

아래 예시처럼 최근 메시지만 잘라서 보내면 토큰이 어느정도 유지될 겁니다.

import openai


def send_question(openai: any, messages: list[dict]) -> str:
    completion = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=messages,
    )

    return completion.choices[0].message.content


if __name__ == '__main__':
    openai.api_key = '<openai_key>'
    messages: list[dict] = []

    while True:
        question = input()
        if question == '':
            break

        if len(messages) >= 4:
            messages = messages[len(messages)-4:]  # 최근 2개의 대화만 가져오기

        messages.append({
            'role': 'user',
            'content': question
        })
        answer = send_question(openai, messages)
        print('--------------ANSWER-------------')
        print(answer)
        print('---------------------------------')
        messages.append({
            'role': 'assistant',
            'content': answer
        })

그런데 이렇게 하면 문제가 있습니다.

최근 2개의 대화에 대한 맥락은 가지고 있지만 유저가 처음에 했던 질문에 대한 질문을 다시 하면 AI는 엉뚱한 답변을 하겠죠?

처음에 했던 대화는 잘렸으니까요.

2. 대화를 요약한다.

정확도는 다소 떨어질 수 있지만 대화를 요약해서 토큰을 줄이는 방법도 있습니다.

다음 예시에서는 summarize 함수로 AI의 응답을 요약하는 기능을 추가하고 테스트했는데, 맥락이 꽤 잘 이어집니다.

import openai


def summarize(openai: any, answer: str) -> str:
    completion = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[{
            'role': 'user',
            'content': f'이 내용 한국어로 한 문장으로 요약해줘 ###\n{answer}\n###'
        }],
    )


    return completion.choices[0].message.content


def send_question(openai: any, messages: list[dict]) -> str:
    completion = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=messages,
    )

    return completion.choices[0].message.content


if __name__ == '__main__':
    openai.api_key = '<openai_key>'
    messages: list[dict] = []

    while True:
        question = input()
        if question == '':
            break

        messages.append({
            'role': 'user',
            'content': question
        })
        answer = send_question(openai, messages)
        print('--------------ANSWER-------------')
        print(answer)
        print('---------------------------------')

        summarized = summarize(openai, answer)
        print('------------SUMMARIZED-----------')
        print(summarized)
        print('---------------------------------')

        messages.append({
            'role': 'assistant',
            'content': summarized
        })

금액 차이는 다음과 같습니다.

요약하는 과정에서도 비용이 추가되는 점을 감안해도 같은 같은 질문을 이어갔을 때 2배 저렴하게 이용할 수 있습니다.

영어로 하면 더 싸다

token은 영어로 질문하고, 답변했을 때 더 저렴합니다.

그래서 DeepL API 를 사용해서 중간에 번역을 하면 토큰을 더 아낄 수 있습니다.

하지만 프로그래밍의 경우 코드까지 번역될 수 있어서 코드블록 내의 내용은 번역하지 않는 게 좋습니다.

한글 499
영어 281

한글 질문

{
  "prompt_tokens": 140,
  "completion_tokens": 359,
  "total_tokens": 499
}

Response

def fib(n):
    _curr = 0
    _next = 1

    for _ in range(n):
        _temp = _next # _next 값을 저장해두어야 함
        _next = _curr + _next
        _curr = _temp # 이전에 저장된 _next 값을 사용함

    return _curr

print(fib(5))

위 코드에서는 _next 값을 이전에 저장해두었다가 이전 값(_curr)과 더한 후, _curr에 이전 _next 값을 대입하는 방식으로 수행한다. 이는 피보나치 수열에서 각 항이 바로 전 두 항의 합이기 때문에, 이전 항(_curr)과 그 이전 항(_temp)을 이용하여 다음 항(_next)을 계산하는 방식이다. \n\n원래의 코드에서는 _next 값을 계산할 때 _curr_next를 더한 값을 이용했다. 이 경우 _curr는 이전 _next 값에 현재의 _next 값을 더한 값이 되어버리기 때문에, 다음 계산에서 오류가 발생한다. 따라서 _next 값을 계산하기 전 새로운 변수인 _temp_next 값을 저장해두고, _next를 계산한 후 _curr 값에는 이전 _next 값을 대입하는 방식을 이용해야 한다.

{
  "prompt_tokens": 112,
  "completion_tokens": 169,
  "total_tokens": 281
}

영어 질문

Response

The code logic is incorrect. In each iteration of the loop, the current value should be updated with the next value, and the next value should be updated with the sum of the current and next values. However, the current value is being updated with the current plus next value, which leads to an incorrect sum. \n\nTo fix this, we need to swap the order of the statements that update the current and next values. The corrected code is shown below:

def fib(n):
    _curr = 0
    _next = 1

    for _ in range(n): 
        _curr, _next = _next, _curr + _next

    return _curr

print(fib(5))

With this corrected code, we get the correct output of 5 as the n-th Fibonacci number.

결론

잘 섞어서 쓰는 게 좋습니다.

어떤 서비스는 질문과 답변의 정확성이 중요할 수 있고,

어떤 서비스는 정확도는 떨어져도 그럴싸한 답변이 필요한 서비스도 있을 수 있습니다.

그래서 초과 스펙이 되지 않도록 기존 서비스의 평균 대화 수, 텍스트 수를 분석해서 적절한 솔루션을 찾는 것이 중요합니다.

profile
블로그 이전완 - https://kwanok.me/blog/
post-custom-banner

3개의 댓글

comment-user-thumbnail
2023년 8월 19일

잘봤습니다~

답글 달기
comment-user-thumbnail
2023년 11월 27일

if len(messages) >= 4:
messages = messages[len(messages)-4:] # 최근 2개의 대화만 가져오기
여기서 혹시 왜 2개의 대화인지 알 수 있을까요 ?
len(message)가 4이면 첫번째 대화만 가져오는거 아닌가요 ?

1개의 답글