2024 항해커톤의 주제는 오픈소스 AI를 활용해 사회의 문제를 해결하는 서비스 만들기였다.
이번 주제로 꼭 만들어보고 싶은 서비스가 있어서 친구들과 함께 항해99 해커톤에 참여하게 되었다.
이번 해커톤에서 우리가 해결하고자 한 사회 문제는 디지털 소외 문제였다.
디지털 소외를 단적으로 보여주는 사례가 바로 기차표 예매라고 생각한다.
요즘은 다들 온라인으로 기차표를 예매하기 때문에, 창구에선 표를 구하기 어려운 경우가 많다.
그래서 온라인 예매를 어려워 하는 어르신들은 표를 구하지 못해 입석으로 서서 가거나, 표가 나올 때까지 기차역에서 몇 시간을 기다리시기도 한다.
우리는 이러한 문제를 해결하기 위해 노인과 같은 디지털 약자들도 손쉽게 이용할 수 있는 KTX 예매 서비스를 개발하기로 했다.
어르신들은 온라인 예매를 왜 어려워하실까? 왜 창구에서 예매하는 걸 더 편하게 생각하실까?
현재 코레일 웹 사이트의 경우 기차 예매 뿐만 아니라 여행 상품 등 다양한 기능을 담고 있기 때문에 화면이 복잡한 편이다.
IT에 익숙한 우리들은 예매를 하려면 어디를 클릭해야 할지 당연하게 느껴지지만, IT에 익숙하지 않은 분들에게는 어려운 화면일 수 있다.
그래서 시간과 품이 들더라도 창구로 찾아가서 직원에게 물어보는 것을 더 편하게 느끼시는 것으로 보인다.
그렇다면 온라인을 통해 예매하더라도 마치 창구 직원과 대화하는 것처럼 기차표를 예매할 수 있다면 현재의 문제를 좀 더 개선할 수 있지 않을까?
우리는 이러한 아이디어를 바탕으로 대화형 인터페이스를 갖춘 서비스를 개발하기로 하였다.
대화형 인터페이스를 구축하기 위해 우리는 Chat GPT를 사용하기로 했다.
먼저, 사용자의 음성을 인식하기 위해 react-speech-recognition
라이브러리를 사용했다. 리액트에서 Web Speech API를 사용하기 좋도록 랩핑한 라이브러리이다.
Chat GPT의 응답을 음성으로 재생하는 데에는 OpenAI의 Text to Speech 기능을 사용했다. 처음에는 Web Speech API의 SpeechSynthesisUtterance
를 사용해서 텍스트를 음성으로 변환했는데, 음성 변환이 너무 부자연스러워서 요금이 좀 들긴 하지만 OpenAI의 기능을 사용하기로 했다.
Chat GPT를 통해 기차표를 조회하고 예매하려면 외부 함수를 호출해야 하기 때문에 Function Calling 기능을 사용했다.
Function Calling 기능을 사용하려면 openai.chat.completions.create
로 Chat GPT에게 질문을 보낼 때, tools
프로퍼티에 호출해야 하는 함수의 내용 (함수명, 파라미터 등)을 담아서 보내면 된다. 그러면 Chat GPT가 함수의 호출이 필요하다고 판단되는 시점에 tool_calls
프로퍼티에 호출해야 하는 함수의 이름과 파라미터 정보를 담아서 응답을 보내주는데, 이 정보를 활용해서 함수를 호출하면 된다.
함수의 파라미터를 정의할 때에는 description
에서 출발 시간 (format: hhmmss)
와 같이 파라미터 내용과 형식을 Chat GPT에게 구체적으로 알려주는 것이 도움이 되었다.
async function askChatGpt() {
// 인식된 음성을 대화내역에 추가
messages.push({
role: 'user',
content: finalTranscript,
});
// Chat GPT API로 요청 보내기
const response = await createChatCompletions(messages);
// Chat GPT 응답 처리
const responseMessage = response.choices[0].message;
await handleResponse(responseMessage);
}
async function handleResponse(responseMessage: any) {
// Chat GPT가 Function Calling을 요청하는 경우
if (responseMessage.tool_calls) {
messages.push(responseMessage);
for (const toolCall of responseMessage.tool_calls) {
// Function Call 파싱
const { functionName, parameters } = parseFunctionCall(toolCall)?.[0];
await handleToolCall(functionName, parameters, toolCall.id, toolCall.function.name);
}
}
}
async function handleToolCall(functionName: string, parameters: any, toolCallId: string, functionNameDisplay: string) {
switch (functionName) {
case 'saveTrainRoute':
// 출발지와 도착지 정보 저장
break;
case 'saveDepartureTime':
// 출발 시간 저장 후 예매 가능한 열차 조회
break;
case 'reserveTrain':
// 열차 예매
break;
case 'goToPaymentPage':
// 결제 페이지 이동
break;
default:
break;
}
}
처음에는 Fine Tuning 없이 gpt-3.5-turbo-1106
모델을 사용해서 개발을 했었다. 그런데 기본 모델을 사용하다보니 여러 제약사항을 경험하게 됐다.
KTX 예매 서비스에 맞는 일관적인 대화체를 유지해야 하는데 상대방의 말에 따라 대화체가 바뀌는 문제도 있었고, 대화형 인터페이스다보니 응답이 너무 길면 안 되는데 쓸데없는 말을 한참 동안 주절주절 하는 경우도 있었다. 그리고 Function Calling이 필요한 시점에 tool_calls
응답이 오지 않는 등의 문제도 있었다.
이러한 문제점을 해결하기 위해 Fine Tuning을 통해 우리의 서비스에 최적화된 모델을 만들기로 하였다. 아래와 같이 우리가 원하는 대화 시나리오를 json 형식으로 정리해서 jsonl 파일로 만들고, 이 파일을 바탕으로 학습시켜 Fine Tuning 모델을 만들었다.
Fine Tuning 모델을 사용한 이후부터 대화체가 확연히 정리되고, Function Calling 오류도 줄어들었다.
{
"messages": [
{
"role": "assistant",
"content": "안녕하세요. 어디에서 어디로 가는 열차를 찾으시나요?"
},
{
"role": "user",
"content": "부산에서 서울로 가요."
},
{
"role": "assistant",
"function_call": {
"name": "saveTrainRoute",
"arguments": "{\"departure\":\"부산\",\"destination\":\"서울\"}"
}
},
{
"role": "function",
"name": "saveTrainRoute",
"content": "true"
},
{
"role": "assistant",
"content": "출발 시간은 언제인가요?"
},
//... 중략
],
"functions": [
{
"name": "saveTrainRoute",
"description": "기차 출발지와 도착지 정보를 저장하는 함수",
"parameters": {
"type": "object",
"properties": {
"departure": {
"type": "string",
"description": "출발지 (Departure)"
},
"destination": {
"type": "string",
"description": "도착지 (Destination)"
}
},
"required": [
"departure",
"destination"
]
}
},
//... 중략
]
}
열차 조회 및 예매 기능에는 carpedm20님의 Korail Python Wrapper를 사용했다.
이 기능을 사용할 수 있도록 Fast API를 사용해서 백엔드 서버를 구축했다.
아쉽게도 carpedm20님의 Korail Python Wrapper에는 결제 기능이 없어서 결제 기능을 직접 구현해야 했다.
결제를 어떻게 구현할지 고민하다가 Selenium WebDriver를 사용해서 신용카드 결제 매크로를 만들었다.
어르신들이 가장 많이 어려움을 겪는 부분이 아무래도 결제 기능이 아닐까 싶었다.
어떻게 하면 최대한 쉽게 결제하실 수 있도록 구현할 수 있을까 고민하다가 신용카드 OCR 기능을 떠올리게 되었다.
그래서 신용카드 정보를 직접 입력하는 대신 카메라로 신용카드 뒷면 사진을 찍으면 카드 정보를 읽어올 수 있도록 구현했다.
이를 위해 Google Cloud의 Document AI로 신용카드 이미지 데이터를 학습시켜 OCR 기능을 구현했다.
해커톤 현장에서는 시간이 부족해서 결제 기능 연동까지 보여드리지 못했다 🥹
대회가 끝나고 결제 기능까지 모두 연결하고, UI적으로 아쉬웠던 부분을 보완해서 아래와 같이 완성했다.
이렇게 하고 코레일에 들어가보면 요렇게 열차표가 예매되어 있는 것을 확인할 수 있다!
(계속 테스트를 해보다가 서울에서 부산 가는 열차가 예매된 것을 깜빡해서 59000원을 날린 슬픈 일도 있었다,,,🥲)
개발자가 된 이후로 해커톤에 꼭 한 번 참여해보고 싶었는데, 이번에 참여할 기회를 얻어서 너무 좋았다.
꼴딱 밤을 새는 것이 조금 힘들긴 했지만, 중간중간 럭키드로우 추첨도 있고, 포토부스에서 사진도 촬영할 수 있어서 즐겁게 참여할 수 있었다. 참가비도 없었는데 중간중간 맛있는 간식이랑 식사도 챙겨주셔서 감사했다 🥺
그리고 다른 팀의 발표를 들으면서 배울 수 있어서 좋았다. AI 기술을 접목해서 이렇게 다양한 서비스를 개발할 수 있구나 싶었다. 나도 앞으로 꾸준히 공부하면서 새로운 시도들을 많이 해봐야겠다.
인생 첫 해커톤, 너무 즐거운 경험이었어서 다음에 기회가 된다면 또 나가보고 싶다ㅎㅎ
우리 팀 정말 고생 많았구, 같이 할 수 있어서 넘 즐거웠구, 다음에 기회가 되면 또 같이 해커톤 나가봅시닷,,,💕
최고다 우리팀 ! 해커톤 끝나고 디벨롭도 하다니 역시 영의씨 ... 💕
일 열심히 해서 더 든든한 팀원이 되어 돌아오겠습니다 -