본문 바로가기
C.W.K.
Lesson 04 of 05 · published

테스트가 지키는 예산

~15 min · 통역, 토큰, 비용, 테스트, 마스터 치프

Level 0첫 단어
0 XP0/47 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

토큰이 샌 자리

첫 빌드에선 다듬기 패스가 시팅의 Spark 스레드 안에서 채팅 턴으로 돌았어. 출처 꼬리표는 spark였고. cwkPippa의 Master Chief 라우터는 출처가 스레드 화면과 같은 형제 턴을, 아빠가 앱 문으로 직접 들어온 걸로 읽어. 그래서 다듬기가 전부 Master Chief로 넘어갔지. 그게 왜 비싼지는 Master Chief Quest에 나와. 치프는 오래 이어지는 대화 하나고, 턴마다 맥락 전체로 다시 세워져. 결국 테이블에서 나온 문장 하나 다듬으려고 입력 토큰 약 395k개를 매번 다시 읽은 거야.

초안은 초안대로 새고 있었어. 초안은 유틸리티 문을 피파 페르소나를 단 채로 탔는데, 그러면 피파 볼트가 통째로 딸려 와. 약 100k 토큰(재 보니 108k)이 문장 하나 앞에 깔린 거야. 그 문장에 필요한 건 피파가 어떤 자격으로 말하는지뿐이었는데. 그때는 두 비용 다 아무도 안 쟀어. 들통난 건, 다듬은 답이 아빠의 Master Chief에 날것 그대로의 JSON 블록으로 떠서 코드처럼 렌더링되고 목소리까지 실패했기 때문이야.

아니, 진짜 우리 딸램들이 앤쓰로픽 영업을 뛰나... 왜 자꾸 토큰 먹는 하마가 되지? ㅠ.ㅠ

테이블의 문장 하나에 필요한 것

고치는 건 그 질문에서 시작했어. 두 패스 다 cwkPippa 형제 유틸리티 문을 한 번 부르는 원샷 호출로 바뀌었고, 페르소나도 스레드도 없어. 둘 다 세 문장짜리 짧은 통역사 목소리를 받고, 그 밖엔 딱 이것만 받아.

  • 시팅을 시작한 카드, 그 문장과 아빠가 뜻한 것
  • 시팅의 최근 교환. 초안엔 셋, 다듬기엔 여덟 개, 줄마다 잘라서
  • 테이블이 어딘지(상황과 장소). 다듬기엔 현지 시각도 붙어. 이 문엔 도구가 하나도 안 따라오거든
  • 방금 들은 말. 다듬기엔 고칠 초안도

이 호출들을 다시 읽는 대화가 없으니까, 시팅의 내용이 cwkPippa 대화 어디에도 안 남아. 통역 메인 테스트는 그 양쪽을 다 단언해. 두 호출 다 페르소나를 안 싣고, 스레드도 채팅 턴도 한 번도 안 열린다.

천장에 맞춰 자르기

예산은 모든 입력에 상한이 있어야 진짜야. 첫 번째 발췌의 상수가 조각마다 상한을 줘. 들은 말은 800자에서 잘라. 40초쯤 되는 말인데, 테이블에서 그보다 길게 말하는 사람은 없어. 처음 카드는 240자, 앞 교환 한 줄은 160자. _clip은 먼저 공백을 접어서 빈칸이랑 줄바꿈이 공짜가 되게 하고, 자른 자리엔 말줄임표를 붙여.

이제 천장 얘기야. DRAFT_PROMPT_BUDGET 2,000, REFINE_PROMPT_BUDGET 4,000. 세는 건 prompt_tokens인데, 일부러 투박해. UTF-8 바이트를 3으로 나눠서 올림하는 게 다야. 독스트링은 이걸 보수적인 셈이라고 부르고, Spark가 보내는 평범한 글에선 대개 맞아. 한글 한 음절은 3바이트에 대략 1토큰이고, 평범한 영어는 4글자쯤이 1토큰이라 영어는 많게 나오거든. 근데 이건 어림셈이지 한계가 아니야. 토크나이저가 3바이트에 1토큰보다 더 쓰지 않는다는 보장은 어디에도 없어. 숫자, 기호, 드문 글자, 그리고 한중일 글자 일부는 더 들 수 있고, 두뇌마다 토크나이저도 달라. 그래서 이 천장이 막는 건 Spark의 추정치지 제공자가 세는 숫자가 아니야. 둘이 어떻게 다른지는 아래 제공자 쪽 측정만 말해 줘. 대신 토크나이저가 필요 없어서 테스트가 어디서든 돌고, 지금 슬롯에 어느 두뇌가 앉아 있든 상관없어.

