내 지식 그래프가 퀴즈를 보내준다: Obsidian + NanoClaw 간격 반복 시스템

· 6분 읽기 Threads Disquiet
목차

Recall SaaS의 간격 반복 아이디어를 채용해, 기존 인프라(NanoClaw + Obsidian + 블로그)만으로 개인 지식 반복 학습 시스템을 구축했다.
쌓아온 지식 그래프를 진짜 내 것으로 만들기 위한 장치다.

지식 그래프 노드에서 퀴즈 카드로 변환되어 Telegram으로 전송되는 아이소메트릭 일러스트

Obsidian vault에 직접 쌓든 AI를 활용하든, 개인 지식 자산이 쌓이고 있다. 그중 일부는 블로그 글로 정리하기도 한다. 문제는 쌓아놓고 다시 보지 않는다는 것이다. AI와 함께 정리하면 속도는 빠르지만, 직접 한 줄 한 줄 타이핑하던 때보다 기억에 남는 양이 확연히 적다. 블로그 리퍼러 분석 중 Recall이라는 SaaS를 발견했는데, AI 요약과 지식 그래프에 더해 간격 반복(Spaced Repetition) 퀴즈 기능을 제공하고 있었다. 저장한 콘텐츠에서 자동으로 퀴즈를 만들어 주기적으로 복습하게 하는 구조다.

아이디어 자체는 매력적이었다. 하지만 Recall을 쓰려면 Obsidian vault의 노트를 다시 Recall에 저장해야 한다. 이미 수백 개의 기술 노트가 vault에 있고, NanoClaw로 Telegram 기반 AI 어시스턴트를 운영하고 있다. 외부 SaaS를 도입하면 콘텐츠 이중 관리가 생기고, 퀴즈 로직을 커스터마이징할 수 없으며, 월 $7의 구독료도 쌓인다. 기존 인프라 위에 간격 반복 퀴즈를 직접 만들면 이 세 가지를 모두 피할 수 있었다.

설계의 핵심: AI와 코드의 역할 분리

처음에는 “NanoClaw의 스킬(SKILL.md)로 전부 처리하면 되지 않나?”라고 생각했다. NanoClaw는 컨테이너 안에서 Claude 에이전트를 실행하고, 스킬을 마운트하면 에이전트가 지시에 따라 행동한다. 퀴즈 생성, SM-2 계산, Telegram 전송까지 모두 스킬 하나로 정의할 수 있을 것 같았다.

하지만 하루 2회 퀴즈를 보내려면 매번 컨테이너를 띄우고 Claude 토큰을 소모해야 한다. 복습 간격 계산은 단순한 사칙연산이다(SM-2 알고리즘의 상세는 아래에서 다룬다). “2.5 x 6 = 15일 후 복습”을 계산하는 데 LLM이 필요한가? LLM은 언어 모델이지 계산기가 아니다. “strawberry에 r이 몇 개인가?”조차 틀리는 모델에 사칙연산을 맡기면 동일 입력에도 매번 다른 결과가 나올 수 있다. 객관식 채점은 userAnswer === correctAnswer로 끝나는 일이다.

역할 분리의 기준은 단순히 “창의적인가, 결정적인가”만이 아니었다. 장애 시 복구 전략과 디버깅 용이성도 고려했다. 코드로 구현한 SM-2 엔진은 단위 테스트로 검증 가능하고, 장애 시 로그를 보면 원인이 바로 드러난다. 반면 LLM 기반 로직은 동일 입력에 다른 출력이 나올 수 있어 재현이 어렵고, API 장애 시 전체 흐름이 멈출 수 있다.

결국 이렇게 나눴다.

역할담당이유
퀴즈 생성스킬 (Claude)노트를 읽고 문제를 만드는 건 LLM이 필요
빈칸 채점 (유연 매칭)Haiku API동의어, 약어, 오타 허용이 필요할 때만
SM-2 계산코드결정적 연산, 테스트 가능, 장애 시 원인 추적 용이
스케줄링 + 전송코드D1에서 due 카드 pull + Telegram API 호출
객관식/O/X 채점코드 (Quiz Worker)단순 비교, 공개 퀴즈 페이지에서도 동일 로직

