Open-Source AI Cookbook documentation

구조화된 생성으로 근거 강조 표시가 있는 RAG 시스템 구축하기

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Open In Colab

구조화된 생성으로 근거 강조 표시가 있는 RAG 시스템 구축하기

작성자: Aymeric Roucher, 번역: 유용상

구조화된 생성(Structured generation)은 LLM 출력이 특정 패턴을 따르도록 강제하는 방법입니다.

이 방법은 여러 가지 용도로 사용될 수 있습니다:

  • ✅ 특정 키가 있는 딕셔너리 출력
  • 📏 출력이 N글자 이상이 되도록 보장
  • ⚙️ 더 일반적으로, 다운스트림 처리를 위해 출력이 특정 정규 표현식 패턴을 따르도록 강제
  • 💡 검색 증강 생성(RAG)에서 답변을 뒷받침하는 소스를 강조 표시

이 노트북은 마지막 예시를 구체적으로 보여줍니다.

➡️ 우리는 답변을 제공할 뿐만 아니라 이 답변의 근거가 되는 스니펫을 강조 표시하는 RAG 시스템을 구축합니다.

RAG에 대한 소개가 필요하다면, 이 쿡북을 확인해 보세요.

이 노트북은 먼저 프롬프트를 통한 구조화된 생성의 단순한 접근 방식을 보여주고 그 한계를 강조한 다음, 더 효율적인 구조화된 생성을 위한 제한된 디코딩(constrained decoding)을 시연합니다.

이 노트북은 HuggingFace Inference Endpoints를 활용합니다 (예제는 서버리스 엔드포인트를 사용하지만, 전용 엔드포인트로 변경할 수 있습니다), 또한 outlines라는 구조화된 텍스트 생성 라이브러리를 사용한 로컬 추론 예제도 보여줍니다.

!pip install pandas json huggingface_hub pydantic outlines accelerate -q
import pandas as pd
import json
from huggingface_hub import InferenceClient

pd.set_option("display.max_colwidth", None)
repo_id = "mistralai/Mistral-Nemo-Instruct-2407"

llm_client = InferenceClient(model=repo_id, timeout=120)

# Test your LLM client
llm_client.text_generation(prompt="대한민국의 수도는?", max_new_tokens=50)

모델에 프롬프트 제공하기

모델에서 구조화된 출력을 얻으려면, 충분히 성능이 좋은 모델에 적절한 지시사항을 포함한 프롬프트를 제공하면 됩니다. 대부분의 경우 이 방법이 잘 작동할 것입니다.

이번 경우, 우리는 RAG 모델이 답변뿐만 아니라 신뢰도 점수와 근거가 되는 스니펫도 함께 생성하기를 원합니다.

이러한 출력을 JSON 형식의 딕셔너리로 생성하면, 나중에 쉽게 처리할 수 있습니다 (여기서는 근거가 되는 스니펫을 강조하여 표시할 예정입니다).

RELEVANT_CONTEXT = """
문서:

오늘 서울의 날씨가 정말 좋네요.
Transformers에서 정지 시퀀스를 정의하려면 파이프라인 또는 모델에 stop_sequence 인수를 전달해야 합니다.

"""
RAG_PROMPT_TEMPLATE_JSON = """문서를 기반으로 사용자 쿼리에 응답합니다.

다음은 문서입니다: {context}


답변을 JSON 형식으로 제공하고, 답변의 직접적 근거가 된 문서의 모든 관련 짧은 소스 스니펫과 신뢰도 점수를 0에서 1 사이의 부동 소수점으로 제공해야 합니다.
근거 스니펫은 전체 문장이 아닌 기껏해야 몇 단어 정도로 매우 짧아야 합니다! 그리고 문맥에서 정확히 동일한 문구와 철자를 사용하여 추출해야 합니다.

답변은 다음과 같이 작성해야 하며, “Answer:” 및 “End of answer.” 를 포함해야 합니다.

Answer:
{{
  “answer": 정답 문장,
  “confidence_score": 신뢰도 점수,
  “source_snippets": [“근거_1”, “근거_2”, ...]
}}
End of answer.

이제 시작하세요!
다음은 사용자 질문입니다: {user_query}.
Answer:
"""
USER_QUERY = "Transformers에서 정지 시퀀스를 어떻게 정의하나요?"
>>> prompt = RAG_PROMPT_TEMPLATE_JSON.format(context=RELEVANT_CONTEXT, user_query=USER_QUERY)
>>> print(prompt)
문서를 기반으로 사용자 쿼리에 응답합니다.

다음은 문서입니다: 
문서:

