Files
kebab/docs/components/rag/README.md
th-kim0823 af8c162e09 docs(components): per-group contributor reference (12 그룹)
docs/components/<group>/README.md 12 페이지 + 인덱스 작성. 각 그룹
페이지가 구성 crate 표 + 구조 mermaid + data flow mermaid + 주요
type/trait/함수 시그니처 + 외부 의존 + 핵심 결정 (HOTFIXES + spec
의 "왜" 통합) + 관련 spec/HOTFIXES 링크. 인덱스가 그룹 wiring
다이어그램 + 진입 가이드 보유.

ARCHITECTURE.md 의 ASCII crate 의존 그래프를 mermaid flowchart 로
교체 (등가 정보, Gitea/GitHub 자동 렌더). docs/components/ 진입
링크 추가.

이 layer 는 contributor 향 — 사용자 향 grand picture 는 README.md
의 logical-architecture diagram 그대로 유지. 진척도는 HANDOFF.md,
per-task spec 은 tasks/INDEX.md 가 기존대로 source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:05:32 +09:00

8.9 KiB

RAG

Retrieval-Augmented Generation pipeline. retrieve → gate → pack → generate → cite-validate → persist 단일 orchestrator. multi-turn 지원 (p9-fb-15) + cancel-safe streaming.

구성 crate

Crate 역할
kebab-rag RagPipeline + AskOpts. retriever / LLM / docs store 를 trait object 로 inject 받아 single-threaded 실행. concrete adapter 의존 금지 (kebab-llm-local / kebab-embed-local / kebab-store-vector 직접 사용 금지 — 모두 trait 너머).

구조

classDiagram
    class RagPipeline {
        +new(cfg, retriever, llm, docs) Self
        +ask(query, opts) Answer
        +ask_with_history(query, history, conv_id, turn, opts) Answer
        -config: Config
        -retriever: Arc~dyn Retriever~
        -llm: Arc~dyn LanguageModel~
        -docs: Arc~SqliteStore~
    }
    class AskOpts {
        k: usize
        explain: bool
        mode: SearchMode
        temperature: Option~f32~
        seed: Option~u64~
        stream_sink: Option~Sender~String~~
        history: Vec~Turn~
        conversation_id: Option~String~
        turn_index: Option~u32~
    }
    class Answer {
        text
        citations: Vec~AnswerCitation~
        refusal_reason: Option~RefusalReason~
        retrieval_summary
        conversation_id
        turn_index
        model_ref
        usage
        trace_id
    }
    class RefusalReason {
        <<enum>>
        NoChunks
        ScoreGate{top_score, threshold}
        LlmStreamAborted
        Other(String)
    }
    RagPipeline ..> AskOpts
    RagPipeline ..> Answer
    Answer ..> RefusalReason

Data flow — pipeline stages

flowchart LR
    Q["query + AskOpts"]
    Expand["1. query expansion<br/>(history 의 직전 answer<br/>첫 200 chars concat)"]
    Retrieve["2. Retriever.search<br/>(k = max(opts.k, default_k))"]
    Gate["3. score gate<br/>top_score >= rag.score_gate?"]
    Pack["4. pack context<br/>chunks 를 [근거 N] 블록으로<br/>+ Citation 보존"]
    History["5. prepend [이전 대화]<br/>(p9-fb-15, char budget 내<br/>oldest drop)"]
    Render["6. render prompt<br/>system + [이전 대화]<br/>+ query + [근거 N..N+m]"]
    Gen["7. LanguageModel<br/>.generate_stream"]
    Stream["stream_sink 로 token 전송<br/>(sink drop = silent swallow)"]
    Validate["8. cite-validate<br/>본문의 [N] 마커 →<br/>AnswerCitation 매핑"]
    Persist["9. answers 행 INSERT<br/>(refusal 도 항상 persist)"]
    Out["Answer<br/>+ refusal_reason"]
    Q --> Expand --> Retrieve --> Gate
    Gate -->|empty hits| Refuse1["NoChunks refusal"] --> Persist
    Gate -->|top < gate| Refuse2["ScoreGate refusal"] --> Persist
    Gate -->|pass| Pack --> History --> Render --> Gen --> Stream
    Gen -->|abort/error| Refuse3["LlmStreamAborted"] --> Persist
    Gen -->|complete| Validate --> Persist
    Persist --> Out

주요 type / trait / 함수

RagPipeline (kebab-rag::pipeline):

  • RagPipeline::new(config: Config, retriever: Arc<dyn Retriever>, llm: Arc<dyn LanguageModel>, docs: Arc<SqliteStore>) -> Self — caller (kebab-app) 가 wire.
  • RagPipeline::ask(&self, query: &str, opts: AskOpts) -> Result<Answer> — single-shot 또는 history 가 빈 multi-turn 첫 호출.
  • RagPipeline::ask_with_history(&self, query, history, conversation_id, turn_index, opts) -> Result<Answer> — convenience: opts 에 3개 필드 stuff 후 ask 호출.

