← 전체 글

코덱스 말고 그록 다회 호출을 택한 실험 기록

· 10 분 분량
한 번에 8,000자를 요구한 결과와 1,200자씩 여덟 번 나눠 요구한 결과를 나란히 놓은 도식 — 왼쪽은 2,000자에서 수렴해 최장 3,182자, 오른쪽은 조각 여덟 개를 조립해 8,995자
같은 모델입니다. 왼쪽은 한 번에 8,000자를 요구한 결과, 오른쪽은 1,200자씩 여덟 번 요구해 조립한 결과입니다.

블로그를 자동으로 만드는 파이프라인에서 키워드를 정한 다음 단계는 “누가 쓰나”입니다. 앞 단계 이야기는 키워드 발굴 글에 있습니다.

제가 담당하던 파이프라인은 이 자리에 두 엔진을 두고 열흘 동안 실험했습니다. 같은 글감으로 나란히 돌려 나온 글을 전부 읽어봤는데, 품질로는 코덱스가 이겼습니다. 그리고 열흘 뒤 코드에서 지운 쪽도 코덱스였습니다.

모델의 답변품질이 단순히 나빠서가 아닙니다. 일정 분량 이상은 아무리 지시해도 안 나오는 천장이 있었고, 그걸 넘은 방법이 모델 교체가 아니라 호출 쪼개기였습니다. 한 번에 8,000자를 요구하는 대신 1,200자씩 여덟 번 부르기로 하자 질문이 달라집니다. 어느 모델이 한 번에 잘 쓰는지가 아니라, 어느 모델이 여러 번 불려도 괜찮은지를 따져야 했습니다.


1. 분량 천장

1.1 막혀 있던 숫자

한국어로 발행된 글을 엔진별로 갈라 한 달치를 봤습니다.

엔진글 수p50가장 긴 글3,500자 이상
그록1,4851,7843,1820건
코덱스5912,7875,14011건

그록은 1,485건 중 3,500자를 넘긴 글이 하나도 없습니다. 프롬프트의 분량 지시를 2,000자 이상에서 3,500자 이상으로 올려봤는데 p50이 1,784자에서 1,938자로 갔습니다. 사실상 안 움직였습니다. 같은 프롬프트에서 코덱스는 2,787자에서 5,500자로 반응했습니다. 지시값을 8,000자까지 올려도 결과는 지시와 관련이 없었습니다. 모델은 자기가 몇 자를 썼는지 세지 못합니다.

문구를 다듬어서 될 문제가 아니라는 뜻이라, 이때 “모델을 바꿔야 하나”로 기울었습니다.

1.2 토큰 상한 탓도 아니었다

서버 설정에 막힌 걸 수도 있어서 토큰 상한과 제한시간을 두 배로 올려 3건을 다시 만들어봤습니다.

GROK_MAX_TOKENS 8192 → 16384 (한 번에 출력할 토큰 수)
GROK_TIMEOUT_MS 90000 → 180000 (생성 제한시간)

2,528자, 2,721자, 2,420자가 나왔습니다. 전체 중앙값이 1,900자대였으니 늘어난 것처럼 보였고, 처음엔 저도 그렇게 봤습니다.

그런데 이 세 글은 서로 다른 블로그의 글이고, 블로그마다 원래 나오던 길이가 다릅니다. 그래서 전체 평균이 아니라 그 블로그가 예전에 쓰던 길이와 비교했습니다.

이번 결과그 블로그가 원래 쓰던 길이그 블로그 최고 기록
9972,528자2,528자3,227자
9032,721자1,960자2,846자
10402,420자1,862자3,253자

997은 원래 쓰던 길이와 한 자도 다르지 않고, 나머지 둘도 그 블로그가 이미 써낸 적 있는 길이 안쪽입니다. 늘어난 게 아니라 원래 그만큼 쓰는 블로그였던 겁니다.

1.3 쪼개니 천장이 사라졌다

한 번에 8,000자를 요구하는 대신 한 번에 1,200자씩 여러 번 요구했습니다.