오늘 서울의 날씨가 정말 좋네요.
Transformers에서 정지 시퀀스를 정의하려면 파이프라인 또는 모델에 stop_sequence 인수를 전달해야 합니다.




답변을 JSON 형식으로 제공하고, 답변의 직접적 근거가 된 문서의 모든 관련 짧은 소스 스니펫과 신뢰도 점수를 0에서 1 사이의 부동 소수점으로 제공해야 합니다.
근거 스니펫은 전체 문장이 아닌 기껏해야 몇 단어 정도로 매우 짧아야 합니다! 그리고 문맥에서 정확히 동일한 문구와 철자를 사용하여 추출해야 합니다.

답변은 다음과 같이 작성해야 하며, “Answer:” 및 “End of answer.” 를 포함해야 합니다.

Answer:
{
  “answer": 정답 문장,
  “confidence_score": 신뢰도 점수,
  “source_snippets": [“근거_1”, “근거_2”, ...]
}
End of answer.

이제 시작하세요!
다음은 사용자 질문입니다: Transformers에서 정지 시퀀스를 어떻게 정의하나요?.
Answer:
>>> answer = llm_client.text_generation(
...     prompt,
...     max_new_tokens=256,
... )

>>> answer = answer.split("End of answer.")[0]
>>> print(answer)
{
  "answer": "Transformers에서 정지 시퀀스를 정의하려면 파이프라인 또는 모델에 stop_sequence 인수를 전달해야 합니다.",
  "confidence_score": 0.95,
  "source_snippets": ["정지 시퀀스를 정의하려면 파이프라인 또는 모델에 stop_sequence 인수를 전달해야 합니다."]
}

LLM의 출력은 딕셔너리의 문자열 표현입니다. 따라서 literal_eval을 사용하여 이를 딕셔너리로 로드합시다.

from ast import literal_eval

parsed_answer = literal_eval(answer)
>>> def highlight(s):
...     return "\x1b[1;32m" + s + "\x1b[0m"


>>> def print_results(answer, source_text, highlight_snippets):
...     print("Answer:", highlight(answer))
...     print("\n\n", "=" * 10 + " Source documents " + "=" * 10)
...     for snippet in highlight_snippets:
...         source_text = source_text.replace(snippet.strip(), highlight(snippet.strip()))
...     print(source_text)


>>> print_results(parsed_answer["answer"], RELEVANT_CONTEXT, parsed_answer["source_snippets"])
Answer: Transformers에서 정지 시퀀스를 정의하려면 파이프라인 또는 모델에 stop_sequence 인수를 전달해야 합니다.


 ========== Source documents ==========

문서:

오늘 서울의 날씨가 정말 좋네요.
Transformers에서 정지 시퀀스를 정의하려면 파이프라인 또는 모델에 stop_sequence 인수를 전달해야 합니다.

잘 작동합니다! 🥳

하지만 성능이 낮은 모델을 사용하는 경우는 어떨까요?

성능이 떨어지는 모델의 불안정한 출력을 시뮬레이션하기 위해, temperature 값을 높여보겠습니다.