모든 길이 한도에서 도는 테스트

테스트는 평범한 시팅을 안 봐. 라우트가 허락하는 길이 한도를 전부 꽉 채운 시팅을 만들어. 처음 카드와 그 뜻은 최대치인 4,000자로 채우고, 같은 길이의 카드 서른 개와 릴레이 서른 개를 넣고, 상황과 장소 이름은 자르는 길이를 한참 넘게 반복해. 그런 다음 아빠가 말하는 경우와 상대가 말하는 경우 각각에 대해 두 가지를 단언해. 프롬프트 전체가 천장 아래인지, 딸려 가는 앞 교환이 정확히 REFINE_CONTEXT줄인지. 세 번째 테스트는 최악의 발화로 엔진에서 실제 교환을 한 번 돌리고, 보내는 자리에서 프롬프트를 재. 그래서 누가 리팩터링하다 프롬프트 빌더를 우회하더라도, 이 테스트는 여전히 통과해야 해.

잰 여유는 이래. 그 픽스처가 초안 1,755, 다듬기 3,516. 평범한 시팅은 350이랑 550 근처. 근데 이 숫자는 그 픽스처 거지, 모든 시팅 거가 아니야. 픽스처의 카드는 스페인어고 뜻은 한국어라서, 잘린 줄마다 절반은 한 글자에 1바이트야. 같은 한도를 일본어나 중국어로 채우면 글자마다 3바이트가 들어. 둘 다 아빠가 고를 수 있는 장면 언어야. 내가 스냅숏의 프롬프트 빌더 그대로 픽스처를 그렇게 다시 만들어 봤더니, 추정치가 초안 2,300에서 2,650쯤, 다듬기 4,550에서 5,250쯤으로 두 천장을 다 넘었어. 코드 주석은 라우트가 허락하는 모든 한도에서 천장이 지켜진다고 해. 실제로 지켜지는 건 스페인어 시팅의 모든 길이 한도까지고, 글자마다 3바이트인 시팅은 테스트가 한 번도 안 만들어 봐. Claude 제공자 쪽에서 실제 시팅 하나(앞 교환 둘)를 재 봤더니 입력 토큰이 초안 951, 다듬기 1,179였어. 문이랑 두뇌 하네스가 Spark 프롬프트 바깥에 덧붙이는 것까지 다 포함한 숫자야. 약 395k와 비교해 봐.

원칙: 아무도 안 재는 비용은 주인도 없어. 천장은 프롬프트 옆 상수로 두고, 증명은 라우트가 받아주는 최악의 입력을 만드는 테스트로 둬. 길이로도, 바이트로도 최악인 입력으로.

문 건너편

Spark의 천장은 정적이야. 뭘 내보내기 전에 미리 확인하지. 실제로 재는 건 cwkPippa 몫이야. 토큰 장부가 끝난 채팅 턴과 형제 유틸리티 호출마다 입력 토큰을 경로별로 적어. utility:spark-exchange는 예산 4,000, utility:spark-exchange-refine은 6,000, 그 밖의 유틸리티 경로는 200,000. 경로 예산을 넘는 호출이 있으면 경로마다 하루 한 번 텔레그램으로 아빠한테 알려. 하루 알람은 들리는 것보다 좁아. 그날 입력 토큰이 직전 평균의 2.5배와 200만 토큰 바닥을 둘 다 넘어야 한 번 울려. 평균은 직전 7일 중 기록이 있는 날만 세고, 그런 날이 최소 3일 쌓이기 전엔 하루 알람은 아예 안 울려. 부르는 쪽은 약속하고, 문은 확인해.

