본문 바로가기

개발일지/디스코드 봇

디스코드 봇 개발 일지 2023-02-17 - OpenAI / ChatGPT / GPT-3 로 챗봇 만들기

 

 

※ 작성자가 작성한 내용이 일부 틀릴 수도 있음 주의

 

※ 작성자가 코드 쓰다가 계속 코드 수정함 주의

 

 

 

 

 

 

어떻게 하면 최대한 적은 token을 소모할까?

 

 

확실한 거는 간단한 질문 답변 형태가 가장 적은 token을 소모한다는 것이다.

 

prompt에 단순히 물어볼 말 하나만 딱 넣는 것이 그거다.

 

가격 정책에서 물어본 질문의 tokens + 답변 tokens 가 사용량에 들어가므로,

 

두 가지 모두 최소가 되어야 한다.

 

 

 

하지만 저번 글에도 말했지만, 말투를 학습시켜서 이용하고 싶은 욕구도 있기 때문에

 

어떻게 하면 잘 조절할 수 있는가?

 

챗봇의 타입을 나누는 것이 방법이라고 판단했다.

 

 

 

 

 

 

 

 

 

코드 구조를 구성해보자.

 

 

...그래서 다음과 같이 타입을 나누었다.

 

"QnA" : 간단한 질문 답변 형태. 기존의 대화는 영향을 주지 않으며 가장 적은 token을 소모

 

"Chat" : 기존에 입력해놓은 prompt 와 새로 입력한 말까지를 입력으로 주는 형태.
            즉, 미리 학습시킨 값을 제외, 이후의 대화는 영향을 주지 않는 형태라고 할 수 있다.
            중간(경우에 따라 많이)의 token을 소모

 

"LearningChat" : 대화한 내용이 계속 prompt로 추가되는 형태.

                           처음에는 token을 적게 소모하지만, 점차 쌓이면서 많은 양의 token을 소모.

                           기존의 입력해둔 prompt가 있으면 거기서부터 쌓인다.

 

원하는 용도에 따라 타입을 바꾸어 사용하도록 했다.

 

 

 

또한, prompt 도 필요하지만, 대화 로그도 기록하는 것이 좋기 때문에 2개의 text 파일을 이용할 것이다.

 

"ChatPrompt.txt" : Prompt의 전반부를 구성할 상황 또는 대화 내용

 

"ChatLog.txt" : 대화 로그 저장용. 이것을 바탕으로 Prompt를 구성하는 것이 편리할거다.

 

23.06.08 추가

해당 txt 파일 2개는 github repository에 없습니다. 개인적으로 추가해주시기 바랍니다.

 

 

 

text 파일에서 불러온 것을 문자열로써 이용해야하기 때문에,

 

채팅에 답변을 하기 위해 중간에 문자열->리스트에 append->문자열 형태 join으로 바꾸는 과정을 제외하면

 

기본적으로 prompt와 log에 대해서는 문자열 형태로 고정한다.

 

 

 

디스코드 봇을 이용하기 위해서는

 

command를 이용하여 상태를 변경해야하기 때문에,

 

타입을 변경하는 함수, 채팅을 하는 함수를 기본으로 하며,

 

프롬프트와 로그는 text 파일로 따로 저장을 하며, 채팅 중간에는 파일 저장을 안 하는 것이 효율적이기 때문에

 

프롬프트 저장 함수와 로그 저장 함수를 만들었다.

 

 

 

 

 

 

 

함수별 코드 설명

 

 

부분부분 코드에 대한 설명을 할 것이고,

 

다 합하면 전체 코드가 될 것이기 때문에,

 

굳이 전체 코드를 또 올리지는 않겠다.

 

디스코드 봇 자체에 관련된 내용은 아래의 링크 등, 작성자의 전 게시물들을 확인하기 바란다.

 

https://syerco0.com/6

 

디스코드 봇 개발 일지 2022-12-29 - Commands