도입부 호출 TITLE / DESCRIPTION + 도입 400자
섹션 호출 × 7 좁은 목차 하나씩 — "이 소주제만 1,200자 이상"
│ 미달(900자 미만)이면 그 섹션만 1회 보강
FAQ + 마무리 호출 질문 5개 + 구어체 마무리
조립 → 게이트 통과 확인

8,995자가 나왔습니다. H2 8개, 게이트 통과, 14콜에 136초. 호출당 부담을 낮추니 2,000자에서 수렴하는 습성이 문제가 되지 않았습니다.

대신 시간이 약 3배가 됩니다. 이 교환이 성립하려면 호출을 늘리는 비용이 싸야 하는데, 두 엔진 다 정액 구독이라 콜을 늘려도 청구액이 늘지 않았습니다. 제약은 시간뿐이었습니다.

그래서 포커스가 바뀌었습니다. 어느 모델이 길게 쓰나가 아니라, (1) 길게 쓰는 모델로 갈 것인가 아니면 (2) 짧게 여러 번 부를 것인가. 두 갈래를 같은 조건에서 재봐야 했습니다.


2. 나란히 재보기

글감 3개를 두 엔진으로 각각 돌려 6편을 만들었습니다. 같은 분할 코드에서 레인만 바꿨습니다.

2.1 그 재료로 발행할 만한 글이 나오나

문장이 매끄러운지는 합격 기준이 아닙니다. 이 재료를 줬을 때 발행할 수 있는 글이 나오는지를 봐야 합니다. 6편에 들어간 출처는 657자에서 1,208자, 검색 결과에서 긁은 요약 수준이었습니다. 그 상태로 다 읽어봤고 한 편도 발행 못 합니다. 떨어지는 이유가 엔진마다 갈렸습니다.

그록코덱스
재료가 모자랄 때있는 걸 돌려막는다. 한 제품 가격이 5회, 브랜드 순위가 4회, 행이 하나뿐인 표소스 밖 일반 지식으로 살을 붙인다. 문장은 자연스럽다
그래서 나는 사고없는 용어(“와이프 프레임”), 도메인 오타, 기관명 지어냄같은 자료를 섹션마다 다른 기관명으로 부른다
재료와 무관한 것문단마다 면책과 헤지 문구를 붙인다

읽는 느낌만 보면 코덱스가 낫습니다. 그런데 떨어진 사유의 성질이 다릅니다. 그록은 줄 재료가 없어서 지어냈고, 코덱스는 재료와 상관없이 자기 습성으로 떨어졌습니다. 앞은 밖에서 고칠 수 있고 뒤는 아닙니다. 이 차이를 4장에서 시험합니다.

엔진과 무관한 결함도 한 겹 있었습니다. 맺음말이 여섯 편 완전히 같고, 모든 섹션에 표가 하나씩 박히고, “에 따르면” 인용 공식이 반복됩니다. 조립 템플릿에서 나오는 것이라 엔진을 바꿔도 남습니다.

2.2 속도 — 호출 하나가 3.4배

같은 6편에 더해, 글 5개를 동시에 시작해 34콜씩 재봤습니다.

항목그록코덱스
호출 하나 평균 실행8.26초27.85초
글 5개 전체 소요61.9초330.2초
처리량4.85글/분0.91글/분
CPU 최고 표본 (2코어 서버)48.57%103.14%

분량은 반대로 코덱스가 편당 1,000자 이상 길었습니다. H2 수와 FAQ는 양쪽이 똑같이 지켰습니다. 조립이 결정론이라 모델이 건드릴 수 없는 부분입니다.

중요한 건 첫 줄입니다. 호출 하나에 걸리는 8.26초 대 27.85초는 동시 처리 개수를 올려도 줄지 않습니다. 병렬화는 호출을 겹치게 할 뿐 하나를 빠르게 하지는 않으니까요. 그리고 글 하나를 8콜로 나누면 이 왕복을 여덟 번 치릅니다.

그럼 이 3.4배는 어디서 오는 걸까요. 부르는 방식을 열어보니 예상과 다른 게 나왔습니다.


3. 왜 코덱스는 느렸나

3.1 둘 다 구독, 둘 다 CLI, 둘 다 회전 토큰

