# RAG 의 바탕으로 쓰기 — 소비자 안내 (W444 · ADR-0047)

> 엔진은 한글 문서의 내용을 정확히 읽어 **근거가 붙은 조각**으로 내준다. 답을 짓는 LLM 은 소비자의 것이다.
> 계약은 `docs/schema.md`(문서 JSON)·`docs/api.md`(HTTP)이고, 이 글은 **어느 문을 쓰면 되나**다.

## 세 길

```
① 읽기만   파일 ─▶ POST /parse  ─▶ 문서 JSON            제 벡터 DB·검색기·LLM 으로
           파일 ─▶ POST /chunk  ─▶ 문서 + 조각(근거)      (이음매는 조각 · ADR-0041)

② 찾기까지 문서를 테넌트에 넣는다 ─▶ POST /documents/upload?tenant=갑   (키가 있는 서버)
           질문 ─▶ POST /context ─▶ 번호 붙은 근거 묶음  ─▶ 제 LLM 의 프롬프트에 그대로

③ 답까지   질문 ─▶ POST /answer ─▶ 발췌 답(지어내지 않음) 또는 llm:true 로 게이트웨이(인용 검증)
```

## ② 를 쓰는 법 — 가장 많이 쓸 길

```bash
# 1) 문서를 테넌트에 넣는다 (한 번) — 바로 읽어 상태를 돌려준다
curl -s -X POST "https://서버/documents/upload?tenant=갑" -H "Authorization: Bearer $KEY" \
     -H 'X-Filename: %EA%B3%B5%EA%B3%A0.hwpx' --data-binary @공고.hwpx
# {"tenant":"갑","filename":"공고.hwpx","bytes":48213,"stored":true,"doc_id":"sha256:…","readable":true,"reason":null,"chunks":12}

# 2) 질문마다 근거 묶음을 받는다
curl -s https://서버/context -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
     -d '{"tenant":"갑","query":"신청기간은?","budget":4000}'
```

응답의 `context` 는 프롬프트에 그대로 붙이는 글이다:

```
[1] 공고.hwpx · 3쪽 · 2026년 지원사업 공고 > 3. 신청기간
신청기간은 8월 28일까지다.

[2] 공고.hwpx · 1쪽 · 2026년 지원사업 공고 > 1. 지원대상
지원대상은 반도체 소부장 기업이다.
```

`sources[n-1]` 이 `[n]` 의 출처다 — `doc_id`·`filename`·`anchors`(쪽·bbox·칸 주소)·`extractors`(OCR 로 읽었는지)·`cut`(잘렸는지).
LLM 에게 「근거 번호를 들어 답하라」고 지시하면, 답의 `[2]` 를 `sources[1].anchors` 로 원문 위에 가리킬 수 있다 (`/view` 가 하는 일).

**지시문은 소비자가 붙인다.** 엔진은 근거만 준다 — 챗봇·업무자동화·평가 서비스가 저마다 다른 지시를 쓴다. 권하는 뼈대:

```
아래 근거 안에서만 답하라. 근거에 없으면 "문서에서 찾지 못했다"고 답하라. 답의 문장마다 근거 번호 [n]을 달아라.
{context}
질문: {question}
```

**답을 지은 뒤에는 `/verify` 로 되묻는다** (W449) — 게이트웨이가 제 답에 거는 규칙 그대로 남의 답을 본다:

```bash
# 3) LLM 의 답을 근거에 대고 본다 — sources 는 2) 의 sources[] 를 그대로 (doc_id + chunk_id · 조각 id 는 문서 안에서만 뜻이 있다)
curl -s https://서버/verify -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
     -d '{"tenant":"갑","answer":"신청기간은 8월 28일까지입니다 [1].","sources":[{"doc_id":"sha256:…","chunk_id":"c0001"}]}'
# {"grounded":true,"sentences":[{"text":"…","refs":[1],"numbers":["28"],"unsupported":[]}],"unknown_refs":[],…}
```

`unsupported` 에 든 숫자는 **근거 어디에도 없는 수**다 — 금액·날짜·건수를 지어낸 자리이고, 그 문장을 버리거나 사람에게 보이는 것은
소비자의 몫이다. 게이트웨이는 같은 자리에서 발췌로 물러난다 (`gateway/answer.py`).

- `budget` 은 **글자** 수다 (기본 4,000 · 최대 40,000). 토큰은 소비자의 토크나이저로 센다 — 한글은 글자 하나가 토큰 1~2개쯤이다.
- `truncated: true` 면 예산에 안 든 조각이 있었다는 뜻이다 — 예산을 늘리거나 `limit` 을 줄인다. 조용히 빈 손을 주지 않는다.
- OCR 로 읽은 조각은 `extractors` 에 `ocr` 이 든다 — 숫자·날짜를 쓸 때는 사람이 한 번 보게 하는 것이 안전하다 (스캔 글자 오차 0.19).

## ① 를 쓰는 법 — 제 검색기가 있을 때

`/chunk` 가 주는 조각마다 `text`·`blocks`·`anchors`·`path`·`kind` 가 있다. 제 벡터 DB 에 `text` 를 넣고 **나머지를 그대로 메타데이터로**
들고 가면, 어떤 검색기를 쓰든 인용(쪽·좌표)이 따라간다. 조각의 글자 수는 `?budget=` 으로 준다 (기본은 `noda.chunk` 의 것).

## 조심할 것

- **못 읽어도 200** 이다 — `access.readable=false · reason` 이 까닭이다. 그 문서는 조각이 0개다.
- 테넌트 문서를 지우면(`DELETE /documents`) 색인에서는 바로 사라지지만 **파싱 캐시의 글자**는 `tools/retention.py purge` 로 지운다.
- 엔진은 **업무 해석을 안 한다** (규칙 9) — 「회신 기한이 언제냐」는 근거를 받은 LLM 이 답한다. 엔진이 내는 것은 문서가 스스로 적은 것
  (제목·조항·붙임·문서번호·시행일·발신·수신)뿐이다.
