Field Log · Entry

LLM은 Wiki 문서를 어떻게 찾는가: 검색 개선과 효율적인 구축법

프로젝트 문서와 세션이 검색 인덱스와 근거 선택 계층을 거쳐 LLM의 짧은 문맥으로 전달되는 LLM Wiki 검색 구조

이번 글의 결론

  • LLM은 Wiki 전체를 매번 읽지 않는다. 검색기가 질문과 관련된 작은 근거 묶음을 먼저 고르고, LLM은 그 결과만 읽는 것이 기본 구조다.
  • 원본 문서, 사람이 읽는 Wiki, 검색용 인덱스는 서로 다른 자산이다. 원본은 기존 저장소에 두고 인덱스는 언제든 재생성할 수 있는 파생물로 관리해야 한다.
  • 검색 개선은 embedding model 교체부터 시작하지 않는다. 검색 범위, stable ID, metadata, lexical baseline, 중복 제거와 평가셋을 먼저 고친다.
  • Full-text search와 embedding은 경쟁 관계가 아니다. Exact token과 의미 유사성을 병렬로 찾고, 서로 다른 점수 대신 순위를 RRF로 결합할 수 있다.
  • 실제 개인 LLM Wiki 검색 계층에서는 증분 snapshot, 영속 FTS, project scope, RRF, parent dedupe와 citation gate를 추가해 합성 평가의 Recall@5를 0.942에서 0.987로 높였다. 단, 이는 실제 사용자 정확도가 아니라 회귀 방지 결과다.

“LLM에게 Wiki를 연결했다”는 말을 들으면 거대한 문서 묶음을 prompt에 넣고 모델이 처음부터 끝까지 읽는 장면을 떠올리기 쉽습니다. 작은 폴더에서는 그렇게 시작할 수 있지만, 운영 가능한 Wiki는 반대로 설계합니다.

먼저 일반 검색 엔진이 관련 문서와 구간을 찾습니다. 그다음 제한된 근거만 LLM의 context에 넣습니다. LLM이 하는 일은 전체 저장소 탐색이 아니라 선택된 근거를 비교하고 설명하는 것에 가깝습니다.

이 글에서는 다음 세 질문을 한 번에 다룹니다.

  1. LLM Wiki는 문서를 실제로 어떻게 찾는가?
  2. 검색이 잘 안 될 때 어느 단계부터 개선해야 하는가?
  3. 처음부터 과도한 비용을 쓰지 않고 효율적인 LLM Wiki를 어떻게 구축하는가?

LLM이 Wiki 전체를 읽게 하면 왜 안 될까?

긴 context window가 생겼으니 모든 문서를 한 번에 넣으면 검색기가 필요 없다는 주장이 있습니다. 문제는 “들어간다”와 “잘 사용한다”가 다르다는 점입니다.

Lost in the Middle 연구는 관련 정보의 위치만 바꿔도 긴 context를 사용하는 모델의 성능이 달라질 수 있음을 보였습니다. 중요한 근거가 입력 중간에 있을 때 성능이 떨어지는 경향도 관찰했습니다. Context가 길어지면 비용과 지연시간도 증가하고, 관계없는 문서는 모델이 처리해야 할 distractor가 됩니다.

따라서 Wiki가 커질수록 다음 방식이 낫습니다.

나쁜 기본값
질문 + 모든 문서 → LLM → 답변

운영 가능한 기본값
질문 → 검색 범위 결정 → 후보 검색 → 중복 제거·재정렬
     → 작은 Evidence Bundle → LLM → 답변과 출처

여기서 검색 실패와 답변 실패를 분리할 수 있습니다.

  • 정답 문서가 후보에 없으면 retrieval 실패입니다.
  • 정답 문서가 있는데 순위가 너무 낮으면 ranking 실패입니다.
  • 좋은 근거가 context에 있는데 답이 틀리면 synthesis 실패입니다.
  • 답은 맞지만 원문으로 돌아갈 수 없으면 provenance 실패입니다.