하나는 API고 하나는 CLI인 게 아닙니다. 둘 다 구독이고, 둘 다 유료 API 키를 쓰지 않습니다. 그록 이미지 생성 함수의 주석이 그걸 못 박아뒀습니다.

worker/src/llm.ts
/** Grok(xAI) image generation through OIDC refresh_token auth.json; no API key path. */

구독 계정의 OIDC 토큰으로 부릅니다. 그록도 원래 CLI로 불렀고 그 경로가 지금도 폴백으로 남아 있습니다. 토큰이 회전하는 것도 똑같아서, 같은 계정으로 두 곳에서 로그인하면 한쪽이 갱신할 때마다 다른 쪽이 무효화됩니다.

코덱스에서는 이게 터졌습니다. 로컬 개발과 운영이 같은 계정을 쓰다가 글감 876개가 한 번에 전멸하고 실패율이 4%에서 53%로 뛰었습니다.

codex exec exit 1: Failed to refresh token: 401 Unauthorized
"Your refresh token has already been used to generate a new access token"

3.2 그록만 중계 계층으로 풀려 있었다

같은 문제인데 한쪽만 구조로 해결돼 있었습니다. 로그인 지점을 한 곳으로 모으는 중계 프록시입니다.

[워커] --POST /v1/chat/completions--> [중계 프록시] --인증 붙여--> [xAI]
인증을 모른다 로그인·토큰 갱신 전담

로그인 지점이 하나뿐이니 회전 충돌이 원천 차단되고, 입구가 OpenAI 호환이라 소비자 코드는 fetch 한 줄입니다. 그록은 워커 코드가 직접 요청을 만들어 한 번에 끝내니 새로 띄우는 프로세스가 없습니다. 코덱스는 codex라는 별도 프로그램이 요청을 만들고, 워커가 할 수 있는 건 그 프로그램을 실행하는 것뿐입니다. 그래서 호출마다 프로세스 생성, 샌드박스 초기화, 인증 파일 읽기가 붙고 재사용이 없습니다.

2.2에서 본 3.4배가 이 껍데기입니다.

3.3 코덱스에는 같은 수를 쓸 수 없었다

그럼 코덱스용 프록시를 세우면 되지 않나. 프록시가 그록의 호출을 fetch로 바꿔줄 수 있었던 건 xAI 쪽에 구독 자격증명으로 부를 수 있는 HTTP 엔드포인트가 있었기 때문입니다. 코덱스는 사정이 다릅니다. ChatGPT 구독으로 쓰는 경로에는 HTTP 입구가 없고, HTTP로 부르려면 API 키를 따로 결제해야 합니다. 그러니 프록시를 세워도 내놓을 수 있는 건 여전히 codex exec이고 프로세스 비용이 프록시 쪽으로 옮겨갈 뿐입니다. (실제로 세워본 건 아니라 이 대목은 추론입니다.)

동시성 상한을 올릴 때 내는 값도 갈립니다. 그록은 답을 기다리기만 하니 동시 5개에 CPU 48.57%인데, 코덱스는 프로그램을 띄우니 동시 3개에 103.14%입니다. 서버가 2코어라 더 올리려면 서버를 사야 합니다. 게다가 코덱스 실행은 구독 쿼터를 보호하려고 프로세스 전역 동시 1개로 묶여 있었고, 계정을 늘려 나누는 정석 해법은 3.1의 회전 토큰 때문에 그 자체가 사고 경로였습니다.

균형을 맞춰 적어두면 그록도 무한하지 않습니다. grokChat은 429 처리도 backoff도 없어서 동시 처리 수는 지금도 4이고 올린 적이 없습니다. 그록의 병렬 상한은 괜찮다기보다 아직 시험받지 않았습니다.


4. 그록의 약점은 재료와 코드로 메웠다

2.1에서 미룬 시험입니다. 그록의 결함이 정말 밖에서 고칠 수 있는 종류인지 확인해야 했습니다.

4.1 재료만 바꿔 다시 만들었다

