커피 시음 후기 기록용 AI 에이전트, 모카 개발기 (1)
마신 커피를 쉽게 알아서 기록해주는 도구를 만들기로 했다. 내가 필요해서.

시작은 집 근처에 생긴 로스터리 겸 핸드드립 전문점이었다. 가게의 시그니처 블렌드를 소개 받아 마셨는데, 커피가 달수도 있다는 걸 그때 처음 알았다. 핸드 드립 커피든 드립백이든 그 전부터 맛있다는 커피는 찾아 마시며 살았는데도 새삼스레 ‘아, 커피는 정말 맛있는거야!’ 라는 생각이 들었다. 그렇게 커피에 빠져들었다. 근데 그렇게 홈카페 시작했는데 정작 내가 무슨 커피를 좋아하는지 알기가 힘들었다. 여태 어떤 커피를 마셔왔는지 데이터가 없으니 당연한 일이었다. 심지어 기억력도 별로다, 나는.
그래서 커피 시음 후기를 쉽게 작성해주는 앱을 개발하기로 했다. 커핑 노트라고 하면 너무 본격적이고 시음 후기라고 하는게 적당하겠다. 시음 후기를 SNS를 통해 공유할 수 있었으면 좋겠다는 친구의 아이디어가 있었다. 후기 카드도 인스타 감성으로 추가하기로 했다. 앱 개발에 필수인 깜찍한 마스코트는 또다른 친구 도움을 받았다. AI를 쓰더라도 캐릭터 디자인을 좀 아는 사람이 만들어야 귀엽지, 나처럼 아무도 모르는 사람이 뽑으면 공포의 강아지 밖에 안나오더라.
간단하게 시작하자, 슬랙봇 모카
슬랙으로 시작한 이유는 간단했다. (1) 굳이 다른 메신저앱을 깔지 않아도 된다. (2) 화면에 크게 신경쓸 필요가 없다.
생각보다 넣고 싶은 데이터가 많다.
처음엔 말로 설명했다. 주절 주절 떠드는 느낌으로. 사실 단순한 감상 후기라면 이정도도 나쁘지 않겠지만… 나는 이 데이터를 쌓아서 나중에 내 취향의 커피를 탐색하는 데도 쓰고 싶었다. 데이터만 있다면 AI 를 시켜서 뭐든 내 취향 커피를 찾는데 도움을 주지 않을까? 하는 게 내 생각이었다. 그럼 생각보다 다양한 데이터가 필요했다.
그래서 Open AI API에 web search 를 사용했다. 결과적으로 말하면 이 시도는 실패했다. 많은 스토어가 그렇듯, 커피의 상세 정보들은 다 이미지에 있었다. 그러다보니 검색해서 텍스트를 가져오는 기능은 대체로 실패했고 별도의 프롬프트로 분리해서 커피 검색 -> 공식 스토어 방문 -> 상세 이미지 탐색 -> OCR로 정보 긁어오기 순으로 진행했다. 다행히 이미지가 원인인게 맞았는지 검색 기능이 정상적으로 동작했다. 덕분에 커피 노트나 오리진을 한땀 한땀 적는 수고를 덜었다.
근데… 굳이 검색해야하나?
생각해보니 굳이 검색하지 않아도 커피 정보를 입력할 수 있는 방법이 있었다. 요즘은 원두를 사면 예쁘게 디자인된 커피 카드를 동봉해준다. 거기엔 커피 이름, 노트, 원산지, 로스터리 등등 참 많은 정보가 담겨있다. 이걸 읽어다가 정보로 쓰면 굳이 검색할 필요가 없지 않나? 하는 생각이 들었다. 그래서 OCR을 붙여 검색 없이 사진에서 정보를 추출해 채워 넣을 수 있도록 했다. 물론, 카드를 주지 않을 수도 있으니까 앞에서 개발했던 기능은 유지했다.
노트 수정은 어떻게 하지?
문제는 수정 기능을 개발하면서 생겼다. 수정 기능을 위해 너무 많은 코드가 들어갔고, 그렇게 많은 코드를 작성했는데도 수정 기능은 정상적으로 동작하지 않았다. 모카는 산미가 더 도드라졌다는 내 후기를 붙여주지 않았다.
이제 와서 고백하자면 나는 모카를 에이전트로 개발할 생각이 없었다. 정말 간단하게 만들고 싶었으니까. 그래서 Open AI API 를 호출하는 Client를 만들고 프롬프트를 그때 그때 갈아끼우면서 필요한 순간만 호출이 일어나도록 설계했다. 결과적으로 이 시도는 아주 처참히 망했다. 당연하게도, 이런 단발성 호출 구조는 코드 복잡성을 아주 아주 크게 키웠다. 사용자 발화가 등록인지 수정인지 파악하기 위한 추측 의도 게이트를 별도로 만들게 했고, 추측 의도로 분기해서 서로 다르게 동작하는 코드를 짜야하고, 그 와중에 Context가 유지되어야 하니 컨텍스트 보관은 또 공통으로 들어가야하고, 기타 등등… 나는 이 모든 것이 AI가 스스로 일하게 하는 것이 아니라 내가 AI를 일하게 만들기 위해 만든 설계에서 비롯됐다는 걸 깨닫고 모카를 에이전트로 전환 했다.
바뀐 건 없어보이지만 많은 것이 바뀌었다
거의 모든 코드를 재작성했다. Open AI API를 반복적으로 호출할 수 있는 구조로 전환하고 노트 검색 기능이나 노트 초안 제안 기능도 AI가 능동적으로 가져다가 사용할 수 있도록 Tool로 전환했다. 에이전트를 개발해본 경험이 없기 때문에 초창기 전환 코드에는 이렇다할 표준이 적용되지 못했다. 1차 개선이 되고 나서야 내겐 Spring AI 가 있었지 라는 생각이 들어 Spring AI 를 표준삼아 리팩토링 했다. Spring AI를 그냥 가져다가 사용하고 싶었는데 개발 당시엔 Response API를 지원하지 않는다는 이야기가 있어서 도입하지 못했다.
슬랙봇에서 앱으로
이렇게 한땀 한땀 대화로 수정하다보니 속이 터졌다. 등록할 땐 알아서 검색도 해주고 아주 편했는데 수정은 내가 수정할 부분을 하나 하나 지적해서 어떻게 수정해달라고 말해야하니, 오히려 말로 하는게 불편했다. 그렇게 하나 하나 지적해서 수정 요청했는데 모카가 잘못 수정하는 일이 잦았다. 잘못된 필드를 수정하거나 잘못된 값으로 수정하거나 아예 수정을 안 하고 수정을 했다고 하거나. 마스코드가 강아지라 버럭 화도 못 내고… 그래서 수정은 명확하고 직관적으로 할 수 있도록 구성하는게 낫겠다는 생각이 들었다. 그래서 폼으로 인터페이스를 수정하기로 마음 먹었다. 슬랙은 인터페이스 커스터마이징에 한계가 있으니 화면을 개발하기로 했다.
대화에서 폼으로
등록 시작 자체는 슬랙봇과 동일하다. 발화를 하고, 해당 발화를 기반으로 해서 1차로 데이터를 채운다. 그리고 미진한 부분이나 잘못 채운 부분은 사용자가 보고 폼에서 즉시 수정한다. 기존에 등록되어 있던 커피인데 모카가 커피 정보를 못찾을 수도 있으니 사용자가 특정 커피 정보를 직접 선택해서 기록에 붙일 수 있도록 하는 화면을 하나 추가했다. 커피 이름 + 로스터리 명으로 같은 커피인지 아닌지를 식별하는데 로스터리 명을 잘못 읽으면 이미 있는 커피 기록인데 새로운 커피 기록이라고 인지할 수 있기 때문이다. 마이야르 토마토 카드처럼 로스터리 명을 명확하게 인지하기 어려운 경우 종종 발생하는 일이라 이번에 화면을 개발하면서 확장했다.
정말 편해진 건 수정이었다. 발화를 하면 모카가 기록을 찾는다. 기록을 찾으면 사용자가 수정 요청한 부분을 수정한다. 제대로 수정되지 않으면 사용자가 직접 폼을 통해 수정하고 저장할 수 있다. 하나씩 다 짚어주면서 수정하지 않아도, 모카가 말 귀를 못알아 들어서 속상해할 일도 없다. 물론 한번에 잘 되면 정말 좋겠지만 AI가 항상 내맘과 같은 건 아니니까. 인터페이스가 곧 하네스가 되는 셈이다.
편하게 탐색하기
화면을 개발했기 때문에 사용자가 기존에 등록된 커피 기록도 훨씬 간편하게 탐색할 수 있게 됐다. 내가 자주 쓸 거 같은 필터도 몇 개 추가했다.
노트 상세에선 로스터리명까지 수정할 수 있도록 확장해뒀다. 로스터리명은 커피 동일성을 체크하는 기준이라 수정 불가 필드로 두고 싶었으나… 로스터리명을 읽는데 오류가 생각보다 잦아서 수정 가능하도록 열어두었다.
어쩌면 이 앱의 하이라이트. 커피 시음 후기이다. 레시피 정보가 있다면 레시피 정보 역시 동일한 디자인의 카드로 다운로드 받을 수 있게 구성했다.
정리
첫 글이니까 회고록 느낌이 나도록 작성했다. 앱은 계속 사용하면서 수정하고 있고 글에서 언급된 것 외에도 검색을 쉽게 하기 위한 Alias 정보 생성이라든지… Context 관리를 위한 인메모리 채팅 맵이라든지… Tool 사용 후 검증을 위한 Validation 이라든지… 아직 잡지 못한 이러 저러한 버그라든지… 여러가지 요소가 있지만 그건 시간이 날 때 천천히 적어야겠다. 끝!
소스 코드 : mocha