IBM Granite 및 Tavily를 사용하여 교정 RAG 에이전트 구축

작성자

Jobit Varughese

Technical Content Writer

IBM

대규모 언어 모델(LLM)은 매우 강력하지만 그 지식은 훈련 데이터 세트로 제한됩니다. 특히 구체적이거나 진화하거나 독점적인 정보에 대한 질문에 답할 때 LLM은 할루시네이션을 느끼거나 일반적이고 관련성이 없는 답변을 제공할 수 있습니다. 검색 증강 생성(RAG)은 LLM에 외부 데이터 소스에서 검색된 관련 정보를 제공하는 데 도움이 됩니다.

그러나 모든 RAG가 동일하게 만들어지는 것은 아닙니다. 교정 검색 증강 생성(cRAG)은 단순히 기존 RAG 위에 구축되는 것이 아니라 상당한 개선을 나타냅니다. 검색된 결과의 품질과 관련성을 평가하여 더욱 견고하게 설계되었습니다. 문맥이 약하거나 관련성이 없거나 신뢰할 수 없는 출처에서 온 경우 cRAG는 답변을 조작하는 대신 수정 조치를 통해 더 나은 정보를 찾거나 명시적으로 답변을 거부하려고 시도합니다. 이 기술은 정책 관련 질문에 대한 답변과 같은 중요한 애플리케이션에서 cRAG 시스템을 더욱 안정적이고 신뢰할 수 있게 해줍니다.

다이어그램 관련 문서

이 튜토리얼에서는 Watsonx 및 LangChain에서 IBM Granite 모델을 사용하여 강력한 교정 RAG(cRAG) 시스템을 구축하는 방법을 알아봅니다. LlamaIndex 또는 LangGraph와 같은 유사한 프레임워크를 사용하여 고유한 노드가 있는 복잡한 RAG 흐름을 구축하는 데에도 사용할 수 있습니다. 미세 조정과 같은 기술은 도메인별 RAG에 대한 특정 LLM의 성능을 더욱 향상시킬 수 있습니다. OpenAI의 LLM(예:ChatGPT 와 같은GPT 모델)도 이러한 에이전트에게 인기 있는 선택이지만, 이 튜토리얼에서는 IBM Granite에 중점을 둡니다.

여기에서는 특정 보험 증권 문서(PDF)에 대한 질문에 대한 답변이라는 사용 사례에 중점을 둘 것입니다. 이 튜토리얼에서는 정교한 RAG 알고리즘을 구현하는 방법을 안내합니다.

  • 자신의 PDF 문서에서 정보를 검색합니다.

  • 내부 문서가 답변을 생성하기에 충분하지 않은 경우 에이전트는 외부 웹 검색(Tavily)을 대체로 사용할 수 있습니다.

  • 에이전트가 관련 없는 외부 결과를 지능적으로 필터링하여 개인 정책에 맞게 답변이 제공되도록 합니다.

  • 에이전트는 부분적 정보가 있는 경우 명확하고 제한적인 응답을 제공하거나 맥락이 누락된 경우 명확한 거절을 제공합니다.

사용 사례: 신뢰할 수 있는 보험 정책 쿼리 에이전트 구축

이 튜토리얼은 보험 정책 문서(PDF 브로셔)를 분석하고 사용자 쿼리에 정확하게 답변하도록 설계된 보험 정책 쿼리 에이전트를 만드는 방법을 보여주는 데모입니다. IBM Granite 모델과 LangChain을 사용하여 소스가 제한된 고품질 답변을 보장하는 강력한 검색 및 검증 단계를 갖춘 에이전트를 구축합니다.

신뢰할 수 있는 RAG의 주요 원칙이 사용 사례에 어떻게 적용되는지 알아보겠습니다.

주요 원칙 적용

내부 지식 기반(PDF): 에이전트의 주요 정보 출처는 제공된 보험 증권 PDF입니다. 이 문서를 검색 가능한 벡터 스토어로 변환합니다.

외부 검색 폴백(Tavily): 내부 지식창고에 정보가 충분하지 않은 경우 에이전트는 Tavily를 통해 외부 웹 소스를 참조할 수 있습니다. Tavily는 AI 에이전트 및 LLM을 위해 특별히 제작된 검색 엔진으로, RAG 기반 애플리케이션을 위한 애플리케이션 프로그래밍 인터페이스(API)를 통해 더 빠른 실시간 검색을 제공합니다.

컨텍스트 채점: LLM 기반 검색 평가자(채점자 역할)는 내부 PDF에서 검색된 항목의 관련성에 점수를 부여하는 동시에 고품질 검색 항목만 포함되도록 보장합니다.

쿼리 재작성: 웹 검색의 경우 에이전트는 사용자의 쿼리를 바꾸어 관련 외부 정보를 찾을 가능성을 높일 수 있습니다.

