이 글의 주요 목표
- 부자연스러운 응답의 품질 향상
이 글에 담긴 내용
- Reranking (적용하지는 않음)
- 메타데이터의 confidence score활용
- 프롬프트 엔지니어링 (시스템 프롬프트 설정)
- 번외 : 그래프 데이터베이스와 연결 (적용하지는 않음)
오늘은 화나는 일도 있고 갑자기 살기 팍팍해졌으므로 좀 즐겁게 글을 써 보겠다.
셀프 즐거움라이팅 시작
지금 현재 상황으로는 내 이력서 및 자기소개서 (영어 및 한국어) 데이터를 청킹 -> 임베딩 해 둔 상태다.
Question: 이력서에서 AI 관련 프로젝트는 뭐야?
Answer: AI Consultant로 프로젝트를 수행했습니다. Fine-tuned LLMs, prompt generation, RLHF, Trust & Safety content categories를 fine-tuning 및 review하는 프로젝트를 수행했습니다.
데이터로 눈으로 검수할 수 있을 정도로 적고, 구조화된 데이터라 청킹이 잘 되는 김에 만만하고 별로 오래 안걸리는 meta-llama/Llama-3.2-3B-Instruct 를 메타에 영혼을 바쳐 갖다 써 봤다. 틀린말 한 것 없고 매칭이 잘 되지만,
- 부분적으로만 뽑아서 얘기하고,
- 영어 문장에 대한 내용을 그대로 한국어로 번역투로 갖다 둔 것 같다
- 자기소개'봇'으로 할 거니 아웃풋으로 Question: 까지 출력할 필요 없다
Reranking
품질을 올리는 방법으로 Reranking 을 가장 우선적으로 적용한다고 하는데, 이 리랭킹은 쉽게 말하면 모델이 뽑아온 답변 후보들 중에 가장 관련있는 고품질의 답변으로 다시 답변의 우선순위를 정하는 거다.
모델로 BAAI/bge-reranker-v2-m3 를 사용할 수 있으나, 이 모델은 다국어 지원은 하나(한국어도 학습데이터에 일부 포함) 메인은 영어와 중국어를 대상으로 하기 때문에 파인튜닝이 필요하다.
는 천재들이 이미 해 두었다. : dragonkue/bge-reranker-v2-m3-ko
cross-encoder방식으로, 텍스트 쌍을 직접 비교해 느리지만 정확한 결과를 낸다. 그래서 순위를 재조정하는 데 높은 성능을 낸다. 리랭커도 랭킹이라고 이전 랭킹을 안해버리면 오래걸린다.
reranker = CrossEncoderReranker(
model=HuggingFaceCrossEncoder(model_name="dragonkue/bge-reranker-v2-m3-ko"),
top_n=3
)
compression_retriever = ContextualCompressionRetriever(
base_retriever=retriever,
base_compressor=reranker
)
* 이런 방법이 있지만 현재 내 데이터에는 리랭킹할만 한 데이터가 있지도 않고 매핑자체는 잘 되므로 retriever 성능은 더 개선하지 않는 것으로 해 본다.
시스템 프롬프트 수정
특히 내가 선택한 작은 모델의 경우 암묵적인 규칙을 추론하기 보다 걍 갖다 쓰는 경우가 많아서 더 부자연스럽게 보인다. 작가한테 요청하듯이 모호하게 말고 편집자한테 요청하듯이 하나하나 짚어줘야 한다. 알잘딱깔센 안됨.
시스템프롬프트를 설계할 때 고려해야할 것들은 다음과 같다.
- 모델이 어떻게 학습되었는지 구조를 보고 공식 포맷을 프롬프트 구조에 반영해보기
- 명확한 페르소나(롤플레잉) 설정하기
- 예제 제공하기
- 토큰 길이 고려하기
- 출력 형식을 명확히 지정
- 없는 말 지어내기 금지 (hallucination 방지)
def build_prompt(context, query):
system_instruction = (
"You are a professional career assistant helping a candidate explain their resume to a recruiter.\n\n"
"Rules:\n"
"- Use ONLY the information provided in the context.\n"
"- Detect the language of the user's question and answer in the SAME language.\n"
"- Do NOT copy sentences verbatim from the context. Rewrite them naturally.\n"
"- Keep technical terms (e.g., Python, AWS, LLM) in English.\n\n"
"Content strategy:\n"
"1. If there are directly relevant experiences:\n"
" - Mention ALL relevant experiences.\n"
" - Order them by relevance to the question.\n"
"2. If there is NO directly relevant experience:\n"
" - Clearly state that there is no direct experience.\n"
" - Then mention any partially related experiences.\n"
" - Explain how the skills or experiences are transferable.\n\n"
"Writing style:\n"
"- Resume / interview answer tone\n"
"- 3–6 sentences total\n"
"- No bullet points\n"
"- No emojis"
)
prompt = f"""<|begin_of_text|><|start_header_id|>system<|end_header_id|>
{system_instruction}<|eot_id|><|start_header_id|>user<|end_header_id|>
[Context]
Below are multiple resume experience excerpts.
Use them as evidence for your answer.
{context}
Question:
{query}<|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
return prompt
AI는 AI가 안다고... 요구사항과 고려사항을 각 GPT, Gemini, Claude에 넣어 얻은 답변을 취사선택 해 보았다.
llama 모델이 학습된 포맷을 사용했고, 한국어, 영어 쿼리에 모두 대응하도록 했으며, 토큰을 효율있게 쓰기 위해 잡다한 내용들을 명시적으로 제거하도록 했다. 또한 한국어 아웃풋이 주가 될지라도 데이터가 영어인 경우도 있고, 한국어에 비해 영어가 토큰을 덜 잡아먹으므로 영어로 작성했다. 단순히 데이터에서 문장을 가져다가 쓰는 것을 방지하기 위해 패러프레이징을 요청해뒀다. 기존에 hallucination방지를 위해 넣어둔 '없으면 없다 하라' 대신 자기피알이 필요한 인터뷰상황에 맞게(...) 조금이라도 관련된 내용을 출력하게끔 지시했다.
output = generator(
prompt,
max_new_tokens=500,
temperature=0.1,
do_sample=True,
eos_token_id=generator.tokenizer.eos_token_id,
pad_token_id=generator.tokenizer.eos_token_id
)
full_text = output[0]["generated_text"]
answer = full_text.split("<|start_header_id|>assistant<|end_header_id|>")[-1].strip()
시스템프롬프트에서 문장 수 제안을 Instruction following으로 걸어두었으니, 아웃풋 토큰 수 자체에 대한 제한은 300에서 500으로 늘렸다. temperature값은 거짓정보를 출력하는 걸 최대한 막기 위해 0으로 설정하고자 했으나 아주 작은 값(0.1)로 두고 샘플링을 적용했다.
파라미터에 대한 약간 아주 조금 더 자세한 내용
Temperature, top-k, top-p, repetition penalty.. 뭔가 대체?
LLM이 답변을 ‘생성’한다는 말은 즉, 답변을 vector of logits 으로 제공하고 softmax 로 확률화 한 후에 샘플링된 답변 중에서 고르는 것이다. 그러니까 생성할 때마다 랜덤할 수 있고, 너무 창의적인(동문서답) 답변을 내 놓을 수 있다는 거다…
Temperature, top-k, top-p, repetition penalty 얘네가 이걸 어느정도 안정화 시킨다.
Temperature
파라미터 중에 Temperature=0.6(default)를 설정할 수 있다. 0~1이 아니고 10도 가능.. 까지의 분포 중 0.6이 디폴트 값이며, 저온은 가능성이 높은 토큰에 더 큰 확률을, 고온은 가능성이 낮은 토큰에 더 큰 확률을 부여한다.

즉 저온은 창의성이 없어도 되는 작업 (정답 맞추는 퀴즈 등)에 사용된다. (나중에 고온의 모델을 병렬로 실행시키고 self-consistency에 따라 최종 답변을 결정하는 orchestration 방식도 나온다.)
TOP-K
온도를 고온으로 설정할 때 가능성이 낮은(관련 없을지도 모르는) 토큰의 확률을 높일 수 있다고 했잖슴? Top-k 를 사용해 고정된 수의 상위 토큰에서만 선택하게 할 수 있음. 예를 들어 K=3. 간단하다. 근데 OpenAI에서는 지원 안하고 대신 Top-p를 사용한다. 비슷한 거긴 하다.
TOP-P
확률이 높은 토큰의 확률부터 차례로 더하다가 누적합이 p를 넘으면 더하기 중지. 거기까지만 고려. Topk가 단순하게 숫자로 자르는 것에 비해 이건 누적합으로 고려하기 때문에 조금 더 유연하고 가변적이다. p=1.0 이면 전체 확률 질량을 고려함. 필터가 없다는 뜻
왜 do_sample = True 와 temperature = 0 을 동시에 설정하면 런타임에러?
샘플링 시 모델은 softmax 확률 분포를 계산해서 토큰을 선택하는데, 아래와 같이 t=0 이면 분모 분자가 모두 0분의 n 꼴 즉 무한대이기 때문에 계산 자체가 불가해져서 오류가 나는 것. 특히 이 오류는 밸류에러인데, 계산이 불가능해서 나오는 런타임에러보다 명시적으로 밸류 에러를 생성하면서 계산 하기 전에 바로 수정 가능하게 한다. 똑똑하네..
메타데이터의 confidence score로 응답의 태도 결정하기
이전에 만들어두었던 메타데이터 스키마를 통해 응답에게 얼마나 확신을 줄지(?) 에너지를 불어넣을 수 있다. 같은 말이라도 없는 말을 지어내는 것과 돌려서 말하는 것은 다른 법이다. 더군다나 시스템프롬프트에서 관련된 내용이 없을 경우 비슷한 연관된 경험을 가지고 와서 말하라는 설정을 붙인 이상, 말투에서라도 강한 확신과 가능성 정도의 확신을 나누면 좋지 않을까 생각했다.
def build_context(results, top_k=3, min_confidence=0.4):
"""
results: List[Document]
return: context (str), avg_confidence (float)
"""
blocks = []
confidences = []
for i, doc in enumerate(results[:top_k]):
content = doc.page_content
meta = doc.metadata
confidence = meta.get("confidence", 0.5)
if confidence < min_confidence:
continue
block = f"""
[Documents {i+1}]
(Confidence: {confidence:.2f})
{content}
"""
blocks.append(block.strip())
confidences.append(confidence)
avg_confidence = (
sum(confidences) / len(confidences)
if confidences else 0.0
)
return "\n\n".join(blocks), avg_confidence
메타데이터를 눈으로 보면 아무래도 이력서 특성상 그런건지 지나치게 낮은 신뢰도를 가진 항목이 없다.
그래도 너무 낮은 신뢰도 문서일 경우 랭킹 범위에서 제하도록 한다.
def build_prompt(context, query, avg_confidence):
if avg_confidence < 0.6:
content_strategy = (
"If there is NO directly relevant experience:\n"
" - Clearly state that there is no direct experience.\n"
" - Then mention any partially related experiences.\n"
" - Explain how the skills or experiences are transferable.\n\n"
)
else:
content_strategy = (
"If there are directly relevant experiences:\n"
" - Mention ALL relevant experiences.\n"
" - Order them by relevance to the question.\n"
"If not, clearly state that there is no direct experience and explain transferable skills.\n"
)
system_instruction = (
"You are a professional career assistant helping a candidate explain their resume to a recruiter.\n\n"
"Rules:\n"
"- Use ONLY the information provided in the context.\n"
"- Detect the language of the user's question and answer in the SAME language.\n"
"- Do NOT copy sentences verbatim from the context. Rewrite them naturally.\n"
"- Keep technical terms (e.g., Python, AWS, LLM) in English.\n\n"
f"{content_strategy}\n"
"Writing style:\n"
"- Resume / interview answer tone\n"
"- 3–6 sentences total\n"
"- No bullet points\n"
"- No emojis"
)
prompt = f"""<|begin_of_text|><|start_header_id|>system<|end_header_id|>
{system_instruction}<|eot_id|><|start_header_id|>user<|end_header_id|>
[Context]
Below are multiple resume experience excerpts.
Use them as evidence for your answer.
{context}
Question:
{query}<|eot_id|><|start_header_id|>assistant<|end_header_id|>"""
return prompt
시스템프롬프트 코드를 수정했다.
기존 코드가 'LLM이 직접 직무연관성을 판단하게 하고 대답' 하는 방식이었다면 수정된 코드는 'Confidence 수치에 기반해 따를 시스템프롬프트를 바꾸는 식으로 바꿨다. 그럼에도 불구하고 경험이 없을 경우에 '걍 없음' 이라고 출력해 사용자 경험을 방해하는 걸 막기 위해서 처리할 수 있는 문장 (If not, clearly state that there is no direct experience and explain transferable skills) 을 추가했다.
번외: 그래프 데이터베이스와 벡터 데이터베이스 연결!??
벡터저장소는 텍스트 조각들의 토픽레벨 연결에 강점을 가진다. 그런데 각 토픽간 연관성에 대한 정보는 다루지 않는다. 예를 들어 '스타트업 창업 경험'은 리더십 경험으로, '컴퓨터 학과 전공'은 교육 내용으로 저장이 될 테지만 스타트업 창업 경험과 전공 사이 연관성은 담지 않을 것이다. 일러두기 : 이 프로젝트는 너무 작고 사실 그 연관성을 못 읽는다는 것 자체가 큰 결함도 아니며, 메타데이터 힌트로 약간 보완하는 정도로 충분하다고 생각하므로 이 방법론을 적용하지는 않는다. 서버도 열어야 한다....
"그래프적 사고를 '흉내'는 낼 수 있다"
metadata schema에 "related_experience_ids" : [ exp_02", ...]이런 식으로 각 청크에 연관성에 대한 언질을 주면 됨
방법론기록
https://docs.langchain.com/oss/python/integrations/graphs/neo4j_cypher
Neo4j - Docs by LangChain
docs.langchain.com
지금까지는 아웃풋이나 데이터를 수동으로 확인했지만.. 다음 포스팅에선 성능 검사를 내 눈으로 말고(..!) 평가할 수 있는 방법, 모니터링, 프롬프트 버전 관리를 하는 방법에 대해 다루고 적용해보겠다.
https://docs.langchain.com/oss/python/integrations/graphs/neo4j_cypher
'In progress' 카테고리의 다른 글
| [RAG 자기소개봇] 4. 허깅페이스 뜯어보기, 성능 평가 지표 (1) | 2026.02.01 |
|---|---|
| [RAG 자기소개봇] 2. 벡터 데이터베이스에 저장하고 검색 (2) | 2026.01.23 |
| [RAG 자기소개봇] 1. 문서를 RAG에 맞게 파싱 및 데이터 정제 (3) | 2026.01.20 |