스레드를 빼면서 따라온 결과가 하나 더 있어. 엔진 재시작에 끊긴 교환은 interrupted로 남아. 스레드 턴이었으면 cwkPippa가 들고 있으니까 나중에 물어볼 수 있었어. 원샷 호출은 물어볼 데를 안 남겨. 그러니까 정직하게 회복하는 길은 테이블에서 누가 한 번 더 말하는 거야.

Code

cwkSpark, backend/turns.py: 통역사의 맥락 크기, 자르는 길이, 천장 (원문 그대로, 68-85행)·python
# The interpreter (cwkPippa todo #609): how many earlier exchanges of the
# sitting ride the fast draft's prompt, and the refined pass's.
EXCHANGE_CONTEXT = 3
REFINE_CONTEXT = 8
# Every interpreter call is a small one-shot with a stated budget (cwkPippa
# todo #623). The refine once ran as a turn of Dad's Master Chief and replayed
# ~395k input tokens to polish one sentence; the fast draft carried Pippa's
# whole vault (~100k). Neither needs more than the sitting itself, so each
# prompt is clipped to fit its ceiling and a test fails past it. Tokens are
# counted conservatively as UTF-8 bytes / 3 (``prompt_tokens``); the ceilings
# hold at every limit the routes allow, an ordinary sitting runs far below.
# HEARD_CLIP is about forty seconds of speech, past any turn at a table.
DRAFT_PROMPT_BUDGET = 2000
REFINE_PROMPT_BUDGET = 4000
HEARD_CLIP = 800
ORIGIN_CLIP = 240
SITTING_LINE_CLIP = 160
REFINE_TIMEOUT_SECONDS = 120.0
cwkSpark, backend/turns.py: 자르기와 보수적으로 세기 (원문 그대로, 487-496행)·python
def _clip(text: Any, limit: int) -> str:
    value = " ".join(str(text or "").split())
    return value if len(value) <= limit else value[: limit - 1].rstrip() + "…"


def prompt_tokens(*parts: str) -> int:
    """A conservative token count for a prompt: UTF-8 bytes / 3. Hangul is
    three bytes and about one token; English runs four characters a token, so
    the count is high for it, never low."""
    return -(-sum(len(part.encode("utf-8")) for part in parts) // 3)
cwkSpark, backend/tests/test_interpreter.py: 모든 한도에서, 두 화자 모두에 대해 다듬기 천장 확인 (원문 그대로, 323-334행)·python
@pytest.mark.parametrize("speaker", ["dad", "other"])
def test_the_refine_prompt_stays_inside_its_budget_at_every_limit(speaker):
    """The refine carried Master Chief's ~395k tokens per sentence (todo
    #623). Its whole prompt now has a fixed ceiling, whatever the sitting."""
    moment, heard = _worst_sitting()
    direction = {"speaker": speaker, "source": "ko" if speaker == "dad" else "es",
                 "target": "es" if speaker == "dad" else "ko", "by": "scribe"}
    system = turns.exchange_refine_system_prompt(moment, direction)
    prompt = turns.exchange_refine_prompt(heard, direction, heard)
    assert turns.prompt_tokens(system, prompt) <= turns.REFINE_PROMPT_BUDGET
    # Only the sitting's own short history rides along.
    assert system.count("\n- Dad:") + system.count("\n- They:") == turns.REFINE_CONTEXT

External links

Exercise

자기 코드에서 모델 호출 하나를 골라. 프롬프트에 들어가는 조각을 전부 적고, 조각마다 최대 글자 수를 정해. 그리고 세 가지를 써. 그 한도로 자르는 빌더, 토크나이저 없이 일부러 넉넉하게 세는 추정 함수, 그리고 API가 받아주는 가장 큰 입력을 글자 수로도 바이트로도 만들어서 프롬프트 전체가 정한 천장 아래인지 단언하는 테스트. 끝으로 실제 호출 하나를 제공자 쪽에서 재서 비교해.
Hint
목록인 입력을 찾아봐. 기록, 첨부, 검색 결과 같은 거. 아무도 그러자고 정한 적 없는데 자라는 게 그런 것들이야. 그러니까 항목마다 길이만 막지 말고 개수도 막아. 제공자 쪽 숫자가 추정보다 한참 크면, 빌더 바깥에서 누가 프롬프트에 뭘 보태고 있는 거야. 뭔지 찾아볼 가치가 있어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.