출처 검증: LLM 기반 검증은 외부 웹 검색 결과가 민간 보험 정책과 관련이 있는지 평가하여 일반 정보나 공공 의료 프로그램(예: Medi-Cal)에 대한 세부 정보를 걸러냅니다. 이 기능은 잘못된 답변이 생성되는 것을 방지하고 스스로 수정할 수 있도록 하여 지식을 개선하는 데 도움을 줍니다.

제한된 생성: LLM에 대한 마지막 프롬프트는 제공된 컨텍스트만 사용하고, 정확한 답변을 제공하고, 정보를 사용할 수 없는 경우를 명시하거나, 명시적인 제한이 있는 부분 답변만 제공하도록 엄격하게 지시합니다. 이 함수는 생성된 응답의 적응성과 신뢰성을 향상시킵니다.

전제조건

watsonx.ai 프로젝트를 생성하려면 IBM® Cloud 계정이 필요합니다. watsonx API 키와 프로젝트 ID에 모두 액세스할 수 있는지 확인합니다. 웹 검색 기능을 사용하려면 Tavily AI용 API 키도 필요합니다.

단계

1단계. 환경 설정

여러 도구 중에서 선택할 수 있지만, 이 튜토리얼에서는 Jupyter Notebook을 사용하여 IBM 계정을 설정하는 방법을 안내합니다.

  1. IBM Cloud 계정을 사용하여 watsonx.ai에 로그인합니다.
  2. watsonx.ai 프로젝트를 생성합니다. 프로젝트 내에서 프로젝트 ID를 가져올 수 있습니다. 관리 탭을 클릭합니다. 그런 다음 일반 페이지의 세부 정보 섹션에서 프로젝트 ID를 복사합니다. 이 튜토리얼에는 이 ID가 필요합니다.
  3. Jupyter Notebook을 만듭니다.

이 단계에서는 이 튜토리얼의 코드를 복사할 수 있는 노트북 환경이 열립니다. 또는 이 노트북을 로컬 시스템에 다운로드하여 watsonx.ai 프로젝트에 자산으로 업로드할 수 있습니다. 더 많은 Granite 튜토리얼을 보려면 IBM Granite 커뮤니티를 확인하세요. 이 튜토리얼은 Github에서도 보실 수 있습니다.

2단계. watsonx.ai 런타임 서비스 및 API 키 설정

  1. watsonx.ai 런타임 서비스 인스턴스를 만듭니다(무료 인스턴스인 Lite 요금제 선택).
  2. 애플리케이션 프로그래밍 인터페이스(API) 키를 생성합니다.
  3. watsonx.ai 런타임 서비스를 watsonx.ai에서 생성한 프로젝트에 연결합니다.

3단계. 패키지 설치

LangChain 프레임워크로 작업하고 IBM® WatsonxLLM을 통합하려면 몇 가지 필수 라이브러리를 설치해야 합니다. 필요한 패키지를 설치하는 것부터 시작해 보겠습니다. 이 세트에는 RAG 프레임워크를 위한 langchain, watsonx 통합을 위한 langchain-ibm, 효율적인 벡터 스토리지를 위한 faiss-cpu, PDF 처리를 위한 PyPDF2임베딩 및 웹 API 호출 요청을 위한 sentence-transformers가 포함됩니다. 이러한 라이브러리는 머신 러닝 및 NLP 솔루션을 적용하는 데 매우 중요합니다.

# Install Libraries
!pip install langchain langchain-ibm faiss-cpu PyPDF2 sentence-transformers requests

참고: GPU가 없어도 되지만 CPU 기반 시스템에서는 속도가 느려질 수 있습니다. 이 단계에서는 이 튜토리얼의 코드를 복사할 수 있는 노트북 환경이 열립니다. 이 튜토리얼은 Github에서도 보실 수 있습니다.

4단계. 필요한 라이브러리 가져오기

그런 다음 필요한 모든 모듈을 가져오고 watsonx 및 Tavily용 API 키를 watsonx 프로젝트 ID와 함께 안전하게 제공합니다.

# Import required libraries

import os
import io
import getpass
from PyPDF2 import PdfReader
from langchain_ibm import WatsonxLLM
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import FAISS
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
import requests
from botocore.client import Config
import ibm_boto3
from langchain.prompts import PromptTemplate
from langchain.tools import BaseTool

# Watsonx
WML_URL = "https://us-south.ml.cloud.ibm.com"
WML_API_KEY = getpass.getpass(" Enter Watsonx API Key: ")
PROJECT_ID = input(" Enter Watsonx Project ID: ")

# Tavily
TAVILY_API_KEY = getpass.getpass(" Enter Tavily API Key: ")