AI는 창의적 판단이 필요한 곳에만, 나머지는 결정적 코드로. 이 원칙이 전체 설계를 관통한다. LLM API에 장애가 나도 기존 퀴즈 풀에서 출제와 채점은 계속 동작한다. 퀴즈 생성 배치만 다음 주로 밀릴 뿐이다.

아키텍처: NanoClaw 위에 퀴즈 모듈 얹기

Obsidian 지식 그래프와 블로그에서 AI가 퀴즈를 생성하고 NanoClaw가 Telegram으로 전송하는 흐름

flowchart TD
    subgraph 소스
        V[Obsidian Vault]
        B[블로그]
    end

    subgraph 스킬
        G[퀴즈 생성 스킬<br>Claude]
    end

    subgraph CF[Cloudflare]
        WEB[공개 퀴즈 페이지<br>/quiz]
        QW[Quiz Worker<br>quiz-api]
        D1[(Cloudflare D1<br>SSOT 문제은행)]
        WEB -->|랜덤 출제 · 채점| QW
        QW --> D1
    end

    subgraph NC[NanoClaw]
        SC[자체 스케줄러<br>setInterval]
        SM[SM-2 엔진]
        TG[Telegram 전송/수신]
        RT[메시지 인터셉트]
        CP[credential proxy<br>Haiku API]
        DB[(SQLite<br>sm2_state)]
        SC -->|아침 / 저녁| SM
        SM -->|SM-2 상태 관리| DB
        SM --> TG
        TG -->|사용자 응답| RT
        RT -->|객관식/OX| DB
        RT -->|빈칸 exact match 실패| CP
        CP --> DB
    end

    V --> G
    B --> G
    G -->|POST /api/quiz/bank| QW
    SM -->|GET /api/quiz/due| QW

NanoClaw는 이미 Telegram Bot API(Grammy), SQLite DB, 컨테이너 에이전트 실행 환경을 갖추고 있다. 퀴즈 문제은행은 Cloudflare D1에 저장하고 Quiz Worker가 API를 제공한다. NanoClaw는 D1에서 복습 대상을 pull하고, 로컬 SQLite에는 SM-2 상태(간격, EF, 복습일)만 관리한다.

퀴즈 생성: 스킬이 하는 일

퀴즈 생성은 주 1회 배치로 실행한다. NanoClaw의 컨테이너 에이전트가 spaced-quiz-generate 스킬을 읽고, 마운트된 vault 노트에서 퀴즈를 만든다.

3가지 유형을 생성한다.

  • 객관식 4지선다: 주력. 코드로 즉시 채점 가능
  • 빈칸 채우기: 핵심 용어/명령어 기억용. 정답을 배열로 저장해 동의어 허용
  • O/X 참거짓: 개념 이해 확인. 비중은 낮게

처음에는 주관식(단답, 서술)까지 5가지 유형을 고려했다. 하지만 유형이 많다고 좋은 건 아니었다. 서술형은 매 응답마다 LLM 채점 토큰이 필요하고, Telegram에서 장문을 타이핑하는 UX도 나쁘다. 단답형은 정답 판정이 모호해진다. 간격 반복의 핵심은 “빠르게 회상하고 즉시 확인받는 것”이므로, 즉시 자동 채점이 가능한 3가지로 좁혔다.

스킬에는 난이도 배분 규칙(easy 20% / medium 50% / hard 30%)과 품질 기준을 정의했다. 핵심 제약은 두 가지다. 노트 본문에 명시적으로 나온 내용만 출제하고, 정답이 모호하거나 논쟁적인 문제는 만들지 않는다. 이 두 제약이 퀴즈 품질의 하한선을 보장한다. 출력은 JSON 배열이라 Quiz Worker의 /api/quiz/bank 엔드포인트로 D1에 바로 적재한다.

실시간 생성 대신 배치를 선택한 이유는 명확하다. 토큰 비용이 예측 가능하고, 생성 실패가 전송을 막지 않으며, 생성된 퀴즈의 품질을 사전에 확인할 수 있다.

SM-2 간격 반복 알고리즘

