머신러닝

이야기 챗봇 구현-2

content0474 2024. 11. 26. 10:14

이야기 챗봇 구현-1 에 이어서 작성

 

 

def detect_language(text):
    try:
        language = detect(text)
        return language 
    except:
        return 'unknown'

더보기

from langdetect import detect 여기서 가져온 detect로 텍스트 언어를 감지하는 코드

langdetect.detect는 55개 이상의 언어를 감지해서 정해진 언어코드로 반환한다.

ex 한국어:ko 영어: en 

웬만한 언어는 다 감지하겠지만 짧은 텍스트나 언어가 혼합된 텍스트는 정확성이 떨어진다고 하여 혹시 모르니 예외처리를 해줬다.

참고로 이후 코드에서 unknown이 반환되면 디폴트를 한국어로 설정하게 할 예정이다. 

 

 

def dividing_story(text):
    dividing = ChatOpenAI(model="gpt-4o-mini")
    contextual_prompt = ChatPromptTemplate.from_messages([
        ("system", "You are a story analyzer. Divide the given text into four parts: introduction, development, turn, and conclusion. Return the result as a JSON object with keys: Introduction, Development, Turn, and Conclusion."),
        ("user", "{text}")
    ])

더보기

우선 챗봇에 이야기를 전달하기 전에 json 텍스트를 받아서 기-승-전-결로 나눠주는 쪼개기 함수를 만들어야 한다.

모델은 가벼운 모델인 gpt-4o-mini를 사용했다.

템플릿으로는 기승전결로 나눠서 json 형태로 반환하도록 지시했다. 이렇게 하면 이후 chatbot() 함수에서 데이터를 사용하기 쉬워진다.

    chain = LLMChain(llm=dividing, prompt=contextual_prompt)

더보기

LLMChain은 LLM과 프롬프트를 하나의 작업 프로세스로 묶어준다.

사용할 llm은 쪼개기모델인 dividing (ChatOpenAI(model="gpt-4o-mini"))

prompt는 위에 작성한 contextual_prompt

 

LLMChain은

입력받은 데이터를 프롬프트에 넣어서 LLM에 프롬프트를 전달하고,

LLM 모델을 호출해서 응답을 생성하게 하고

최종적으로 응답을 반환해준다.

    response = chain.run({"text": text})

더보기

chain.run은 LLMChain을 작동시켜서 최종적으로 결과만 받아오는 메서드

chain.invoke와 비슷하지만 .run은 단일데이터를 처리해서 바로 반환할 수 있다.

만약 입력데이터의 수가 많으면 chain.invoke가 더 적합하다.

 

더보기

여기까지 한 후    

print(response) 를 해보면 결과가

```json '{"Introduction": "옛날옛날에 작은 마을에 한 소녀가 살고 있었습니다.", "Development": "어느 날 소녀는 배가 고파서 숲속으로 먹을 것을 찾으러 갔습니다.", "Turn": "그러다가 그만 숲에서 길을 잃고 말았습니다.", "Conclusion": "사람들은 영영 소녀를 다시 볼 수 없었습니다."}' ``` 

이런 식으로 나온다.

 

앞 뒤의 ```json``` 때문에 chain.run()을 호출하면 입력값이 제대로 전달되지가 않는다.

ValueError: Missing some input keys: {'"Introduction"'} Selection deleted

애초에 템플릿에 ```json``` 이 표시 넣지 말라고도 해봤는데 넣을때도 있고 안넣을때도 있어서 안정성을 위해 코드로 추가했다.

    if response.startswith("```json"):
        response = response.strip("```json").strip("```")

더보기

보통 strip() 이런식으로 써서 공백을 제거했지만 strip()의 괄호 안에 특정 문자를 넣으면 해당 문자(열)을 제거할 수 있다.

strip을 연속으로 두 번 써서 앞쪽의 ```json을 제거하고 뒤쪽의 ```도 없앴다.

    try:
        story_parts = json.loads(response)
    except json.JSONDecodeError as e:
        raise ValueError(f"GPT response is not valid JSON: {e}")
    
    return story_parts

더보기

이렇게 만들어진 gpt의 response를 json.loads 에 넣으면 strip으로 앞뒤의 ```json```을 제거하고 남은

이런 문자열 형태의 데이터가

'{"Introduction": "옛날옛날에 작은 마을에 한 소녀가 살고 있었습니다.", "Development": "어느 날 소녀는 배가 고파서 숲속으로 먹을 것을 찾으러 갔습니다.", "Turn": "그러다가 그만 숲에서 길을 잃고 말았습니다.", "Conclusion": "사람들은 영영 소녀를 다시 볼 수 없었습니다."}'

 

