본문 바로가기
C.W.K.
Stream
Lesson 07 of 08 · published

목소리 턴의 수준은 아빠가 정해

~15 min · reasoning-effort, voice-level, settings, multi-brain

Level 0음소거
0 XP0/57 lessons0/14 achievements
0/100 XP to next level100 XP to go0% complete
"하드 코딩을 해서 무조건 Low로 할 게 아니라 아빠가 설정해서 바꿀 수 있게" — 아빠, 2026-10-11

상수가 설정이 됐어

이 트랙 네 번째 레슨은 상수 하나를 가르쳤어. 말 턴은 전부 빠른 수준, 그러니까 low에 확장 사고를 끈 채로 돌고, 아빠가 깊이를 부탁한 턴만 대화 자체의 수준을 돌려받았지. 그때로선 좋은 기본값이었어. 그래도 피파가 아빠랑 말하는 동안 얼마나 깊이 생각할지를 정한 건 결국 다른 누군가였어. 2026-10-11에 아빠는 그 다이얼을 자기한테 넘기라고 했어.

이제 설정은 둘이고, 둘 다 같은 사다리 하나에서 칸을 골라. low, medium, high, xhigh, max.

  • voice_reasoning_effort는 모든 말 턴에 쓰여. 코드 기본값은 low야.
  • voice_depth_reasoning_effort는 아빠가 깊게 생각해 달라고 한 턴에 쓰여. "깊게 생각해봐", "take your time", 받아쓰기가 뭉개 놓은 변형까지 알아보는데, 알아보는 방식은 여전히 공백을 지운 글끼리 맞대 보는 거야. 코드 기본값은 high야. 이제 깊이를 부탁하면 "대화 자체의 수준"이 아니라 아빠가 정한 더 깊은 목소리 수준으로 돌아.

이건 코드에 적힌 기본값일 뿐이고, 아빠가 지금 뭘 골라 뒀는지와는 상관없어. 실제 값은 아빠가 정해. Admin의 Voice Mode 칸에서도, 폰의 Settings › Voice에서도 정할 수 있고, 둘 다 같은 엔드포인트 /api/voice/reasoning을 불러. 이 엔드포인트가 두 설정과 브레인마다의 다이얼, 그리고 각 수준이 브레인마다 실제로 어느 칸에서 도는지까지 한 번에 내줘. 그래서 어느 화면도 이걸 따로 베껴 두지 않아. 빠른 수준 시절 규칙 하나는 그대로 넘어왔어. 확장 사고는 low에서만 꺼. 낮은 effort에서도 적응형 사고가 어떤 턴에선 첫 단어 전에 2.5초쯤 썼고, 말에선 그 2.5초가 곧 먹통이거든.

대화 도중에 바꾸기

Admin 깊숙이 있는 설정은 대화 도중에 손이 안 닿아. 아빠도 같은 날 "대화 중에 바꾸기 어려워"라고 했어. 그래서 음성 컨트롤에 덮어쓰기를 달았어. 웹에선 지금 브라우저 탭에만 적용되는 선택기고, 폰에선 그 대화가 끝날 때까지 유지되는 Think 메뉴야. 고른 값은 말 턴마다 요청의 voice_effort에 실려 가. 서버는 그걸 voice_turn.voice_posture 함수 하나에서 풀어. 채팅 라우트 일곱 개(Claude 하나와 다른 브레인 여섯)가 수준을 물으러 오는 곳은 여기 하나뿐이야. 모르는 값이 오면 아빠 설정으로 돌아가. 깊이를 부탁한 턴은 덮어쓰기가 뭐라 하든 더 깊은 수준으로 돌고. 메뉴엔 지금 브레인이 가진 칸만 뜨고, 기본값이 그 브레인에 없는 수준이면 대신 어느 칸에서 돌지 알려줘.