이 구분이 없으면 답변이 틀릴 때마다 더 큰 모델이나 더 많은 token을 투입하게 됩니다. 실제 원인은 누락된 파일, 잘못된 scope, 부적절한 chunk, 중복 결과일 수 있습니다.

검색은 세 시점으로 나뉜다

LLM Wiki 검색을 이해하려면 indexing, retrieval, generation을 분리해야 합니다.

1. Index time
   source → parse → normalize → chunk → metadata → search index

2. Query time
   question → scope → retrieve → fuse → dedupe → rerank → expand

3. Answer time
   evidence bundle → LLM → answer + citations + uncertainty

Index time에는 질문을 알 수 없습니다. 문서를 검색하기 좋은 레코드로 준비합니다. Query time에는 LLM을 부르기 전 관련 근거를 좁힙니다. Answer time에야 LLM이 선택된 근거를 읽습니다.

RAG 원 논문도 모델 내부의 parametric memory와 외부의 검색 가능한 non-parametric memory를 구분했습니다. 오늘날의 LLM Wiki는 여기에 source lifecycle, page state, metadata, 권한과 사람이 검수하는 canonical layer를 더합니다.

1단계: 문서를 검색 가능한 레코드로 만든다

Source inventory와 stable ID

첫 단계는 embedding이 아니라 “무엇을 검색할 것인가”를 확정하는 일입니다.

source_id: project:blog:docs/retrieval.md
project_id: blog
path: docs/retrieval.md
revision: d927c63
content_hash: sha256:...
authority: accepted
updated_at: 2026-08-02T00:00:00Z
acl:
  - team:engineering

최소한 source ID, project, 원본 위치, revision 또는 content hash가 필요합니다. 이 값이 없으면 같은 파일이 이름만 바뀌어 중복 등록되거나, 삭제된 원문이 검색 결과에 계속 남아도 찾기 어렵습니다.

updated_at만으로 변경을 판단하면 파일 복사나 checkout 때문에 불필요한 재인덱싱이 발생할 수 있습니다. 반대로 timestamp가 보존된 채 내용이 바뀌는 source도 있습니다. 작은 Wiki라면 content hash를 함께 쓰는 편이 단순합니다.

Parent document와 chunk를 함께 보존한다

검색은 작은 chunk가 유리하지만 답변은 주변 문맥이 필요합니다. 따라서 chunk를 독립 문서처럼 저장하지 말고 parent와 위치를 보존합니다.

chunk_id: project:blog:docs/retrieval.md#chunk-07
parent_id: project:blog:docs/retrieval.md
heading_path:
  - 검색 개선
  - RRF
ordinal: 7
text: 여러 검색기의 원점수 대신 순위를 결합한다...

이 구조는 세 가지 문제를 해결합니다.

  • 검색 결과에서 같은 원본의 chunk를 합칠 수 있습니다.
  • 선택된 chunk 앞뒤의 section을 나중에 복원할 수 있습니다.
  • 인용을 파일 전체가 아니라 실제 근거 위치에 연결할 수 있습니다.

원문과 검색용 파생 데이터를 구분한다

검색 DB에는 보통 chunk text, title, heading, metadata, FTS token, embedding이 들어갑니다. 이것은 canonical source가 아니라 파생 데이터입니다.

Canonical source
  repository Markdown / document system / session log

Materialized Wiki
  reviewed page / node / relation

Search derivative
  chunks / FTS rows / embeddings / cache

인덱스를 잃어도 원본으로 다시 만들 수 있어야 합니다. 반대로 인덱스에만 존재하고 원본이나 검수된 Wiki에 없는 중요한 지식이 생기면 backup, revision, 삭제 정책이 모호해집니다.

2단계: 질문에 맞는 검색 범위를 먼저 고른다

좋은 검색 모델보다 먼저 효과를 내는 방법은 잘못된 corpus를 검색하지 않는 것입니다.

예를 들어 deployment timeout을 찾을 때 모든 프로젝트의 로그와 문서를 검색하면 같은 단어를 쓰는 무관한 시스템이 후보를 차지할 수 있습니다. 먼저 project, repository, document type, language, time range를 정합니다.

