예약된 일간 브리핑은 모형한테 하루를 조망해달라고 해. 선반이 뭘 실었는지, 열어본 기사 몇 개가 깊이 뭐라고 했는지, 소셜 네트워크 온도가 어땠는지. 절 셋, 코퍼스 섹션 셋.
어느 아침 브리핑이 지나가듯 말했어. 받은 자료에 온도 샘플이 안 들어와 있다고. 샘플은 수집됐었어. 통계는 네 개라고 했고. 코퍼스엔 없었지. 시스템 전체에서 이 결함을 처음 신고한 게 생성된 텍스트였어. 모니터링이 얼마짜리였는지 정확히 말해주는 대목이야.
딴 데서 자란 게 이걸 깨뜨렸어
조립은 평범했어. 헤드라인 섹션 짓고, 추출본, 그다음 샘플, 이어붙이고, 글자 수 상한으로 자르기. 독자한테 선반이 몇 개일 땐 그게 먹혔지. 그다음 선반이 대충 두 배가 됐고, 그 헤드라인만으로 상한 전체를 넘겼어.
샘플 수집 코드는 아무것도 안 바뀌었어. 아무것도 실패 안 했고. 마지막에 붙은 섹션이 그냥 끝에서 조용히 떨어져 나갔어. 마지막에 붙었다는 이유로. 그리고 마지막에 붙은 게 작고 유일한 것들이었지. 하필 덩치 큰 섹션이 본 행사처럼 느껴져서 먼저 쓰이니까. 꼬리 자르기는 코드 작성 순서로 우선순위를 준다는 거고, 그건 아무것도랑 상관관계가 없어.
먼저 예약하고, 남은 걸 채워
수리는 우선순위를 뒤집어. 작고 대체 불가능한 섹션을 짓고 재. 상한에서 빼. 남은 게 헤드라인 몫이고, 헤드라인이 그걸 명시적으로 채우다가 몫이 떨어지면 멈춰. 나중에 잘리는 게 아니라.
구별을 정확히 적을 값어치가 있어. 두 버전 다 상한 아래 코퍼스를 만들거든. 자르는 버전에선 상한이 어느 섹션이 존재할지 정해. 예산 버전에선 저자가 어느 섹션이 존재할지 정하고 상한은 덩치 큰 쪽이 폭을 얼마나 받을지 정하고. 같은 한도, 반대 의미야.
최후의 안전장치 자르기는 남기되, 표시해
절대적 잘라내기는 여전히 끝에 있어야 해. 예산에도 산수 버그가 있고, 딱딱한 컨텍스트 한계를 넘는 건 짧아진 코퍼스보다 나쁜 실패니까. 차이는 이제 정상 운영에선 거기 닿을 수 없어야 하고, 발동하면 문장 중간에서 끝나는 대신 눈에 보이는 표식을 붙인다는 거야. 스스로를 알리는 잘라내기는 진단이고, 조용한 건 이 강의 전체가 다루는 그 버그야.
맥락이 부족할 땐 배분이 설계 결정이니까, 결정으로 만들어. "그다음 맞게 자름"으로 끝나는 조립은 그 결정을 누군가 코드를 쓴 순서한테 위임한 거고, 제일 작고 제일 유일하고 제일 명시적으로 요구되는 섹션부터 먼저 실패할 거야.
Code
대체 불가능한 건 예약, 덩치 큰 건 예산, 최후의 자르기엔 표식·python
# BEFORE: append, then cut. The cap decides which sections exist.
corpus = header + shelves_section + extracts_section + samples_section
corpus = corpus[:CORPUS_MAX_CHARS] # <- extracts and samples die here
# AFTER: reserve the small, irreplaceable sections; the bulky one
# fills what remains, round-robin, and stops on its own.
def build_corpus(con) -> tuple[str, dict]:
stats = {"tabs": 0, "articles": 0, "extracts": 0, "samples": 0}
# Small, unique, and named by clauses of the instruction -> reserved.
samples_section = _section("X temperature", _sample_lines(con))
extracts_section = _section("Opened in depth", _extract_lines(con))
previous = _section("Yesterday's brief", _prior_brief(con))
reserved = (len(CORPUS_HEADER) + len(samples_section)
+ len(extracts_section) + len(previous))
shelf_budget = CORPUS_MAX_CHARS - reserved
# Bulky and repetitive -> gets the remainder, ROUND-ROBIN so that
# running out costs depth uniformly instead of erasing whole topics.
lines, spent, dropped = [], 0, 0
shelves = [list(s) for s in _shelf_headlines(con)]
for rank in range(max((len(s) for s in shelves), default=0)):
for shelf in shelves:
if rank >= len(shelf):
continue
cost = len(shelf[rank]) + 1
if spent + cost > shelf_budget:
dropped += 1
continue
lines.append(shelf[rank])
spent += cost
stats["articles"] += 1
stats["headlines_dropped"] = dropped # disclosed, not hidden
corpus = CORPUS_HEADER + "\n".join([*lines, extracts_section,
samples_section, previous])
if len(corpus) > CORPUS_MAX_CHARS: # belt and braces, and LABELLED
corpus = corpus[:CORPUS_MAX_CHARS] + "\n[corpus truncated]"
return corpus, stats