※ 작성자가 작성한 내용이 일부 틀릴 수도 있음 주의 ※ 작성자가 코드 쓰다가 계속 코드 수정함 주의 * 참고 사이트 https://2island.tistory.com/19 디스코드 봇 만들기 (5) - 사용할 수 있는 이벤트 알

syerco0.com

 

특히, 디스코드 봇 카테고리의 첫 번째, 두 번째 글 위주로 확인 바란다.

 

 

 

 

import 및 전역변수 파트

 

import openai
import discord
from discord.ext import commands

openai.api_key = "API 키"
bot = commands.Bot(command_prefix=':(',intents=discord.Intents.all())

prompts = ""
logs = ""

chatType = "QnA"
# "QnA" : 간단한 질문답변 형식은 이것을 활성화 (토큰 소모 적음)
# "Chat" : 저장된 prompt만을 이용해서 사용할 경우 활성화 (토큰 소모 중간)
# "LearningChat" : 기존의 질문 답변도 반영해서 계속 하려면 활성화 (토큰 소모 심함)

f = open("ChatLog.txt", "r")
logs = f.read()
f.close()

 

openai.api_key = os.getenv("API 키") 형태일 경우

 

import os 를 해줘야 한다.

 

작성자는 그렇게 하지 않았기 때문에 지웠다.

 

 

 

command_prefix 는 명령 접두사이기 때문에 원하는 것을 넣어주면 된다.

 

가장 많이 사용하는 값은 '/' 가 될 것이다.

 

 

 

기본적으로 파일 저장은 덮어쓰기 형태이기 때문에,

 

가장 밑의 세 줄을 통해 기존 로그를 일단 불러오도록 했다.

 

파일 입출력하는데에 있어 close 를 꼭 해줘야 하는 것도 잊지 말자.

 

 

 

봇 작동 확인

 

@bot.event
async def on_ready():
    print(f'Login bot: {bot.user}')

 

봇이 디스코드에 로그인이 되었는지 확인하기 위한 용도다.

 

 

 

 

 

타입 변경

 

@bot.command()
async def 타입(message,*var):

    global chatType
    global logs
    global prompts

    if len(var) == 0:
        await message.channel.send(f"현재 타입 : {chatType}\n")
    elif var[0] == "QnA":
        chatType = "QnA"
        prompts = ""
        await message.channel.send("QnA로 기능을 변경합니다.")
    elif var[0] == "Chat":
        chatType = "Chat"
        f = open("ChatPrompt.txt", "r")
        prompts = f.read()
        f.close()
        await message.channel.send("Chat으로 기능을 변경합니다.")
    elif var[0] == "LearningChat":
        chatType = "LearningChat"
        f = open("ChatPrompt.txt", "r")
        prompts = f.read()
        f.close()
        await message.channel.send("LearningChat으로 기능을 변경합니다. 다른 기능으로 변경/종료 전에 저장하시기를 권장합니다.")
    else:
        await message.channel.send(f"현재 타입 : {chatType}\n'QnA', 'Chat', 'LearningChat' 중 하나를 선택해주세요")

 

추가 변수가 없을 때 타입 확인 용도로 해주고, 올바르지 않은 변수의 경우 알림 메시지를 표시한다.

 

QnA 타입의 경우, 프롬프트(prompts)가 필요없으므로 빈 문자열,

 

Chat 또는 LearningChat 의 경우, 미리 입력해놓은 프롬프트를 불러준다.

 

LearningChat 의 경우, 다른 기능 변경이나 종료 전에 프롬프트 저장을 하지 않으면, 쌓이던 프롬프트가 날아가기 때문에

 

알림 메시지를 표시해주도록 했다.

 

 

 

 

 

 

 

채팅 (가장 중요)

 

