이야기 챗봇 구현-2
이야기 챗봇 구현-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에서 계속
여기까진 그나마 쉽다..