EP 10

페퍼야, 이게 뭔지 알아?


2026년 5월·7 min read·#architecture#triage#llm

처음에 triage는 3가지였다.

small_talk
,
recall
,
action
.

충분할 것 같았다. 쓰다 보니 아니었다.


3가지로는 부족하다

"오늘 날씨 어때"와 "GOOGL 평단가 얼마야" — 둘 다 정보 요청이다. 근데 처리 방식이 완전히 다르다.

날씨는 외부 API를 호출해야 한다. 평단가는 내 Vault에서 찾아야 한다. 같은

recall
이나
action
으로 묶을 수 없다.

"내일까지 보고서 제출해야 해"와 "은수한테 알림 보내줘" — 둘 다 할 일 관련이다. 근데 하나는 내가 할 일이고, 하나는 페퍼가 실행하는 일이다. 이것도 다른 처리가 필요하다.

그래서 6가지로 늘렸다.

small_talk  — LLM 지식으로 충분한 것. "오늘 뭐 먹지?"
recall      — 내 Vault에서 꺼내는 것. "GOOGL 평단가 얼마야?"
store       — Vault에 저장하는 것. "GOOGL 245달러로 저장해줘"
task        — 내가 할 일 등록. "내일 보고서 제출해야 해"
lookup      — 실시간 외부 정보. "오늘 날씨 어때"
capability  — 페퍼가 직접 실행. "은수한테 알림 보내줘"

경계가 애매한 케이스들

분류가 명확하면 좋겠지만, 현실은 그렇지 않다.

"북한 요즘 어때" — 이건 뭔가. LLM 훈련 데이터로 알 수 있는 역사적 맥락도 있고, 실시간 뉴스도 있다. 의견을 묻는 것 같기도 하고.

"이번 주 일정 다 지워줘" — 이건 조회 후 삭제 두 단계인가, capability 하나인가.

이런 케이스들을 프롬프트에 직접 박아뒀다.

- "요즘", "최신", "현재" + 외부 정보 → lookup
- 삭제·일괄 변경은 내부 조회 포함해도 → capability 하나 (멀티스텝 분리 금지)
- 의견을 묻는 형태여도, 현재 진행 중인 사건이라면 → lookup

경계 케이스 처리가 프롬프트 엔지니어링의 대부분이었다.


멀티스텝 처리

"날씨랑 환율 둘 다 알려줘"처럼 intent가 두 개인 경우.

단일 intent이면 배열 길이 1, 복수면 순서대로 배열에 넣는다.

// 단일
{ intents: ['lookup'], confidence: 0.95 }

// 멀티
{ intents: ['lookup', 'lookup'], confidence: 0.85 }
// 같은 intent 두 번이어도 배열에 두 번 — 하나로 합치지 않는다

// 현재는 멀티스텝 stub
if (triageResult.intents.length > 1) {
  responseText = '여러 가지를 한 번에 처리하는 기능은 곧 지원할게요!'
}

멀티스텝 실행은 아직 stub이다. 플래너가 필요하다. 지금은 "곧 지원할게요"로 처리하고 백로그에 넣어뒀다.


테스트: 50개 케이스, 94% 통과

triage가 맞게 분류하는지 확인하는 테스트를 50개 만들었다.

T-001: "안녕" → small_talk ✅
T-012: "GOOGL 평단가 얼마야?" → recall ✅
T-023: "오늘 날씨 어때" → lookup ✅
T-031: "은수한테 3시에 알림 보내줘" → capability ✅
T-044: "요즘 AI 트렌드 어때?" → lookup ❌ (small_talk으로 오분류)
T-048: "KHH 회의 내용 정리해서 저장해줘" → store+task ❌

48/50. 94%.

실패한 2개는 gray zone이다. "요즘 AI 트렌드"는 LLM 훈련 데이터로 충분하기도 하고 실시간 정보가 필요하기도 한 케이스. 지금은 수용 가능한 수준이라고 판단했다.

나중에 실제 사용 데이터가 쌓이면 피드백을 반영해서 개선할 수 있는 구조도 백로그에 넣어뒀다. 👎 리액션이

pepper_logs.user_feedback = -1
로 기록되는 게 이미 있으니까.


모델 비용도 여기서 나뉜다

triage 자체는 가장 저렴한 모델로 돌린다.

await generateWithFallback(['1A', '1C'], prompt, ...)
// 1A: Gemini Flash-Lite (가장 저렴)
// 1C: Claude Haiku (fallback)

판단에 비싼 모델을 쓸 필요는 없다. 무거운 모델은 실제 응답을 생성할 때만 쓴다. recall은 Sonnet, small_talk은 Flash-Lite. 용도별로 다르게 배치한다.

EP 01에서 STATE A/B/C를 설계할 때 "가벼운 판단은 저렴한 모델로"라고 했는데, 그게 코드로 구현된 모습이다.


triage 6-way가 잡히면서 페퍼가 확실히 덜 멍청해졌다.

다음은 실제로 lookup이 실행되고, capability가 실행되고, STATE B에서 페퍼가 스스로 코드를 짜는 순간이다. Phase 0의 진짜 골인 지점.