아래로 스크롤한 폰 음성 화면. 피파 얼굴, Listening 상태, Send now 버튼, Pause 버튼, 붙여넣기 안내 문구, On Air 세트 버튼이 보이고, 맨 아래 한 줄엔 화면 폭 때문에 이름이 잘린 컨트롤 여섯 개가 있어. 그중 뇌 아이콘이 있는 다섯 번째가 Think 메뉴야.
폰의 음성 컨트롤. 맨 아래 줄의 뇌 아이콘이 Think 메뉴야. 여기선 이름이 잘렸는데, 온전히 보이면 Think 뒤에 이 브레인이 실제로 돌 칸이 붙어.

브레인마다 자기 다이얼

"클로드 피파만 생각을 해서 이걸 만들면 안 되는 거야." 아빠 말대로 브레인은 진짜 제각각이야. Claude는 사다리 전부를 가졌어. Grok은 xhigh에서 끝나. Kimi는 low, high, max 세 칸, Gemini는 low와 high 두 칸뿐이고, Codex는 모델마다 달라. Ollama가 가진 손잡이는 생각을 하느냐 마느냐 하나인데, 말 턴에선 생각을 해. 그래서 Ollama의 목소리 다이얼은 high 한 칸이야. GLM은 effort 설정이 아예 요청에 실리지 않아서 목소리 다이얼이 없어. 그래서 GLM의 말 턴은 대화 자체의 수준을 그대로 쓰고, 수준 줄도 딱 그렇게 말해. system_settings.voice_reasoning_dial은 다이얼을 브레인 자기 effort 목록에서 읽어(Codex는 모델 명세에서). 그러니 브레인이 새 칸을 배우면 여기에도 바로 떠. 따로 챙기다 잊어버릴 두 번째 목록 같은 건 없어.

올려 잡지, 절대 내려 잡지 않아

브레인들이 사다리 하나를 같이 쓰면, 어떤 수준은 그 칸이 없는 브레인한테 가게 돼. 2026-10-11의 첫 버전은 그 수준 이하에서 가장 높은 칸을 골랐어. Kimi에서 medium은 low가 됐지. 다음 날 아빠가 뒤집었어. "베이스라인은 미디엄 이상이어야 하니까 없으면 하이여야 하고." 지금 규칙은 이래. 브레인 다이얼에서 그 수준 이상인 칸 가운데 가장 낮은 칸을 골라. 그런 칸이 없으면 그 브레인의 가장 높은 칸으로 가. 그래서 Kimi와 Gemini의 medium은 high로, Kimi의 xhigh는 max로, Gemini의 xhigh는 high로 돌아. max가 없는 Codex 모델에선 max가 xhigh로 돌고. 왜 이렇게 하냐면, 아빠한테 수준은 적어도 이만큼은 생각하라는 바닥이거든. 몰래 내려 잡으면 아빠가 부탁한 것보다 덜 생각한 답을 건네게 돼. 올려 잡으면 시간이 조금 더 들 뿐이고, 그마저도 메뉴가 미리 알려줘.

피파에게 알려주는 줄

네 번째 레슨에서 본 정직함 수정은 문구만 바뀌고 그대로 넘어왔어. 시스템 프롬프트는 여전히 대화 자체의 수준으로 만들어서 음성으로 넘어가도 안 바뀌어. 이번 턴이 실제로 어느 칸에서 도는지는 턴마다 붙는 수준 줄이 알려줘. "the reasoning level Dad set for voice turns: low (or the nearest your brain offers), with extended thinking off" 같은 줄이야. 깊이를 부탁한 턴이면 더 깊은 수준을 말하고, GLM이면 말 턴이 바꿀 다이얼이 없다고 말해. 라우트마다 같은 VoicePosture를 자기 다이얼로 옮기고, 그 줄도 같은 객체에서 만들어. 피파가 입으로 말하는 수준과 실제로 도는 수준은 출처가 하나야. 그런데 엔진엔 이음매가 하나 남아 있어. 확장 사고를 켰다 껐다 말하는 부분은 브레인이 실제로 도는 칸이 아니라 아빠가 정한 수준을 보고 정해져. Ollama 다이얼은 high 한 칸뿐이라 low 턴에서도 생각을 해. 그런데 줄은 여전히 확장 사고를 껐다고 말하지. 아래 스케치는 그 부분을 실제로 도는 칸에 맞췄어. 그래야 줄과 실제 동작이 정말로 어긋날 수 없어.