SM-2(SuperMemo 2)는 1987년에 만들어진 간격 반복 알고리즘이다. Anki가 채택하면서 널리 알려졌다. 핵심은 단순하다.

  • 맞히면 복습 간격을 늘린다
  • 틀리면 간격을 1일로 초기화한다
  • 쉽게 맞힌 문제는 더 빠르게 간격이 늘어난다

각 카드마다 **Easiness Factor(EF)**라는 값을 관리한다. 맞힐 때마다 EF에 따라 다음 복습까지의 일수가 결정된다. EF는 최소 1.3으로 제한해서 완전히 잊히는 카드가 없도록 한다.

Telegram에서 퀴즈에 응답하면 quality 점수로 변환한다.

  • 정답 + 30초 이내 응답: quality 5 (쉽게 맞힘)
  • 정답: quality 4 (맞힘)
  • 오답: quality 1 (틀림)

이 계산은 사칙연산과 조건분기뿐이라 코드 몇십 줄로 정확하게 구현할 수 있다. 23개의 단위 테스트가 SM-2 계산의 정확성을 보장한다. 잘 맞히는 문제는 간격이 점점 늘어나 한 달 뒤에나 다시 나오고, 자주 틀리는 문제는 매일 출제된다. 퀴즈는 틀릴수록 자주 찾아온다.

SM-2 대신 Anki가 최근 채택한 FSRS(Free Spaced Repetition Scheduler)도 검토했다. FSRS는 사용자의 실제 학습 데이터로 파라미터를 최적화하므로 SM-2보다 효율적이지만, 유의미한 최적화를 하려면 수백 건 이상의 응답 데이터가 필요하다. 초기 단계에서는 SM-2가 적합했다. D1과 로컬 SQLite 모두에 응답 이력을 저장하고 있으므로, 데이터가 충분히 쌓이면 FSRS로의 전환은 알고리즘 모듈만 교체하면 된다.

퀴즈 응답 처리: 라우팅에서 채점까지

NanoClaw는 Telegram에서 다양한 대화가 오간다. 퀴즈 응답인지 일반 대화인지를 어떻게 구분하고, 유형별로 어떻게 채점하는가?

Telegram의 onMessage 핸들러에서 퀴즈 인터셉트를 먼저 처리한다. 전체 흐름은 이렇다.

  1. pending 확인: DB에 응답 대기 중인 퀴즈가 있는지 조회
  2. 패턴 매칭: 사용자 메시지가 퀴즈 응답인지 판별
    • 객관식: 숫자 한 글자 (1, 2, 3, 4)
    • O/X: O, X, ,
    • 빈칸: 짧은 텍스트
    • “스킵”, “나중에” → 스킵 처리
    • 패턴 불일치 → 일반 대화로 통과 (퀴즈는 pending 유지)
  3. 채점: 유형별 분기
    • 객관식/O/X → 코드에서 즉시 비교
    • 빈칸 → 코드에서 normalize + exact match 시도 → 실패 시 Haiku API로 유연 매칭
  4. 피드백 전송: 정답/오답 + 해설 + 다음 복습일
  5. SM-2 갱신: quality 점수 계산 → DB 업데이트

이 방식의 장점은 퀴즈가 일반 대화를 방해하지 않는다는 것이다. 퀴즈가 와 있어도 “오늘 날씨 알려줘”라고 보내면 그냥 일반 대화로 처리된다. 24시간 TTL이 만료되면 무응답(quality 0)으로 처리하고 pending에서 제거한다.

빈칸 채우기에서 LLM fallback을 두는 이유는 “ClusterPolicy”가 정답인데 “cluster policy”나 “k8s” = “Kubernetes” 같은 약어 매칭이 필요하기 때문이다. 여기서 컨테이너 에이전트 대신 credential proxy를 통한 직접 Haiku API 호출을 선택했다. 컨테이너를 띄우면 응답까지 20초 이상 걸리지만, API 직접 호출은 2-3초면 끝난다. 퀴즈 피드백은 즉각성이 중요하다. 답을 보내고 30초를 기다려야 한다면 학습 경험이 크게 나빠진다.

스케줄링과 전송: 자체 setInterval 루프