print(" Credentials loaded.")

os는 운영 체제와 함께 작동하는 데 도움이 됩니다.

io를 사용하면 데이터 스트림으로 작업할 수 있습니다.

GetPass는 API 키와 같은 민감한 정보를 캡처하는 안전한 방법을 사용하며 화면에 입력을 표시하지 않습니다.

PyPDF2.PdfReader를 사용하면 PDF에서 콘텐츠를 추출할 수 있습니다.

langchain_ibm.WatsonxLLM을 사용하면 LangChain 프레임워크 내에서 IBM Watsonx Granite LLM을 쉽게 사용할 수 있습니다.

langchain.embeddings.HuggingFaceEmbeddings는 HuggingFace 모델을 가져와 의미 검색에 중요한 텍스트 임베딩을 생성합니다.

langchain.vectorstores.FAISS는 벡터 인덱스를 구축하고 쿼리할 수 있는 효율적인 벡터 스토리지 및 유사성 검색을 위한 라이브러리입니다.

Langchain.text_Splitter.RecursiveCharacterTextSplitter는 큰 텍스트 구성 요소를 메모리에 맞지 않는 문서를 처리하는 데 필요한 작은 청크로 분할하는 데 도움이 됩니다.

langchain.schema.Document는 연결된 메타데이터가 있는 임의의 텍스트 단위를 나타내므로 langchain의 빌딩 블록이 됩니다.

requests는 API 외부에 HTTP 요청을 하는 데 사용됩니다.

botocore.client.Config는 AWS/IBM Cloud Object Storage 클라이언트에 대한 구성 설정을 정의하는 데 사용되는 구성 클래스입니다.

ibm_boto3는 클라우드 객체 스토리지와 상호 작용하는 데 도움이 되는 Python용 IBM Cloud Object Storage SDK입니다.

langchain.prompts.PromptTemplate는 언어 모델에 대한 재사용 가능한 프롬프트를 만드는 방법을 제공합니다.

langchain.tools.BaseTool은 LangChain 에이전트에 사용할 수 있는 사용자 지정 도구를 빌드하는 기본 클래스입니다.

이 단계에서는 텍스트를 처리하고, 임베딩을 생성하고, 임베딩을 벡터 데이터베이스에 저장하고, IBM의 watsonx LLM과 상호 작용하는 데 필요한 모든 도구와 모듈을 설정합니다. 다양한 데이터 유형을 소싱, 쿼리 및 검색할 수 있는 실제 RAG 시스템을 만드는 데 필요한 모든 부분을 설정합니다.

단계 5. IBM Cloud Object Storage에서 PDF 로드 및 처리

이 단계에서는 IBM Cloud Object Storage에서 보험 정책 PDF를 로드합니다. 이 코드는 PDF를 읽고, 텍스트 내용을 읽고, 텍스트를 더 작고 관리하기 쉬운 청크로 분할합니다. 이러한 청크는 숫자 임베딩으로 변환되어 FAISS 벡터 스토어에 저장되어 나중에 로컬 컨텍스트에서 의미론적 유사성 검색을 준비하여 검색 결과를 최적화합니다.

import os, types
import pandas as pd
from botocore.client import Config
import ibm_boto3

def __iter__(self): return 0

cos_client = ibm_boto3.client(service_name='s3',
ibm_api_key_id='YOUR_IBM_API_KEY',
ibm_auth_endpoint="https://iam.cloud.ibm.com/identity/token",
config=Config(signature_version='oauth'),
endpoint_url='https://s3.direct.us-south.cloud-object-storage.appdomain.cloud')

bucket = 'YOUR_BUCKET_NAME'
object_key = 'YOUR_OBJECT_KEY'

streaming_body_2 = cos_client.get_object(Bucket=bucket, Key=object_key)['Body']
pdf_bytes = io.BytesIO(streaming_body_2.read())

reader = PdfReader(pdf_bytes)
text = ""
for page in reader.pages:
extracted = page.extract_text()
if extracted:
text += extracted

print(f" Extracted {len(text)} characters from PDF.")
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_text(text)
print(f" Split into {len(chunks)} chunks.")

embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vectorstore = FAISS.from_texts(chunks, embeddings)

print(f" Created FAISS index.")

ibm_boto3.client는 클라이언트가 IBM Cloud Object Storage와 상호 작용할 수 있도록 지원합니다.

Bucket은 PDF가 들어 있는 클라우드 객체 스토리지 버킷의 이름입니다.

Object_key는 클라우드 객체 스토리지 버킷에 있는 PDF의 이름입니다.

cos_client.get_object(...).read()는 클라우드 객체 스토리지에서 PDF 파일의 내용을 바이트 단위로 검색합니다.