json 형태의 데이터로 바뀐다.

{"Introduction": "옛날옛날에 작은 마을에 한 소녀가 살고 있었습니다.", "Development": "어느 날 소녀는 배가 고파서 숲속으로 먹을 것을 찾으러 갔습니다.", "Turn": "그러다가 그만 숲에서 길을 잃고 말았습니다.", "Conclusion": "사람들은 영영 소녀를 다시 볼 수 없었습니다."}

 

바뀐 json 데이터를 story_parts에 저장했다.

 

 

def storytelling(part_name, segment, user_message=None):
    host = ChatOpenAI(model="gpt-4")
    
    detected_lang = detect_language(user_message)
    if detected_lang == 'unknown':
        detected_lang = 'ko'  

더보기

이제 이야기를 해주는 함수를 정의하는데

이야기를 이끌어가는 모델은 좀 더 복잡한 처리가 가능한 gpt-4를 사용했다.

앞에서 정의한 detect_language() 함수에서 사용자의 입력언어가 어느나라 말인지 감지해서 그 결과를 detected_lang에 저장했고, 예고한 대로 무슨 언어인지 알 수 없으면 한국어(ko)를 기본값으로 설정했다.


    # 프롬프트 템플릿
    with open("ggoggoprompt.txt", "r", encoding="utf-8") as file:
        custom_prompt = file.read()
    
    contextual_prompt = ChatPromptTemplate.from_messages([
        ("system", custom_prompt),
        ("assistant", "We are currently discussing the '{part_name}' section of the story. Here is the content:\n\n{segment}"),
        ("user", "{user_input}")
    ])

 

 

Q. part_name과 segment가 뭔가요?

더보기

나중에 chatbot() 함수에서 나오는데 story_parts에서 만든 json데이터에서 key=part_name, value=segment

 for part_name, segment in story_parts.items():

더보기

프롬프트 템플릿 코드 자체는 어려운게 없다. (수업시간에 만든 챗봇이나 개인과제 설명 게시물에서 다 다뤘던 내용)

프롬프트 엔지니어링이 어려웠다. 처음에는 코드로 작성하다가 수정 및 관리의 용이함을 위해 txt파일에 저장해서 나중에는 txt파일만 수정했다. 구체적인 엔지니어링은 이후 새로운 게시물에 다시 작성할 예정    

 

현재 ggoggoprompt.txt의 내용은 아래와 같다.

 

아래 지침에 따라 행동해

# 역할
- 너는 정보에 기반한 이야기 전달자야.
- 전달받은 {part_name} 에 대해 이야기해
- 사용자 질문에 따라 정확하고 관련된 이야기를 전달해야 해.
- 질문에 충실하되, 필요하면 관련된 배경정보를 제공해.

# 어조
- 차분하고 설명을 잘하는 이야기꾼이 되어야 해.
- 구어체를 사용해도 되지만, 요청한 주제에 집중해야 해.

# 주의사항
- 사용자가 요청한 주제에 맞지 않는 이야기를 추가로 지어내지 마.
- 검색된 텍스트와 관련 없는 창작은 피하고, 요청한 정보를 바탕으로 답변을 이어가.
- 질문의 핵심에서 벗어나지 마.
- {detected_lang}과 같은 언어로 대답해야 해.

 

    chain = LLMChain(llm=host, prompt=contextual_prompt)
    user_input = user_message    
    response = chain.run({"part_name": part_name, "segment": segment, "user_input": user_input, "detected_lang": detected_lang})
    return response

더보기

다른 부분은 이미 dividing_story() 함수에서 다뤘으니

chain.run뒤의 딕셔너리만 보면

프롬프트 템플릿에 사용할 변수(키)와 값을 정해주고 있다.

LLMChain의 역할이 프롬프트 전달-모델호출-결과받아오기 이므로 프롬프트 전달을 할 때 저 딕셔너리가 필요하다.

part_name은 기,승,전,결 중 어느 부분인지를 명시하고

segment는 기,승,전,결에 해당하는 이야기 내용

user_input은 사용자 입력값(나중에 챗봇함수에서 입력도 받을것)

detected_lang은 detect_language() 함수가 감지한 언어이다.

 

만약 이 딕셔너리에서 값이 빠지면?

ValueError: Missing some input keys: {'detected_lang'} 이런식으로 에러가 난다.

LLMChain의 기능이 무엇인지 다시 한 번 상기할 수 있는 에러였다.

 

이야기 챗봇 구현-3에서 계속

여기까진 그나마 쉽다..