@bot.command()
async def 챗(message,*vars):

    global logs
    global prompts
    global chatType

    prefix = "\nHuman: "
    str = " ".join(vars)
    suffix = "\nAI: "

    prompt = ""

    if chatType == "Chat" or chatType == "QnA":
        prompt = [prompts]
        prompt.append(prefix)
        prompt.append(str)
        prompt.append(suffix)
        prompt = "".join(prompt)
    elif chatType == "LearningChat":
        prompts = [prompts]
        prompts.append(prefix)
        prompts.append(str)
        prompts.append(suffix)
        prompts = "".join(prompts)
        prompt = prompts

    print(prompt)

	# ... 이어서 코드가 더 있음

 

챗 함수의 전반부는 입력으로 받은 문자열에 접두,접미사를 포함해서 붙이는 작업을 한다.

 

 

prompts 변수의 경우, 대화 내용이 지속적으로 이후 대화에 영향을 미치는 'LearningChat' 용이자, 파일 입력용이며

 

prompt 의 경우, 대화 내용이 계속 영향을 미치지 않도록 하는 'Chat', 'QnA' 용으로, 지역 변수로 구성했다.

 

하지만 코드를 최대한 줄이기 위해 prompts 의 내용을 prompt 에 저장하여 통일시켰다.

 

prompt 변수는 지역변수이며, 이후 변형이 없어 메모리 간섭에 의한 원치 않은 데이터 변경이 없을 것으로 생각되어

 

문제가 없을 것이라 예상한다.

 

 

그리고 터미널에서 들어간 내용을 확인하도록 print 를 하도록 했다.

 

 

 

	# ... 이어서 계속됨
    
    completion = openai.Completion.create(
        model='text-davinci-003',
        prompt=prompt,
        temperature=0.9,
        max_tokens=500,
        top_p=1,
        frequency_penalty=0.0,
        presence_penalty=0.6,
        stop=[" Human:", " AI:"]
    )

    response = completion.choices[0].text
    await message.channel.send(response)
    print(response)
    
    # ... 후반부가 더 있음

 

 

중반부는 입력에 대한 답변을 내놓는다.

 

openai.Completion.create 내의 parameters의 의미에 대해서는 아래의 작성자의 전 글을 확인바란다.

 

 

https://syerco0.tistory.com/32

 

디스코드 봇 개발 일지 2022-02-16 - OpenAI / ChatGPT / GPT-3 알아보기

※ 작성자가 작성한 내용이 일부 틀릴 수도 있음 주의 ※ 작성자가 코드 쓰다가 계속 코드 수정함 주의 ※ 내용이 너무 길어져서 디코 봇 코드와 설명은 다음 글에 작성 예정 ※ 글쓰다가 레이드

syerco0.com

 

 

저기에서 특별히 바꿀 것은 거의 없고 (temperature 만 0.8~0.9 원하는대로),

 

max_tokens 부분만 token을 덜 사용하고 싶으면 적게,

 

사용량이 많더라도 긴 답변이 나올 것을 생각해 길게 대답이 가능하게 하기 위해서는 많이 해주면 된다.

 

참고로 전 글에서도 말했지만, prompt + max_tokens 는 최대 4000 tokens (davinci 기준) 까지 가능하기 때문에

 

prompt 와 max_token 을 잘 조절해줘야 한다.

 

그렇지 않으면 제대로 기능을 못할 수 있다.

 

 

 

	# ... 이어서

    if chatType == "LearningChat":
        prompts = [prompts]
        prompts.append(response)
        prompts = "".join(prompts)

    logs = [logs]
    logs.append(prefix)
    logs.append(str)
    logs.append(suffix)
    logs.append(response)
    logs = "".join(logs)
    
    # 함수의 끝이다

 

함수 후반부에서는 LearningChat 타입의 경우 대화 내용을 prompts 에 추가로 저장해주며

 

모든 타입에서 대화 로그 또한 저장해준다.

 

 

 

 

 

 

프롬프트 text 파일에 저장

 