같은 글감, 같은 경로에서 출처 주입만 추가해 6편을 재생성했습니다. 검색 스니펫 대신 출처 URL의 본문을 파싱해 넣었더니 출처가 657자에서 3,513자로, 1,208자에서 14,148자로 늘었습니다.

수혜는 그록이 다 받았습니다. 팩트가 3개에서 10개 이상으로 늘고, 같은 가격 반복이 5회에서 3회로 줄고, 행 하나짜리 표가 사라졌습니다. 뜬구름 조언이 평수별 가격표 같은 실데이터로 바뀌었고, 2.1에서 지적한 깨진 토큰은 전부 없어졌습니다.

오답까지 교정됐습니다. 출처 없이는 “ETF도 환차익 비과세”라고 틀리게 썼는데, 출처가 있으면 “ETF는 전체 수익 과세, 개별 채권만 비과세”로 맞게 구분합니다. 소스에 답이 있으면 지어내지 않습니다.

코덱스는 달랐습니다. 헤지와 면책 스팸이 그대로고, 같은 자료를 섹션마다 다른 기관명으로 부르는 문제도 재발했습니다. 원래 좋던 게 더 촘촘해진 정도입니다. 2.1의 예상이 맞았습니다.

4.2 프롬프트가 아니라 코드 대조로 막았다

“출처에 없는 숫자를 쓰지 마라”는 지시는 원래 프롬프트에 있었습니다. 그런데 결과를 코드로 대조하지 않았습니다. 검사가 없으면 지켜졌는지 알 방법이 없습니다.

가장 먼저 고친 건 출처를 자르던 부분입니다. 파싱으로 긴 본문을 가져와도 생성 직전에 출처당 400자로 다시 잘라 정보 밀도가 사라지고 있었습니다. 이걸 4,000자로 늘리고, 생성 프롬프트와 수치 검증이 같은 근거 블록을 보게 했습니다.

그다음 숫자를 코드가 대조하게 했습니다.

① 생성된 조각에서 단위 붙은 숫자를 전부 추출 "4,800만 원", "3년", "0.2%"
② 모델에게 준 출처 본문(근거 블록)과 대조
③ 출처에 없는 숫자가 하나라도 있으면 그 조각 실패
④ 그 조각만 1회 재생성 → 또 남으면 발행 안 보냄

④가 이 장치의 값입니다. 글 전체를 버리지 않고 걸린 조각만 다시 만듭니다.

이건 사실 검증이 아니라 문자열 대조입니다. 글자가 똑같은지만 보니, 자료의 월 단가로 연 합계를 계산하면 계산이 맞아도 걸립니다. 그래도 걸린 것 대부분은 진짜였습니다. 지적된 숫자 37개를 자료와 하나씩 맞춰보니 31개(84%)가 자료 어디에도 없었습니다. 한 글은 자료에 숫자가 92개 있는데도 700만원이라고 썼고, 자료엔 2,384만원·2,001만원이 있었습니다. 실거래가 표를 보면서 그 옆에 있을 법한 금액을 적습니다.

이사청소 첫 재생성 출처에 없는 비율 20건 검출
→ 조각 재시도 후에도 남음
→ 글 전체 차단
새 실행 근거 밖 수치 0건인 결과만 통과

2.1에서 지적한 템플릿 결함도 규칙으로 풀었습니다. 섹션마다 표를 강제하던 걸 없애 데이터 행이 2개 이상일 때만 쓰게 하고, 여섯 편이 똑같던 맺음말은 본문 템플릿에서 떼어 참고자료 뒤에 별도 조각으로 생성하며 목표 키워드를 금지했습니다.

지표이전이후
목표 키워드 반복18회12회
”에 따르면”11회0회
표 행 수26행7행
출처 밖 단위 수치0건

바꾼 건 문구가 아니라 막는 위치입니다. 그럴듯하게 쓰라는 템플릿에서, 출처 안의 사실만 쓰게 하고 코드가 결과를 검증하는 구조로 옮겼습니다. 그러자 코덱스가 잘하던 몫의 상당 부분이 모델이 아니라 파이프라인에서 나오게 됐습니다.


5. 떼어내고, 남긴 것

5.1 분할과 직렬 락은 곱해진다