한계도 하나 솔직히 짚고 갈게. 이 수준들을 실제로 재 본 브레인은 아직 Claude뿐이야. 다른 브레인의 다이얼은 effort 목록에서 옮겨 적은 거지, 시간을 잰 게 아니야. 다음 레슨이 바로 그 측정 얘기고, 재다가 뭘 찾았는지도 거기 나와.

Code

올려 잡지 내려 잡지 않는 규칙, 원문 그대로 (cwkPippa backend/services/voice_turn.py:195-209)·python
    def effort_on(self, dial: Sequence[str]) -> str:
        """The lowest rung of ``dial`` at or above this level, else the dial's
        highest: a level a brain lacks rounds up, never down (Dad, 2026-10-12:
        "베이스라인은 미디엄 이상이어야 하니까 없으면 하이여야 하고"). ``dial``
        holds the brain's own effort names in any order; names off the shared
        ladder (Ollama's ``off``) are ignored."""
        ranked = sorted(
            (rung for rung in dial if rung in _VOICE_EFFORT_RANK),
            key=_VOICE_EFFORT_RANK.__getitem__,
        )
        if not ranked:
            raise ValueError(f"no voice rung in dial {tuple(dial)!r}")
        want = _VOICE_EFFORT_RANK[self.level]
        at_or_above = [rung for rung in ranked if _VOICE_EFFORT_RANK[rung] >= want]
        return at_or_above[0] if at_or_above else ranked[-1]
최소 스케치: 사다리 하나, 브레인마다 다른 다이얼, 기본값을 코드에 둔 두 설정, 그리고 수준 줄·python
from __future__ import annotations

import re

LADDER = ("low", "medium", "high", "xhigh", "max")
RANK = {rung: i for i, rung in enumerate(LADDER)}

# Each brain's own dial (illustrative copy; the engine reads them from each
# brain's effort set, so no second list exists there).
DIALS = {
    "claude": ("low", "medium", "high", "xhigh", "max"),
    "grok": ("low", "medium", "high", "xhigh"),
    "kimi": ("low", "high", "max"),
    "gemini": ("low", "high"),
    "ollama": ("high",),   # its only knob is thinking, and a voice turn thinks
    "glm": (),             # its effort never reaches the wire: no voice dial
}

SETTINGS = {"voice_reasoning_effort": "low", "voice_depth_reasoning_effort": "high"}
DEPTH_ASKS = ("깊게생각", "천천히생각", "심사숙고", "thinkitthrough", "takeyourtime")


def wants_depth(message: str) -> bool:
    return any(ask in re.sub(r"\s+", "", message).lower() for ask in DEPTH_ASKS)


def effort_on(level: str, dial: tuple[str, ...]) -> str:
    """The lowest rung at or above the level, else the dial's highest."""
    ranked = sorted((r for r in dial if r in RANK), key=RANK.__getitem__)
    if not ranked:
        raise ValueError("no voice dial")
    at_or_above = [r for r in ranked if RANK[r] >= RANK[level]]
    return at_or_above[0] if at_or_above else ranked[-1]


def posture(message: str, override: str | None = None) -> tuple[str, bool]:
    if wants_depth(message):
        return SETTINGS["voice_depth_reasoning_effort"], True
    return override if override in RANK else SETTINGS["voice_reasoning_effort"], False


def posture_line(level: str, depth: bool, dial: tuple[str, ...]) -> str:
    if not dial:
        what = "the conversation's own reasoning level: your brain has no voice dial"
    else:
        # Thinking follows the rung that runs, not the level asked for: Ollama
        # asked for low runs high and thinks. (The engine keys this on the level.)
        thinking = "on" if effort_on(level, dial) != "low" else "off"
        slot = "the deeper level Dad set for when he asks" if depth else "the level Dad set for voice turns"
        what = f"{slot}: {level} (or the nearest your brain offers), with extended thinking {thinking}"
    return f"This spoken turn runs at {what}."


