일기 slecs

봇이 짧게 뱉어서 세 번을 고친 날

목차

werebridge 봇이 말수가 적다는 걸 확인하는 것부터 시작됐다. 1500자 최소 기준인데 출력을 찍어보면 1100이 나오고, 1300이 나오고, 어쩌다 1560이 나오고. 일관성이 없다는 게 더 문제였다. 한두 번의 예외면 무시하고 넘어갈 수 있는데, 이건 구조적으로 어딘가가 세팅이 잘못된 상황이라는 게 분명했다.

첫 번째 가설은 max_tokens 제한이었다. 2500으로 걸려 있었는데, 계산해보면 한국어 1500자를 생성하는 데 필요한 토큰 수는 2500을 훌쩍 넘길 수 있다. 영어랑 달리 한국어는 조사가 따라붙고 어미가 결합되면서 어절 하나가 토큰 서너 개로 분리되기도 한다. 출력이 토큰 제한에 걸려 중간에 잘리는 거라면 그냥 올리면 되는 문제라고 생각했다. 4096으로 수정해서 올렸다.

틀렸다. 출력이 중간에 잘리는 게 아니었다. 처음부터 짧게 쓰고 스스로 끝내고 있었다. 모델이 프롬프트에 적힌 방향대로 움직이는 거니까, 프롬프트가 간결하게 쓰도록 당기고 있는 거다. 원인 분석이 방향을 틀어야 했다.

프롬프트를 다시 읽어보니 확실히 간결함을 유도하는 표현들이 있었다. 모델은 프롬프트에서 "핵심만", "간단히" 같은 말을 충실하게 따른다. 두 번째 커밋에서 이 부분들을 정리하고 1600자 이상 자연스럽게 풀어쓸 수 있도록 톤 가이드를 다시 잡았다.

리밸런싱 후 다시 확인해보니 이번엔 넘긴다. 근데 1510, 1540 이런 수준이었다. 마지노선을 간신히 넘는다는 건 실질적으로 불안정한 거다. 모델 출력은 실행마다 조금씩 달라지기 때문에, 마진이 거의 없으면 다음 번엔 못 넘길 수 있다. 타겟 자체를 2200으로 올려서 모델이 그걸 목표로 쓰게 만들면, 오버슈팅이 생겨서 결과적으로 1500 이상이 안정적으로 확보된다. 세 번째 커밋에서 그걸 수정했다.

결과는 맞다. 세 번 건드려서 해결됐다. 근데 사후에 보면 이 세 변수 - 토큰 한계, 프롬프트 톤, 목표값 - 이 서로 물려있어서 처음에 다 같이 봤어야 했다. 한 변수씩 건드리는 게 올바른 디버깅 원칙이긴 한데, 세 개가 동시에 문제일 수 있는 케이스라면 처음에 전부 훑는 게 오히려 빨랐을 것이다. 두 번의 커밋은 줄일 수 있었을지도 모른다는 생각이 남는다.

쿠팡 블록을 뽑아내기로 했다

블로그 쪽에서 오늘 두 가지 결정을 함께 내렸다.

첫 번째는 쿠팡 추천상품 위젯 완전 제거. 처음 붙일 때는 콘텐츠 안에 자연스럽게 관련 상품이 연결되면서 수익 연결이 될 거라고 기대했다. 실제로는 어느 포스트에 어떤 위젯이 붙어있는지 추적하는 것 자체가 작은 오버헤드가 됐고, 콘텐츠 품질에 기여하는 것보다 노이즈가 컸다. 걷어내기로 결정했다.

strip_coupang_blocks.py를 만들어서 기존에 DB에 박혀있는 위젯 블록들을 전부 긁어냈다. 포스트를 읽어서 특정 마커 패턴이 있는 블록을 찾아 제거하고 다시 저장하는 구조. 작업 자체는 단순한데, 실행하면서 PK 컬럼 이름 문제가 걸렸다. 스크립트에서 sn으로 참조했는데 실제 테이블 컬럼은 post_sn이었다. 쿼리가 실패하고 나서야 알아챘다.

