지난번에는 독서한 내용을 잊지 않는 시스템의 전체적인 개요에 대해 설명했습니다.
이번에 가장 중요한 포인트를 결론부터 말씀드리겠습니다.
그것은,
상태(state)는 카드 마크다운 파일 한 장에 모두 담고, 출제와 채점은 그것을 읽고 쓰는 것만으로 처리하는 것입니다.
| 어려웠던 점 | 사람이 정한 방식 | AI 부하가 수행한 작업 | 결과 |
|---|
| 첫 번째 버전은 답변 본문을 남기지 않아, 무엇에 어떻게 답했고 몇 점이었는지 되돌아볼 수 없었습니다. 같은 날 다른 형식을 사용해 답변하면 하루에 2단계씩 진행되는 카드가 있었습니다. | 답변은 카드 답변 기록에 남깁니다 (공개해도 무방합니다. 고유명사는 쓰지 않습니다). 진도는 하루에 1단계까지만 나갑니다. 모범 답안에 책에 없는 내용을 추가하지 못하게 합니다. | 카드의 형식, 3문제 선택 방식, 채점 지시문, 상태 업데이트, 테스트 | |
| 카드 한 장에 상태와 답변 전체가 담깁니다. 출제도 채점도 그 파일을 읽고 쓰는 것만으로 작동합니다. | | | |
지난번(#1)에는 왜 만들었는지, 그리고 9월 30일 하루 만에 세 번이나 수정했던 경위를 작성했습니다. 이번에는 현재 작동하고 있는 카드/출제/채점의 실제 모습을 보여드리겠습니다.
스마트폰으로 한 문제에 답변하면 다음 순서로 진행됩니다.
카드는 파일이 단 하나, 원본(正本)입니다. 페이지는 그날의 문제를 보여주는 역할만 할 뿐, 상태를 가지고 있지는 않습니다. 함수 역시 카드를 읽고 쓰면 끝납니다. 데이터베이스는 사용하지 않았습니다.
답변할 때마다 카드 파일에 커밋(commit)이 하나씩 생깁니다. 작성하는 것은 recall-bot이라는 이름으로, 커밋 제목에는 날짜가 들어갑니다. GitHub 기록을 보면 언제 어떤 카드를 답변했는지 추적할 수 있습니다.
카드는 책의 아이디어 하나당 마크다운 파일 하나입니다. 파일명이 카드 이름이 됩니다.
---
title: 카드 제목 (아이디어를 한마디로)
type: card
...
본문(本文)의 목차는 다음 5가지입니다.
- 주장(主張):아이디어를 자신의 말로 한 문장으로 만든 것
- 근거(根拠):하이라이트된 요점(약 100자)과 Kindle 해당 위치 링크, 자신의 메모
- 왜 자신에게 중요한가:자신의 업무나 생활에 비추어 한 줄
- 사용할 장면:「〇〇할 때, 이것을 사용한다」의 형태
- 답변 기록:최신순으로, 1회 답변을 하나의 글머리 기호로
답변 기록은 첫 번째 줄에 날짜・질문 형식・점수・합격 여부・한마디 피드백을 작성합니다. 그 아래에는 들여쓰기하여 질문・답변・점수 내역・모범 답안을 추가합니다 (형식은 #1에 게시했습니다).
첫 줄의 구분자는 「|」입니다. 피드백 안에 「|」나 개행이 들어가면, 줄 형식이 무너져 읽을 수 없게 됩니다. 그래서 작성하기 전에 반드시 이 함수를 거칩니다.
def sanitize(text):
"""Keep a value on one history line: no pipes (column separator) or newlines."""
text = html.unescape(str(text or ""))
...
「|」는 전각 「|」로, 개행은 「 / 」로 대체합니다. 이 한 줄의 형식이 일정하기 때문에, 나중에 합격률을 계산할 때도 이 줄만 읽으면 됩니다.
AI가 만든 카드는 주장(主張)만 AI 초안입니다. 「왜 자신에게 중요한가」와 「사용할 장면」은 비워져 있고, 대신 표시어(目印)의 줄이 들어 있습니다.
| 항목 | 표시어의 줄 |
|---|
| 주장 | (AI 초안. 첫 번째 /recall에서 자신의 말로 수정) |
| ... | |
이 표시어의 줄이 하나라도 남아있다면, 그 카드는 「아직 작성되지 않은」 카드입니다. 매일 아침, 하루 한 문제까지, 이곳을 채울 질문(보완)이 나옵니다. 보완 질문은 채점하지 않습니다.
표시어는 줄 전체가 완전히 일치할 때만 표시어로 취급합니다. 답변 안에 「(미기입」이라는 글자가 있어도 반응하지 않습니다.
출제하는 쪽과 채점하는 쪽은 다른 곳에서 작동합니다. 출제는 GitHub Actions, 채점은 Vercel입니다. Vercel의 함수는 출제 쪽 코드를 읽어올 수 없기 때문에, 같은 판정을 두 곳에 작성했습니다. 두 판정이 어긋나지 않도록, 모든 카드에서 양쪽 답변이 동일한지 확인하는 테스트가 있습니다.
매일 아침 Actions는 모든 카드를 읽고 다음 코드로 3문제를 선택합니다.
# 보완 후보는 자신의 메모가 붙은 하이라이트를 근거로 하는 카드를 먼저 내보낸다
unfilled = sorted([c for c in cards if is_unfilled(c)],
key=lambda c: (prio(c), 0 if "自分のメモ" in c["body"] else 1))
...
정렬 순서는 다음과 같습니다.
- 아직 작성하지 않은 카드 보완 (우선순위가 높은 순서대로, 자신의 메모가 있는 것을 먼저)
- 다음에 출제될 날짜가 된 카드 중, 지난번에 불합격했던 것
- 합격률이 낮은 것
- 우선순위가 높은 것, 기한을 넘긴 일수가 긴 것
위 목록 순서대로 가져가며, 같은 책의 카드는 하루에 1장까지만 합니다. 보완 질문은 하루에 1문제까지입니다. 채점되는 질문이 부족한 날에는 비어있는 자리를 보완으로 채웁니다. 다른 책의 카드가 부족하면 3문제보다 적어도 괜찮습니다.
'같은 책은 하루 1장' 규칙은 제가 10월 1일에 정했습니다. 9월 28일 첫 설계에도 '다른 책을 섞기(Interleave)'라고 적혀 있었습니다.
지난번 불합격했던 카드를 가장 먼저 배치한 것은 10월 4일이고, 합격률이 낮은 순서로 추가한 것은 10월 8일입니다. 이는 불합격된 카드를 다음 날 확실히 다시 출제하기 위함입니다.
합격률은 답변 기록의 첫 번째 줄 중 합/불이 표시된 행만 세어 계산합니다. 보완 질문은 계산하지 않습니다. 아직 채점 기록이 없는 카드는 합격률을 1(100%)로 간주합니다. 이는 기록이 없는 카드가 합격률이 낮은 카드보다 먼저 나오지 않도록 하기 위함입니다.
같은 카드라도 연속으로 합격한 횟수(streak)에 따라 질문의 유형이 바뀝니다.
| streak | 유형 | 질문 방식 | 답변의 축 |
|---|
| 0 | 회상 (Recall) | 책을 보지 않고, 자신의 말로 설명하기 | 정확성・구체적 예시・적용 조건・다음 행동 |
| ... | | | |
4회차부터는 이 네 가지를 순환합니다.
질문 끝에는 '답변의 축'이 붙어 있습니다. 이것은 10월 4일에 추가했습니다. 무엇을 써야 할지 틀이 있으면 백지 상태에서 막히지 않고 쓰기 시작할 수 있기 때문입니다.
QUESTION_TEXT = {
"recall": "책을 보지 않고, 이 생각을 자신의 말로 설명해 주세요.",
"apply": "이번 주 실제 상황을 1가지 들고, 그 상황에서 이 생각을 어떻게 사용할지 설명해 주세요.",
...
스마트폰에서는 다음과 같이 나옵니다.
비교 질문(Contrastive question)의 경우, 비교할 상대 카드를 1장 골라 질문에 첨부합니다. 상대는 이미 자신의 말이 들어가 있고, 다른 책의 카드 중 카드와 날짜로 정해진 1장입니다. 날이 바뀌면 상대도 바뀝니다. 이 선택 방식은 글을 쓰기 전 시험에서 발견된 오류를 수정한 것입니다. #3에서 작성하겠습니다.
매일 아침의 Actions는 선택한 3문항을 하나의 JSON에 작성하고, 페이지와 함께 GitHub Pages에 올립니다.
{
"date": "2026-09-30",
"questions": [
...
JSON은 매일 아침 덮어쓰고, 전날 분은 남기지 않습니다. 과거 답변은 각 카드의 답변 기록을 보면 알 수 있기 때문입니다. 질문의 문구와 카드 이름은 JSON에 작성하기 전에 HTML용 형식으로 바꿉니다.
페이지 안의 작은 스크립트가 이 JSON을 읽어 질문을 배열합니다. fields는 그 질문에 대한 입력란입니다. 보완 질문에서는 주장・왜 중요한지・사용 장면 3개의 칸을 처음부터 분리해서 보여줍니다. 하나의 칸에 쓰게 한 후 나중에 나누는 방식은, 구분하는 방법을 잘못할 걱정이 생기기 때문입니다.
답변을 보낼 때는 카드 이름과 질문 유형을 함께 보냅니다. #1에서 작성했던 Issue로 답변하던 때의 혼동은 이제 일어나지 않았습니다.
답변 후 결과는 브라우저 안에만 남깁니다. 같은 날 페이지를 다시 열어도 결과가 나옵니다. 사라져도 곤란하지 않습니다. 원본은 카드 파일이기 때문입니다.
채점은 Vercel의 Python 함수가 Claude Haiku 4.5를 단 한 번 호출하여 진행합니다. 지시문은 다음과 같습니다.
prompt = f"""あなたは学習コーチです。以下のカードの主張と、ユーザーの回答を4軸で採点してください。
カード「{title}」
主張: {claim_text}
...
""".format()
점수와 피드백, 모범 답안을 한 번의 호출로 JSON 형태로 받습니다. 함수는 돌아온 문구에서 {와 }로 둘러싸인 부분을 추출하여 읽습니다.
AI에게 전달하는 것은 카드 주장과 근거, 그리고 저의 답변만입니다. 책 전체 내용은 전달하지 않습니다. 채점은 카드에 적은 것을 기준으로 합니다.
채점을 기다리는 동안 페이지 버튼은 '전송 중...'이 됩니다. 채점이 돌아오면 해당 질문 칸에 점수와 모범 답안이 나옵니다. 통신이 실패했을 때는 입력을 지우지 않고 버튼을 '재전송'으로 되돌립니다. 스마트폰에서 길게 쓴 답변이 실패로 사라지는 일은 없습니다.
4가지 관점을 0점에서 2점까지 매겨, 총 8점 만점입니다. 합/불 여부는 다음 두 줄에서 결정됩니다.
총점 5점 이상이고, 주장의 정확성이 1점 이상이면 합격입니다. 주장과 다른 답은 구체적인 예시나 다음 행동으로 점수를 얻어도 통하지 않습니다.
이번 회차 피드백에는 '책의 근거에 없는 개념을 가져오고 있다'라고 적혀 있었습니다. 채점도 카드에 적은 주장과 근거를 기준으로 하고 있는 것 같습니다.
합격하면 상태가 1단계 올라가고, 다음에 나올 날이 늘어납니다. 불합격이면 1단계 내려가고, 다음날 다시 나옵니다.
| 결과 | 카드의 업데이트 |
|---|
| 그날 처음 합격 | 상태를 1단계 올리고, streak을 1개 증가시킵니다 |
| ... | |
| 불합격으로 streak이 1개 줄면, 질문의 유형도 1개 전으로 돌아갑니다. 예를 들어 대조(対比) 질문에서 불합격되면, 다음날에는 적용 질문이 나옵니다. | |
안정 카드는 처음 올라왔을 때는 30일 후, 안정적으로 합격하면 60일 후에 나옵니다. 이 규칙도 기사를 쓰기 전 시험에서 코드가 틀린 것을 발견하고 수정했습니다.
보완(補完) 질문에는 AI를 호출하지 않습니다. 전달받은 세 개의 칸을 카드의 절에 적기만 합니다.
적는 것은 아직 표시된 줄이 남아있는 절만입니다. 이미 쓴 절은, 전달받아도 다시 쓰지 않습니다. 세 개 모두 작성되어 있으면, 아무것도 쓰지 않고 '이미 기입 완료'라고 반환합니다.
작성한 카드는 '복습 필요(要復習)'가 되어 다음날부터 채점되는 질문으로 돌아갑니다. 작성 직후에는 기억하는 연습이 되지 않기 때문입니다. 다만, 이미 '학습 중(学習中)'보다 앞서 나간 카드는 상태를 되돌리지 않습니다.
답변은 1줄로 맞춘 후에 적습니다. 개행은 ' / '로 대체하고, 줄 맨 앞의 '#'는 제목으로 읽히지 않도록 ' #'로 합니다.
10월 1일에 같은 카드를 하루에 두 번 답변했습니다. 기억하는 질문에서 합격하여 '학습 중'이 되고, 이어서 사용하는 장면 질문에서 합격하여 '확인 필요(要確認)'로 진행되었습니다. 하루에 2단계입니다.
처음 규칙은 '전과 다른 날・다른 유형의 질문에 합격했을 때만 진전한다'였습니다. 코드는 '같은 날이고, 게다가 같은 유형'일 때만 멈추고 있었기 때문에, 유형을 바꾸면 같은 날이라도 진행되고 있었습니다.
AI는 또 하나의 오류를 발견했습니다. 새로운 답변은 히스토리의 맨 위에 추가하는 데, '직전 답변'으로 가장 오래된 줄과 비교하고 있었습니다.
저는, 유형에 관계없이 '하루 1단계까지'로 정했습니다. 그날 처음 합격한 것만으로 진행합니다. 위의 passed_today가 그 판정입니다. 다음 단계로 진전하는 것은, 다른 날에도 다시 합격했을 때가 되었습니다.
채점 지시문에서 가장 긴 것은 모범 답안의 규칙입니다. 10월 2일에 추가했습니다.
모범 답안은 AI가 '본인이 작성한 듯한 1인칭'으로 작성합니다. 모범 답안은 답변을 한 후에 반드시 읽습니다. 만약 책에 없는 이야기나 숫자가 섞여 있다면, 그것을 본의 내용으로 기억해 버립니다.
그래서, 본의 내용으로 쓰는 것은, 카드의 주장과 근거에 있는 것만으로 했습니다. '자신의 구체적인 예시'만은, 예시로 만들어도 좋다고 합니다. 구체적인 예시가 지어낸 이야기라도, 본의 내용의 거짓이 되지는 않기 때문입니다.
새로 읽은 책의 카드는, 매일 아침 5시 17분에 Claude Sonnet 5.5가 만듭니다. 전달하는 것은, 그 책의 하이라이트와 메모, 이미 존재하는 카드 이름 목록, 태그 목록입니다.
규칙은 다음과 같습니다.
- 만드는 것은, 그 책에 부족한 장수까지 (1권당 3장까지). 가치 있는 아이디어가 적으면, 적어도 좋고, 0장이어도 좋습니다.
- 고르는 순서는, 자신의 메모가 붙은 하이라이트, 책의 중심 주장, 다른 책의 카드와 연결되는 논점입니다.
- 이미 존재하는 카드와 같은 논점은 피한다. 목록을 전달하여 중복되지 않게 합니다.
- 인용은, 하이라이트 본문에서 요점만 한 글자 그대로 뽑아낸다. 바꿔 쓰기・요약・단어 추가는 하지 않는다. 중간을 생략할 때는 '…'를 사용한다.
- 인용을 뽑아낸 하이라이트의 위치(Location)를, 전달된 값 그대로 반환한다.
- 주장은 1~2문장의 초안. 제가 나중에 제 말로 고친다.
답변은 정해진 형태의 JSON으로 돌려받고, 스크립트가 한 장씩 의심합니다.
제목이 기존 카드와 겹치거나, 인용문이 원문에 한 글자도 포함되지 않거나, 너무 긴 것은 버립니다. 인용문의 상한선은 120자로 설정했습니다. 이는 규칙적인 '약 100자'에 약간의 여유를 둔 수입니다.
새로운 카드는 우선순위를 '높음'으로 지정합니다. 보완 질문은 우선순위가 높은 순서로 나오기 때문에, 방금 읽은 책의 카드가 다음 날 아침 보완 자료로 나옵니다.
이번 회차에서는 제가 결정한 것과 AI가 수행한 것을 나누어 작성하겠습니다.
제가 결정한 것은 다음과 같습니다.
- 답변 본문도 카드에 남깁니다. 공개 리포지토리에 나와도 됩니다. 대신, 답변에는 고유명사를 쓰지 않습니다 (10월 1일)
- 진도는 하루에 한 단계까지만 나아갑니다. 질문의 형태는 가리지 않습니다 (10월 1일)
- 같은 책의 카드는 하루에 한 장까지입니다 (10월 1일)
- 모범 답안에, 책에 쓰여 있지 않은 내용을 추가하지 않도록 합니다 (10월 2일)
AI가 수행한 것은 카드 형식, 선택 로직 코드, 채점 지시문, 상태 업데이트, 보완 문구 수정, 표시 여부 판단, 테스트입니다. 하루에 두 단계 진도가 나간 원인과 기록 비교 방식의 오류를 발견한 것도 AI였습니다.
카드를 하나의 Markdown 파일로 만든 것은 9월 28일 첫 설계 때부터입니다. Obsidian에서 열어도 GitHub에서 열어도 같은 한 장을 읽을 수 있습니다.
Mac에서도 답변하거나 카드를 만들 수 있습니다. Claude Code의 명령어를 실행하기 전에, 카드 리포지토리를 가져와서 사용하고 끝나면 다시 돌려줍니다. 같은 카드를 Mac과 스마트폰으로 같은 날 작성하면 충돌이 발생하므로, 매일의 답변은 스마트폰에 맡기고, Mac에서는 새로운 카드를 만들거나 깊이 있게 되돌아볼 때만 사용합니다.
테스트는 현재 106건 있습니다. 네트워크에는 연결하지 않고, GitHub와 Claude를 가짜로 대체하여 작동시킵니다.
다음 시간에는 사용을 시작한 후 막히고 고친 것과, 글을 쓰기 전에 테스트를 통해 발견한 두 가지 차이점, 그리고 10일간의 수치를 작성하겠습니다.
-
Claude API 요금
-
Mermaid 상태 전이 다이어그램
-
Mermaid 시퀀스 다이어그램
-
Obsidian