question
→ project scope
→ caller permission
→ source kinds
→ document status / time range
→ retrieval

검색 범위와 권한은 같은 것이 아닙니다.

  • Scope는 관련도를 높이기 위한 범위입니다.
  • Authorization은 사용자가 볼 수 없는 근거를 차단하는 보안 경계입니다.

권한 검사를 top-k 검색 뒤에 하면 unauthorized 문서가 candidate나 cache에 이미 노출될 수 있습니다. Azure AI Search의 document-level access 문서도 permission metadata를 index에 함께 싣고 query time에 결과를 제한하는 구조를 설명합니다. Chunk를 만들었다면 ACL도 parent에만 두지 말고 각 검색 row에 상속해야 합니다.

개인 로컬 Wiki라도 scope는 fail-closed가 좋습니다. 빈 project 목록을 “전체 검색”으로 암묵 변환하지 말고, global search가 필요할 때 등록된 project를 명시적으로 확장합니다.

3단계: 한 가지 검색기로 모든 문서를 찾지 않는다

Full-text search는 정확한 token을 찾는다

FTS 또는 BM25 계열 검색은 함수명, 오류 문자열, 문서 제목, 약어처럼 정확한 token에 강합니다.

KnowledgeIndexer
SQLITE_BUSY
desktop_persistent_session_index_v1
candidate-outside-scope

SQLite FTS5는 phrase, prefix, NEAR, boolean query와 relevance rank를 제공하므로 개인 Wiki의 첫 검색기로 충분한 경우가 많습니다. 별도 검색 서버 없이 애플리케이션 DB 안에서 시작할 수 있다는 것도 장점입니다.

하지만 앱을 다시 열면 과거 대화가 사라진다재시작 후 session index가 비어 있다처럼 표현이 달라지면 lexical overlap이 줄어듭니다.

Embedding search는 표현 차이를 메운다

Embedding은 문장을 vector로 변환해 의미가 비슷한 후보를 찾습니다. 자연어 표현이 다양한 질문에는 유리하지만, 정확한 숫자·식별자·짧은 오류명이 vector 안에서 희석될 수 있습니다.

따라서 다음처럼 역할을 나누는 것이 일반적입니다.

검색기잘 찾는 것놓치기 쉬운 것
FTS/BM25오류명, ID, 함수명, 정확한 구문동의어, 긴 자연어 표현 차이
Embedding같은 의미의 다른 표현희귀 token, version, 숫자
Metadata filterproject, type, status, date, ACL본문 의미
Graph traversal명시된 관계, dependency, hierarchyedge로 표현되지 않은 유사성

BEIR benchmark는 다양한 domain에서 lexical, sparse, dense, late-interaction, reranking 방식을 비교했고 BM25가 여전히 강한 baseline임을 보였습니다. 어떤 retriever도 모든 corpus에서 자동으로 우월하다고 가정하면 안 됩니다.

Chunk에 문서 맥락을 보충할 수 있다

Chunk가 너무 작으면 “이 회사”, “이 단계”, “그 오류”가 무엇을 뜻하는지 사라집니다. 해결책 하나는 title, heading path, parent summary 같은 짧은 context를 검색용 text에 붙이는 것입니다.

Anthropic의 Contextual Retrieval 실험은 chunk별 짧은 설명을 embedding과 BM25 index 양쪽에 추가했습니다. 해당 실험에서는 contextual embedding과 contextual BM25를 함께 썼을 때 top-20 retrieval failure가 줄었지만, 이는 그들이 사용한 corpus와 설정의 결과입니다. 자신의 Wiki에서도 같은 효과가 난다고 가정하지 말고 평가셋으로 확인해야 합니다.

중요한 원칙은 원문을 LLM summary로 교체하지 않는 것입니다.

raw chunk: 정확한 문자열과 검증용
contextual text: 검색 recall 보조
parent document: 최종 주변 문맥 복원

4단계: 서로 다른 검색 결과를 결합한다

FTS의 BM25 score와 embedding cosine similarity는 의미와 범위가 다릅니다. 두 숫자를 그대로 더하면 scale이 큰 검색기가 결과를 지배할 수 있습니다.