AskOpts (Clone, Debug, PartialEq 안 함Sender 가 PartialEq 구현 안 함):

  • k: usize — retrieval top-k. 실효는 max(opts.k, config.search.default_k) (config default = floor).
  • explain: bool — true 시 answers.packed_chunks_json 에 packed-context JSON 저장. refusal 은 항상 persist.
  • mode: SearchMode — pipeline 내부에서 mode 안 정함, caller 가 inject (lexical/vector/hybrid).
  • temperature / seed: Option<...> — config 기본값 override per call.
  • stream_sink: Option<mpsc::Sender<String>> — 매 TokenChunk::Token 동기 forward. receiver drop 시 SendError silent swallow + 생성 계속 (answers 행 보존).
  • history: Vec<Turn> (p9-fb-15) — newest-first prepended [이전 대화] 블록. cfg.rag.max_context_tokens * 4 문자 budget 초과 시 oldest 부터 drop.
  • conversation_id / turn_index (p9-fb-15) — Answer.conversation_id / turn_index 로 흘러가서 wire payload 가 same-conversation 식별 가능.

Answer (kebab-core::answer, re-export kebab-rag):

  • text + citations: Vec<AnswerCitation> + refusal_reason: Option<RefusalReason> + retrieval_summary: AnswerRetrievalSummary + conversation_id + turn_index + model_ref + usage + trace_id.
  • RefusalReason: NoChunks (retrieval 비음), ScoreGate { top_score, threshold } (낮은 신뢰), LlmStreamAborted (mid-stream 중단, p9-fb-15), Other(String).

상수 / 헬퍼 (pipeline.rs):

  • SYSTEM_PROMPT_RAG_V1prompt_template_version 가 가리키는 system prompt. 변경 시 cascade per §9.
  • expand_query_with_history(query, &history) -> String — 직전 answer 첫 200 chars concat. LLM-based standalone-question rewriting 은 out of scope (P+).

외부 의존

  • crate dep: kebab-core + kebab-config + kebab-search (Retriever trait 만) + kebab-llm (trait 만) + kebab-store-sqlite (DocumentStore + put_answer helper).
  • 외부 lib: serde/serde_json, regex (citation marker [N] 매칭), time (timestamps), blake3 (TraceId 채굴), thiserror, anyhow.
  • 외부 서비스: 없음 (concrete adapter 가 가져옴).

핵심 결정

  • single-threaded synchronous orchestrator. : pipeline 의 9 stages 가 다 sequential dependency. 동시성 가치 없음. async 도입하면 caller (kebab-app, TUI worker) 가 자체 thread spawn — 단순함이 깨짐. LanguageModel::generate_stream 의 blocking iterator 가 자연스럽게 fit.

  • opts.k = max(opts.k, config.search.default_k) floor. : 사용자가 kebab ask --k 0 같은 실수 시 retrieval starvation. config default 가 floor → "내가 더 넓히려면 높은 값 pass" 만 의미 있게 동작.

  • stream_sink drop = silent swallow (abort 안 함). : TUI 가 cancel 누르면 receiver drop. pipeline 이 즉시 abort 하면 answers 행 persist 안 됨 → debug 어려움. 끝까지 generate 후 row write, sink 만 무시 = answers 보존 + UX 자연스러움.

  • refusal 도 항상 answers 행 INSERT. : 운영 분석 필수 — score gate 가 너무 높아서 거부 비율 분석, ScoreGate top_score 분포 등. row 부재 = 회고 불가능. row write 실패는 tracing::warn! 만 (caller 는 in-memory Answer 받음).

  • 모든 hit 의 chunk fetch 실패 시 NoChunks refusal collapse. : search → pack 사이 chunks 삭제 (다른 process 의 reset 등) 발생 시 빈 [근거] 블록을 LLM 에 보내면 self-refusal — 진단 misleading. 구조 원인을 알면 명시적 NoChunks 가 정확.

  • history query expansion = 직전 answer 첫 200 chars concat. : full LLM-based standalone-question rewriting 이 정확하지만 한 번 더 LLM call → latency 2배 + 비결정. 200 chars concat 이 cheap deterministic, retrieval 확장 효과 충분 (대부분의 follow-up "그것" / "그게 뭐였지" 가 직전 answer 키워드 caret). spec §3.8 가 LLM-based 를 P+ 로 marking.

  • conversation_id / turn_index optional. : single-shot ask 가 절반 이상의 사용. 빈 Vec::new() history 와 함께 None / None 으로 ask 호출 = 기존 behavior 동일. multi-turn caller (ask_with_history) 만 채움.

  • prompt budget 초과 시 oldest history drop (newest-first 보존). : 최근 turn 가 follow-up 컨텍스트 핵심. budget = cfg.rag.max_context_tokens * 4 (chars-per-token proxy). spec §3.8.

  • forbidden deps: kebab-llm-local / kebab-embed-local / kebab-store-vector 직접 사용 금지. : pipeline 가 trait 만 의존하면 test 가 mock 으로 swap 가능. 어댑터 직접 import = 테스트가 ONNX 모델 다운로드 / Ollama 서버 / LanceDB 디렉토리 필요. 단위 테스트 격리 보장.

관련 spec / HOTFIXES