분할 생성을 켜자 코덱스에서는 그게 족쇄로 작동했습니다.

perSection = clamp(600, 1200, budget / N) ← 상한 1,200 이 실제로 물린다
목차 N=4 기준 → 섹션 합계 3,200 + 도입·FAQ 800 ≈ 4,000자

코덱스는 단발로 이미 5,786자를 씁니다. 섹션당 상한이 오히려 천장으로 작동해 최소값이 5,328자에서 4,387자로 내려갔습니다. 분할은 단발 천장이 낮은 그록에는 정확한 처방이지만, 이미 길게 쓰는 코덱스에는 반대로 작동합니다.

더 큰 문제는 3.3의 락과 만나는 지점입니다. 글당 8콜이면 동시 1개짜리 락을 8번 잡는데, 락 대기가 그냥 await라 대기 중에도 슬롯을 반납하지 않습니다. 락과 아무 상관 없는 그록 글이 자리가 없어 시작조차 못 합니다. 20.2시간에 41건, 시간당 2건.

그래서 엔진 선택을 설정 파일 한 곳으로 모으고 코덱스를 지웠습니다. 순삭제 287줄. 이제 엔진을 떼는 일이 설정에서 이름을 지우는 것으로 끝납니다. 떼고 나서 첫 밤 배치는 174건을 착수해 실패율 11.5%, 시간 초과로 죽은 글 0건이었습니다.

5.2 이 구조의 전제는 출처다

재료를 늘려도 그록은 소스가 안 덮는 곳에서 여전히 지어냅니다. 다회 호출 전략은 출처가 있을 때만 성립합니다. 그래서 근거는 생성하지 않고 결제해서 사 옵니다. 그록은 출처에 없는 보수율을 기준처럼 제시하고 스스로 모순되는 표 셀을 쓰기도 합니다. 따라서, 검색 API로 SERP를 받고 출처 URL의 본문을 파싱해 저장합니다.

① 글감 수집 LLM 1콜
② 콘텐츠 계획 LLM 2-4콜 + 검색 API ← 여기가 가장 비싸다
③ 본문 생성 LLM 8-11콜. ② 가 모아둔 출처를 재사용한다

③이 싼 이유는 ②가 이미 낸 값을 다시 쓰기 때문입니다. 계획에 출처가 있으면 본문 단계는 검색 API를 안 불러서, 8콜에서 11콜로 나눠도 1건 17.2원입니다. 막아야 할 곳도 본문이 아니라 글감이었습니다.

5.3 이긴 칸이 양쪽에 흩어져 있다

열흘 동안 잰 것을 한 장으로 줄이면 이렇습니다.

그록코덱스
한 번에 쓰는 분량3,342 - 4,003자5,074 - 5,498자
호출 하나 평균 실행8.26초27.85초
처리량4.85글/분0.91글/분
부르는 방식HTTP fetch 1회codex exec 프로세스 실행
동시 실행워커 CPU 를 거의 안 쓴다전역 1개로 묶임. 동시 3개에 CPU 103%
재료를 늘리면크게 좋아진다. 오답도 교정거의 그대로
재료와 무관한 결함없음헤지·면책 문구, 출처명 불일치

굵은 칸이 양쪽에 흩어져 있습니다. 그래서 “어느 모델이 더 좋냐”에는 답이 없습니다. 기준을 정해야 답이 나옵니다.

한 가지 짚어둘 것은, 이 결론이 코덱스라는 모델이 아니라 코덱스를 구독으로 쓰는 형태에 붙어 있다는 점입니다. 넷째와 다섯째 줄은 CLI라서 생긴 값입니다. 종량제 API로 붙이면 호출이 HTTP 한 번이 되니 그 두 줄이 사라집니다.

그래서 이 열흘 동안 한 일은 더 잘 쓰는 모델을 고르는 일이 아니었습니다. LLM을 바꿔야만 얻어지는 몫이 얼마인지 먼저 재봤고, 글감 재료와 코드 대조로 얻어지는 몫이 그보다 컸습니다. 그래서 파이프라인을 그 쪽에 맞췄습니다.