초개인화 AI 뉴스레터 서비스(Baking News) 개발 수기
Baking News: 꼭 너만을 위한 AI 뉴스레터(https://baking-news.vercel.app)
Baking News라는 이름의 초개인화 AI 뉴스레터 서비스를 운영중에 있습니다. 독자의 문해력 수준과 관심사를 반영한 맞춤 뉴스레터를 제공하는 서비스입니다.
RSS 업데이트에서 내가 관심있어할 만한 소식을 정리해서 하나의 데일리 아티클로 재구성해줍니다. 다음 링크에서 직접 사용해보실 수 있습니다.
이는 RSS 리더의 현대적 재해석입니다. RSS 리더는 여러 출처의 미디어를 한번에 모아볼 수 있다는 장점이 있지만, 너무 많은 글이 올라오다보니 모든 글을 읽기에는 어려움이 있었습니다. 이러한 피로감을 없애기 위해 "뉴스레터"라는 포맷을 택했습니다. 뉴스레터는 중요한 글만 골라서 매일매일 고정된 시간에 보내주기 때문에 번잡함을 덜 수 있습니다.
이런 스택을 사용했습니다
LLM: Claude
Frontend: 라이브러리 없음
Backend(User Data): PocketBase
Backend(Content Generation): Deno + Hono
어떤 LLM을 사용할까?
이러한 기능들을 규칙기반으로 제공하기는 어렵습니다. 그렇기에 관심사 분류와 뉴스레터 생성에 LLM을 적용하였습니다. 크게 다음과 같은 LLM을 고려해보았습니다
이름 | 개발사 | 강점 | 단점 |
---|---|---|---|
GPT 3.5 | 오픈AI | 많은 레퍼런스가 있다 | |
HyperCLOVA | 네이버 | 한국어를 잘 한다 | Context Length가 작다 |
Gemini Pro | 구글 | 무료 티어 존재 | 말을 잘 안듣고 환각이 심함 |
Command-R | Cohere AI | 무료 티어 존재 | 한국어 답변에 오류문자, 영단어를 섞어서 생성함 |
Claude Haiku | Anthropic | 가볍고 똑똑함, 비용 |
비용이 부담되었기에 무료 티어가 있는 Gemini(구글)와 Command-R(Cohere)을 1순위로 고려했었습니다. 그러나 Gemini는 환각 현상이 너무 심했고, Command-R은 한국어 답변에 오류문자와 영단어를 섞어서 생성하는 오류가 있기에 채택할 수 없었습니다.
결국에는 Haiku를 선택했습니다. Haiku는 GPT 3.5보다 성능이 좋고(Arena ELO 기준) 가격도 더 저렴했습니다. 기대한 효과는 아니였지만, Haiku의 빠른 텍스트 생성 속도 덕분에 서비스 사용성 개선에도 큰 도움이 되었습니다.
LLM 없이는 안될까?
최초에 구현한 버전에서는 주제 분류 또한 LLM으로 구현했습니다. 프롬프트는 다음과 유사합니다.
[
{ "id": "661b367fff8da70ab2a2694e", "title": "Baking News(초개인화 AI 뉴스레터 서비스) 개발 수기" },
{ "id": "10998561", "title": "전세계 경제 불안한데…“하반기 위기 올까” 인공지능으로 예측해보니" },
...
]
Classify above articles into following topics: ["주제1", "주제2", "주제3"]
Follow this format:
\`\`\`
type Result = Record<"주제1" | "주제2" | "주제2", string[] /* article ids */>
\`\`\`
Start your response with "{"
이렇게 작은 일 조차 LLM으로 구현해야 하나 싶은 생각이 들었습니다. LLM을 대체하기 위해 Rerank, STS 등을 계산할 수 있는 모델을 시도해보았습니다. 그러나 둘 다 기대한 만큼의 성능을 내지 못했고, 다시 LLM으로 돌아오게 되었습니다. Rerank와 STS는 문장을 분류하기 위해 만들어진 모델은 아닙니다. 문장과 문장간의 의미론적 유사도를 계산하기 위해 만들어진 모델이기에 주제 분류 태스크를 잘 수행하지 못한 것으로 보입니다.
추후 기회가 된다면 Zero-Shot Classification 모델을 직접 개발해보고 싶다는 생각이 들었습니다.
이 도전을 하면서 다음의 포스트를 작성했었습니다
브라우저에서 RoBERTa 실행해보기
개발하기
다음과 같은 구조로 개발하였습니다
사용자가 구독중인 RSS 피드에서 아티클을 가져옵니다.
뉴스레터 생성 서버에 아티클을 전달합니다
Claude를 통해 아티클을 생성합니다
사용자의 관심사에 일치하는 기사를 추출합니다. 제목을 통해 분류합니다.
관심사와 일치하는 기사의 본문을 가져와서 뉴스레터를 생성합니다
LLM 서비스를 개발하면서 가장 난감했던 부분은.. 인공지능이 말을 더럽게 안듣는다는 점이였습니다. 뉴스레터를 생성할 때 지켜야 할 지시사항을 적어주었습니다. 반말로 작성하라, 인용문에는 마크다운으로 링크를 달아줘라, 문장형 제목을 사용해라 등등의 지시를 했는데, 안듣습니다. 원하는 방향을 완벽히 구현하고 싶었지만 결국 포기하고 어느정도 수준 이상을 일관적으로 달성하는데에 위안을 삼았습니다.
특히 Gemini를 테스트하면서 얘 정말 말 안듣는다는 생각을 많이 했습니다. 지맘대로 합니다. 결과물도 일관적이지 않습니다. Claude는 말을 잘 들어서 큰 스트레스 없이 개발을 했습니다...😂
LLM은 RTOS가 아니에요: 스트리밍되는 마크다운을 브라우저에 보여주기
이런 문제가 생길거라고는 전혀 생각하지 못했습니다.
기사를 가져오고, LLM으로 뉴스레터를 생성하는게 전부일줄 알았습니다. 이 서비스에서 이런 재밌는 엔지니어링을 하게 될줄은 몰랐습니다. ...
뉴스레터는 LLM의 출력값이 길기에 전체 결과를 받기까지는 오랜 시간이 걸립니다. 몇십초라는 긴 시간을 기다리고서라도 뉴스레터를 받아보겠다는 사용자는 없을겁니다. 그렇기에 사용자의 기다림을 최소화하기 위해서, LLM의 결과를 스트리밍으로 보여줄 수 있습니다.
그러나 대부분의 마크다운 렌더러는 스트리밍 렌더링을 지원하지 않습니다. 물론 결과가 업데이트 될 때 마다 새로 렌더링을 할 수 있지만, 이는 성능 및 사용성을 저하할 수 있습니다. 또한 저는 Gemini의 "서서히 나타나는 텍스트 애니메이션"을 구현하고 싶었습니다.
그렇기에 스트리밍되는 마크다운을 올바르게 보여주는 컴포넌트를 직접 개발하게 되었습니다. 지금까지 프로그래밍 언어의 파서와 인터프리터를 구현한 경험이 여러 번 있었기에, 부담 없이 도전하였습니다. 다음과 같이 구현하였습니다:
렌더링할 문자열을 글자 단위로 자른다
각 글자를 큐에 넣는다
큐의 길이가 5 이상이 될 때 까지 기다린다
큐를 shift한 결과에 따라 각 요소의 파서로 전달한다
shift한 값이 `*`일 때
다음 shift의 값이 `*` 라면 Bold 파서로 전달함
다음 Shift의 값이 ` `라면 List 파서로 전달함
shift한 값이 `#`일 때는 Heading 파서로 전달함
아무 경우도 아니라면 마지막 엘리먼트에 텍스트로 append함
텍스트를 Append할 때 span 태그로 감싸서 애니메이션 효과를 적용하였고, "서서히 나타나는 텍스트 애니메이션" 효과를 구현할 수 있었습니다. 기회가 된다면 라이브러리로 공개하고 싶습니다.
데이터 스토리지: Pocketbase
PocketBase - Open Source backend in 1 file
Pocketbase는 보안 정책을 설정하여 프론트엔드가 DB에 안전하게 직접 접근할 수 있도록 해주는 API 서비스입니다. Supabase과 유사한 오픈소스 프로젝트이며, 사용성도 유사했습니다. 실제로 사용해보니 장단이 있었습니다.
장점
간편한 설정과 SQLite
Postgres, GoTrue, Kong 등 여러 프로젝트로 구성되어있는 Supabase와 달리, Pocketbase는 바이너리 파일 하나만 실행하면 됩니다. 그렇다고 기능에 큰 하자가 있는 것도 아니였습니다.
바이너리 파일 하나로만 서버를 구현하면 디비는 어디에 있을까요? SQLite에 데이터를 저장합니다. 작은 서비스에서 굳이 큰 상용 디비를 채택할 필요가 없습니다. SQLite는 단순하게 백업하고 되돌리고 이동할 수 있으며, 심지어는 속도도 다른 디비보다 빠릅니다(PocketBase / 6. Can I use database X?).
단점
TypeScript 지원이 부실함
라이브러리 자체는 타이핑을 지원하지만, 디비 스키마에 타입 완성이 제공되진 않습니다. 스키마 타이핑을 지원하도록 해주는 라이브러리 patmood/pocketbase-typegen(Typescript generation for pocketbase records)가 있지만, 공식 라이브러리는 아닙니다. Join문의 타입을 수동으로 작성해줘야 하는 문제가 있었지만 만족하며 사용했습니다.
또한 디비에 보안정책을 설정할 때 독자 문법을 사용합니다. 보안정책 문법에 제가 원하는 오퍼레이터가 없어서, 수많은 Boolean operator의 연계로 구현한 기억이 납니다.
이와 유사한 정책을 구현해야 했습니다:
score 컬럼이 [ 100, 200, 300 ]인 레코드만 허용
이를 다음과 같이 작성해야 합니다
score = 100 || score = 200 || score = 300
Filter collection by `IN` operator on `id` field · pocketbase/pocketbase · Discussion #4463
Pocketbase 사용 경험을 총평 하자면.. "괜찮은데?"입니다. 위에서 말한 것과 같은 단점이 있지만, 싱글 파일 바이너리에 SQLite가 주는 가벼움은 훌륭했습니다. 앞으로도 작은 프로젝트에서는 애용할 것 같습니다.
UI 라이브러리 없는 프론트엔드
충분히 작은 프로젝트는 굳이 UI 라이브러리를 쓰지 않고 구현하는 편입니다. 페이지가 고작 네개밖에 되지 않는 이번 프로젝트도 UI 라이브러리를 쓰지 않았습니다. 이는 사실 반항적인 면도 있었습니다. 저는 과한 추상화를 꺼려합니다. "웹 브라우저" 위에서 "LLM 서비스를" 개발하면서 할 수 있는 말은 아니지만 말이죠😂
아무튼 이번 프로젝트는 UI 라이브러리 없이 개발하였습니다. 다만 앞으로 어느 정도의 데이터 흐름이 있는 서비스는 꼭 라이브러리를 끼고 만들어야겠다는 다짐을 했습니다.. 디자인 시스템을 따라 설계된 UI를 라이브러리 없이 개발하는 것도 힘들었습니다..
Baking News 프론트엔드의 미래에 대해 고민을 하던 중 다음과 같은 방안을 발견했습니다
프레임워크 없이 만든 프론트엔드에 작은 부분부터 Svelte 도입하기
https://tilnote.io/home?pages=6618b0a8fcc83b745e72b896
WebComponent 호환을 사용하면 점진적으로 프레임워크를 적용할 수 있기에, 이 방향으로 점차 프레임워크를 적용해나갈 것 같습니다.
앞으로의 계획
현재 공개되어 있는 서비스는 대회(해군창업경진대회) 참가용으로 만든 MVP입니다! 작동은 잘 되지만 엉성한 부분도 많고, 더 구현하고 싶은 기능들도 있습니다.
앞으로 이러한 개선을 해 나갈 예정입니다:
Baking News만의 디자인 만들기
현재 디자인은 타 프로젝트의 디자인 시스템을 그대로 따와서 만들었습니다. Baking News의 방향성을 반영한 디자인을 다시 만들어볼 예정입니다SolidJS로 재작성하기
SNS에 광고 집행하기!
Baking News는 제가 처음으로 제대로 빌드해본 소비자용 제품입니다. 제품의 완성도를 올린 후에는 인스타, 트위터 광고를 집행해서 유입을 만들어보고 싶습니다
급한 대회 일정에 5일만에 빌드한 프로젝트 치고는 여러 시도를 해보며 많은걸 배워갑니다. 우리 Baking News 많이 사랑해주세요!
> 해군사관학교 권, 양, 한, 남 수병과 함께 만들었습니다 :) 다들 수고했어요~~