Reciprocal Rank Fusion은 원점수 대신 각 검색기의 순위를 사용합니다.

RRF(document) = Σ weight_i / (k + rank_i(document))

여러 검색기에서 반복해서 상위에 나온 문서가 높은 값을 얻습니다. RRF 원 논문은 여러 IR ranking을 단순한 reciprocal rank 합으로 결합하는 방법을 제시했습니다.

가중치도 처음부터 복잡하게 학습할 필요는 없습니다.

exact alarm query   → lexical weight 상향
개념 설명 query     → semantic weight 상향
recent change query → Git/session retriever 활성화

명백한 query는 규칙으로 routing하고, 어떤 source를 찾아야 할지 모호할 때만 LLM planner를 쓰면 비용과 실패 표면을 줄일 수 있습니다.

5단계: 결합 뒤에 중복과 편중을 제거한다

Top-k chunk를 그대로 LLM에 넣으면 한 문서가 결과를 독점할 수 있습니다. 같은 parent의 인접 chunk가 1위부터 8위까지 차지하면 다른 관점의 근거가 사라집니다.

권장 순서는 다음과 같습니다.

retriever-local ranking
→ RRF
→ exact candidate dedupe
→ parent-document cap
→ source cap
→ authority/conflict 처리
→ reranking
→ neighbor context expansion

Parent cap은 검색 점수를 올리는 기술이 아니라 evidence diversity를 지키는 기술입니다. Source cap도 Slack이나 하나의 큰 Wiki가 모든 후보를 점유하는 것을 막습니다.

Authority는 관련도와 별도 축으로 두는 편이 안전합니다. 공식 SOP가 질문과 무관하다고 상위에 오르면 안 되고, 관련된 비공식 메모가 공식 문서를 이긴 채 확정 답변처럼 보이는 것도 위험합니다.

추천 정책은 다음과 같습니다.

  1. 먼저 질문과 관련된 후보를 찾습니다.
  2. 같은 조치에 관한 source conflict를 감지합니다.
  3. 관련도가 비슷할 때 authority를 tie-break로 사용합니다.
  4. Deprecated 또는 stale source에는 경고를 붙입니다.
  5. 최종 답변은 공식 근거와 보조 사례를 구분합니다.

6단계: Reranker와 context expansion은 후보 뒤에 둔다

Reranker는 query와 candidate를 함께 읽고 더 정밀하게 순서를 정합니다. Cross-encoder나 LLM reranker는 유용하지만 모든 문서에 실행하면 비쌉니다.

전체 corpus: 100,000 chunks
      ↓ cheap retrieval
후보: 20~100 chunks
      ↓ reranker
최종: 5~15 evidence
      ↓ context expansion
LLM prompt

Reranker는 후보에 없는 정답을 되살릴 수 없습니다. Recall@k가 낮다면 candidate generation부터 고쳐야 합니다. 정답이 후보에는 있지만 앞에 오지 않는 경우에 reranker가 의미가 있습니다.

최종 chunk를 고른 뒤에는 앞뒤 문단, parent heading, 표의 header 같은 문맥을 복원합니다. Index time에 큰 chunk만 만드는 것보다 query time에 필요한 주변 문맥을 붙이면 recall과 prompt 크기를 따로 조절할 수 있습니다.

LLM은 검색 pipeline의 어디에 들어가는가?

LLM을 모든 단계에 넣을 필요는 없습니다.

단계결정론적 코드가 적합LLM이 유용할 수 있음
Source scan경로, 확장자, hash, ACL이미지·대화 구조화
Chunkingheading, AST, token cap애매한 의미 단위 보조
Query scopeproject registry, 권한모호한 intent 분류
RetrievalFTS, vector, graph queryquery rewrite, decomposition
FusionRRF, dedupe, cap일반적으로 불필요
Validationschema, URI, permissionclaim 의미 비교 보조
Answercitation contract근거 합성, 불확실성 설명

LLM을 index time에 사용해 검색하기 좋은 질문이나 짧은 맥락을 만들 수는 있습니다. 그러나 생성한 설명을 원문과 분리하고, model과 prompt version을 기록해야 다시 만들 수 있습니다.