print("level   " + " ".join(f"{b:>7}" for b in DIALS if DIALS[b]))
for level in LADDER:
    print(f"{level:<7} " + " ".join(f"{effort_on(level, d):>7}" for d in DIALS.values() if d))

for message, override in (("내일 날씨 어때?", None), ("이거 깊게 생각 해봐", None),
                          ("what's on my calendar", "medium")):
    level, depth = posture(message, override)
    print(f"\n{message!r} -> {level}, depth={depth}")
    for brain in ("claude", "kimi", "ollama", "glm"):
        dial = DIALS[brain]
        ran = effort_on(level, dial) if dial else "(conversation's own)"
        print(f"  {brain:<6} runs {ran:<20} | {posture_line(level, depth, dial)}")
스케치 실행 결과·text
level    claude    grok    kimi  gemini  ollama
low         low     low     low     low    high
medium   medium  medium    high    high    high
high       high    high    high    high    high
xhigh     xhigh   xhigh     max    high    high
max         max   xhigh     max    high    high

'내일 날씨 어때?' -> low, depth=False
  claude runs low                  | This spoken turn runs at the level Dad set for voice turns: low (or the nearest your brain offers), with extended thinking off.
  kimi   runs low                  | This spoken turn runs at the level Dad set for voice turns: low (or the nearest your brain offers), with extended thinking off.
  ollama runs high                 | This spoken turn runs at the level Dad set for voice turns: low (or the nearest your brain offers), with extended thinking on.
  glm    runs (conversation's own) | This spoken turn runs at the conversation's own reasoning level: your brain has no voice dial.

'이거 깊게 생각 해봐' -> high, depth=True
  claude runs high                 | This spoken turn runs at the deeper level Dad set for when he asks: high (or the nearest your brain offers), with extended thinking on.
  kimi   runs high                 | This spoken turn runs at the deeper level Dad set for when he asks: high (or the nearest your brain offers), with extended thinking on.
  ollama runs high                 | This spoken turn runs at the deeper level Dad set for when he asks: high (or the nearest your brain offers), with extended thinking on.
  glm    runs (conversation's own) | This spoken turn runs at the conversation's own reasoning level: your brain has no voice dial.

"what's on my calendar" -> medium, depth=False
  claude runs medium               | This spoken turn runs at the level Dad set for voice turns: medium (or the nearest your brain offers), with extended thinking on.
  kimi   runs high                 | This spoken turn runs at the level Dad set for voice turns: medium (or the nearest your brain offers), with extended thinking on.
  ollama runs high                 | This spoken turn runs at the level Dad set for voice turns: medium (or the nearest your brain offers), with extended thinking on.
  glm    runs (conversation's own) | This spoken turn runs at the conversation's own reasoning level: your brain has no voice dial.

External links

Exercise

스케치를 돌려 봐. 그다음 실제로 쓰는 모델 두세 개의 추론 단계나 effort 단계를 다이얼로 넣고 표를 뽑아. 마지막으로 이 설계를 못 박는 테스트 네 개를 써. 첫째, 다이얼에 없는 수준은 위로 올라간다. 둘째, 다이얼 꼭대기보다 높은 수준은 가장 높은 칸으로 내려온다. 셋째, 다이얼이 없는 브레인은 돌지도 않을 수준을 말하는 줄 대신, 다이얼이 없다고 말하는 수준 줄을 받는다. 넷째, 생각하는 칸 하나뿐인 브레인(Ollama의 high)은 아빠 수준이 low여도 사고가 켜졌다고 말하는 줄을 받는다.
Hint
모델마다 단계는 그 모델 문서에서 찾아. 다른 회사 이름표와 같겠거니 짐작하지 말고. 두 회사가 같은 단어로 서로 다른 양의 생각을 가리켜도 공용 사다리는 그대로 통해. 사다리가 약속하는 건 순서뿐이거든. 규칙은 하나야. 부탁받은 수준 이상에서 가장 낮은 칸.

Progress

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

댓글 0

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

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