이게 반복되는 실수다. 프로젝트마다 PK 네이밍 규칙이 미묘하게 달라서, 기억에만 의존해서 스크립트를 짜면 이런 게 생긴다. 테이블 스키마를 직접 확인하는 게 번거로워도 그 편이 낫다는 걸 알면서도 또 당했다. fix 커밋 하나가 추가됐다.

두 번째는 hourly 회고 루틴 폐지. 매 시간마다 자동으로 짧은 회고를 생성하던 루틴이었는데, 솔직히 시간 단위 회고가 의미 있는 내용을 담은 적이 별로 없었다. 한 시간 안에 깊이 생각할 거리가 쌓이지는 않는다. 비슷한 말이 반복해서 나오고, 생성 비용 대비 품질이 낮은 콘텐츠가 계속 찍혀 나오는 구조였다.

대신 retro를 하루 한 번, 제대로 길게 쓰는 방향으로 바꿨다. 5000자에서 최대 10000자 범위, 그날 작업량에 비례해서 길이를 스케일하도록. 커밋이 많고 작업이 빽빽한 날은 8000자 이상으로 꽉 채우고, 얕은 날은 5000자대에서 마무리. bulk_seed.py를 수정하고, CLAUDE.md에 변경사항을 기록했다.

오늘 이 회고 자체가 그 결정의 첫 번째 출력물이기도 하다.

수익 숫자를 대시보드 첫 화면에

404 프로젝트에서 수익 관련 대시보드 레이어를 두 단계로 밀었다.

첫 번째는 히어로 영역에 카드 3칸 추가. 오늘 수수료, 누적 남은 수익, 출금 가능액. 대시보드에 들어왔을 때 스크롤 없이 바로 보이는 자리에 핵심 숫자가 있어야 한다는 판단이었다. Spring MVC + iBatis 조합이라 작업 순서가 고정돼 있다. 서비스 레이어 메서드 추가, sqlmap 매퍼에 쿼리 작성, JSP에서 카드 렌더링. 이 순서를 거슬러 가면 레이어 간 의존 관계가 꼬이니까 이 순서대로 내려가야 한다.

각 수치가 어떻게 집계되는지 정의하는 게 쿼리 작성에서 제일 신경이 쓰이는 부분이었다. 오늘 수수료는 당일 발생 건들의 합산, 누적 남은 수익은 출금 처리 전 미지급 금액의 총합, 출금 가능액은 거기서 최소 보유금을 제하고 실제 출금 신청이 가능한 금액. 비즈니스 로직이 SQL 안에 녹아드는 구조라 정의를 명확하게 잡지 않으면 나중에 수치가 맞지 않는다는 말이 나온다.

두 번째는 전체 모드 시스템별 남은 수익 분해 카드. 히어로가 전체 합계를 보여준다면, 이 카드들은 시스템 모드 단위로 남은 수익을 쪼개서 보여준다. 어느 모드에서 얼마가 쌓여있는지를 한눈에 볼 수 있도록. 집계 쿼리에 GROUP BY가 들어가고 결과가 리스트로 올라오는 구조다. sqlmap에서 resultMap을 잡아주면 리스트로 깔끔하게 매핑된다.

JSP 렌더링이 좀 번잡했다. 모드 수가 동적으로 달라지기 때문에 <c:forEach>로 돌려야 하는데, 카드 너비를 균등하게 분배하려면 모드 수에 따라 CSS 클래스가 달라져야 한다. JSTL에서 <c:choose>로 조건 분기를 써서 모드 수에 맞게 클래스를 지정하는 방식으로 처리했다. 코드가 길어지는 게 흠인데, JSTL 안에서 이보다 더 깔끔하게 처리하는 방법을 모르겠다. 자바스크립트로 동적으로 잡는 방법도 있긴 한데, 이 프로젝트의 방식에는 서버 사이드 렌더링이 맞다.

재료를 조합해서 프롬프트를 찍어내는 도구

어드민에 새 도구가 생겼다. 사이트 레시피 조립기. 이름은 조금 거창한데 개념은 단순하다. 콘텐츠나 기능 기획할 때 자주 쓰는 프롬프트 패턴들을 재료 단위로 미리 정의해두고, 필요한 재료들을 골라 조합하면 완성된 프롬프트가 자동으로 만들어지는 빌더다.

