Files
kebab/docs/components/llm
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
..

LLM

텍스트 + vision 생성 모델 인터페이스. LanguageModel trait + Ollama HTTP 어댑터. streaming 결과 + 항상 Done 종료 보장.

구성 crate

Crate 역할
kebab-llm LanguageModel trait re-export + MockLanguageModel (feature mock, default OFF). 새 type 추가 금지 — 순수 facade.
kebab-llm-local OllamaLanguageModelreqwest::blocking 기반 Ollama POST /api/generate 어댑터. line-delimited JSON streaming 디코드.

구조

classDiagram
    class LanguageModel {
        <<trait kebab-core>>
        model_ref() ModelRef
        context_tokens() usize
        generate_stream(req) Iterator~Result~TokenChunk~~
    }
    class GenerateRequest {
        system: String
        user: String
        stop: Vec~String~
        max_tokens: usize
        temperature: f32
        seed: Option~u64~
        images: Vec~String~ [base64]
    }
    class TokenChunk {
        <<enum>>
        Token(String)
        Done {finish_reason, usage}
    }
    class FinishReason {
        <<enum>>
        Stop
        Length
        Aborted
        Error(String)
    }
    class OllamaLanguageModel {
        +new(cfg) Result~Self~
        -client: reqwest::blocking::Client
        -endpoint: String
        -model: String
        -context_tokens: usize
    }
    class MockLanguageModel {
        feature = "mock"
        deterministic test double
    }
    class LlmError {
        <<error>>
        ConnectionRefused
        HttpStatus
        Decode
        ...
    }
    LanguageModel <|.. OllamaLanguageModel
    LanguageModel <|.. MockLanguageModel
    LanguageModel ..> GenerateRequest
    LanguageModel ..> TokenChunk
    TokenChunk ..> FinishReason
    OllamaLanguageModel ..> LlmError

Data flow

flowchart LR
    Req["GenerateRequest<br/>{system, user, stop,<br/>max_tokens, temp, seed, images}"]
    Wire["JSON wire<br/>POST /api/generate<br/>stream: true"]
    Lines["line-delimited JSON<br/>frames"]
    Token["TokenChunk::Token(...)"]
    DoneOk["TokenChunk::Done<br/>{finish_reason: Stop|Length, usage}"]
    DoneErr["TokenChunk::Done<br/>{finish_reason: Error/Aborted, usage}"]
    Iter["Iterator~Result~TokenChunk~~<br/>(lazy, Send)"]
    Req --> Wire --> Lines
    Lines -->|frame| Token --> Iter
    Lines -->|done frame| DoneOk --> Iter
    Lines -->|error/abort| DoneErr --> Iter
    Caller["caller (kebab-rag)<br/>+ assert_finish_chunk"] --> Iter

주요 type / trait / 함수

Trait (kebab-core, re-export kebab-llm):

  • LanguageModel::model_ref() -> ModelRef — provider/model/version 식별. Answer.model_ref 으로 흘려서 wire payload 가 자가 식별.
  • LanguageModel::context_tokens() -> usize — 모델 별 max prompt+completion 합. RAG 가 budget 계산에 사용.
  • LanguageModel::generate_stream(req: GenerateRequest) -> Result<Box<dyn Iterator<Item = Result<TokenChunk>> + Send>> — async 안 됨, 매 next() 가 blocking. 모든 stream 이 마지막에 TokenChunk::Done 으로 끝남 (error 케이스 포함, §0 Q5).

GenerateRequest (kebab-core::traits):

  • images: Vec<String> (base64) — 빈 vec = text-only path. 비어있지 않으면 vision-capable adapter 가 images: [...] 로 wire 에 포함 (Ollama). 다른 backend 는 다르게 라우팅. #[serde(default)] — older snapshot 호환.

TokenChunk:

  • Token(String) — partial text. 누적은 caller 책임.
  • Done { finish_reason: FinishReason, usage: TokenUsage } — 항상 마지막. finish_reason::Aborted 가 cancel signal, Error(s) 가 mid-stream 실패.