io. BytesIO는 PDF 원시 바이트를 PdfReader에서 사용할 수 있는 형식의 메모리 내 바이너리 스트림으로 변환합니다.

PdfReader는 PDF에서 텍스트를 구문 분석하고 추출할 수 있는 객체를 만듭니다.

page.extract_text()는 PDF에서 단일 페이지의 텍스트를 추출합니다.

RecursiveCharacterTextSplitter는 추출된 텍스트를 50자가 겹치는 500자의 청크로 분할하여 모든 문맥을 유지하도록 구성됩니다.

splitter.split_text(text)는 PDF 텍스트의 모든 페이지를 더 작은 청크로 분할하는 작업을 실행합니다.

HuggingFaceEmbeddings는 텍스트 청크를 조밀한 벡터 표현으로 변환하도록 사전 훈련된 문장 변환기 모델을 로드합니다.

FAISS.from_texts(chunks, embeddings)는 텍스트 청크를 의미적 유사성을 기준으로 검색할 수 있는 메모리 내 FAISS 인덱스를 구축합니다.

이 단계에서는 클라우드에서 LLM 지원 텍스트로 PDF 문서를 완전히 수집하고 실시간 검색을 위한 편리한 인덱싱을 처리합니다.

6단계. LLM 및 도구 초기화

이 단계에서는 에이전트의 추론을 유도하고 이를 Tavily 웹 검색 기능과 통합하도록 IBM Granite LLM을 구성합니다. LLM의 매개변수는 사실적이고 안정적인 응답을 위해 설정됩니다.

llm = WatsonxLLM(
model_id="ibm/granite-3-2b-instruct",
url=WML_URL,
apikey=WML_API_KEY,
project_id=PROJECT_ID,
params={
"max_new_tokens": 300, # ~2-3 paragraphs, good for corrective RAG
"temperature": 0.2, # low temperature = more factual, stable answers
}
)

print(" Watsonx Granite LLM ready.")
class TavilySearch(BaseTool):
name: str = "tavily_search"
description: str = "Search the web using Tavily for extra info."

def _run(self, query: str):
response = requests.post(
"https://api.tavily.com/search",
json={"api_key": TAVILY_API_KEY, "query": query}
)
response.raise_for_status()
return response.json()['results'][0]['content']


tavily_tool = TavilySearch()

WatsonxLLM은 IBM Watsonx에 대한 LLM 래퍼를 인스턴스화하여 Granite 모델과의 상호작용을 허용합니다.

model_id="ibm/granite-3-2b-instruct"는 명령 기반 생성형 AI 작업을 위해 설계된 IBM Granite 모델(27억 개의 매개변수 명령 모델)입니다.

class TavilySearch(BaseTool)는 Tavily API를 사용하여 웹 검색을 수행하기 위한 사용자 지정 LangChain 도구를 정의합니다.

tavily_tool = TavilySearch()는 사용자 지정 Tavily 검색 도구의 실행 가능한 인스턴스를 만듭니다.

watsonxLLM을 초기화하면 이전에 설정한 자격 증명의 URL, apikeyproject_id 값이 전달되어 서비스에 인증 및 연결됩니다. "max_new_tokens": 300과 같은 파라미터는 응답 길이를 제한하고 "temperature": 0.2는 아웃풋 창의성을 제어하여 보다 결정론적인 결과를 선호합니다.

TavilySearch 클래스 정의에는 해당 기능에 대한 설명이 포함되어 있습니다. 해당 논리는 def _run(self, query: str) 메서드 내에 포함되어 있습니다. 이 메서드에서는 TAVILY_API_KEY와 JSON 페이로드에 검색 쿼리를 포함하여 Tavily API 엔드포인트에 HTTP POST 요청을 합니다. 그런 다음 response.raise_for_status()로 HTTP 오류가 있는지 확인하고 JSON 응답을 구문 분석하여 첫 번째 검색 결과의 콘텐츠 스니펫에 액세스합니다.

이 단계에서는 텍스트 생성을 위한 언어 모델을 설정하고 언어 모델 지식을 보강하는 방법으로 외부 웹 검색 도구를 포함합니다.

7단계. 프롬프트 템플릿 및 헬퍼 함수 정의

이 단계에서는 RAG 프로세스의 여러 단계에서 LLM의 동작을 안내하는 다양한 프롬프트 템플릿을 정의합니다. 이 접근 방식에는 내부 문서 청크의 관련성 점수를 매기는 프롬프트, 더 나은 웹 검색을 위한 사용자 쿼리 재작성, 웹 검색 결과의 출처 확인을 위한 중요한 새 프롬프트가 포함되어 있습니다. 청크에 점수를 매기고 벡터 스토어에서 검색하기 위한 헬퍼 함수도 정의됩니다.

# Define Prompt Templates and Helper Functions