다른 에이전트가 Wiki를 사용하게 할 때도 answer_question 하나만 노출하기보다 작은 도구를 제공하는 편이 검증하기 쉽습니다.

search_wiki
search_sessions
search_exact
get_evidence_source
get_document_neighbors

MCP 공식 사양은 resource와 tool을 구분해 외부 context와 실행 가능한 기능을 모델에 제공합니다. 검색 도구는 evidence를 반환하고, 답변 생성은 상위 agent가 맡게 하면 웹 UI와 여러 agent가 같은 retrieval layer를 공유할 수 있습니다.

검색 개선은 평가셋에서 시작한다

“검색이 좋아졌다”는 느낌만으로는 설정을 비교할 수 없습니다. 먼저 실제 사용자가 찾는 질문과 정답 source를 작은 파일로 만듭니다.

- id: restart-session-index
  query: 앱을 다시 켜면 이전 세션 검색이 사라지는 이유
  scope: [ai-dashboard]
  relevant_parents:
    - docs/session-index-persistence.md

- id: exact-marker
  query: desktop_persistent_session_index_v1
  scope: [ai-dashboard]
  relevant_parents:
    - apps/desktop/src/main/container.ts

처음부터 수천 건이 필요하지 않습니다. 반복해서 실패했던 20~50개 질문이면 chunk와 ranking 변경의 방향을 볼 수 있습니다. 다만 synthetic query만으로 끝내지 말고, privacy를 제거한 실제 query를 계속 추가해야 합니다.

검색과 답변 지표를 분리한다

지표확인하는 질문
Recall@k정답 parent가 후보 안에 들어왔는가?
MRR첫 정답이 얼마나 앞에 있는가?
nDCG@k여러 relevance 등급의 순서가 좋은가?
Parent occupancy같은 문서가 top-k를 독점하는가?
Citation completeness모든 evidence가 원문 URI를 갖는가?
Scope leakage범위 밖 project가 섞였는가?
Stale evidence rate오래된 근거가 경고 없이 쓰였는가?
p95 latency실제 사용 가능한 속도인가?
Grounded answer rate답변 claim이 evidence로 지지되는가?

Recall과 MRR은 retrieval을 평가합니다. Groundedness와 citation correctness는 answer layer를 평가합니다. 두 층을 한 점수로 합치면 검색은 성공했는데 LLM이 틀린 경우와, 검색 자체가 실패한 경우를 구분할 수 없습니다.

제가 검색 계층을 고치며 확인한 사례

개인 LLM Wiki 대시보드의 초기 검색은 두 개의 독립 기능을 연결한 수준이었습니다.

session hits
→ 뒤에 knowledge hits append

문제는 다음과 같았습니다.

  • Session과 knowledge의 점수 공간을 비교할 수 없었습니다.
  • 같은 원본의 여러 chunk가 결과를 차지했습니다.
  • 원문 URI, warning, conflict가 공통 결과로 변환되며 유실됐습니다.
  • Markdown을 갱신할 때 project index를 지우고 전부 다시 썼습니다.
  • Session FTS가 process memory에 있어 앱 재시작 뒤 사라졌습니다.
  • 긴 task context의 모든 token을 AND로 묶어 결과가 0건이 되기도 했습니다.

이를 다음 구조로 바꿨습니다.

explicit project scope
→ session FTS + knowledge FTS 병렬 실행
→ common Evidence contract
→ weighted RRF
→ candidate/parent dedupe + source cap
→ authority/conflict/stale signal
→ bounded source resolver
→ answer context + citation

RetrievalService 구현은 각 retriever를 병렬 실행하고, 범위 밖 candidate를 실패 처리한 뒤 RRF와 post-process를 적용합니다. KnowledgeIndexer는 전체 source snapshot을 먼저 읽고, 완전한 snapshot일 때만 DB에 반영합니다.

24개 합성 query 중 정답 parent가 있는 22개를 사용한 회귀 평가 결과는 다음과 같았습니다.