OllamaLanguageModel (kebab-llm-local::ollama):

  • OllamaLanguageModel::new(&kebab_config::Config) -> anyhow::Result<Self>config.models.llm.endpoint + model + context_tokens + temperature + seed 읽음. lazy connect — network 안 침, 첫 generate_stream 에서 실패 surface.
  • 내부: reqwest::blocking::Client — top-level async 표면 없음. (참고: reqwest 0.12 의 blocking 이 private current-thread tokio runtime wrap 해서 cargo tree 에 tokio 보임. invariant = "top-level tokio dep 없음 + async surface 노출 안 함".)
  • streaming decode: BufReader::lines() 위에서 serde_json::from_str 로 frame 별 lazy parse → TokenChunk::Token yield.

LlmError (kebab-llm-local::error):

  • ConnectionRefused / HttpStatus(code) / Decode(json error) / Timeout / Aborted / 그 외 — Err 로 first chunk 전 surface 가능.

테스트 도구 (kebab-llm):

  • assert_finish_chunk(chunks: &[TokenChunk]) — 마지막이 Done 이어야 — 모든 stream contract pin.
  • MockLanguageModel (feature mock, default OFF) — deterministic test double. 실 adapter 만 Err 가능, mock 은 항상 stream 시작 후 yield.

외부 의존

  • kebab-llmkebab-core 만 (re-export crate).
  • kebab-llm-localkebab-llm + kebab-config, reqwest (blocking feature, JSON), serde + serde_json, thiserror, anyhow.
  • 외부 서비스: Ollama HTTP (default http://127.0.0.1:11434). default 모델 gemma4:e4b (OCR / caption / RAG 모두 같은 family — 단일 모델 다운로드면 전 시스템 동작).

핵심 결정

  • kebab-llm = trait re-export only, 새 type 금지. : kebab-rag 등 downstream 이 use kebab_llm::LanguageModel 안정 surface 의존. 어댑터 (Ollama/llama.cpp/candle) 는 별 crate. swap config-only.

  • synchronous + blocking + stream iterator. : §0 Q5 가 streaming 명시. async 가 trait object 와 잘 안 맞음 (Rust async-in-trait 안정성 + Send bound 복잡). reqwest::blocking + line-delimited frame 의 Iterator 가 caller 코드 단순. RAG 가 동기 소비 + UI thread 가 별도 worker 로 spawn.

  • 모든 stream 이 Done 으로 끝남 (error 포함). : caller 가 partial accumulation 한 텍스트 + finish reason 함께 받음. Done(Error) vs Iterator::next() = None 차이가 contract 명확. assert_finish_chunk 가 invariant pin.

  • lazy connect (생성자에서 network 안 침). : kebab init / kebab doctor 가 Ollama 안 떠도 동작해야 함. 첫 generate_stream 에서 Err 가 나는 게 사용자 기대 — startup 이 죽으면 진단 어려움.

  • Ollama 가 default backend. : macOS / Linux 모두 single-binary install, GGUF 모델 다운로드 한 줄. local-first 의 핵심. llama.cpp / candle 어댑터는 future P+ — LanguageModel trait 그대로라 swap 가능.

  • default 모델 gemma4:e4b (OCR / caption / RAG 통일). : OCR (P6-2) + caption (P6-3) + RAG 가 같은 family 사용 → 사용자가 모델 1개만 ollama pull. variant (gemma4:26b 등) 으로 override 가능. (HOTFIXES P6-2.)

  • GenerateRequest.images: Vec<String> 추가 (P6-3). : 기존 trait 가 text-only 였는데 caption 이 vision 필요. base64 image vec 으로 wire 형식 통일 — Ollama 의 images 필드와 1:1. text-only caller 모두 images: Vec::new() 마이그레이션 + #[serde(default)] 로 snapshot 호환. (HOTFIXES P6-3.)

  • MockLanguageModelErr 안 던짐. : mock 은 deterministic — first chunk 전 connection-refused 같은 케이스 시뮬레이션 안 함. 실 adapter (OllamaLanguageModel) 가 그 분기 책임. RAG 의 RefusalReason::LlmStreamAborted 분기 (p9-fb-15) 는 실 adapter 만 trigger.

관련 spec / HOTFIXES