번호를 손으로 매기다가
이 글을 쓰기 전까지 블로그에는 글이 열다섯 편 있었고, 그중 열세 편이 연재 두 개에 들어 있습니다. Google Cloud Next '26을 다시 읽는 열 편과, 이 검색창을 만든 기록 세 편입니다.
연재 소속은 글 맨 위에 직접 적습니다. series: {id: next26, order: 4} 같은 한 줄입니다. 연재 이름은 따로 series.yaml에 있습니다.
새 글을 쓸 때마다 같은 질문 세 개가 생깁니다. 이 글은 어느 연재일까. 몇 번째일까. 중간에 끼는 글이라면 뒤 편 번호를 전부 한 칸씩 밀어야 하는데, 그걸 빠뜨리지 않을까.
번호가 틀리면 독자 눈에 그대로 보입니다. 글 끝의 '이어서 읽을 글'이 이 순서를 따라가기 때문입니다.
규칙부터 시도했습니다
LLM을 붙이기 전에 규칙으로 얼마나 되는지부터 봤습니다. 제목에 "3편" 같은 표기가 있으면 되겠다고 생각했습니다.
열다섯 편 제목을 훑어봤습니다. 번호가 있는 제목은 하나도 없었습니다. 번호는 파일 이름에만 있었습니다.
그다음으로 group을 떠올렸습니다. Next '26 묶음에 속한 글은 연재겠지 하고요. 그런데 이 묶음에는 열두 편이 있고 그중 두 편은 연재가 아닙니다. Cloud Run 배치 글과 예시 글은 같은 행사 이야기라 묶였을 뿐, 순서가 있는 이야기는 아닙니다.
같은 행사 묶음이라는 사실만으로는 연재라고 말할 수 없었습니다. 연재는 읽는 순서가 있는 이야기이고, 이건 글 내용을 읽어야 알 수 있습니다.
후보를 좁히는 데까지만 규칙으로
그래도 규칙이 쓸모없지는 않았습니다. 새 글이 어느 연재와 가까운지 좁히는 데는 충분했습니다. 이 단계에는 LLM을 부르지 않습니다.
연재와 글 사이의 가까움을 다섯 가지 신호로 잽니다.
| 신호 | 가중치 | 보는 것 |
|---|---|---|
| 온톨로지 겹침 | 0.45 | 글이 다룬 개념·개체 벡터와 연재 평균 벡터의 코사인 유사도 |
| 제목·소제목 용어 | 0.20 | 두 쪽 용어 집합의 겹침 비율 |
| 발행일 근접도 | 0.15 | 가장 가까운 편과의 날짜 차이, 1 ÷ (1 + 일수 ÷ 3) |
| 같은 group | 0.10 | 연재 안에 같은 group의 글이 있는지 |
| 본문 링크 | 0.10 | 서로를 링크했는지 |
점수가 0.12보다 낮으면 후보에서 뺍니다. 후보는 최대 세 개만 LLM에게 넘깁니다. 후보가 하나도 없으면 LLM을 부르지 않고 "연재 아님"으로 끝납니다.
이 단계만 따로 쟀습니다. 연재의 각 편을 하나씩 빼고 후보를 다시 뽑게 했더니, 열세 편 모두 자기 연재가 후보에 있었고 1순위도 모두 맞았습니다.
여기에는 단서를 달아야 합니다. 두 연재는 날짜도 group도 주제도 확연히 다릅니다. 쉬운 시험이었습니다.
더 중요한 건 연재가 아닌 두 글입니다. 이 둘도 1순위 후보가 Next '26 연재였습니다. 같은 group이고 날짜도 가깝기 때문입니다. 규칙은 "가깝다"고는 말하지만 "아니다"라고는 말하지 못합니다.
LLM에게는 닫힌 선택지만 줍니다
"아니다"를 판단하는 일은 LLM에게 맡겼습니다. 다만 자유롭게 답하게 하지는 않았습니다.
프롬프트에는 후보 연재의 이름과 편 목록, 새 글의 제목·설명·소제목·본문 앞부분(2,500자 이내)을 넣습니다. 그리고 이렇게 답하게 합니다.
decision: 이어진다(join) 또는 아니다(none).series_id: 후보로 준 id 중 하나.after: 이 글이 바로 뒤에 놓일 편의 번호. 0이면 1편 앞입니다.evidence: 두 글에 모두 실제로 나온 표현 1~3개.reason: 한 문장.
프롬프트에는 같은 제품이 나온다는 것만으로는 부족하고, 확신이 없으면 none이라고 적었습니다. 틀리게 묶는 쪽이 빠뜨리는 쪽보다 나쁘다는 기준입니다.
답이 오면 코드가 값을 검사합니다. 후보 밖의 id는 버립니다. after가 범위를 벗어나도 버립니다. evidence에 적은 표현이 새 글과 연재 글 양쪽에 실제로 없어도 버립니다. 검사에 걸리면 걸린 이유를 붙여 다시 묻고, 세 번까지 받습니다.
Upstage 문서도 같은 이야기를 합니다. JSON 모드는 출력이 JSON으로 파싱된다는 것만 보장하고, 스키마를 지킨 응답이어도 값을 신뢰할 수 있다는 뜻은 아니라고 적혀 있습니다. 이 도구는 JSON 모드로 답을 받습니다. 모델이 형식을 지키는 것과 내용이 맞는 것은 다른 문제라서, 값 검사는 코드가 맡아야 했습니다.
판정이 join으로 나와도 바로 믿지 않습니다. 같은 모델에게 "이 글을 그 편 바로 뒤에 놓아도 되는가"를 한 번 더 묻고, 두 번 다 같은 답이 나올 때만 제안으로 남깁니다.
글을 보내기 전에는 보안 가드레일부터 통과시킵니다. 사내 IP나 토큰처럼 보이는 표현이 본문에 있으면 LLM을 부르지 않고 "보안 검사에 걸림"으로 끝냅니다. 외부 API로 나가는 글은 발행할 글이지만, 아직 공개 전인 초안일 수 있기 때문입니다.
첫 평가는 13편 중 5편이었습니다
만든 걸 믿으려면 시험이 필요했습니다. 검색을 고칠 때 쓴 방식과 같습니다. 이번에는 빼기 시험을 했습니다.
연재의 각 편을 하나씩 소속 정보만 지운 복사본으로 만들고, 원래 연재와 원래 위치로 돌아오는지 봅니다. 연재가 아닌 두 글은 "해당 없음"이 나와야 합니다. 15편 전부가 시험 문제인 셈입니다.
첫 결과는 이랬습니다.
| 지표 | 첫 시도 | 고친 뒤 |
|---|---|---|
| 원래 연재로 합류 (13편) | 5편 | 12편 |
| 위치까지 정확히 일치 | 4편 | 4~5편 (앞뒤 한 칸 허용 6~8편) |
| 연재 아닌 글을 해당 없음으로 | 2편 중 2편 | 두 번 모두 전부 |
연재가 아닌 글은 처음부터 둘 다 걸러냈습니다. 문제는 연재의 글을 연재라고 알아보지 못하는 쪽이었습니다. 보수적으로 만든 대가를 그대로 치른 셈입니다.
"합류 5편"이 이상해서 실패한 답을 직접 찍어 봤습니다. 원인이 둘이었습니다.
첫째는 시험 방식에서 왔습니다. 한 편을 빼면 번호에 구멍이 생깁니다. 3편짜리 연재에서 2편을 빼면 남은 글이 1편과 3편이 됩니다. 프롬프트에도 "1편, 3편"이라고 적혔고, 모델은 그 번호를 믿고 after: 3이라고 답했습니다. 허용 범위는 0에서 2입니다. 맞는 연재를 골라 놓고도 위치 검사에서 떨어진 것입니다.
프롬프트에 보이는 번호를 순서값이 아니라 1부터 이어지는 위치로 바꿨습니다. 원인은 단순했습니다.
둘째는 재확인 질문이 너무 깐깐했습니다. "다음 편으로 자연스럽게 이어지는가"라고 물었더니, Next '26처럼 서로 다른 주제를 한 행사의 틀로 묶은 연재의 글이 줄줄이 탈락했습니다. 이 열 편은 이야기가 이어지는 구조가 아닙니다. 같은 틀 안의 병렬 주제입니다.
질문을 "같은 틀의 한 편으로 읽히고 이 자리가 앞뒤와 크게 어긋나지 않는가"로 바꿨습니다. 같은 제품 이름만 나오고 다른 틀의 글이면 여전히 "no"입니다.
고친 뒤에는 열세 편 중 열두 편이 원래 연재로 돌아왔습니다. 남은 한 편은 Cross-Cloud Lakehouse 글입니다. 처음에는 어느 단계에서 떨어졌는지 알 수 없어서, 탈락 이유를 평가 출력에 남기도록 고쳤습니다. 다시 돌려 보니 이유는 '응답 규칙 위반'이었습니다. 세 번 다시 물어도 검사를 통과하는 답이 나오지 않았다는 뜻입니다.
표 아래 숫자의 범위는 같은 평가를 두 번 돌린 결과입니다. 두 번째에는 이 글도 연재가 아닌 글로 들어가 세 편이 되었고, 세 편 모두 해당 없음이었습니다.
위치는 사실 맞힐 수 없는 문제였습니다
합류는 12편으로 올라갔는데 위치는 4~5편에 머물렀습니다. 같은 평가를 두 번 돌렸더니 정확히 맞는 편 수가 5편, 4편으로 흔들렸습니다. 이건 모델이 못해서만은 아니라고 봅니다.
Next '26 열 편은 발행일이 모두 10월 31일입니다. 병렬 주제라서 개요를 첫 편에 두는 것 말고는 순서를 정할 근거가 글 안에 없습니다. 지금 순서는 제가 정한 순서입니다. 같은 글을 두고 두 사람이 다른 번호를 매겨도 틀렸다고 하기 어렵습니다.
그래서 위치는 제안이지 정답이 아니라고 취급하기로 했습니다. 도구는 위치를 제안하되 사람이 승인하기 전에 고칠 수 있게 두었습니다. 서사가 이어지는 쪽에서는 위치가 더 잘 맞았습니다. 두 번째 평가에서 앞뒤 한 칸까지 허용하면 3편짜리 블로그 검색 연재는 세 편 모두, 열 편짜리 Next '26 연재는 다섯 편이 맞았습니다.
한 가지 더 챙겼습니다. Upstage 문서에 따르면 solar-pro4는 별칭이고, 지금은 solar-pro4-260806이라는 버전을 가리킵니다. 별칭이 새 버전으로 옮겨 가면 동작이 조금 달라질 수 있다고 문서가 적고 있습니다. 어느 날 판정이 조용히 달라져도 알아채기 어렵습니다. 그래서 평가를 명령 하나(make series-eval)로 묶어 두었습니다. 모델 이름이 바뀌거나 의심스러울 때 다시 돌려 보면 됩니다.
로컬 모델로도 돌려 봤습니다
외부 API로 글이 나가는 게 마음에 걸려서, 같은 평가를 맥미니의 로컬 EXAONE 3.5 7.8B로도 돌려 봤습니다. 프롬프트와 검사는 그대로입니다.
| 지표 | Solar Pro 4 | 로컬 EXAONE 7.8B |
|---|---|---|
| 원래 연재로 합류 (13편) | 12편 | 10편 |
| 위치까지 정확히 일치 | 4~5편 | 3편 (앞뒤 한 칸 허용 5편) |
| 연재 아닌 글을 해당 없음으로 | 전부 (2/2, 3/3) | 3편 중 1편 |
숫자만 보면 합류가 10편이라 나쁘지 않아 보입니다. 안쪽을 보면 다릅니다. Next '26 열 편은 전부 합류시켰고, 블로그 검색 세 편은 전부 '응답 규칙 위반'으로 탈락했습니다. 세 번 다시 물어도 근거 표현 검사를 통과하는 답을 내지 못한 겁니다.
더 걸리는 건 연재가 아닌 글 쪽입니다. 두 편을 연재에 넣어 버렸습니다. Cloud Run 배치 글은 Next '26 연재의 3편 뒤에, 지금 쓰고 있는 이 글은 블로그 검색 연재의 3편 뒤(맨 끝)에 놓겠다고 했습니다. 이 도구가 가장 피하려던 틀리게 묶는 일이 그대로 나왔습니다.
재확인 질문을 통과했다는 뜻이기도 합니다. 같은 모델에게 두 번 물으면 같은 착각을 두 번 하는 모양입니다. 검증을 다른 모델에게 맡기는 쪽도 생각해 볼 만한데, 이번에는 시험해 보지 않았습니다.
이 비교는 한 번 돌린 결과라서 흔들림은 모릅니다. 그래도 방향은 분명했습니다. 로컬 7.8B 모델은 후보를 고르는 규칙 단계까지는 필요 없고, 판정에 쓰기에는 너무 헐겁습니다. 판정은 Solar Pro 4로 두고, 글이 외부로 나가는 건 보안 검사와 승인 단계로 관리하기로 했습니다.
제안은 파일로 받고, 반영은 한 줄만 고칩니다
판정 결과는 글을 바로 고치지 않고 pages/series_proposals.yaml에 씁니다. 대기 상태(pending)로만 남습니다.
제안 파일 한 건의 모양
proposals:
- id: join:series-auto-llm-approval
kind: join
status: pending # approved로 바꾸면 반영 대상
slug: series-auto-llm-approval
series: blog-search
after: 3 # 3편 뒤에 놓는다는 제안. 직접 고쳐도 된다
evidence: [...] # 두 글 모두에 실제로 나온 표현
reason: ...
judged:
<slug>: {hash: ..., decision: none|join} # 본문이 그대로면 다시 부르지 않는 캐시
사람이 status를 approved로 바꾸고 publisher series --apply를 돌리면 반영될 내용이 먼저 보입니다. 이 명령은 아무것도 쓰지 않습니다. --write를 붙여야 실제로 씁니다.
반영 도구는 글의 series 한 줄만 건드립니다. 줄이 있으면 바꾸고, 없으면 머리말을 닫는 줄 바로 앞에 넣습니다. 본문과 다른 줄은 한 글자도 바꾸지 않습니다. YAML을 통째로 읽었다가 다시 쓰면 따옴표와 줄바꿈이 흩어지기 때문에, 이렇게 정규식으로 한 줄만 고치는 쪽이 안전했습니다.
쓰기 전에는 바뀔 글을 pages/legacy/series-backup/에 날짜별로 복사합니다. 이 프로젝트는 git을 쓰지 않습니다. 되돌릴 수단이 이 복사본뿐이라서, 백업을 먼저 만드는 순서는 양보하지 않았습니다.
중간에 끼워 넣는 경우를 위해 편 번호를 한꺼번에 다시 매기는 계산도 넣었습니다. 4편 뒤에 새 글이 들어오면 뒤 편이 모두 한 칸씩 밀립니다. 이 계산과 한 줄 수정, 백업은 LLM 없이 시험합니다.
연재 없는 글끼리는 새 연재로
이미 있는 연재에 붙이는 것 말고, 연재가 없는 글끼리 새 연재를 만들자고 제안하는 길도 있습니다. 연재가 없는 글 중 서로 점수가 높은 글을 2~4편씩 묶고, LLM에게 이야기가 이어지는지 묻습니다.
연재 이름은 LLM이 지어 줍니다. 그냥 두면 "완벽 가이드"나 "총정리" 같은 이름이 나오기 쉬워서, 이름은 8~30자이고 글 제목에 나온 용어를 반드시 포함해야 한다는 검사를 걸었습니다. id도 영문 소문자와 하이픈만 허용하고, 이미 있는 id와 겹치면 버립니다. 편이 하나뿐인 연재는 만들지 않습니다. 홈의 연재 영역도 두 편 이상일 때만 보이게 할 생각이라 규칙이 맞물립니다.
지금 데이터로는 새 연재 제안이 나오지 않았습니다. 연재 없는 글은 Cloud Run 배치 글과 예시 글 둘뿐이고, 둘의 판정은 "해당 없음"이었습니다. 이 기능은 글이 더 쌓여야 쓸 기회가 생깁니다.
새 글을 쓸 때의 순서
실제 흐름은 이렇게 정리했습니다.
- 글을 쓰고
publisher check로 보안·SEO·출처를 확인합니다. make enrich로 검색 보강과 임베딩을 갱신합니다.make series로 연재 판정을 돌립니다. 새로 판정이 필요한 글만 LLM을 부르고, 본문이 그대로인 글은 캐시를 씁니다.pages/series_proposals.yaml의pending항목을 읽고, 맞으면approved로 바꿉니다. 위치가 어색하면after를 직접 고칩니다.publisher series --apply로 바뀔 파일을 확인하고--write로 반영합니다.- 빌드합니다.
판정 한 번에 호출이 몇 번 나가는지는 세어 보지 않았습니다. 후보가 없으면 0번이고, 후보가 있으면 판정과 재확인이 최소 두 번, 검사에 걸리면 세 번까지 재시도가 붙습니다. 평가는 열여섯 편을 한꺼번에 판정하므로 그보다 훨씬 많이 나가서 자주 돌릴 일은 아닙니다. 모델 별칭이 바뀐 것 같을 때나 프롬프트를 고친 뒤에만 돌립니다.
이 글에 직접 써 봤습니다
이 글을 쓰고 나서 도구에 이 글을 직접 물어봤습니다. 규칙 단계의 1순위 후보는 블로그 검색 연재였습니다. 점수는 0.36입니다. 같은 Blog 묶음이고 발행일이 가까워서 group과 날짜 신호가 높았고, 온톨로지 겹침은 0.37이었습니다. Next '26 연재는 0.17로 한참 낮았습니다.
LLM의 판정은 "해당 없음"이었습니다. 재확인 단계에서 이어지지 않는다고 봤습니다. 제안은 하나도 쌓이지 않았고, 제안 파일에는 판정 캐시만 남았습니다.
이 판정이 맞다고 봅니다. 블로그 검색 연재는 검색창을 만든 이야기입니다. 이 글은 연재를 관리하는 도구 이야기입니다. 같은 블로그 작업이라는 것 말고는 이야기가 이어지지 않습니다. 규칙 점수만 보면 4편으로 넣기 쉬워 보이는데, 판단이 점수를 넘어서야 하는 자리를 도구가 잘 짚었습니다.
그래서 이 글은 연재 없이 혼자 둡니다. 연재가 아닌 글을 연재로 묶으라는 제안이 나오지 않는 것도 이 도구가 해야 할 일입니다.
써 보면서 남은 것
글이 열다섯 편이라 이 평가는 표본이 작습니다. 12/13이라는 숫자에 큰 의미를 두기는 어렵습니다. 그래도 실패 사례를 계속 쌓으면 검색 평가 세트처럼 쓸 만해질 것입니다. 위치는 지금 방식으로는 흔들리는 숫자라서, 글이 늘면 앞뒤 두 글과 한 쌍씩 비교해 자리를 정하는 방식으로 바꿀지 고민해 봐야 합니다.
바꿀 부분이 두 군데 있습니다. 하나는 '응답 규칙 위반'을 더 잘게 나누는 것입니다. 어떤 검사에서 몇 번 걸렸는지 남겨야 Lakehouse 글이 왜 빠지는지 알 수 있습니다. 다른 하나는 JSON 모드 대신 스키마를 강제하는 방식으로 옮기는 것입니다. 형식 오류는 줄겠지만, 문서가 말한 대로 값의 신뢰는 여전히 코드가 검사해야 합니다.
LLM에게 맡길 때 가장 먼저 정한 건 어디까지 맡기느냐였습니다. 연재에 넣을지 말지의 판단과 위치 제안은 맡겼습니다. 파일을 고치는 일은 맡기지 않았습니다. 제안이 틀려도 pending에서 멈추고, 승인과 반영은 사람 손을 거칩니다.
글이 수십 편으로 늘면 이 승인 단계가 번거로워질 수 있습니다. 그때는 제목에 번호가 있고 연재 id가 확실한 경우만 자동 반영하는 예외를 검토하면 됩니다. 지금은 틀린 연재가 독자에게 보이는 비용이 승인 한 번보다 훨씬 큽니다.