# Prompt for scoring the relevance of retrieved chunks
scoring_prompt_template = PromptTemplate.from_template(
"""
You are an evaluator. Score the relevance of the context chunk to the given insurance question.

Question: "{query}"

Context:
\"\"\"
{chunk}
\"\"\"

Respond only in this format:
Score: <0-5>
Reason: <one line reason>
"""
)

# Prompt for rewriting the user's query for better web search results
rewrite_prompt_template = PromptTemplate.from_template(
"""
You are a helpful assistant. Improve the following question to be clearer for an insurance information search.
Focus on making the query more specific if possible.

Original Question: "{query}"

Rewrite it to be clearer:
"""
)

# NEW: Prompt for verifying if Tavily context is from a relevant source (private policy vs. public program)
CONTEXT_SOURCE_VERIFICATION_PROMPT = PromptTemplate.from_template(
"""
You are an expert at identifying if a piece of text is from a general, public, or unrelated source
versus a specific, private, or relevant policy document.

Read the following context and determine if it appears to discuss general information,
public health programs (like Medi-Cal, Medicaid, Medicare, NHS, government-funded programs, state-funded),
or information that is clearly *not* specific to a private insurance policy like the one
the user might be asking about (assuming the user is asking about their own private policy).

If the context explicitly mentions or heavily implies public health programs, or is too general
to be useful for a specific private policy question, respond with "NO".
Otherwise (if it seems like it *could* be from a private policy context, a general insurance guide,
or does not explicitly mention public programs), respond with "YES".

Context:
\"\"\"
Response:
"""
)


# Function to score chunks using the LLM
def score_chunks(chunks, query):
scored = []
for chunk in chunks:
prompt = scoring_prompt_template.format(query=query, chunk=chunk)
response = llm(prompt).strip()

try:
# Extract score using more robust parsing
score_line = [line for line in response.splitlines() if "Score:" in line]
if score_line:
score = int(score_line[0].replace("Score:", "").strip())
else:
score = 0 # Default to 0 if score line not found
except Exception as e:
print(f" Could not parse score for chunk: {e}. Response: {response[:50]}...")
score = 0 # Default to 0 on error

scored.append((chunk, score))
return scored

# Function to retrieve documents from FAISS vector store
def retrieve_from_vectorstore(query):
# Retrieve top 8 similar documents from your PDF content
docs = vectorstore.similarity_search(query, k=8)
return [doc.page_content for doc in docs]

print(" Prompt templates and helper functions defined.")

이 단계에서는 RAG 프로세스의 여러 단계에서 LLM의 동작을 안내하는 다양한 프롬프트 템플릿을 정의합니다. 내부 문서 청크의 관련성을 점수화하고, 더 나은 웹 검색을 위해 사용자 쿼리를 다시 작성하는 프롬프트와 웹 검색 결과의 출처가 포함되어 있는지 확인하는 중요한 새 프롬프트가 추가되었습니다. 청크에 점수를 매기고 벡터 스토어에서 검색하기 위한 헬퍼 함수도 정의됩니다.

PromptTemplate.from_template은 프롬프트를 구성하기 위한 재사용 가능한 템플릿을 만드는 LangChain의 유틸리티 함수입니다.

scoring_prompt_template은 LLM이 평가자 역할을 하고 질문에 따라 특정 컨텍스트 청크에 관련성 점수(0~5)를 할당하도록 지시하는 프롬프트를 정의합니다.

rewrite_prompt_template은 LLM이 사용자의 원래 질문을 검색하기 쉽게 개선하거나 더 명확하게 만들도록 안내하는 프롬프트를 정의합니다.

CONTEXT_SOURCE_VERIFICATION_PROMPT는 텍스트(예: 웹 검색)가 비공개 정책 컨텍스트에서 가져온 것인지, 일반 또는 공개 소스에서 가져온 것인지 확인하도록 LLM에 지시하는 프롬프트를 정의합니다.

def score_chunks(chunks, query)는 텍스트 청크 목록과 쿼리를 받은 다음 LLM을 사용하여 각 청크의 관련성을 점수화하는 함수를 정의합니다.

def retrieve_from_vectorstore(query)는 FAISS 벡터 스토어에서 가장 유사한 문서를 검색하는 함수를 정의합니다.

score_chunks 함수 내에서는 빈 점수 목록이 초기화됩니다. 각 청크에 대해 scoring_prompt_template은 특정 쿼리 및 청크로 형식이 지정됩니다. 그런 다음 이 프롬프트가 LLM으로 전송되고 응답이 제거됩니다. 이 함수는 모델 응답에서 "Score:" 줄을 식별하여 정수 점수(관련성 또는 관련성이 없는 것으로 단순화된 경우 이진 점수)를 추출하려고 시도합니다. 그런 다음 구문 분석된 점수 또는 기본 점수와 함께 청크가 점수 목록에 추가됩니다. 시스템의 이 부분은 검색 평가자 또는 채점자 역할을 합니다.