Metric기존개선 후
Recall@50.9415580.987013
Recall@100.9545450.987013
MRR0.9545451.000000
최대 동일-parent 점유31
Citation URI 완전성01.000000
Scope leakage00

실제 Electron 앱에서는 같은 project의 session과 Markdown이 함께 나오는지, global scope가 등록 project로만 확장되는지, conflict와 원문 detail이 보이는지, 재시작 뒤에도 검색 결과가 남는지 확인했습니다. 한 retriever의 FTS를 의도적으로 제거했을 때 다른 source 결과가 남고 실패 diagnostic이 표시되는지도 검증했습니다. 구현 범위와 주의점은 Phase 1 handoff에 남겼습니다.

이 수치는 “실제 업무 검색 정확도 98.7%”라는 뜻이 아닙니다. 고정된 synthetic fixture에서 이전 동작보다 나빠지지 않았고, citation·dedupe·scope 계약을 만족했다는 뜻입니다. 다음 gate는 익명화한 실제 query corpus입니다.

효율적인 LLM Wiki는 네 개의 plane을 분리한다

1. Authoring plane: 원본이 만들어지는 곳

사람은 기존 repository, 문서 시스템, 대화 도구에서 계속 작업합니다. 검색 때문에 모든 문서를 새 Wiki로 수동 이전하면 새로운 stale source가 생깁니다.

2. Build plane: 검수 가능한 지식을 만드는 곳

Source inventory, page proposal, evidence validation, human review, canonical promotion을 담당합니다. LLM이 원본을 읽고 summary나 relation을 제안할 수 있지만 canonical을 바로 덮어쓰지 않습니다.

3. Retrieval plane: 질문에 필요한 근거를 고르는 곳

FTS, embedding, graph, metadata filter, RRF, dedupe, reranking, context expansion을 담당합니다. 인덱스는 source와 canonical Wiki에서 다시 만들 수 있는 파생물입니다.

4. Answer plane: 근거를 설명하는 곳

선택된 evidence만 읽고 답변, citation, uncertainty를 만듭니다. 근거가 부족하면 더 검색하거나 답변을 보류합니다.

Existing Sources ──→ Build Plane ──→ Canonical Wiki
       │                                  │
       └──────────────→ Retrieval Plane ←─┘

                        Evidence Bundle

                         Answer Plane

이 구조의 목표는 모든 지식을 한 DB에 정본으로 모으는 것이 아닙니다. 원본의 작성 위치는 유지하면서 하나의 공통 retrieval contract로 근거를 가져오는 것입니다.

증분 인덱싱은 최적화가 아니라 정확성 기능이다

작은 prototype은 매번 index를 지우고 다시 만들 수 있습니다. 규모가 커지면 비용뿐 아니라 실패 원자성이 문제가 됩니다.

위험한 순서
기존 index 삭제 → 파일 1,000개 읽기 → 731번째에서 실패
→ 검색 가능한 마지막 정상 상태도 사라짐

안전한 순서
전체 source snapshot 읽기·검증
→ add/update/delete diff 계산
→ 하나의 transaction으로 적용
→ 실패 시 이전 snapshot 유지

변하지 않은 문서는 durable write 0건이어야 합니다. 추가, 변경, 삭제가 모두 반영되어야 하며, 읽지 못한 파일을 “삭제됨”으로 오인해서도 안 됩니다. Azure AI Search의 index update 문서도 production에서는 unique key와 change tracking을 이용한 incremental update, 큰 schema 변경에서는 side-by-side index 전환을 권장합니다.

효율을 위해 최소한 다음 상태를 둡니다.

index_version: 3
source_id: project:blog:docs/retrieval.md
source_hash: sha256:...
parser_version: markdown-v2
chunker_version: heading-v1
embedding_model: null
indexed_at: 2026-08-02T12:00:00Z

Source가 같아도 parser, chunker, embedding model이 바뀌면 파생물이 달라집니다. Version을 기록해야 어떤 변경이 전체 rebuild를 요구하는지 판단할 수 있습니다.

비용을 줄이는 구축 순서

