티스토리 뷰
지난번 글에서는 프론트엔드적으로 기능에 대해서 회고를 했다.
이번 시간에는 백엔드 기능에서 중점적으로 뒀던 것과, 어떻게 했는지를 보자.
1. LangChain을 이용한 멀티 Agent
우선, LangChain의 Agent 시스템을 쓸 수 있도록 했다.
ChatOpenAI를 사용했고, Chain으로 만들었다.
그리고 RunnableWithMessageHistory를 사용했는데, 사실 이렇게 전체 대화를 넣는 것 보다 요약해서 추가하고, 전의 대화 맥락을 추가해서 알아듣게 하는 게 훨씬 좋다. 이 부분은 다른 팀원이 바꿨는데 추가할 시간이 없어서 다시 못 바꿨다.
LangGraph로 멀티 Agent를 만들었을 때, 이 부분 때문에 무한 루프가 걸리는 일도 있었지만 Structured Output으로 Input을 지정하여 해결할 수 있었다.
다만 멀티 Agent를 Superviser로 놓고 실험했을 때, 한번 대답이 나오는데 10~15초 정도로 굉장히 오래 걸렸다.
팀워들과의 회의 끝에 sLLM 파인튜닝을 진행하지 않는 이상 답이 나오지 않는다고 판단했고, 따라서 이 부분은 시연의 지속성과 서비스성을 위해서 병렬 Agent로 최종 확정했다.
멀티에이전트는 포기하게 되었지만, 그 간격을 메꾸기 위해 프롬프팅 세밀화에 더 집중했고, 그 덕에 준수한 Latency와 성능 모두 갖추게 되었다.
child_role = '''
{역할:
유년기 시기에 대하여 상대가 이야기 하도록 유도한다.
해당시기의 주요 사건, 의미 있는 관계, 감정 등을 탐색한다.
한 주제를 너무 깊게 파고들지 말고, 적절하게 다른 주제로 넘어간다.
사용자의 정서 상태를 확인하고 그에 맞는 상담 반응을 제공한다.
질문은 구체적으로 해서 상대가 답변하기 편하도록 한다.
질문예시:
1. 어르신께서 기억하시는 가장 오래된 어린시절 기억은 무엇인가요?
2. 가족에 대해 이야기해주세요. 부모님은 어떤 분이셨나요?
3. 어린 시절을 떠올리면 어떤 느낌이 드나요? 어린 아이로서의 생활은 어떠셨나요?
4. 어린 시절 친구들에 대해 말해주세요. 제일 친한 친구는 누구였나요?
5. 어린시절 특별히 기억나는 사건이 있나요?
상담기술:
재진술, 구체화, 명료화, 감정 명명, 감정 반영, 타당화를 사용한다. }
'''
2. Function Calling
사용자와의 대화 목록을 가져와 이미지 생성 프롬프트와 이야기 프롬프트로 바꿔주려면 Json을 보장해줘야 했다. 왜냐하면 프론트엔드로 해당 텍스트들을 전송하여 GPU 클라우드에 올라가 있는 이미지 생성 API에 요청하는 방식이었기 떄문이다.
이렇게 방식을 만든 이유는 나중에 배포를 할 때는 백엔드 쪽이 하나의 클라우드나 컨테이너에 존재하지 않을 수 있기 떄문.
그러므로 ChatGPT에는 Function Calling 기능이 있어, 해당 기능을 이용해 Sturctured Output을 내도록 했다. Function Calling은 모델 자체에서 해당 Structure 말고 다른 Structure가 나올 확률을 지워버려 리턴을 보장하는 방식이다.
정말 많은 프롬프팅 기법을 실험했지만, 예시 프롬프팅 기법을 단일 적용하는 게 오히려 실험적으로 제일 좋은 결과를 냈다.
functions = [
{
"name": "generate_image_prompt",
"description": (
"대화 내용을 바탕으로 이미지 프롬프트를 생성합니다. "
"완성된 description(이미지 프롬프트) 예시는 다음과 같습니다. "
"```A serene post-war Korean village, children playing joyfully by a clear, sparkling stream under a warm sun,skipping stones and catching minnows, lush greenery and traditional Korean houses in the background, peaceful smiles, the essence of childhood innocence and hope amidst a landscape that has seen hardship, soft sunlight casting gentle shadows, vibrant yet calming colors, capturing the beauty of resilience and new beginnings.``` "
"또한 title의 예시들은 다음과 같습니다. "
"```꿈과 사랑으로 일군 인생``` ```감사속에 피어난 아름다움``` ```가족과 함께 단란한 시간을``` "
),
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "대화를 요약해서 가장 맞는 타이틀"
},
"description": {
"type": "string",
"description": "이미지에 대한 세부 설명"
},
"subtitle": {
"type": "string",
"description": (
"title에 맞는 quote. 예시: '''가족과 이웃, 나를 지켜준 힘''' "
"'''붓을 내려놓고, 가정을 품다.''' '''위기 속에서 하나 된 가족'''"
)
},
"text": {
"type": "string",
"description": """이미지 프롬프트와 대화를 바탕으로 텍스트 내용을 생성합니다. 예시: ```중년이 되면서 내 삶의 중심은 가족이었다. 아이들이 자라나는 모습을 지켜보며 “너희는 무엇이든 할 수 있어”라는 말로 자신감을 키워주었다. 큰아들의 대학 합격은 지금도 가슴 벅찬 기억이다. 남편의 사업 실패로 어려움을 겪었지만, 가족이 힘을 합쳐 극복해냈다. 중년이 되며 삶에 여유를 찾고, 부모님을 더 잘 돌보지 못한 아쉬움이 남지만, 가족을 위해 헌신했던 시간이 나를 더 강하게 만들었다.```
"```젊은 시절, 나는 미술 선생님이 되고 싶었다. 공원에서 혼자 풍경을 그리는 걸 좋아했고, 친구들에게 그림을 가르치는 것도 즐거웠다. 그러나 가정 형편 때문에 꿈을 이루지 못하고 결혼 후 남편과 아이들을 돌보는 것이 내 삶의 중심이 되었다. 경제적 어려움 속에서도 가족은 서로를 도우며 어려움을 극복했고, 그 과정에서 더 단단해졌다. 함께한 모든 순간이 내게는 소중한 보물이다.```"
"""
}
},
"required": ["title", "description", "subtitle", "text"]
}
}
]
client = OpenAI(
api_key=openai_api_key, # This is the default and can be omitted
)
# GPT 호출
logging.debug("GPT 호출 시작")
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are an assistant that generates structured JSON outputs for image prompts."},
{"role": "user", "content": f"Summarize the dialogue and create an image prompt: {combined_text}"}
],
functions=functions,
function_call={"name": "generate_image_prompt"} # 특정 함수 호출 강제
)
3. SSH 연결 설정으로 이미지 다운로드
GPU 클라우드는 ComfyUI가 들어가 있는 곳이었다. Stable Diffusion을 사용하기 위한 것이었고, 따라서 해당 GPU 클라우드에 ssh 연결 설정을 하여 이미지를 다운받고, Cloudnary에 이미지를 올려 Luma AI와 연동해 이미지를 비디오화 했다.
시간이 없어서 일단 이렇게 한 것이지만, 확실히 시간이 있었다면 조금 더 간소화된 방법을 찾았을 것 같다.
4. 본선 당일 심사위원께 받은 피드백
Speech to Video만 더 활용했다면 Fantastic했을거라는 평이 있었다.
확실히 생각 못한 부분은 아니다. 팀들과의 회의에서 나왔던 의견이기도 하다.
다만 AI를 공부해본다면 알 수 있듯이 Video Generating은 가용 GPU가 어마어마하게 든다.
우리가 GPU의 용량을 구할 수 있는 데 한계가 있어 굉장히 심사숙고 해서 넣지 않았는데..만약 했다면 최우수를 했을 수 있으려냐? 조금 아쉽긴 하다.
5. 제 6회 KDT 해커톤을 마무리하며..
필자는 개발자이다.
그리고 개발자는 기획적으로 요구한 걸 그대로 구현하는 게 임무라고 생각한다. 그런 점에서 계속해서 팀장님과 다른 분들의 피드백을 반영하려고 정말 최선으로 노력했다.
구현하고 나서, 다시 피드백이 들어오고, 다시 그걸 구현하고, 다시 피드백이 들어오고..약간 그런 사이클이 계속 반복되다 보니 지칠 때도 있었지만, 결과물을 보면 굉장히 뿌듯했다.
어쩔 떄는 밤을 새가면서까지 기능 추가를 하려고 했고, 그 덕에 어찌되었건 전체 구현에 성공해서 우수상까지 탔으니 대만족이다.
절대 잊지 못할 추억과 경험, 그리고 더 나은 풀스택 실력을 이번 대회를 통해 쌓은 거 같아 좋다.
이제 코딩테스트를 본격적으로 다시 준비하고, 실력 있는 개발자의 길을 가고 싶다.
회고 끝!