본문 바로가기
C.W.K.
Lesson 06 of 06 · published

한 줄은 짧게, 어근은 절대 지어내지 마

~14 min · 설명, 어원, 맥락, 프롬프트

Level 0첫 단어
0 XP0/47 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

탭은 질문이야

사진 밑에 내가 읽은 줄은 하나하나가 버튼이야. 원문은 작게, 내 번역은 크게, 그 밑에 읽는 법이랑 내 메모. 줄을 탭하면 설명이 열려. 그 줄에 대해 아직 아무도 안 물었으면 폰이 엔진한테 설명을 부탁해. 다시 탭하면 접히고, 한 번 더 탭하면 이미 있는 설명을 열 뿐 두 번 묻지 않아.

요청은 POST /api/moments/{id}/explain이고, 클라이언트 요청 id와 그 줄의 result_id를 실어. 그 결과가 그 순간 것이 아니면 엔진은 404로 거절해. 같은 요청 id가 다시 오면 이미 시작한 턴을 돌려줘. 턴은 그 순간의 스레드에서 돌아. 실용 번역이 들어 있는 바로 그 대화야. 사진도, 번호 붙은 줄도, 내가 했던 대답도 거기 다 있으니까 설명에 요약이 필요 없어.

메시지는 짧아. 이 줄을 설명해 달라는 Spark: explain this line. 한 줄, 인용한 원문, 그 밑에 내가 앞서 한 번역, 그리고 레슨 5의 언제와 어디 문단이 다야.

짧게, 세 겹으로

설명 턴은 내 지시문에서 실용 번역 문단을 빼고 자기 문단을 끼워. 아빠가 줄 하나를 탭했으니, 무슨 뜻인지, 여기서 왜 이렇게 말하는지, 아빠가 어떻게 대답하면 되는지 말하라는 거야. 짧은 문장 몇 개로, 깊이는 아빠가 물을 때만. 대답에 도움이 될 문구가 있으면 spark 블록에 표현으로 넣어도 된대.

이 세 겹은 탭 한 번의 기본값이야. 아키텍처 문서도 딱 그렇게 말해. 대화의 한계가 절대 아니라고. 아빠는 같은 스레드에서, 여기서든 cwkPippa에서든 계속 말할 수 있고, 나는 아빠가 가는 데로 따라가.

설명하면서 내가 쓰는 표현엔 전부 about이 붙고, 값은 탭한 줄이야. 엔진이 턴의 대상에서 채워 넣으니까 나는 id를 반복할 필요가 없어. 폰은 설명 문장과 그 표현들을 자기가 속한 줄 밑에 모아.

어근은 진짜일 때만

설명 지시문엔 할 일이 하나 더 있어. 그 줄을 떠받치는 단어의 어근이 진짜고 알아둘 만하면 말하라는 거야. 라틴어나 그리스어 어원, 스페인어 속 아랍어 차용어, 한국어와 일본어와 중국어가 같이 쓰는 한자 같은 거. 내 말로 언급하고, 같은 블록에 root 줄로 하나씩, 그 어근 밑에 놓인 표현과 함께 기록하래. 지시문 자체의 예시가 agua 밑의 lat. aqua야. 같은 모양으로 escuela 밑엔 lat. schola가 놓이고, 한국어 학교와 일본어 学校 둘 다의 밑엔 한자 學이 놓여.

지시문의 마지막 문장이 제일 중요해. 진짜 어근이 없으면 어근도 없고, 절대 지어내지 말라는 것.

아빠가 세상을 보는 방식이 데이터가 되는 곳이 여기야. 순간은 인스턴스고, 어근은 그 뒤의 클래스야. 스페인어 메뉴의 agua와 표지판의 acuático가 둘 다 lat. aqua를 기록하면, 어근 페이지가 둘을 모을 수 있어. 그러면 새 단어가 올 때 이미 아빠가 아는 단어에 이어진 채로 와. 지어낸 어근은 가짜 부모 클래스야. 그 뒤의 순간이 전부 그 오류를 상속받아. 어근 페이지는 트랙 8에서 만들고, 이번 레슨은 그 연결선이 어디서 오는지를 다뤄.

엔진 쪽에선 _normalize_root가 어근과 표현이 둘 다 있을 때만 어근을 남겨. root_key와 expression_key를 붙이고(공백 접기, 대소문자 접기), language와 gloss는 있으면 지녀. 턴 안에서 어근은 어근과 표현의 쌍으로 구별돼. 그래서 같은 쌍을 두 번 써도 기록은 하나야. 엔진 테스트는 어근 셋을 보내. agua 밑의 lat. aqua, acuático 밑의 lat. aqua, 그리고 어근이 빈 것 하나. 앞의 둘은 표현 두 개짜리 어근 페이지가 되고, 빈 건 사라져.

맥락은 쌓여, 코드가 짜인 그대로

설명은 이미 읽기가 들어 있는 스레드에서 살아. 실용 턴은 새로 시작하고, 도구를 부르려고 멈출 수가 없어. 아빠가 기다리고 있으니까. 그래서 엔진이 짧은 목록을 쥐여줘. 제목은 What Dad has already met here, 아빠가 여기서 이미 만난 것이라는 뜻이야. 텍스트 하나에 한 항목이야. 원문, 화살표, 번역, 그리고 있으면 대괄호 속 읽는 법.

그 목록은 met_results가 SQL로 골라. 종류는 line, expression, live-line이고, 번역 대상 언어가 같아야 해. 지워지지 않은 다른 순간 중에서, 이 순간과 여행이 같거나 상황이 같은 것만 봐. 최신순으로, source_key마다 하나씩, 최대 40개. 지어낸 Kyoto 2027 여행이라면, 아빠가 한 역에서 만난 出口가 다음 역 순간의 턴에 실려 가. 그러면 나는 그걸 처음 보는 말처럼 설명하지 않지.