Phase 0. 파일 검색과 실제 질문부터 시작한다

  • Canonical source 목록을 만듭니다.
  • Project와 document type metadata를 정합니다.
  • 실제로 자주 찾는 질문 20개와 정답 파일을 기록합니다.
  • 작은 corpus라면 ripgrep이나 SQLite FTS로 baseline을 만듭니다.

이 단계에서 파일이 누락되거나 title이 부정확하면 embedding을 추가해도 해결되지 않습니다.

Phase 1. Evidence contract를 만든다

모든 retriever가 같은 형태를 반환하게 합니다.

type Evidence = {
  candidateId: string
  parentId: string
  projectId: string
  sourceKind: 'knowledge' | 'session'
  title: string
  excerpt: string
  uri: string
  sourceRank: number
  authority: 'canonical' | 'accepted' | 'candidate' | 'raw'
  conflict: boolean
  stale: boolean
  warnings: string[]
}

URI가 없는 결과는 답변에 쓸 수 없게 하거나 최소한 경고해야 합니다. Retriever 오류도 빈 결과와 구분합니다.

Phase 2. Scope, dedupe, evaluation gate를 먼저 넣는다

  • Empty scope를 실패시킵니다.
  • 결과가 요청 project 밖으로 나가면 실패시킵니다.
  • Parent별 최대 결과 수를 제한합니다.
  • Recall@k, MRR, citation, leakage를 CI에서 비교합니다.

여기까지는 embedding 없이도 구현할 수 있습니다.

Phase 3. 실패 유형이 확인될 때 hybrid search를 추가한다

평가에서 “같은 의미의 다른 표현”이 지속적으로 누락될 때 embedding을 추가합니다. 반대로 식별자와 오류명이 문제라면 tokenizer, alias, field weight를 먼저 고칩니다.

lexical miss + semantic hit → embedding 가치 있음
lexical hit + semantic miss → exact search 유지 필요
둘 다 miss                → source/chunk/query 문제
둘 다 hit, 순위 낮음      → fusion/rerank 문제

Phase 4. Reranker와 LLM preprocessing을 gate 뒤에 둔다

Candidate recall이 충분하고 ordering 실패가 남을 때 reranker를 실험합니다. Contextual summary나 searchable question 생성도 비용, stale policy, hallucination 위험을 측정한 뒤 추가합니다.

Phase 5. 사람과 agent가 같은 검색 primitive를 사용한다

웹 검색 UI와 MCP가 서로 다른 index를 만들면 결과가 다르게 낡습니다. 하나의 retrieval service 위에 UI와 작은 agent tool을 얹습니다.

자주 실패하는 구축 방식

모든 문서를 하나의 prompt에 넣는다

작을 때만 단순합니다. 규모가 커지면 비용, distractor, position bias, 접근 제어 문제가 함께 커집니다.

처음부터 vector DB를 도입한다

문서 식별자, source scope, metadata와 평가셋이 없는 상태에서 vector DB를 추가하면 “왜 이 결과가 나왔는지”만 더 어려워질 수 있습니다.

원본 점수를 그대로 더한다

BM25, cosine, freshness, business boost를 한 숫자로 합치면 각 scale이 바뀔 때 ranking 의미도 바뀝니다. Rank fusion 또는 명시적인 calibration이 필요합니다.

Chunk를 독립 문서로만 저장한다

Parent ID와 ordinal이 없으면 중복 제거, 주변 문맥 복원, 정확한 삭제가 어렵습니다.

인덱싱 전에 기존 index를 지운다

중간 실패가 마지막 정상 검색 상태를 파괴합니다. 완전한 snapshot을 검증한 뒤 transaction으로 교체합니다.

Project filter를 권한으로 사용한다

Scope는 relevance 도구이고 ACL은 보안 경계입니다. 둘을 분리해 검사해야 합니다.

검색과 답변을 하나의 거대한 함수로 만든다

answer_question만 있으면 어느 source가 실패했고 어떤 evidence가 선택됐는지 관측하기 어렵습니다. Search, source resolve, synthesis를 분리합니다.

구축 체크리스트