retrieve_from_vectorstore 함수는 쿼리를 기반으로 가장 관련성이 높은 8개 문서 청크를 찾기 위해 vectorstore.similarity_search를 구현하고, 이렇게 검색된 LangChain 문서 객체에서 page_content를 검색합니다.

이 단계에서는 수정 RAG 시스템에 대한 개념적 스캐폴딩을 구축하여 LLM이 컨텍스트를 평가하고 내부 및 외부 지식 소스에서 지식을 검색하는 방법을 평가합니다.

8단계. 교정 RAG 논리 구현

초기 검색은 PDF의 벡터 스토어를 스캔하는 기능입니다.

컨텍스트 점수는 검색된 PDF 청크를 관련성에 따라 컨텍스트 점수로 가져옵니다.

PDF에서 관련 컨텍스트가 충분하지 않은 경우 Tavily로 폴백한 다음 Tavily로 쿼리합니다(웹 검색).

소스 검증은 Tavily 결과를 사용하기 전에 개인 정책과 관련이 있는지 확인하는 LLM 기반 단계입니다. 이 함수는 공공 의료 프로그램에서 오해의 소지가 있는 답변을 방지합니다.

쿼리 재작성 및 두 번째 Tavily 검색은 여전히 좋은 컨텍스트가 없으면 쿼리를 다시 작성하고 Tavily 검색을 다시 시도합니다.

관련 컨텍스트가 있는 경우 최종 결정이 내려지면 답변을 작성하라는 프롬프트와 함께 LLM으로 전송됩니다. 모든 실행 가능한 시도 후에도 관련 컨텍스트가 없는 경우 정중한 거부를 보냅니다.

# Implement the Corrective RAG Logic

MIN_CONTEXT_LENGTH = 100 # Adjust this based on how much minimal context you expect for a partial answer
SIMILARITY_THRESHOLD = 3 # Only scores >= 3 used for vector store chunks

def corrective_rag(query: str, policy_context_keywords: list = None):
"""
Executes the Corrective RAG process to answer insurance queries.

Args:
query (str): The user's question.
policy_context_keywords (list, optional): Keywords related to the specific policy
(e.g., ["Super Star Health", "Care Health Insurance"]).
Used to make external searches more specific. Defaults to None.
Returns:
str: The final answer generated by the LLM or a predefined refusal.
"""
retrieved_context_pieces = [] # To store all relevant pieces found throughout the process

# Initial vector search & Scoring (from your PDF)
chunks_from_vectorstore = retrieve_from_vectorstore(query)
scored_chunks_vector = score_chunks(chunks_from_vectorstore, query)
good_chunks_vector = [chunk for chunk, score in scored_chunks_vector if score >= SIMILARITY_THRESHOLD]
retrieved_context_pieces.extend(good_chunks_vector)

current_context = "\n\n".join(retrieved_context_pieces)
print(f" Context length after initial vector scoring: {len(current_context)}")

# Prepare specific query for Tavily by optionally adding policy keywords
tavily_search_query = query
if policy_context_keywords:
tavily_search_query = f"{query} {' '.join(policy_context_keywords)}"

# Fallback: Tavily direct search (only if current context is too short from vector store)
if len(current_context) < MIN_CONTEXT_LENGTH:
print(f" Context too short from internal docs, trying Tavily direct with query: '{tavily_search_query}'...")
tavily_context_direct = tavily_tool._run(tavily_search_query)

if tavily_context_direct:
# --- NEW STEP: Verify Tavily Context Source ---
# Ask the LLM if the Tavily result seems to be from a private policy context or a public program
verification_prompt = CONTEXT_SOURCE_VERIFICATION_PROMPT.format(context=tavily_context_direct)
is_relevant_source = llm(verification_prompt).strip().upper()

if is_relevant_source == "YES":
retrieved_context_pieces.append(tavily_context_direct)
current_context = "\n\n".join(retrieved_context_pieces) # Re-combine all good context
print(f" Context length after Tavily direct (verified and added): {len(current_context)}")
else:
print(f" Tavily direct context source rejected (e.g., public program): {tavily_context_direct[:100]}...")
# Context is NOT added, so it remains short and triggers the next fallback or final refusal

# Fallback: Rewrite query + Tavily (only if context is still too short after direct Tavily)
if len(current_context) < MIN_CONTEXT_LENGTH:
print(" Context still too short, rewriting query and trying Tavily...")
rewrite_prompt = rewrite_prompt_template.format(query=query)
improved_query = llm(rewrite_prompt).strip()