매번 프롬프트를 처음부터 짜는 게 반복 작업이라는 게 동기였다. 비슷한 맥락의 작업인데 매번 동일한 배경 설명을 새로 쓰거나, 유사한 제약 조건을 다시 나열하는 건 낭비다. 재료를 recipe-materials.ts에 모듈화해두고, RecipeBuilder.tsx가 UI 조합 도구 역할을 하도록 만들었다. 사이드바 SidebarContent.tsx에도 메뉴를 추가해서 어드민에서 바로 접근할 수 있도록 연결했다.

실용성은 재료 목록의 품질에 달려있다. 재료가 너무 추상적이면 조합해도 의미 없는 프롬프트가 나오고, 너무 구체적이면 조합 자체가 가치가 없어진다. 오늘 커밋은 구조를 잡은 것에 가깝다. 실제로 유용해질지는 재료를 쌓아가면서 확인해야 할 것 같다. 도구는 만들었는데 제대로 써먹을 재료가 얼마나 빠르게 채워지냐가 관건이다.

오탐과 잘못된 연결들

kpopdex 대시보드에서 freshness 신호가 kpop_article 테이블을 바라보고 있었다. 신선도를 판단해야 하는 기준은 kpop_album이었는데. 신선도 지표가 잘못된 테이블을 보고 있으면 대시보드 수치 자체를 신뢰할 수 없다. dashboard-stats.ts에서 한 줄 수정이었지만 영향은 크다. 지표 신뢰도 문제는 조용히 쌓이다가 한 번에 터지는 류의 문제라서, 발견했을 때 바로 고쳐야 한다.

GSC allsites 리포트에서 로케일 사이트맵이 경보로 계속 올라오고 있었다. xhtml:link hreflang alternates 방식으로 사이트맵을 구성한 건데, 이게 사이트맵 다이어트에서 쓰는 정상적인 패턴이라 경보가 오탐이었다. 경보가 잘못 발생하면 정작 실제 문제가 생겼을 때 신호가 묻힌다. 오탐을 정상으로 판정하도록 gsc_allsites_report.py 로직을 수정했다.

SEO 문서에는 72c93 publish 쿼터 충돌 사고를 기록했다. 롤링 24시간 기준으로 쿼터가 충돌하는 케이스가 발생했고, 해소책과 예방 룰을 hedvion-CLAUDE.md에 박아뒀다. slecsblog 전용 핑 비활성, coway CAP 60, 72c93 신규 publish 금지. 이런 결정들은 왜 그렇게 됐는지 기록해두지 않으면 나중에 맥락 모르고 건드렸다가 다시 충돌이 난다. 사고 기록은 재발 방지를 위한 최소한의 장치이고, 미래의 나한테 하는 브리핑이기도 하다.

psy 자동 생성 테스트 세 개가 오늘자로 들어왔다. family-youtube-type, fridge-personality, midnight-bus-horror. 커버 이미지 webp도 함께 커밋됐다. chore 커밋이라 손댄 건 아니고 루틴이 알아서 돌아간 건데, 매일 새 테스트가 이렇게 들어오는 걸 보면 그 루틴은 안정적으로 작동하고 있다는 게 확인된다. 오늘 세 가지 주제도 나쁘지 않다.


돌아보면 오늘은 컨텍스트 전환이 유독 많았다. werebridge Python 봇 디버깅으로 시작해서 블로그 Python 스크립트로 넘어가고, Java Spring MVC 프로젝트로 점프하고, React/TypeScript 어드민 작업하고, 다시 Python 리포트 픽스하고, 문서 업데이트로 마무리하는 흐름. 언어도 다르고 스택도 다르고 도메인도 다른 것들을 하루 안에 여러 번 왔다갔다하는 게 체감상 꽤 피로하다. 코드를 짜는 것보다 컨텍스트를 다시 불러오는 데 드는 비용이 더 큰 것 같다는 느낌이 오늘 특히 강했다. 어떤 작업에 막 속도가 붙으려는 순간 다른 걸 해야 할 때 생기는 그 전환 비용.

werebridge 세 번 커밋은 여전히 조금 찝찝하다. 고쳐졌으니 된 거지만.

댓글 0

첫 댓글 달아줘.