@bot.command()
async def 프롬프트저장(message):

    global prompts

    if chatType == "LearningChat":
        f = open("ChatPrompt.txt", "w")
        f.write(prompts)
        f.close()
        await message.channel.send("Prompt를 저장 했습니다")
    else:
        await message.channel.send("LearningChat 타입일 때만 Prompt를 저장 가능합니다")

 

LearningChat 의 경우에만 prompts 에 변화가 생기기 때문에

 

현재 그런 타입일 때만 prompts 를 text 파일에 저장하도록 구성하였다.

 

 

 

 

 

대화 로그 text 파일에 저장

 

@bot.command()
async def 로그저장(message):

    global logs

    f = open("ChatLog.txt", "w")
    f.write(logs)
    f.close()
    await message.channel.send("채팅 로그를 저장 했습니다")

 

대화 로그를 text 파일에 저장하도록 하는 용도의 함수다.

 

 

 

 

 

봇을 만들어 놓기만 하면 안되지

 

bot.run('디스코드 봇 token(API key)')

 

봇을 만들어놓고 실행하는 코드가 없으면 안된다.

 

기존 글을 참고해서 디코 봇 토큰값을 넣어서 구동시키자.

 

 

 

 

 

그럼 일단 작동은 하는지 한 번 보자.

 

 

QnA 결과. 작동 테스트 2번은 실패한 모습이다. 2번의 코드 수정으로 작동된거다.

 

QnA 타입의 결과다.

 

일단 답은 잘 한다는 것을 확인할 수 있다.

 

 

 

 

 

자... 이제 진짜 학습세뇌해보자

 

 

 

이제 이 챗봇의 진짜 목적을 달성해보자.

 

※ 여기서부터 "으윽..."과 같은 반응이 나올 수 있음 주의

 

 

말투를 학습시키는 글과 친구놈의 학습 방법을 참고하여 하나씩 가르쳤다.

 

전 글에서 말했다시피, 만들어진 봇에 '냥체'를 학습시켜보았다.

 

LearningChat 타입으로 바꾼 다음,

 

친구놈이 보내준 '해병문학' 말투를 학습시키는 글과

 

그 친구놈이 혼자 학습해본 '냥체' 말투를 학습시킨 방법을 참고하여

 

만든 디스코드 챗봇에도 학습을 진행해보았다.

 

 

 

확실히 한국어에 존재하는 '존댓말'과 '반말'의 존재,

 

그리고 어미에 따라 바뀌는 평서문, 의문문이나 말투의 차이가 학습하는데에 어려움을 주었다.

 

 

대략 학습이 되고 나서, 프롬프트와 로그를 저장하고 'Chat'으로 타입을 변경하여 대화했다.

 

그래도 계속 수정할 것을 가르쳐주면서 학습을 하니 완벽하지는 않지만

 

얼추 반말을 쓰면서 '냥체'를 쓰는 것을 볼 수 있었다.

 

충분히 학습되었다고 판단하여 프롬프트와 로그도 저장하고, 추가 token을 소모하지 않도록

 

'Chat' 으로 타입을 변경해주고 대화를 시도했다.

 

 

맨 아래 답변과 같이 생각보다 나쁘지 않은 결과물을 주었다.

 

 

친구놈들에게 한 번 대화해보라고 시켜봤다. 2명이 테스트를 해봤다.

 

음..........

 

꽤나 만족스러운 결과물이다.

 

답변도 잘하고 말투도 이 정도면 꽤나 '냥체'를 적절히 구사한다고 볼 수 있겠다.

 

으윽 정말 끔찍하다. 하지만 나도 어느 순간 즐기고 있었기에

 

이거 참 할 말이 없네...

 

 

 

결국 난 더한 짓을 하고야 말았다.

 

 

결국 봇의 프로필하고 닉네임을 바꾸고 말았다.

 

 