# Add policy keywords to the rewritten query too
if policy_context_keywords:
improved_query = f"{improved_query} {' '.join(policy_context_keywords)}"

print(f" Rewritten query: '{improved_query}'")
tavily_context_rewritten = tavily_tool._run(improved_query)

if tavily_context_rewritten:
# --- NEW STEP: Verify Rewritten Tavily Context Source ---
verification_prompt = CONTEXT_SOURCE_VERIFICATION_PROMPT.format(context=tavily_context_rewritten)
is_relevant_source = llm(verification_prompt).strip().upper()

if is_relevant_source == "YES":
retrieved_context_pieces.append(tavily_context_rewritten)
current_context = "\n\n".join(retrieved_context_pieces) # Re-combine all good context
print(f" Context length after rewritten Tavily (verified and added): {len(current_context)}")
else:
print(f" Tavily rewritten context source rejected (e.g., public program): {tavily_context_rewritten[:100]}...")

# --- Final Decision Point ---
# Now, `current_context` holds ALL the "good" and "verified" context we managed to gather.
# The decision to call the LLM for an answer or give a hard refusal is based on `current_context`'s length.

# Final check for absolutely no good context
# This triggers only if *no* relevant internal or external context was found or verified.
if len(current_context.strip()) == 0:
print(" No good context found after all attempts. Returning absolute fallback.")
return (
"Based on the information provided, there is no clear mention of this specific detail "
"in the policy documents available."
)

# If we have *any* context (even if short), pass it to the LLM to process
# The LLM will then decide how to phrase the answer based on its prompt instructions
# (exact, partial, or full refusal if context is irrelevant or insufficient based on its own reasoning).
final_prompt = (
f"You are a careful insurance expert.\n"
f"Use ONLY the following context to answer the user's question. If the context is too short "
f"or does not contain the answer, you must indicate that.\n"
f"Context:\n```\n{current_context}\n```\n\n" # Pass the gathered context
f"User's Question: {query}\n\n" # Pass the original query for the LLM's reference
f"NEVER add new details that are not in the context word-for-word.\n"
f"If the context clearly says the answer, give it exactly as written in the context, but in prose.\n"
f"If the context does not mention the topic at all, or the answer is not in the context, say:\n"
f"\"I'm sorry, but this information is not available in the provided policy details.\"\n"
f"If the context partially mentions the topic but does not directly answer the specific question (e.g., mentions 'dental' but not 'wisdom tooth removal'), reply like this:\n"
f"\"Based on the information provided, here’s what is known: [quote relevant details from the context related to the broad topic.] "
f"There is no clear mention of the specific detail asked about.\"\n"
f"Do NOT assume. Do NOT make up extra information.\n"
f"Do NOT generate extra questions or conversational filler.\n"
f"Final Answer:"
)

return llm(final_prompt)

print(" Corrective RAG logic implemented.")

policy_context_keywords 매개변수의 첫 번째 패스를 사용하면 보험에서 특정 용어(예: 이름, 보험사)를 추가하여 Tavily에 대한 검색 범위를 좁힐 수 있습니다.

MIN_CONTEXT_LENGTH는 검색된 컨텍스트의 최소 허용 길이를 정의합니다.

SIMILARITY_THRESHOLD는 청크가 "양호"로 간주되어야 하는 최소 관련성 점수를 정의합니다.

def corrective_rag(...)는 전체 교정 RAG 워크플로를 조정하는 기본 함수를 정의합니다.

corrective_rag 함수는 관련 컨텍스트를 수집하기 위해 retrieved_context_pieces를 생성하는 것으로 시작됩니다. 먼저 쿼리를 기반으로 PDF 벡터 스토어에서 chunks_from_vectorstore를 가져와 점수를 매긴 다음, scored_chunks_vector가 언어 모델을 사용하여 관련성을 평가합니다. SIMILARITY_THRESHOLD를 충족하는 good_chunks_vector만 유지됩니다. 그런 다음 이 조각들로부터 current_context를 컴파일합니다.

current_contextMIN_CONTEXT_LENGTH 미만이면 시스템에서 웹 검색을 시도합니다. 잠재적으로 policy_context_keywords를 통합하는 tavily_search_query를 구성합니다. 직접 검색(tavily_context_direct)이 수행됩니다. 결정적으로, 웹 검색 결과(is_relevant_source)가 공공 프로그램이 아닌 개인 정책에서 나온 것인지 확인하기 위해 verification_prompt가 생성되어 LLM으로 전송됩니다. '예'인 경우 컨텍스트가 추가된 것입니다.

컨텍스트가 충분하지 않은 경우 시스템은 쿼리를 다시 작성할 준비를 합니다. rewrite_prompt를 사용하여 LLM에서 improved_query를 얻은 다음 두 번째 웹 검색(tavily_context_rewritten)을 수행합니다. 이 새로운 컨텍스트도 동일한 소스 검증을 거칩니다.

