이번 시간에는 Claude Code의 claude -p로 청크마다 맥락 문장을 만들고, 맥락을 붙이기 전과 후의 검색 결과를 Docker의 Qdrant, bge-m3, multilingual-e5-small, Kiwi BM25로 측정하며 배워가는 시간입니다. 모델 출력은 비결정적이고 temperature나 시드를 줄 옵션도 없어서, 같은 명령을 다시 돌리면 맥락 문장과 숫자가 달라질 수 있고, opus 별칭은 나중에 다른 모델로 풀릴 수 있습니다.
예를 들어서 우리가 "Kafka 컨슈머에 동일한 결제 이벤트가 중복 도착해도 청구가 한 차례만 일어나게 만들려면?"이라는 질문을 한다고 해볼게요. 정답은 Kafka at-least-once 글의 "외부 API에는 멱등 키를 보냅니다" 소제목 아래 청크예요.
글 제목과 글 안의 작은 제목(소제목)만 붙인 청크에 Claude가 쓴 맥락을 더 붙이면 이런 질문의 정답을 더 잘 찾을까요? 실제로 재 보면 맥락 덕분에 찾게 된 질문도, 오히려 놓치게 된 질문도 있었어요.
Contextual Retrieval은 Anthropic이 2024년 9월 "Introducing Contextual Retrieval" 글에서 소개한 방법이에요. 청크를 임베딩하거나 BM25 색인에 넣기 전에, 글 전체와 청크를 LLM에 주고 "이 청크가 글의 어디에서 무엇을 다루는지"를 짧게 쓰게 해서 그 문장을 청크 앞에 붙입니다. 질문 쪽은 바꾸지 않아요. 기존 글이 쓴 프롬프트는 이렇습니다.
<document>
{{WHOLE_DOCUMENT}}
</document>
Here is the chunk we want to situate within the whole document
<chunk>
{{CHUNK_CONTENT}}
</chunk>
Please give a short succinct context to situate this chunk within the overall document for the purposes of improving search retrieval of the chunk. Answer only with the succinct context and nothing else.
기존 글은 상위 20개 청크에 정답이 없는 비율("1 minus recall@20")이 맥락 임베딩으로 5.7%에서 3.7%(35% 감소), BM25까지 더해 2.9%(49%)가 됐다고 보고해요. 기준선은 맥락이 없는 청크이고, 맥락은 Claude 3 Haiku로 만들었고 보통 50~100토큰이며, 프롬프트 캐시를 쓰면 문서 100만 토큰당 1.02달러가 든다고 적혀 있어요.
이 글은 청크마다 글 제목과 소제목을 이미 앞에 붙여요. 제목만 붙여도 검색이 나아질 수 있으니까, 이 글의 기준선은 제목이 붙은 청크(T1)예요. 맥락의 효과는 T1과 비교해서 말할게요.
제목을 붙이는 효과가 더 궁금하시면 청크 크기를 다룬 글을 참고해 주세요.
실습 준비
이 글의 명령은 모두 실습 코드의 rag-contextual-retrieval/ 디렉터리에서 실행합니다.
compose.yaml:embed프로필의emb(모델 실행)와search프로필의qdrant·client(검색·채점)requirements.txt: 컨테이너에 설치할 고정 버전 패키지 9개prep.py: 매니페스트(글마다 제목·태그·본문 파일을 적은*.json)에 등록된 글을 읽어 문서 파일out/docs-all.jsonl과out/docs.jsonl을 만드는 스크립트slugs.txt: 측정 때 고정한 문서 88편 목록ctx.py: 청크 나누기, 임베딩, 색인, 검색, 채점, 짝 비교 명령을 모은 컨테이너용 스크립트questions-h.jsonl: 평가 질문 H 묶음 50개questions-p.jsonl: 평가 질문 P 묶음 30개questions-gap.jsonl: 평가 질문 G 묶음 20개gen.py: 호스트에서claude -p로 청크마다 맥락을 만드는 스크립트
맥락 생성은 macOS 호스트의 claude -p로 하고, 호출 스크립트 gen.py는 호스트 Python 3.13의 표준 라이브러리만 써요. 호스트의 claude에 구독으로 로그인돼 있어야 하고, API 키로 돌리시려면 gen.py가 호출 전에 지우는 환경 변수 목록 STRIP에서 ANTHROPIC_API_KEY를 빼 주시면 됩니다.
색인·검색·채점은 Docker에서 해요. 임베딩은 bge-m3(밀집 1024차원, 최대 8192토큰)와 multilingual-e5-small(이하 e5, 384차원, 최대 512토큰)이고, BM25는 Kiwi 형태소에 식별자를 통째로 남기는 토큰 나누기예요. compose.yaml의 emb는 모델을 올리는 일(청크와 질문 임베딩, 토큰 수 세기)을 맡고, qdrant와 client는 검색과 채점을 맡아요. 두 프로필은 동시에 띄우지 않아요.
requirements.txt는 이렇게 되어 있어요.
--extra-index-url https://download.pytorch.org/whl/cpu
torch==2.14.1+cpu
sentence-transformers==6.1.0
transformers==5.18.0
qdrant-client==1.19.1
numpy==2.5.3
FlagEmbedding==1.4.2
kiwipiepy==0.24.0
kiwipiepy-model==0.24.0
bm25s==0.3.11
ARTICLES_DIR는 발행 글의 매니페스트(*.json)와 본문(*.mdx)이 들어가 있는 디렉터리예요. 여러분 문서로 따라 하시고 싶을 때는 같은 형식으로 매니페스트를 만들어 주시고, 질문 파일 셋도 그 문서에 맞춰 새로 만들어 주셔야 해요. 이 사이트 글 본문도 측정 뒤(2026년 10월 13일) 다듬어졌기 때문에, 지금 사이트에서 받아 돌리시면 같은 숫자가 나오지 않아요.
dc는 프로젝트 이름을 고정한 docker compose이고, emb는 임베딩 컨테이너에서, py는 검색 컨테이너에서 Python 스크립트를 실행하는 함수예요. 아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.
export ARTICLES_DIR=/path/to/articles
dc() { docker compose -p rctxret "$@"; }
emb() { dc --profile embed exec -T emb python "$@"; }
py() { dc --profile search exec -T client python "$@"; }
prep.py는 매니페스트에 등록된 글을 읽고, 처음 실행할 때 RAG 태그 글을 뺀 목록을 slugs.txt로 남겨요. 다음부터는 이 목록만 읽어요. zip에는 측정 때 만든 slugs.txt(88편)가 들어 있어서, 그대로 실행하시면 아래 출력의 첫 줄("새로 만들었습니다") 없이 이 목록을 읽어요.
mkdir -p out
dc --profile embed up -d
dc --profile embed exec -T emb pip install -q -r requirements.txt
emb prep.py
slugs.txt를 새로 만들었습니다
매니페스트 97편 중 RAG 태그 9편, slugs.txt 88편
글 길이(자) 최소 11,301, 중앙값 27,018, 최대 48,832, 합계 2,427,550
slugs.txt sha256 8f5094f92590f345f12bf182002630071f67c6ad2b27869779cd8383ddf23fa7
out/docs.jsonl sha256 799a9854d8b98ebb6dc928439a219577cd5f7c9df8a22b24675949c610b11dea
RAG 태그 글을 뺀 이유는 그 글 본문에 평가 질문이 그대로 인용돼 있어서예요. 그 글들을 넣으면 질문 문장을 그대로 담은 청크가 정답 글보다 먼저 걸려요.
청크와 변형
각 글을 문단 단위로 800자까지 묶어 청크로 나눠요. 색인에 넣는 청크 텍스트는 아래 변형으로 만들고, 청크 경계는 모든 변형에서 같아요.
- T1:
{글 제목}\n{절 제목}\n\n{본문}. 기준선이에요. - T0: 본문만 있는 변형이에요. T1에서 글 제목, 소제목 두 줄과 본문 맨 앞의
## 절 제목줄을 지워요. 기존 글의 기준선에 맞춘 참고선이에요. - A1:
{맥락}\n\n{T1}. Claude가 만든 맥락을 앞에 붙여요.
정답 판정은 모든 변형에서 같은 청크 번호의 T1 텍스트로 해요. 맥락 문장에 정답 문장이 섞여 들어가도 판정이 바뀌지 않게 하려는 거예요.
emb ctx.py chunks
문서 88편, 청크 4369개, T1 평균 628자, T0 평균 549자
글 하나의 청크 수 최소 20, 중앙값 50, 최대 85
본문이 절 제목 한 줄뿐이라 T0가 빈 청크 29개
out/chunks.jsonl sha256 659e19d6d175b312df7d64a34f8ea5cd5dc78c8972137efc105828c660ac0bf7
질문 100개와 판정 규칙
질문은 세 묶음이고, 모두 정답 글(gold_slug)과 그 글에서 그대로 옮긴 정답 문장(gold_passage)이 있어요. 정답 문장을 온전히 담은 청크가 정답 청크예요.
- H(50개): 개념 질문 15개, 설정 키·오류 코드 같은 식별자가 든 질문 25개, 이름이 비슷한 대상을 헷갈리기 쉬운 질문 10개예요.
- P(30개): H의 1~30번 질문을 같은 뜻으로, 정답 문장과 낱말이 덜 겹치게 바꿔 쓴 질문이에요. 번호가 H와 짝이에요. 맨 앞의 Kafka 결제 질문이 P의 26번이에요.
- G(20개): 정답 청크에는 질문의 핵심어가 없고 같은 글의 다른 청크에만 있는 질문이에요. Contextual Retrieval이 겨냥하는 경우라서 따로 보고해요.
맥락 문장은 한 줄도 보지 않은 상태에서 질문을 썼고, 다 쓴 뒤 sha256을 남기고 나서 맥락 생성을 시작했어요. qcheck는 정답 문장이 청크 하나에 온전히 들어 있는지 확인하고 정답 청크 번호를 out/gold.json에 남겨요.
emb ctx.py qcheck
H 50개: 정답 글 없음 0, 정답 문장 사라짐 0, 청크 경계에 걸림 0 [], 정답 청크가 둘 이상 0 []
P 30개: 정답 글 없음 0, 정답 문장 사라짐 0, 청크 경계에 걸림 0 [], 정답 청크가 둘 이상 0 []
G 20개: 정답 글 없음 0, 정답 문장 사라짐 0, 청크 경계에 걸림 0 [], 정답 청크가 둘 이상 0 []
G 자동 확인 위반 0건
questions-h.jsonl sha256 8b308583d73941ccfcd059020aca03cb4bfa564e2e0e1b3bcd056d99cd2bc374
questions-p.jsonl sha256 5597a5544534f5f94ecad48e64b621fc4f1619c8c93692e71f903e84e44e88d3
questions-gap.jsonl sha256 8373c0373e6fd873542797c62a55624e36be4b1e8c34b66e3244c057718dd7a6
판정 규칙은 실행 전에 정했어요. 질문마다 상위 20개 청크로 계산해요.
- 실패율@5: 정답 청크가 상위 5개에 없는 질문의 비율이에요.
- 실패율@20: 같은 기준의 상위 20개예요. 기존 글의 "1 minus recall@20"에 맞춘 보조 지표예요.
주지표는 H+P 80개의 실패율@5이고, 판정할 비교는 둘이에요. (주1)은 bge-m3 밀집 검색에서 T1 대 A1, (주2)는 bge-m3 + BM25 하이브리드에서 둘 다 T1 대 둘 다 A1이에요.
질문마다 맥락으로 살린 질문과 잃은 질문을 세고, 부호 검정 양측 확률과 부트스트랩 95% 구간(시드 42, 10,000회, H와 P의 같은 번호를 한 묶음으로 뽑음)을 내요. 구간이 0을 포함하지 않고 A1 쪽으로 기울면 "성립", 0을 포함하면 "이 질문 수로는 구분할 수 없다", 반대쪽이면 "반대로 나왔다"로 쓸게요. 실패율@20, e5, BM25 단독, T0, G 묶음은 보조로만 써요.
tokens는 T0와 T1 변형 파일을 만들고 토큰 수를 셉니다.
emb ctx.py tokens
T0: 청크 4369개, 평균 549자, out/var-T0.jsonl sha256 74cdfeac7b95cffbab41ed8ecd42e67890253e29f3591dce1b1e6d261ebd0bc2
T1: 청크 4369개, 평균 628자, out/var-T1.jsonl sha256 c1b0c5eb58e00792a8383e941ab56965c9227591c6245c15e993ea6b1d42858b
T0: 평균 549자, bge-m3 토큰 평균 238 최대 447(8192 초과 0), e5 토큰 평균 240 최대 449, e5 512 초과 0개
T1: 평균 628자, bge-m3 토큰 평균 276 최대 490(8192 초과 0), e5 토큰 평균 278 최대 492, e5 512 초과 0개
T1 청크는 bge-m3 토큰 최대 490개라 e5의 512토큰 안에 모두 들어가요.
실습 1: 청크 4,369개에 맥락 만들기
gen.py의 모든 호출은 작업 디렉터리 밖의 빈 임시 디렉터리에서 다음 조합으로 돌아요.
--safe-mode --setting-sources project: CLAUDE.md, 플러그인, 훅, MCP 서버와 사용자settings.json을 읽지 않아요.--effort high: effort를 명시해요. 주지 않으면--setting-sources project상태에서 "auto (currently medium)"이 돼요.--tools "": 도구를 모두 빼요.--no-session-persistence --output-format json: 세션 파일을 남기지 않고, 결과를 JSON 하나로 받아요.
문서는 --system-prompt에, 청크와 기존 글의 지시는 사용자 메시지에 둬요. 지시 끝에는 "Answer in Korean, the language of the document." 한 문장만 더했어요. 한 글의 청크는 한 워커가 차례로 보내 앞 호출이 쓴 캐시를 뒤 호출이 읽게 하고, 워커 4개가 서로 다른 글을 동시에 처리합니다.
결과는 out/ctx/A1/<청크 번호>.json에 claude -p 출력 그대로 남아요.
python3 gen.py run A1
python3 gen.py show out/ctx/A1/01556.json
맨 앞 질문의 정답 청크(1556번) 결과 JSON이에요(필드를 골라 JSON 그대로 옮겼어요).
{"type": "result", "subtype": "success", "is_error": false, "num_turns": 1, "duration_ms": 5029, "total_cost_usd": 0.016629199999999997, "result": "이 부분은 Kafka at-least-once 전달에서 중복을 견디는 컨슈머를 다루는 글 가운데, 결제 컨슈머를 offset commit 전에 SIGKILL로 죽였다가 재시작하자 `order-1003`이 두 번 승인된 실험에 이어지는 해결책입니다. 이벤트 ID(`evt-3` 등)를 `Idempotency-Key` 헤더로 결제 API에 보내 중복 승인을 막는 원리를 설명합니다. 이어서 결제 서버(`payapi.py`)를 새로 띄우고 `pay-key` 그룹으로 `pay.py`를 offset 2에서 죽였다가 재실행하는 재현 실험을 시작합니다.", "structured_output": null, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 978, "cache_read_input_tokens": 17386, "output_tokens": 266, "cache_creation": {"ephemeral_1h_input_tokens": 978, "ephemeral_5m_input_tokens": 0}}, "modelUsage": {"claude-opus-5-5": {"contextWindow": 1000000, "provider": "firstParty", "costUSD": 0.016629199999999997}}}
맥락에는 바로 앞 실험에서 무슨 일이 있었는지가 요약돼 있어요. 청크 본문만으로는 드러나지 않던 이야기가 앞에 붙은 거죠.
python3 gen.py stats A1
emb ctx.py ctxlen A1
출력에서 결과 수, 비용, 시간, 길이 부분만 옮겼어요(발췌).
A1: 결과 4369/4369개, 실패로 남은 청크 0개, 버린 시도 0번 {}
modelUsage 키 {'claude-opus-5-5': 4369}, num_turns {1: 4369}
입력 토큰 합 93,122,527 중 cache_read 0.932
total_cost_usd 합 $94.25(API 단가 환산 추정), 호출당 최소 0.0105, 중앙값 0.0187, 90번째 0.0218, 최대 0.2761
첫 시작 2026-10-13T10:41:56+09:00, 마지막 끝 2026-10-13T12:31:36+09:00, 경과 1.83시간(멈춘 시간 포함)
문서 토큰(글마다 두 번째 이후 호출 cache_read 중앙값의 합, 글 88편) 1,666,805, 문서 백만 토큰당 추정 비용 $56.54
A1: 맥락 4369개
글자 수 최소 164, 중앙값 346, 90번째 431, 최대 637
bge-m3 토큰 수 최소 89, 중앙값 172, 90번째 215, 최대 311
한글 비율 20% 미만 0개(최소 0.24, 중앙값 0.57)
질문 문장이 그대로 들어간 맥락 0개
- 4,369호출이 모두 한 번에 성공했고,
modelUsage의 키는 모두claude-opus-5-5,num_turns는 모두 1이었어요. - 맥락은 중앙값 346자, bge-m3 토크나이저로 172토큰이에요. 프롬프트에 "short succinct"라고 적혀 있어도 기존 글이 말한 50~100토큰보다 길어요. 토크나이저가 달라서 대략만 견줄 수 있어요.
- 입력 토큰의 93.2%를 캐시에서 읽었고, 워커 4개로 1.83시간 걸렸어요.
- 합계 94.25달러(API 단가 환산 추정)를 문서 토큰 166만 7천 개로 나누면 문서 100만 토큰당 56.54달러로, 기존 글의 1.02달러의 55배예요. 기존 글은 Claude 3 Haiku에 글당 청크 10개 남짓을 가정했고, 이 글은 Opus 5.5에 effort high, 글당 청크 중앙값 50개라 조건이 많이 달라요.
맥락을 붙인 A1 변형 파일을 만들고 길이를 셀게요.
emb ctx.py variants A1
emb ctx.py lens A1
A1: 청크 4369개, 평균 980자, out/var-A1.jsonl sha256 c042162beb726b8a09f1bc9ec0a3cda14c065a50260d1a51f663f9a63cfc0f98
A1: 평균 980자, bge-m3 토큰 평균 449 최대 752(8192 초과 0), e5 토큰 평균 451 최대 754, e5 512 초과 1454개
A1 청크는 bge-m3에는 모두 들어가지만, e5에서는 4,369개 중 1,454개가 512토큰을 넘어 청크 본문의 끝이 잘려요. e5 결과는 이 잘림과 함께 읽어 주셔야 해요.
실습 2: 색인하고 검색하기
변형마다 청크를 임베딩해 out/emb/<변형>-<모델>.npy로 저장해요. bge-m3는 T1과 A1만, e5는 T0, T1, A1을 계산해요. 질문 벡터는 모델마다 처음 임베딩할 때 함께 만들어 둬요. 200개마다 나눠 저장하니까 중간에 끊겨도 같은 명령으로 이어서 계산할 수 있어요.
emb ctx.py embed T1 m3
emb ctx.py embed A1 m3
for v in T0 T1 A1; do emb ctx.py embed $v e5; done
각 명령 출력의 마지막 줄이에요.
T1 m3 청크 4369개 완료, 계산 시간 합계 10037초(2.8시간), 청크 하나에 2.30초
A1 m3 청크 4369개 완료, 계산 시간 합계 17134초(4.8시간), 청크 하나에 3.92초
T0 e5 청크 4369개 완료, 계산 시간 합계 856초(0.2시간), 청크 하나에 0.20초
T1 e5 청크 4369개 완료, 계산 시간 합계 2852초(0.8시간), 청크 하나에 0.65초
A1 e5 청크 4369개 완료, 계산 시간 합계 1253초(0.3시간), 청크 하나에 0.29초
이제 임베딩 단계를 내리고 검색 단계를 띄워서 변형마다 Qdrant 컬렉션 v_<변형>을 만들게요. 컬렉션마다 e5 벡터와 BM25 희소 벡터를, T1과 A1에는 bge-m3 벡터를 넣어요. BM25 사전은 변형 텍스트로 만들기 때문에, 맥락에 나온 낱말도 사전에 들어가요.
검색기는 모두 정확 검색(exact=True)으로 상위 20개를 가져와요. 하이브리드는 밀집 상위 20개와 BM25 상위 20개를 클라이언트에서 RRF(Reciprocal Rank Fusion, 검색기마다 1/(k+순위)를 더하는 방식, 순위는 0부터, k=2)로 섞어요.
dc --profile embed stop
dc --profile search up -d
dc --profile search exec -T client pip install -q -r requirements.txt
for v in T0 T1 A1; do py ctx.py index $v; py ctx.py search $v; done
py ctx.py hybrid
py ctx.py score --detail
score --detail 출력에서 주지표 두 비교와 BM25 줄만 옮겼어요(발췌).
조건마다 H+P 80개와 G 20개의 실패 수(실패율), 문서 recall@5와 MRR@10은 H+P
bge-m3 밀집 T1 H+P 실패@5 27/80 (0.338) @20 16/80 (0.200) G 실패@5 6/20 @20 1/20 recall@5 80/80 MRR@10 0.931
bge-m3 밀집 A1 H+P 실패@5 30/80 (0.375) @20 9/80 (0.113) G 실패@5 3/20 @20 2/20 recall@5 79/80 MRR@10 0.925
BM25 T1 H+P 실패@5 35/80 (0.438) @20 13/80 (0.163) G 실패@5 3/20 @20 1/20 recall@5 78/80 MRR@10 0.862
BM25 A1 H+P 실패@5 30/80 (0.375) @20 11/80 (0.138) G 실패@5 4/20 @20 1/20 recall@5 79/80 MRR@10 0.909
하이브리드 bge-m3+BM25 T1 H+P 실패@5 27/80 (0.338) @20 11/80 (0.138) G 실패@5 4/20 @20 0/20 recall@5 80/80 MRR@10 0.943
하이브리드 bge-m3+BM25 A1 H+P 실패@5 25/80 (0.312) @20 5/80 (0.062) G 실패@5 3/20 @20 0/20 recall@5 80/80 MRR@10 0.948
- 맥락(T1 → A1): BM25 35 → 30, bge-m3 하이브리드 27 → 25로 줄었고, bge-m3 밀집은 27 → 30으로 늘었어요.
- 실패율@20: bge-m3 밀집 16 → 9, bge-m3 하이브리드 11 → 5로, 상위 5개보다 차이가 컸어요.
- 문서 recall@5와 MRR@10은 이미 높아서 차이가 드러날 자리가 거의 없어요.
실습 3: 질문마다 짝지어 비교하기
출력의 m3는 bge-m3 밀집 검색, hm3는 bge-m3 + BM25 하이브리드예요. m3:A1은 bge-m3 밀집으로 A1 색인을 검색한 결과이고, HP는 H+P 80개라고 보시면 됩니다.
py ctx.py pairs
출력 앞부분의 주지표 두 비교예요(발췌).
[(주1) m3:T1 대 m3:A1, HP 80개, 실패율@5]
m3:T1 실패 27/80 (0.338), m3:A1 실패 30/80 (0.375), 차이(m3:A1 - m3:T1) +0.037
둘 다 맞힘 44, 둘 다 놓침 21, m3:T1만 맞힘 9, m3:A1만 맞힘 6
한쪽만 맞힌 15개의 부호 검정 양측 확률 0.607
차이 95% 구간(같은 번호 묶음 50개 부트스트랩 10,000회, seed 42) [-0.074, +0.152] 0 포함: 예
[(주2) hm3:T1 대 hm3:A1, HP 80개, 실패율@5]
hm3:T1 실패 27/80 (0.338), hm3:A1 실패 25/80 (0.312), 차이(hm3:A1 - hm3:T1) -0.025
둘 다 맞힘 50, 둘 다 놓침 22, hm3:T1만 맞힘 3, hm3:A1만 맞힘 5
한쪽만 맞힌 8개의 부호 검정 양측 확률 0.727
차이 95% 구간(같은 번호 묶음 50개 부트스트랩 10,000회, seed 42) [-0.115, +0.053] 0 포함: 예
미리 정한 규칙대로 판정하면 두 주지표 모두 "이 질문 수로는 구분할 수 없다"예요.
- (주1) bge-m3 밀집: 맥락으로 살린 질문 6개, 잃은 질문 9개로 오히려 잃은 쪽이 많았지만, 부호 검정 확률 0.607, 구간 [-0.074, +0.152]로 0을 포함해요.
- (주2) bge-m3 + BM25 하이브리드: 살린 질문 5개, 잃은 질문 3개, 확률 0.727, 구간 [-0.115, +0.053]이에요.
같은 출력의 보조 비교에서 하이브리드의 실패율@20(살림 7, 잃음 1, 확률 0.070)은 구간 위 끝이 +0.000에 걸쳤어요. 보조 비교를 11개 봤으니까, 이 하나를 결론으로 올리지는 않을게요.
정리
범위: Claude Code 2.1.296 claude -p, Opus 5.5(claude-opus-5-5), effort high로 만든 맥락(A1), 이 사이트 글 88편의 청크 4,369개, bge-m3·e5·BM25, 질문 100개(H 50, P 30, G 20).
- 제목과 소제목을 붙인 청크(T1)에 맥락을 더하자 H+P 80개 실패율@5가 bge-m3 밀집 27 → 30, 하이브리드 27 → 25였고 두 비교 모두 95% 구간이 0을 포함했어요. 제목을 이미 붙인 색인이라면 맥락을 더하기 전에 여러분 질문으로 짝 비교부터 해 보세요.
- 실패율@20은 하이브리드 11 → 5, bge-m3 밀집 16 → 9로 줄었지만 보조 지표였고 구간이 0을 포함했어요. 상위 20개를 LLM에 넘기는 구성이라면 그 깊이에서 다시 재 보세요.
- A1 생성은 4,369호출에 추정 94.25달러였고, 문서 100만 토큰당 56.54달러로 기존 글의 1.02달러의 55배였어요. 모델과 effort, 글당 청크 수가 기존 글과 다르니, 비용은 여러분의 청크 수와 모델로 먼저 어림해 보세요.
- 맥락이 늘 돕지는 않았어요. bge-m3 밀집에서는 맥락으로 살린 질문이 6개, 잃은 질문이 9개였어요. 비슷한 주제의 글이 많은 코퍼스에서는 잃는 질문도 질문 단위로 함께 확인해 주세요.