배경 및 목적
게임의 정보를 입력해서 데이터를 가져오는 단계가 지난 주까지의 진행상황이었다면, 캐릭터들간의 대화 내용을 만들어내는 것이 주요 진행 항목이었음.
참고 자료
LLama Index 사례집
Build your own OpenAI Agent: https://docs.llamaindex.ai/en/stable/examples/agent/openai_agent/
GPT Builder Demo
https://docs.llamaindex.ai/en/stable/examples/agent/agent_builder/
활용 툴
Visual Studio Code
ChatGPT
GPTs
Cluade
실행 과정
작업 1. 추가적인 데이터 제작
기존에 가지고 있던 데이터로 대화를 만들려고 했을 때, 어색한 부분이 많이 있었습니다. 그래서 Genius 게임에 대한 정보를 GPT에 제공하고, 이 내용을 기반으로 사람 간의 소통할 때 사용할만한 대화 내용 스크립트를 만들도록 하였습니다.
물론 큰 틀에서 작성한 후 Claude 로 2차 가공하였습니다.
제작된 대화 내용 예시들을 Json으로 저장하였습니다.
{
"round_1_dialogues": [
{
"category": "협상 및 연합 제안",
"dialogues": [
"우리 둘이 연합하면 서로의 승점을 극대화할 수 있어. 어떻게 생각해?",
"이 카드를 너에게 줄 테니, 다음 라운드에서는 나를 도와줘.",
"우리 이번 라운드에서 서로 패스를 해서 카드를 아껴보는 건 어때?",
"내가 먼저 낮은 점수를 내고 다음에는 네가 높은 점수를 내면 어때?",
"지금 당장은 승점을 포기하더라도 나중에 우리가 함께 이길 수 있어."
]
},
{
"category": "심리전 및 기만",
"dialogues": [
"이번에는 3을 낼 거라고 확신해. 그러니 네가 이길 거야.",
"너는 언제나 과감하게 행동하더라. 아마 이번에도 그럴 거 같아.",
"내 카드는 이미 다 예상했겠지만, 정말로 그 카드일까?",
"이번에 이길 수 있을지 모르겠네. 그냥 안전하게 가는 게 나을까?",
"나는 확실히 낮은 카드를 낼 거야. 그래서 이번엔 무승부로 가자."
]
},
{
"category": "협력 후 배신 유도",
"dialogues": [
"이번 한 번만 나에게 승점을 몰아주면, 다음 라운드에서 너를 도와줄게.",
"우리가 함께 다른 플레이어들을 무너뜨리고 나서, 결승에서 싸우자.",
"너만 믿고 있을게. 이번 라운드에서 승점을 챙길 수 있도록 도와줄게.",
"이건 네가 절대 혼자서 할 수 없는 일. 나와 함께라면 확실히 이길 수 있어.",
"너에게 도움이 될 카드가 있어. 이걸로 우리가 서로 이득을 볼 수 있어."
]
},
{
"category": "위협 및 경고",
"dialogues": [
"네가 만약 나를 돕지 않는다면, 다른 사람과 손잡을 수밖에 없어.",
"이번에 날 방해하면, 나중에 내가 네 전략을 다 깨부술 거야.",
"우리 협력하지 않으면 둘 다 이길 수 없을 거야. 잘 생각해봐.",
"너의 다음 움직임에 따라 이 게임의 향방이 결정될 거야. 신중하게 생각해.",
"이 상황에서 나를 적으로 돌리면, 나중에 크게 후회할 거야."
]
},
{
"category": "전략적 계산 및 정보 교환",
"dialogues": [
"네 카드를 봤을 때, 이번 라운드에서 우리가 이길 수 있는 확률이 높아.",
"나는 이번에 이길 확률이 높아 보이는데, 너는 어떻게 생각해?",
"우리 각각 가진 카드를 계산해보면, 다음 라운드에서는 어떻게 나갈까?",
"이 정보를 공유하면 서로의 승점을 극대화할 수 있을 것 같아.",
"네가 가진 카드가 뭔지 알면, 이번 라운드에서 어떻게 대응할지 정할 수 있을 것 같아."
]
},
{
"category": "함정 카드 유도",
"dialogues": [
"이번에 너는 높은 카드를 내고, 나는 낮은 카드를 내볼까? 이렇게 하면 이길 거야.",
"네가 예상한 카드일 수도 있고 아닐 수도 있어. 한번 확인해볼래?",
"이 카드를 사용할까 생각 중인데, 너도 같이 해볼래?",
"이번 라운드에서 나를 방심하게 만들어, 다음 라운드에 큰 카드를 내도록 해줄게.",
"너도 나도 이길 수 있는 전략이 있어. 하지만 너 먼저 이걸 내봐."
]
},
{
"category": "심리적 압박",
"dialogues": [
"너는 항상 이런 상황에서 조급해 하더라. 이번에도 그런 실수를 할까?",
"이 카드는 정말 마지막 순간에 너를 무너뜨릴 수 있을 거야.",
"네가 이기길 바래, 하지만 정말로 이길 수 있을까?",
"이건 그냥 게임이지만, 그 결과는 너에게 중요할 수도 있어.",
"지금 이 순간이 가장 중요한 결정이 될 거야. 잘 생각해."
]
},
{
"category": "카드 교환 제안",
"dialogues": [
"내가 가진 1장을 너에게 줄 테니, 너의 2장을 나에게 줄래?",
"이 카드를 줄게. 너에게 필요한 카드지? 그 대신에 네 승점을 나눠줘.",
"지금 우리가 교환을 하면 서로에게 이익이 될 거야. 어때?",
"내 카드를 줄 테니, 너도 이번 라운드에서 나를 도와줘.",
"카드를 교환하고 나서 서로 도울 수 있어. 이 방법이 좋을 거야."
]
}
]
}
작업 2. 툴 사용 방식 변경
기존에는 저장된 데이터 기반의 응답 방식이었다면, 대화 방식으로 전환이 필요하였습니다. Lanchain에서 사용하던 Agent 방식을 희망하였습니다. 먼저 LLamaIndex Docs에 있는 항목들 중 여러 개를 검색해봤고 다양한 캐릭터가 나오면서도 별도의 지식이 필요한 항목인만큼 GPT Builder Demo 항목을 참고해보면 좋을 것이라고 판단했습니다. (하지만 선택하지 않았습니다.)
GPT Builder Demo 항목을 예시로 삼아서 작성하였고, Wikipedia의 데이터를 가지고 Agent를 통해서 원하는 결과를 도출해내는 것이 흥미로웠습니다.
선택하지 않은 이유: 예시에 있는 케이스는 잘 동작하는데 커스터마이즈가 진행되면 데이터를 잘못 불러오는 경우가 많았습니다.
다른 옵션을 선택하기 위해 OpenAI에서 제공하는 Agent 기능을 LLama Index에 적용하도록 하는 기능을 사용하기로 하였습니다.
2차 OpenAI Agent with Tool Call Parser
OpenAI Agent의 기능을 고스란히 사용할 수 있음.
GPT Builder 에 비해 커스터마이징이 좋음
예시 사례에 따라서 제작을 해봤습니다.
import json
import random
from typing import Sequence, List
from llama_index.llms.openai import OpenAI
from llama_index.core.llms import ChatMessage
from llama_index.core.tools import BaseTool, FunctionTool
from openai.types.chat import ChatCompletionMessageToolCall
import nest_asyncio
nest_asyncio.apply()
def gameplayers():
"""두 명의 캐릭터를 랜덤으로 선택하여 반환합니다."""
character_names = ["Min-Soo", "Eun-Jung", "Jae-Hoon", "Ji-Hoon", "Hyun-Ah", "Young-Joon", "Eun-Bi", "Joon-Soo", "Ji-Soo", "Tae-Hyun", "Mi-Rae", "Sun-Woo"]
return random.sample(character_names, 2)
player_tool = FunctionTool.from_defaults(fn=gameplayers)
def stretegys():
# JSON 파일 로드
with open('./game_data/round1_expected_diaglogue.json', 'r', encoding='utf-8') as file:
dialogue = json.load(file)
# 각 카테고리와 대화 리스트를 추출
strategies = []
for item in dialogue['round_1_dialogues']:
category = item['category']
detail_dialogue = item['dialogues']
strategies.append({'category': category, 'dialogues': detail_dialogue})
return strategies
dialogue_tool = FunctionTool.from_defaults(fn=stretegys)
from llama_index.agent.openai import OpenAIAgent
from llama_index.llms.openai import OpenAI
llm = OpenAI(model="gpt-4o-mini")
agent = OpenAIAgent.from_tools(
[player_tool,dialogue_tool], llm=llm, verbose=True
)
response = await agent.achat("select 2 name")
print(str(response))
#결과
# Added user message to memory: select 2 name
# === Calling Function ===
# Calling function: gameplayers with args: {}
# Got output: ['Eun-Jung', 'Ji-Hoon']
# ========================
# Here are the selected names: Eun-Jung and Ji-Hoon.
OpenAI Agent를 사용할 경우 간단한 프롬프트 (select 2 name)만 활용해서도 원하는 'Eun-Jung', 'Ji-Hoon'을 가져옴을 확인할 수 있었습니다.
작업 3. 프롬프트 변환을 통한 기능 확인
프롬프트를 변경해서 실제로 어떻게 나오는지 확인해보았습니다.
response = agent.stream_chat(
"select 2 name? Once you have the answer, use that name to write a"
" dialogue message in Korean language about the 123 game."
" They are player and they play the game together"
)
response_gen = response.response_gen
for token in response_gen:
print(token, end="")
# 결과
# **대화:**
# **준수:** 선우야, 오늘 1·2·3 게임 할 준비 됐어? 정말 기대돼!
# **선우:** 응, 준수! 나도 기대돼. 이번에는 꼭 이기고 싶어. 어떤 전략을 쓸까?
# **준수:** 나는 카드 교환을 활용해볼 생각이야. 서로 필요한 카드를 주고받으면 더 유리할 것 같아.
# **선우:** 좋은 생각이야! 그리고 심리전을 활용해서 상대방을 혼란스럽게 해보자. 내가 낮은 카드를 낼 테니, 너는 높은 카드를 내서 이길 수 있을 거야.
# **준수:** 그럼 그렇게 해보자! 이번 라운드에서 승점을 많이 얻어보자!
# **선우:** 좋아! 이제 시작해보자!
# ---
# 이렇게 준수와 선우가 1·2·3 게임에 대해 이야기하는 장면을 그려보았습니다.
위와 같은 방식은 일반적인 대화일 뿐 지니어스 게임에서의 대화와는 사뭇 달랐기에, 다양한 방식으로 프롬프트를 변환했지만 원하는 결과는 나오지 않았습니다.
작업 4. LLama Index 기능의 통합
지난 시간에 배운 LLama Index 데이터 로드하는 기능을 활용해 프롬프트에 반영하는 아이디어를 만들었습니다.
[최종 코드]
모듈 불러오기
import json
import random
import os.path
from typing import Sequence, List
import logging
import nest_asyncio
from llama_index.llms.openai import OpenAI
from llama_index.core.llms import ChatMessage
from llama_index.core.tools import BaseTool, FunctionTool
from openai.types.chat import ChatCompletionMessageToolCall
from llama_index.core import (
VectorStoreIndex,
SimpleDirectoryReader,
StorageContext,
load_index_from_storage,
)
from llama_index.agent.openai import OpenAIAgent
from llama_index.llms.openai import OpenAI
nest_asyncio.apply()
OPENAI_API_KEY=os.getenv('OPENAI_API_KEY')
LLama Index VectorStore 제작하기
'''
game_data에 있는 데이터들을 기반으로 첫번째 메인 매치에 대한 정보를 출력하도록 함.
출력한 결과를 main_match_description에 저장
'''
# 로깅 레벨을 ERROR로 설정하여 경고 메시지 숨기기
logging.getLogger().setLevel(logging.ERROR)
documents = SimpleDirectoryReader("game_data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("첫번째 메인 매치에 대해서 한글로 설명하세요")
main_match_description=response.response
GamePlayer 함수 제작
'''
Function call agent에서 사용할 3명의 캐릭터를 랜덤으로 선택하여 반환하는 tool 제작
'''
def gameplayers():
character_names = ["Min-Soo", "Eun-Jung", "Jae-Hoon", "Ji-Hoon", "Hyun-Ah", "Young-Joon",
"Eun-Bi", "Joon-Soo", "Ji-Soo", "Tae-Hyun", "Mi-Rae", "Sun-Woo"]
return random.sample(character_names, 3)
player_tool = FunctionTool.from_defaults(fn=gameplayers)
Stretegys 함수 제작
'''
round1에서 사용할만한 대화 내용이 담겨져 있는 json 파일을 불러들이고, 해당 대화 내용을 tool 호출이 있을 때 사용하도록 데이터 진행 준비
'''
def stretegys():
# JSON 파일 로드
with open('./game_data/round1_expected_diaglogue.json', 'r', encoding='utf-8') as file:
dialogue = json.load(file)
# 각 카테고리와 대화 리스트를 추출
strategies = []
for item in dialogue['round_1_dialogues']:
category = item['category']
detail_dialogue = item['dialogues']
strategies.append({'category': category, 'dialogues': detail_dialogue})
return strategies
dialogue_tool = FunctionTool.from_defaults(fn=stretegys)
LLM 및 Function Call 설정
'''
gpt-4o-mini 기능을 활용하면서 요청되는 내용에 자동으로 player_tool과 dialogue_tool을 선택할 수 있도록 기능 구현
'''
llm = OpenAI(model="gpt-4o-mini")
agent = OpenAIAgent.from_tools(
[player_tool,dialogue_tool], llm=llm, verbose=True
)
최종 호출
'''
랜덤으로 3명의 이름을 선정하고, 3명이서 사전에 정의된 주제로 논의하도록 함.
main_match_description을 변수로 저장하여 입력
'''
response = await agent.astream_chat(
"select 3 name? Once you have the answer, use that name to write a"
f"playing game scenario in Korean language about the {main_match_description}."
"They really want to win from their competitor. You must add their own stretegies to scenario"
)
response_gen = response.response_gen
async for token in response.async_response_gen():
print(token, end="")
최종 결과
### 1차 메인 매치 시나리오: 1·2·3 게임
**참가자:** 태현, 재훈, 준수
**게임 설명:**
1·2·3 게임은 1, 2, 3 카드로 상대방보다 높은 카드를 내어 승점을 얻고, 가장 높은 승점을 가진 플레이어가 우승하는 게임입니다. 각 플레이어는 1, 2, 3이 적힌 카드를 각 3장씩, 총 9장의 카드를 받습니다. 대결은 메인 홀의 테이블에서 1:1로 진행되며, 승리한 플레이어는 승점 1점을 획득합니다.
**게임 시작:**
태현, 재훈, 준수는 각자 카드를 나누어 받고, 첫 번째 라운드를 시작합니다. 태현은 심리전을 통해 재훈에게 압박을 가합니다.
**태현:** "이번에는 3을 낼 거라고 확신해. 그러니 네가 이길 거야."
재훈은 태현의 말에 혼란스러워하며, 자신의 카드를 조심스럽게 선택합니다. 준수는 두 사람의 대화를 듣고 전략을 세웁니다.
**준수:** "이번 한 번만 나에게 승점을 몰아주면, 다음 라운드에서 너를 도와줄게."
재훈은 준수의 제안에 고민하지만, 태현의 심리적 압박에 의해 결국 높은 카드를 내기로 결정합니다.
**1라운드 결과:**
태현이 2를 내고 재훈이 3을 내어 재훈이 승리합니다. 재훈은 승점 1점을 얻고, 태현은 카드를 폐기합니다.
**2라운드 시작:**
준수는 태현과 대결하게 됩니다. 준수는 태현에게 협력 제안을 합니다.
**준수:** "우리 둘이 연합하면 서로의 승점을 극대화할 수 있어. 어떻게 생각해?"
태현은 준수의 제안에 동의하고, 두 사람은 서로의 카드를 교환하기로 합니다. 태현은 낮은 카드를 내고, 준수는 높은 카드를 내어 승리합니다.
**2라운드 결과:**
준수가 승리하여 승점 1점을 얻고, 태현은 또 다시 카드를 폐기합니다.
**3라운드 시작:**
재훈과 준수가 대결합니다. 재훈은 준수에게 심리전을 시도합니다.
**재훈:** "이번에 이길 수 있을지 모르겠네. 그냥 안전하게 가는 게 나을까?"
준수는 재훈의 말에 혼란스러워하며, 자신의 카드를 결정합니다. 결국 준수가 높은 카드를 내어 승리합니다.
**최종 결과:**
- 재훈: 1점
- 태현: 0점
- 준수: 2점
준수가 가장 높은 승점을 얻어 우승하며, 가넷 1개를 획득합니다. 태현은 가장 낮은 승점을 얻어 탈락 후보가 됩니다. 게임이 끝난 후, 세 사람은 서로의 전략과 심리전을 돌아보며 다음 게임을 준비합니다.
결과 및 인사이트
LLama Index에 대해서 물어볼 곳이 없다는 것이 가장 어려웠습니다.
또한 초기 계획에 비해 진척사항이 더딘 점도 우려되었습니다.
하지만 실제로 구현된 내용을 보니 마음에 들고 있습니다.
현재는 너무 교과서적인 내용으로 담겨져있는데, 실전과 같은 내용으로 작성해보고 싶습니다.
(물론 목소리와 디자인 그림을 입히는 것은 추가적인 것이죠)