>>> answer = llm_client.text_generation(
...     prompt,
...     max_new_tokens=250,
...     temperature=1.6,
...     return_full_text=False,
... )
>>> print(answer)
{
    "answer": adjectistiques Banco Comambique-howiktenल्ल 없을Ela Nal realisticINTEn обор reminding frustPolit lMer Maria Banco Comambique-howiktenल्ल 없을Ela Nal realisticINTEn обор музы inférieurke Zendaya alguna7 Mons ram incColumn Orth manages Richie HackAUcasismo<< fpsTIvlOcriptive Ou Tam psycho-Kinsic Serum SecurityülY on Hazard SautéFust St I With 모 clans Eddy Bindingtsoke funeral Stefano authenticitatcontent。

적으로ებულიização finnotes fins witCamera 하나 ls Metallurne couleur platinum/c وأنت textarea Golfyyzuhalten assume prog_reset"Piagn Ameth amivio COR '',
ze Columbia padchart": Poul?"

       φsin den Qu tiendas Mister�cling tercero política’avenir emploi banque inertکا …
anic lucommon-contagsbor ruvisending frustPolit lMer Maria Banco Comambique-howiktenल्ल 없을Ela Nal realisticINTEn обор музы inférieurke Zendaya alguna7 Mons ram incColumn Orth masses frustPolit lMer Maria Banco Comambique-howiktenल्ल 없을Ela Nal realisticINTEn обор музы inférieurke Zendaya alguna7 Mons ram incColumn Orth manages Richie HackAUcasismo<< fpsTIvlOcriptive Ou Tam psycho-Kinsic Serum SecurityülY on Hazard SautéFust

출력이 올바른 JSON 형식조차 아닌 것을 확인할 수 있습니다.

👉 제한된 디코딩(Constrained decoding)

JSON 출력을 강제하기 위해, 우리는 제한된 디코딩을 사용해야 합니다. 여기서 LLM이 문법이라고 불리는 일련의 규칙에 맞는 토큰만 출력하도록 강제합니다.

이 문법은 Pydantic 모델, JSON 스키마 또는 정규 표현식을 사용하여 정의할 수 있습니다. 그러면 AI는 지정된 문법에 맞는 응답을 생성합니다.

예를 들어, 여기서는 Pydantic 타입을 따릅니다.

from pydantic import BaseModel, confloat, StringConstraints
from typing import List, Annotated


class AnswerWithSnippets(BaseModel):
    answer: Annotated[str, StringConstraints(min_length=10, max_length=100)]
    confidence: Annotated[float, confloat(ge=0.0, le=1.0)]
    source_snippets: List[Annotated[str, StringConstraints(max_length=30)]]

생성된 스키마가 요구 사항을 올바르게 나타내는지 확인해 보세요.

AnswerWithSnippets.schema()

클라이언트의 text_generation 메서드를 사용하거나 post 메서드를 사용할 수 있습니다.

>>> # Using text_generation
>>> answer = llm_client.text_generation(
...     prompt,
...     grammar={"type": "json", "value": AnswerWithSnippets.schema()},
...     max_new_tokens=250,
...     temperature=1.6,
...     return_full_text=False,
... )
>>> print(answer)

>>> # Using post
>>> data = {
...     "inputs": prompt,
...     "parameters": {
...         "temperature": 1.6,
...         "return_full_text": False,
...         "grammar": {"type": "json", "value": AnswerWithSnippets.schema()},
...         "max_new_tokens": 250,
...     },
... }
>>> answer = json.loads(llm_client.post(json=data))[0]["generated_text"]
>>> print(answer)
&#123;
  "answer": " neces恨bay внеpok Archives-Common Propsogs’organpern 공격forschfläche elicous neces恨bay внеpok món-�","confidence": 1,"source_snippets": ["Washington Roman Humналеualion", "_styleImplementedAugust lire",
  ""]

                                                            }
&#123;
  "answer": " بخopuerto կար因數 kavuts mi Firefox Penguins er sdபெர erinnert publiée 물리 DK\(&#123;}^&#123;\ Cis بخopuerto կար因數"
,
  "confidence": 0.7825484027713585
,
  "source_snippets": [

"Transformerграни moisady отгaನ", ", migrations ceproductionautal",
"Listeners accelerating loocae"
]
}

✅ 높은 temperature 설정으로 인해 답변 내용은 여전히 말이 되지 않지만, 생성된 출력 텍스트는 이제 우리가 문법에서 정의한 정확한 키와 자료형을 가진 올바른 JSON 형식입니다!

이제 이 출력물을 추가 처리를 위해 파싱할 수 있습니다.

Outlines를 사용해서 로컬 환경에서 문법 활용하기

Outlines는 Hugging Face의 Inference API에서 출력 생성을 제한하기 위해 내부적으로 실행되는 라이브러리입니다. 이를 로컬 환경에서도 사용할 수 있습니다.

이 라이브러리는 로짓(logits)에 편향(bias)을 적용하는 방식으로 작동하여, 사용자가 정의한 제약 조건에 부합하는 선택지만 강제로 선택되도록 합니다.

schema_as_str
import outlines

repo_id = "Qwen/Qwen2-7B-Instruct"
# 로컬에서 모델 로드하기
model = outlines.models.transformers(repo_id)

schema_as_str = json.dumps(AnswerWithSnippets.schema())

generator = outlines.generate.json(model, schema_as_str)

# Use the `generator` to sample an output from the model
result = generator(prompt)
print(result)

제약 생성(constrained generation)을 사용하여 Text-Generation-Inference를 활용할 수도 있습니다 (자세한 내용과 예시는 문서를 참조하세요).

지금까지 우리는 특정 RAG 사용 사례를 보여주었지만, 제약 생성은 그 이상으로 많은 도움이 됩니다.

예를 들어, LLM judge 워크플로우에서도 제약 생성을 사용하여 다음과 같은 JSON을 출력할 수 있습니다:

{
    "score": 1,
    "rationale": "The answer does not match the true answer at all.",
    "confidence_level": 0.85
}

오늘은 여기까지입니다. 끝까지 따라와 주셔서 감사드립니다! 👏

< > Update on GitHub