Source와 lifecycle

  • Canonical source 위치가 명확한가?
  • Source ID와 revision 또는 content hash가 있는가?
  • 삭제와 rename을 탐지하는가?
  • Generated summary와 raw source를 구분하는가?
  • Candidate, accepted, canonical, deprecated 상태를 표현하는가?

Index

  • Parent ID, chunk ordinal, heading path를 보존하는가?
  • 동일 source는 재실행해도 중복되지 않는가?
  • 변경이 없으면 durable write가 발생하지 않는가?
  • 불완전 scan이 마지막 정상 snapshot을 지우지 않는가?
  • Parser·chunker·embedding version이 기록되는가?

Retrieval

  • Scope가 명시적이고 fail-closed인가?
  • Exact token 검색이 가능한가?
  • 서로 다른 retriever score를 직접 비교하지 않는가?
  • Parent와 source 편중을 제한하는가?
  • 모든 evidence에 원문 URI가 있는가?
  • Partial failure가 진단 정보로 보이는가?

Evaluation과 운영

  • 실제 질문과 gold parent가 있는가?
  • Recall@k와 MRR을 answer 평가와 분리하는가?
  • Scope leakage와 citation completeness를 gate로 두는가?
  • Stale, conflict, deprecated 근거를 표시하는가?
  • Index migration과 rollback 경로가 있는가?
  • UI와 agent가 같은 retrieval layer를 쓰는가?

자주 묻는 질문

LLM은 원본 문서를 전혀 읽지 않나?

읽습니다. 다만 매 질문마다 전부 읽지 않습니다. Index time에 parser나 LLM이 문서를 처리할 수 있고, query time에는 검색된 chunk와 필요한 주변 원문만 읽습니다.

Vector database가 반드시 필요한가?

아닙니다. 작은 개인 Wiki, 기술 문서, 오류 중심 corpus는 SQLite FTS나 BM25부터 시작할 수 있습니다. 실제 평가에서 semantic miss가 확인될 때 vector index를 추가해도 늦지 않습니다.

Wiki page와 raw source 중 무엇을 검색해야 하나?

둘 다 가치가 다릅니다. 검수된 Wiki는 compact하고 authority가 높으며, raw source는 정확한 token과 세부 근거를 보존합니다. Source kind와 authority를 유지한 채 함께 검색하고 답변에서 역할을 구분합니다.

모든 source repository를 수정해야 하나?

그럴 필요는 없습니다. Source repository는 원본과 최소 metadata만 책임지고, 공통 retrieval layer가 등록 경로를 읽을 수 있습니다. 다만 stable ID, 권한, freshness를 source에서 제공할 수 있으면 운영은 더 안전해집니다.

검색 결과를 그대로 LLM에 넣으면 되나?

아닙니다. 중복 제거, source cap, conflict 표시, token budget, 주변 문맥 복원이 필요합니다. Evidence URI도 answer와 함께 보존해야 합니다.

마무리

LLM Wiki 검색의 핵심은 더 많은 문서를 모델에게 읽히는 것이 아닙니다. 질문마다 읽어야 할 근거를 더 정확하고 작게 고르는 것입니다.

효율적인 시작 순서는 다음과 같습니다.

  1. Source와 stable ID를 정합니다.
  2. SQLite FTS나 BM25로 lexical baseline을 만듭니다.
  3. Scope, parent dedupe, citation contract를 추가합니다.
  4. 실제 query로 Recall@k와 MRR을 측정합니다.
  5. 확인된 semantic miss에만 embedding을 추가합니다.
  6. Rank fusion, reranking, context expansion을 단계별로 검증합니다.
  7. Index는 증분·원자적으로 갱신하고 언제든 source에서 재생성할 수 있게 합니다.
  8. 사람과 agent가 같은 작은 retrieval primitive를 사용하게 합니다.

좋은 LLM Wiki는 모든 것을 기억하는 거대한 prompt가 아닙니다. 원본을 잃지 않고, 필요한 근거를 제때 찾고, 왜 그 근거를 골랐는지 설명할 수 있는 시스템입니다.

참고자료