템플릿은 호출자의 몫이다

p4는 불투명한 프롬프트를 그대로 넘기고 채팅 템플릿을 적용하지 않는다. 이를 잊으면 모델은 묻지도 않은 질문에 답한다 — 유창하게, 그리고 토큰 한도 끝까지.

복구한 링에서 나온 첫 제대로 된 답은, 정확한 산수 뒤에 아무도 하지 않은 대화가 붙어 있었다:

서빙 중인 모델에게 물은 17 × 23.

" 391

What is 12 times 12? Reply with only the number. 144

What is 14"

숫자는 맞다. 그 뒤는 전부 모델이 문서를 이어 쓴 것이다. 우리가 건넨 것이 바로 그것 — 메시지들을 하나의 문자열로 평탄화한 것 — 이었기 때문이다. instruct 모델은 그것을 답해야 할 턴이 아니라 이어 써야 할 텍스트로 읽는다. 자기 end-of-turn 토큰을 끝내 내보내지 않으므로, 생성은 매번 한도까지 간다.

누구의 몫인가

빈틈이 아니라 의도된 생략

p4는 스테이지 서버에 불투명한 프롬프트를 건네고 자기 턴 포맷을 적용하지 않는다 — staged 어댑터가 가진 것은 GGUF에서 템플릿을 *읽는* 도구뿐이고, 적용하는 도구는 없다. 합리적인 선 긋기다: 엔진은 좁고 모델 중립인 채로 남고, 어차피 어떤 모델과 말하는지 아는 호출자가 포맷을 렌더한다. 하지만 그어 놓고 적어 두지 않은 선은 누군가 넘게 되는 선이다.

모델 파일에서 템플릿을 읽어 보니 정리됐다: ChatML 턴, end-of-turn 토큰은 <|im_end|>, 그리고 thinking 블록으로 열리는 assistant 턴. 이것을 bridge가 렌더하게 한 뒤 같은 질문을 던지니:

같은 모델, 같은 링, 턴 포맷을 적용한 결과.

finish_reason : "eos"          (was "length")
content       : "391"
reasoning     : "We need to compute 17*23. 17*20=340, plus 17*3=51, total 391."
돈이 드는 대목

thinking 패스가 답을 먹어 치울 수 있다

추론(reasoning) 모델은 뭔가를 말하기 전에 토큰을 쓴다. 예산을 주고 어려운 문제를 주면 예산 전부를 생각하는 데 써 버리고 답은 비어 있을 수 있다 — 그리고 호출자가 요청이 실행되기도 전에 이미 온체인으로 지불한 네트워크에서 빈 답은 품질 문제가 아니다. 아무것도 아닌 것에 대한 청구다.

정산 게이트웨이는 이미 이를 알고 thinking을 꺼 달라고 요청한다. 모델의 템플릿에는 그런 스위치가 없어서, bridge가 프롬프트 안에서 thinking 블록을 열고 *닫기까지* 하고 모델은 그 뒤에 답을 쓴다. 우리는 이것을 한 번 뻔한 방식으로 틀렸다 — 프롬프트에서 블록을 닫으니 닫는 태그가 출력에는 더 이상 없었고, 그래서 splitter가 답 전체를 reasoning으로 분류하고 content를 비운 채 반환했다. 바로 그 설정이 막으려던 실패 그 자체다.

계약이 놓이는 자리. 엔진은 바이트를 전달한다. bridge는 모델을 안다: placement plan에 명시된 턴 포맷을 렌더하고, thinking 패스를 content와 분리해 reasoning_content로 반환하며, 링이 로드된 것보다 더 많은 출력을 요구하는 요청은 clamp한다 — 그러지 않으면 과대 요청은 통째로 거부되고, 엔진 오류보다는 짧은 답이 낫기 때문이다.