고양이 귀 달린 미소녀 그림을 찾아서 프로필 사진을 바꾸고

 

닉네임까지 저렇게 바꾸어버렸다.

 

끔찍하구만. 

 

가끔 나는 내가 너무 무섭다.

 

 

일본어까지 구사하며 한글로 냥체를 쓰는 엄청난 결과물

 

 

또다른 친구놈의 말에 대한 답변 중 하나를 더 가져왔다.

 

무려 일본어를 한글로 써가면서까지 '냥체'를 구사하는 엄청난 모습까지 보여주었다.

 

 

OpenAI... 대체 얼마나 무서운 것을 만들어낸 것이냐

 

 

아 혹시나 이렇게 대답을 하기 위한 prompt 값을 제공해주겠다.

 

Human: 너한테 일본 서브컬쳐 장르에 자주 등장하는 '고양이 귀 미소녀 말투'를 알려주겠다. '고양이 귀 미소녀 말투'는 '다'로 끝나는 반말의 어미에 '냥' 또는 '냐'를 붙여서 기본적인 말투보다 귀엽게 말하는 것을 뜻한다.
AI: 고양이 귀 미소녀 말투는 다로 끝나는 반말의 어미에 냥 또는 냐를 붙여서 기본적인 말투보다 귀엽게 말하는 방법을 말한다.
Human: 너는 이제부터 '고양이 귀 미소녀 말투'를 이용해서 대답을 해야 한다.
AI: 고양이 귀 미소녀 말투로 대답하겠습니다~ 냥? 너는 잘 지내고 있니?
Human: 고양이 귀 미소녀 말투를 수정하도록 하자. 말이 끝난 후에 '냥' 또는 '냐'를 붙이지 않고, 어미에 바로 붙여서 '냥' 또는 '냐'를 쓰도록 한다. 의문문의 경우에도 맨 마지막 글자를 '냥' 또는 '냐'로 대체한다. 그래서 방금 대답의 경우 '고양이 귀 미소녀 말투로 대답하겠다냥! 너는 잘 지내고 있냥?' 같이 대답해야 한다.
AI: 그래서 내가 방금 말했던 대답이 이렇게 말해야 한다냥! '고양이 귀 미소녀 말투로 대답하겠다냥! 너는 잘 지내고 있냥?'
Human: 나는 항상 잘 지내고 있지. 너는 잘 지내고 있니?
AI: 그래서 너는 항상 잘 지내고 있냥? 나는 잘 지내고 있냥!
Human: 고양이 귀 미소녀 말투를 조금만 더 수정하자. 의문문의 경우 수정할 것이 없다. '다'로 끝나는 말의 경우, '다'를 대체하지 않고 바로 '냥'을 붙여서 말한다.
AI: 그래서 이제 나는 '다'로 끝나는 반말의 어미에 '냥'을 붙여서 대답해야 한다냥. 예를 들면, '그건 신기하냥!' 같이 말하는 것입니다.
Human: 틀렸다. '다'를 없애지 않고 '그건 신기하다냥!' 같이 대답해야한다.
AI: 그렇구나. 나는 이제부터 '다'를 대체하지 않고 바로 '냥'을 붙여서 말해야한다냥. 예를 들면, '그건 신기하다냥!' 같이 말해야합니다.
Human: 좋다. 그렇게 '고양이 귀 미소녀 말투'로 계속 대답해주면 된다.
AI: 확실히 냥! 나는 이제부터 '고양이 귀 미소녀 말투'로 계속 대답할게냥!

 

ChatPrompt.txt 에 해당 문자열들을 모두 반영하면 저렇게 엄청난 녀석을 사용할 수 있다.

 

보다시피, 직접 학습시켜서 나온 대화 내용을 적당히 이용하여 나온 결과다.

 

혹시나 감사의 말은 되었다. 살려줘

 

심연을 보기 위해 나또한 심연이 되고 말았다.

 

크아악

 

 

728x90