NanoClaw에는 task-scheduler가 있지만, 이는 컨테이너 에이전트를 실행하기 위한 스케줄러다. 퀴즈 전송은 DB에서 due 카드를 조회하고 Telegram 메시지를 보내는 것이 전부라 컨테이너를 띄울 필요가 없다.

그래서 quiz.ts 내부에 자체 setInterval 루프를 구현했다. 프로덕션 수준의 작업 큐가 아니라 개인용 단일 프로세스이므로 이 방식으로 충분하다. 프로세스가 재시작되면 다음 스케줄 시점에 자동으로 복구된다.

  • 퀴즈 전송: 하루 2회, 아침과 저녁
  • 퀴즈 생성: 주 1회 토요일 배치 — 이것만 컨테이너 에이전트 호출

전송 시 due 카드 선택 우선순위는 overdue > due today > new card 순이다. 한 번에 1~2개만 보내서 학습 부담을 줄였다.

Telegram에서의 실제 퀴즈 경험

실제 Telegram에서 퀴즈가 오면 이런 형태다.

[퀴즈] SM-2 계산을 AI가 아닌 코드가 담당하는 이유는?

1. API 비용 절감을 위해
2. 결정적 연산이므로 정확성 보장과 토큰 불필요
3. AI가 수학 계산을 못해서
4. 코드가 더 빠르게 학습하므로

(번호로 답해주세요)

번호를 입력하면 즉시 피드백이 온다.

정답!

SM-2 알고리즘은 결정적(deterministic) 연산이므로 매번 동일한
결과가 보장되어야 하고, LLM 토큰을 소비할 필요가 없다.
AI는 퀴즈 생성과 빈칸 채점처럼 LLM이 필요한 작업만 담당한다.

다음 복습: 내일

정답이면 SM-2 간격에 따라 다음 복습일이 점점 늘어난다. 1일 → 3일 → 7일 → 15일. 틀리면 다시 1일로 돌아온다.

적용 범위와 한계

이 시스템이 적합한 경우와 그렇지 않은 경우가 있다.

적합한 경우:

  • 이미 지식 베이스(Obsidian, Notion 등)를 운영하고 있고, 기존 콘텐츠를 활용하고 싶을 때
  • 개인용 메시징 허브(NanoClaw 등)가 이미 있어서 인프라 추가 비용이 적을 때
  • 기술 지식처럼 팩트 기반 학습이 주 목적일 때

부적합한 경우:

  • 지식 베이스 없이 처음부터 시작한다면 Recall 같은 SaaS가 진입장벽이 낮다
  • 서술형 학습이 주 목적이라면 퀴즈 형식의 한계가 있다
  • 여러 사용자가 함께 쓰려면 인증/권한 체계를 추가해야 한다

vault와 블로그 글에서 퀴즈를 생성하여 D1 문제은행에 적재한다. 주 1회 배치로 자동 확장되며, 새 노트가 추가되면 다음 배치에서 자동 반영된다. 아직 초기 단계라 퀴즈 품질이나 난이도 분포는 운영하면서 조정해야 할 부분이다. AI가 만든 퀴즈가 항상 완벽하진 않으므로, 3회 연속 오답인 문제는 문제 자체에 오류가 있을 가능성을 의심하고 검토 플래그를 남기도록 했다.

이 문제은행을 블로그 방문자도 풀어볼 수 있도록 공개 퀴즈 페이지를 제공한다. Cloudflare D1을 SSOT로 두고 Quiz Worker가 랜덤 출제와 채점을 처리한다. 틀린 문제의 출처 글로 바로 이동할 수 있어서, 퀴즈가 학습 루프의 일부가 된다.

결국 이 시스템의 핵심은 AI와 코드의 역할 경계를 명확히 그은 것이다. NanoClaw의 “Skills Over Features” 철학에서 스킬은 LLM이 필요한 창의적 작업을, 코드는 반복적이고 결정적인 작업을 담당한다. 이 경계를 명확히 하면 불필요한 토큰 소모 없이 즉각적인 사용자 경험을 제공할 수 있다. 기존 인프라(Grammy, SQLite, 컨테이너, credential proxy)를 그대로 재활용해서 새로 만든 건 quiz.ts 하나뿐이다.

이어서 읽기