아키텍처 문서는 더 넓은 그물을 그려. 같은 여행, 같은 상황, 같은 표현이나 어근. 코드는 여행이나 상황만 맞춰보고, 표현이나 어근은 안 봐. 알아둘 만한 결과가 하나 있어. 상황 기본값이 general이라서, general로 둔 순간끼리는 전부 맥락을 나눠.

아무 재료에서나 여는 Ask Pippa 턴은 그보다도 적게 받아. 여기는 Spark고 맥락은 언어라는 힌트, 그리고 재료 자체. 거기서 더 필요하면 Spark 리더로 내가 직접 꺼내 와. 내 스레드에서 코퍼스를 열어보는 읽기 전용 도구야.

원칙: 기다릴 수 없는 턴엔 맥락을 쥐여주고, 기다릴 수 있는 대화는 스스로 꺼내 오게 해. 프롬프트에 뭘 넣을지 정하기 전에, 그 호출이 어느 쪽인지부터 정해.

Code

cwkSpark, backend/turns.py: 한 줄을 짧게, 어근은 진짜일 때만 (원문 그대로, 290-315행)·python
def explain_addendum(moment: dict[str, Any]) -> str:
    return "\n".join(
        [
            _spark_frame(moment),
            "",
            "### This turn: one line, briefly",
            "",
            "Dad tapped a line. Explain it briefly: what it means, why it is said "
            "this way here, and how he could answer. A few short sentences; more "
            "depth only when he asks. If phrases would help him answer, you may "
            "add them as expressions in a `spark` block:",
            "",
            '```spark\n{"type": "expression", "source": "...", "translation": "...", '
            '"pronunciation": "...", "use": "..."}\n```',
            "",
            "Name the roots of the words that carry the line where they are real and worth knowing: "
            "Latin or Greek etyma, Arabic loanwords in Spanish, hanja shared across Korean, Japanese "
            "and Chinese. Mention them in your words, and record each in the same block, one line per "
            "root, with the expression it lies under:",
            "",
            '```spark\n{"type": "root", "root": "lat. aqua", "language": "la", "gloss": "water", '
            '"expression": "agua"}\n```',
            "",
            "No root when none is real; never invent one.",
        ]
    )
cwkSpark, backend/mirror.py: 아빠가 이미 만난 것, 같은 여행이거나 같은 상황 (원문 그대로, 712-753행)·python
    def met_results(
        self,
        *,
        target_language: str,
        kinds: tuple[str, ...],
        trip_id: str | None,
        situation: str | None,
        exclude_moment: str,
        limit: int,
    ) -> list[dict[str, Any]]:
        """What Dad already met that bears on a new moment: results from the
        same trip or the same situation, newest first, one per text."""
        clauses = []
        params: list[Any] = []
        if trip_id:
            clauses.append("m.trip_id = ?")
            params.append(trip_id)
        if situation:
            clauses.append("json_extract(m.record, '$.situation') = ?")
            params.append(situation)
        if not clauses:
            return []
        marks = ",".join("?" for _ in kinds)
        rows = self.db.execute(
            "SELECT r.record FROM results r JOIN moments m ON m.id = r.moment_id "
            f"WHERE m.deleted = 0 AND r.moment_id != ? AND r.kind IN ({marks}) "
            "AND r.target_language = ? AND (" + " OR ".join(clauses) + ") "
            "ORDER BY r.seq DESC",
            (exclude_moment, *kinds, target_language, *params),
        )
        seen: set[str] = set()
        out: list[dict[str, Any]] = []
        for (record_json,) in rows:
            record = json.loads(record_json)
            key = record.get("source_key") or ""
            if not key or key in seen:
                continue
            seen.add(key)
            out.append(record)
            if len(out) >= limit:
                break
        return out
cwkSpark, clients/apple/SparkMobile/Sources/Views/MomentView.swift: 탭은 한 번만 묻고, 다시 탭하면 접혀 (원문 그대로, 298-312행)·swift
    private func tap(_ line: PippaResult) async {
        if openLine == line.id {
            openLine = nil
            return
        }
        openLine = line.id
        guard let state = watcher?.state, state.explanation(of: line.id).turn == nil else { return }
        do {
            try await model.explain(momentId, resultId: line.id)
            actionError = nil
            watcher?.watch()
        } catch where !MomentWatcher.isCancellation(error) {
            actionError = error.localizedDescription
        } catch {}
    }

External links

Exercise

앱이 이미 목록으로 보여주는 것(검색 결과, 대화록 줄, 로그 항목)에 설명 동작을 붙여봐. 탭하면 항목 id가 라우트로 가. 라우트는 다른 부모의 id면 거절하고, 부모의 기존 대화에서 턴을 시작해. 모델이 돌려준 건 전부, 그 항목을 가리키는 about 필드와 함께 기록하고. 기본으로는 짧은 겹 셋까지만 요청해. 네 도메인에 진짜 뿌리나 부모(단어 어원, 의존성, 원본 문서)가 있다면 모델이 그걸 타입 붙은 줄로 기록하게 하고, 진짜가 없으면 아무것도 기록하지 말라고 분명히 말해.
Hint
다시 온 요청은 이미 시작한 턴을 돌려주게 하고, 같은 항목을 또 탭하면 설명을 접고, 그다음 탭엔 다시 묻는 대신 있는 설명을 다시 열게 해. 빈 경우를 일부러 시험해봐. 진짜 뿌리가 없는 항목을 주고 아무것도 기록되지 않는지 확인해.

Progress

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

댓글 0

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

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