마지막으로 if len(current_context.strip()) == 0이 마지막 검사입니다. 모든 시도 후에도 관련 컨텍스트가 발견되지 않으면 사전 정의된 거부 메시지가 반환됩니다. 그렇지 않으면 확인된 모든 컨텍스트가 포함된 final_prompt가 생성되어 언어 모델로 전송되어 최종 답변을 생성합니다.

전체 corrective_rag 함수는 교정 RAG의 단계별 검색, 채점 및 확인 기능을 세부적으로 처리합니다. 이를 통해 지식 기반과 지식 스트림을 지속적으로 업데이트할 수 있으며, 강력하고 상황에 맞는 답변의 이점을 누릴 수 있습니다.

9단계. 시스템 테스트

마지막으로, 샘플 쿼리로 corrective_rag 함수를 실행합니다. PDF 문서와 관련된 policy_context_keywords를 제공하는 것이 중요합니다. 이러한 키워드는 Tavily 웹 검색이 실제 정책과 더 관련성이 높아져 일반 또는 공공 의료 프로그램 정보가 컨텍스트를 오염시키는 것을 방지하는 데 도움이 됩니다.

정보의 흐름을 이해하기 위해 컨텍스트 길이와 검증 결과에 대한 인쇄문을 관찰합니다.

query = "How does the policy cover for In-Patient Hospitalisation?"
result = corrective_rag(query)

print("\n FINAL ANSWER:\n")
print(result)

policy_specific_keywords = ["Super Star Health", "Care Health Insurance"]는 업로드한 보험 증권과 관련된 키워드 목록을 정의하여 웹 검색 결과의 범위를 좁히는 데 도움이 됩니다.

query = "..."는 사용자가 질문할 수 있는 특정 질문을 정의합니다.

result = corrective_rag(query, policy_context_keywords=policy_specific_keywords)는 기본 corrective_rag 함수를 호출하고 사용자의 쿼리 및 정책별 키워드를 전달하여 전체 RAG 프로세스를 시작합니다.

print("\n FINAL ANSWER (...)")는 생성된 답변을 인쇄하기 전에 명확한 헤더를 표시합니다.

print(result)corrective_rag 시스템에서 반환된 최종 답변을 아웃풋합니다.

이 단계에서는 샘플 쿼리와 키워드를 사용하여 전체 교정 RAG 시스템을 호출하는 방법을 보여주며 실제 시나리오에서 엔드투엔드 기능을 시연합니다.

주요 이점

구현된 교정 RAG는 복잡한 요청에 대한 포괄적인 정보를 검색하기 위해 내부 PDF 지식 베이스와 외부 서비스(Tavily)를 완벽하게 조정했습니다.

LLM 기반 채점 및 중요 소스 검증을 사용하여 검색된 컨텍스트를 정확하게 평가하고 필터링하여 유효하고 신뢰할 수 있는 정보가 사용되고 있는지 확인합니다.

이 시스템은 사용자 쿼리를 지능적으로 재작성하여 보다 타겟팅된 고품질 정보를 요청함으로써 외부 검색을 개선할 수 있는 기능을 보여주었습니다.

제한적 생성을 사용하면 일반적으로 신뢰할 수 있고 상황에 따라 정확한 답변이 생성되었으며, 확인된 정보가 충분하지 않은 경우 시스템은 정중하게 답변을 거부했습니다.

이 예시에서는 watsonx의 LangChain 및 IBM Granite LLM을 사용하여 보험 정책에 대한 질문과 같은 민감한 도메인에서 강력하고 신뢰할 수 있는 AI 기반 애플리케이션을 개발하는 방법을 보여주었습니다.

관련 솔루션
비즈니스용 AI 에이전트

생성형 AI로 워크플로와 프로세스를 자동화하는 강력한 AI 어시스턴트 및 에이전트를 구축, 배포, 관리하세요.

    watsonx Orchestrate 살펴보기
    IBM AI 에이전트 솔루션

    믿을 수 있는 AI 솔루션으로 비즈니스의 미래를 설계하세요.

    AI 에이전트 솔루션 살펴보기
    IBM Consulting AI 서비스

    IBM Consulting AI 서비스는 기업이 AI 활용 방식을 재구상하여 혁신을 달성하도록 지원합니다.

    인공 지능 서비스 살펴보기
    다음 단계 안내

    사전 구축된 앱과 스킬을 사용자 정의하든, AI 스튜디오를 사용하여 맞춤형 에이전틱 서비스를 구축하고 배포하든, IBM watsonx 플랫폼이 모든 것을 지원합니다.

    watsonx Orchestrate 살펴보기 watsonx.ai 살펴보기