Jump to content

멀티턴, 멀티 쿼리 CS 시나리오를 위한 Advanced RAG 시스템 구현하기


Recommended Posts

RAGReasoningAPI_heroImg.png.f2381e5cd1695aae0d1a1e80f88ac844.png

Quote

본 쿡북을 읽기 전, 내용을 효과적으로 이해하시려면, 먼저 리랭커 활용법 : CS 문의에 답변하는 RAG 시스템 구현하기 CookBook을 읽어보시는 것을 권장합니다.

 

LLM의 발전은 다양한 산업 분야에서 변화를 가져오고 있지만, LLM 단독으로는 최신 정보에 대한 접근성이나 답변의 신뢰성 측면에서 한계를 지니고 있습니다. 이러한 한계를 극복하기 위해 RAG 시스템이 등장하였고, 이와 관련하여 리랭커 활용법 : CS 문의에 답변하는 RAG 시스템 구현하기 CookBook에서는 RAG 시스템을 구성할 수 있는 도구 중 하나인 리랭커에 대한 내용을 소개했습니다.

이번 쿡북에서는 RAG 시스템의 핵심 구성 요소로 사용될 수 있는 리랭커와 RAG Reasoning 이 둘을 체이닝하여 더욱 강력한 성능을 발휘하는 방법에 대해 다룹니다. 특히, 멀티 턴, 멀티 쿼리 등을 포함하는 복잡한 시나리오에서 정확하고 신뢰할 수 있는 RAG 시스템을 구축하는데 필요한 가이드를 제공하고자 합니다.

 

1.리랭커, RAG Reasoning API 소개


리랭커 API에 대한 내용은 이전 쿡북에서도 담고 있기 때문에 해당 섹션에서는 리랭커에 대한 간단한 설명와 RAG Reasoning 모델의 역할과 작동 원리, 그리고 이 둘을 효과적으로 결합했을 때 얻을 수 있는 시너지 효과에 대해 알아보겠습니다.

 

1.1 리랭커

리랭커는 검색 API로 불러온 결과값을 보정합니다. 입력된 쿼리와 가장 관련성 높은 정보(문서)만을 선별하고 압축 요약하여 이를 바탕으로 RAG 답변을 만들어 낼 수 있습니다.

 

1.2 RAG Reasoning

RAG Reasoning은 RAG 용도로 튜닝된 추론 모델로, 검색 API를 호출하여 DB 내 문서들을 검색하고 그 결과를 바탕으로 적절한 답변을 생성합니다. RAG Reasoning은 인용 출처, 인덱싱 표기 등 신뢰성 있는 답변 표기가 가능하도록 튜닝된 모델을 기반으로 합니다. 또한, Function calling 형식을 갖추고 있기 때문에, 단일 또는 여러 개의 (검색) 함수를 정의해두고, 모델이 상황에 맞게 최적의 함수를 자율적으로 선택하여 호출하게 할 수 있습니다. 이처럼 RAG Reasoning은 유연하고 지능적인 함수 호출 능력과 더불어, 다양한 정보를 통합한 고품질의 LLM 답변 생성을 가능하게 합니다.

 

1.3 리랭커와 RAG Reasoning을 체이닝할 때 얻을 수 있는 장점

리랭커 기반 RAG 시스템은 간편한 구현과 싱글 쿼리에 대한 높은 검색 정확도를 제공하지만, 일상에서 흔히 발생하는 멀티 쿼리나 멀티 턴 대화를 처리하기는 어렵습니다. RAG Reasoning만으로 RAG 시스템을 구현하는 경우에도, 검색 API로부터 받은 결과를 통으로 추론 과정에 활용하게 될 가능성이 높기 때문에 토큰 효율적으로 모델을 이용하기에 불리할 수 있습니다. 이러한 한계를 극복하기 위해, 본 쿡북에서는 리랭커와 RAG Reasoning 체이닝 전략을 활용합니다. 리랭커와 RAG Reasoning을 결합한 Advanced RAG 시스템은 복합적인 요청의 유저 쿼리와, 멀티 턴 대화에서의 대응력을 높이고, RAG 답변의 품질을 향상 시키는 데 강점이 있습니다.

RAGReasoningAPI_img.png.84eded6e13bb8c8c988b87d14717d206.png

체이닝 구조에서는 유저의 쿼리가 들어오면, 먼저 RAG Reasoning이 쿼리에 맞는 검색 함수를 호출합니다. 함수(검색 API와 대응)로 부터 검색된 문서들은 리랭커로 전달됩니다. 리랭커는 검색 결과를 필터링하고 관련성 높은 정보를 선별하여 RAG Reasoning으로 보내줍니다. RAG Reasoning은 이 고품질의 정보를 바탕으로 최종 답변을 생성합니다. 

이러한 체이닝 구조를 통해, 다음과 같은 이점들을 얻을 수 있습니다:

  • 멀티 쿼리 지원: 사용자의 멀티 쿼리에 대해 각 질문의 의도를 정확히 파악하고 필요한 정보를 검색하여 통합적인 답변을 제공할 수 있습니다.
  • 멀티 턴 대화 지원: RAG Reasoning이 대화의 맥락과 이전 질문-답변 이력을 관리하여 정보를 재구성할 수 있습니다. 이를 통해 멀티 턴 속에서도 일관성 있고 정확한 답변을 제공합니다.
  • 답변 신뢰도 및 정확성 향상: 리랭커의 필터링 및 보정 기능이 더해진 RAG Reasoning은 더욱 정확하고 신뢰할 수 있는 출처 기반으로 답변을 생성할수 있게 됩니다. 이는 LLM의 환각 현상을 줄이는 데 효과적입니다.

 

2. 리랭커, RAG Reasoning 체이닝 시나리오


2.1 네이버 클라우드 플랫폼 CS 대응을 위한 RAG 시스템 구현

지금부터는 네이버 클라우드 플랫폼 서비스 CS 대응에 활용될 수 있는 RAG 시스템을 구축해보고자 합니다.  먼저, 네이버 클라우드 플랫폼의 다양한 CS용 데이터를 토대로 검색 가능한 형태의 API를 만든 뒤, 해당 API와 리랭커 그리고 RAG Reasoning 을 체이닝하여 구성할 계획입니다. CS 분야는 상품에 관련된 다양한 종류의 방대한 문서를 다루는 특성상, 고객 대응 담당자가 모든 정보를 완벽하게 숙지하기 어려운 현실적인 한계가 있습니다. 그렇기에 RAG 기술을 통해 대규모 데이터를 효율적으로 검색하고, 고객 문의에 대한 정확하고 일관된 답변을 신속하게 제공함으로써 관리자의 업무 효율성을 향상시킬 수 있습니다. 

이번에 구축해 볼 CS 문의 대응을 위한 RAG 시스템은 사용 가이드 데이터와 FAQ 데이터를 검색가능한 형태로 전처리하여 구상한 뒤, 리랭커와 RAG Reasoning을 체이닝한 RAG 시스템을 적용하여 고객 문의에 대한 정확하고 근거있는 답변을 제공하는 것을 목표로 합니다. 체이닝 구조는 싱글 쿼리뿐만 아니라, 멀티 쿼리 대응에도 용이합니다. 참고로, 멀티 쿼리 시나리오는 유저가 "A에 대해 알려줘. 그리고 B에 대한 내용도 궁금해."와 같이 하나의 문장 안에 여러 개의 독립적이거나 상호 연관된 질문을 포함하는 경우를 의미합니다. 우리가 구현할 RAG 시스템은 다음과 같은 워크플로우로 실행됩니다.

Quote
  1. RAG Reasoning에 유저 쿼리를 입력합니다.
  2. RAG Reasoning은 입력된 쿼리를 분석하여 쿼리 목적에 맞는 검색 함수를 호출합니다. (RAG Reasoning은 Function Calling 형식으로 다양한 목적의 함수들을 정의하여 호출할 수 있지만, 본 예시에서는 시나리오 이해를 돕기 위해 편의상 검색 함수를 정의한다고 표현합니다.)
  3. 함수에 대응되는 검색 API에서 입력된 쿼리와 관련된 문서를 검색합니다.
  4. 리랭커를 통해 해당 문서들을 선별하고 내용을 압축하여 RAG Reasoning에 전달합니다.
  5. 한번 더 RAG Reasoning은 문서들을 통해 검색 함수 호출 여부를 판단합니다.
  6. 재 검색이 필요한 경우 검색 , 리랭커 호출 과정을 반복하여 답변에 필요한 문서들을 누적합니다.
  7. 최종적으로 누적한 모든 문서를 종합하여 답변을 생성합니다.

자세한 전체 파이프라인에 대한 코드 구현은 '2.2 코드 구성' 파트에서 서술하겠습니다.

 

2.1.1 사용 데이터

  • 사용 가이드 : 네이버 클라우드 플랫폼의 가이드 센터에서 제공하는 '사용 가이드' 를 활용합니다.
    • 유저 가이드에 대한 자세한 정보는 NCloud 가이드에서 확인할 수 있습니다.
  • FAQ 데이터: 네이버 클라우드 '자주 묻는 질문'을 가공한 데이터셋 입니다.
Quote

서비스는 지속적인 기능 개선 및 업데이트가 이루어지므로, 사용자 가이드와 FAQ셋의 내용 또한 빈번하게 변경됩니다. 따라서 본 쿡북에서는 2025년 1분기 시점에 업데이트된 사용 가이드와 FAQ 데이터셋을 기준으로 구성하였습니다.

이와 더불어 본 쿡북에서 제공하는 예시 데이터 파일은 간단한 구현을 위해 네이버 클라우드 플랫폼의 CLOVA Studio 상품과 관련된 가이드 내용만을 포함하고 있습니다.

 

2.1.2 검색 API 구현

네이버 클라우드 플랫폼의 가이드 문서와 자주 묻는 질문(FAQ) 데이터를 기반으로, 유저 쿼리에 가장 적합한 문서를 찾아 제공하는 검색 API를 구성합니다. 검색 API는 사용자가 가지고 있는 데이터(혹은 데이터베이스)를 검색 가능한 형태로 만들어 RAG Reasoning에 제공하는 역할을 합니다. Function Calling 형식의 RAG Reasoning 내부에 함수를 정의하고 해당 함수에 맞는 검색 API를 1대1로 매핑함으로써, 유저 쿼리에 맞는 함수와 검색 API를 호출할 수 있습니다. 사용자는 데이터 구조별로 검색 API를 분리 구성하고, 이를 각각 독립적인 함수로 RAG Reasoning 내부에 정의함으로써 보다 정교한 RAG 시스템을 구현할 수 있습니다.

  • 데이터 준비 : 사용 가이드와 FAQ 데이터를 결합하고 중복 제거, 이미지 제거 등 전처리 작업을 수행하고 csv 형태로 통합합니다.
  • 임베딩 생성 : CLOVA Studio의 embedding 모델 API를 호출하여 각 텍스트 데이터에 대해 벡터 임베딩을 생성하고, 이를 새로운 컬럼으로 추가합니다.
    • CLOVA Studio의 embedding에 대한 자세한 정보는 embedding 가이드에서 확인할 수 있습니다.
    • 구현의 편의를 위해 임베딩 파일을 첨부해 두었습니다. (파일: CLOVA_Studio.csv)
  • 검색 API 구성 : 유저 쿼리를 임베딩한 뒤 기존에 생성한 문서 임베딩 간의 코사인 유사도를 계산하여 가장 관련성 높은 상위 문서들을 반환하는 검색 API를 구성합니다. FastAPI 기반으로 활용할 수 있도록 설정해주었습니다.
Quote
# 핵심 기능만 간소화한 코드 예시

import os
import pandas as pd
import numpy as np
from numpy import dot
from numpy.linalg import norm
import ast
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
import requests

app = FastAPI()

#쿼리 임베딩 생성
def get_embedding(input_text, api_key):
    EMBEDDING_API_URL = "https://clovastudio.stream.ntruss.com/v1/api-tools/embedding/v2" 
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    data = {"text": input_text}
    response = requests.post(EMBEDDING_API_URL, headers=headers, json=data)
    response.raise_for_status()
    embedding = response.json()["result"]["embedding"]
    return embedding

def start():
    file_path = 'CLOVA_Studio.csv'
    if os.path.isfile(file_path):
        print(f"{file_path} 파일이 존재합니다.")
        df = pd.read_csv(file_path)
        df['ID'] = df.index
        df['embedding'] = df['embedding'].apply(ast.literal_eval)
    else:
        print('임베딩 파일을 찾을 수 없습니다.')
    return df

#코사인 유사도 계산
def cos_sim(A, B):
    return dot(A, B)/(norm(A)*norm(B))

#후보 문서 추출
def return_answer_candidate(df, query, top_k, api_key): 
    embedding = get_embedding(query, api_key)
    df["similarity"] = df.embedding.apply(lambda x: cos_sim(np.array(x), np.array(embedding)))
    top_k_doc = df.sort_values("similarity", ascending=False).head(top_k)
    return top_k_doc

#응답 데이터 생성
def return_results(df, query, top_k, api_key):
    result_lst = []
    result = return_answer_candidate(df, query, top_k, api_key)
    for i in range(0, top_k):
        result_lst.append({"id": str(result.iloc[i]['ID']), "content":str(result.iloc[i]['text'])})
    return result_lst

class SearchInput(BaseModel):
    query: str
    top_k: int

class SearchResult(BaseModel):
    id: str
    content: str

class SearchOutput(BaseModel):
    result: List[SearchResult]

df = start()

api_key = "API_KEY_HERE" #실제 API키 입력

# FastAPI 설정
@app.post("/search/", response_model=SearchOutput) # 로컬 주소에 /search/를 더해줍니다.
async def search(input: SearchInput) -> SearchOutput:
    results = return_results(df, input.query, input.top_k, api_key)
    print(results)
    return SearchOutput(result=results)

 쿡북 코드 예시는 아래와 같은 로컬 환경 경로에 검색 API를 구현하는 것을 전제로 합니다

http://127.0.0.1:8000/

다음 명령어를 터미널에 입력하면 FastAPI 서버를 실행할 수 있습니다.

uvicorn filename:app --reload 

 

2.2 코드 구성

 먼저 필수 라이브러리를 임포트 합니다.

import requests
import json
  • 예제 코드는 Python 3.11.1에서 실행 확인하였으며, 최소 Python3.7 이상을 필요로 합니다.
  • 간단한 예시를 위해 tqdm 라이브러리는 포함하지 않았지만, 실제 문서의 양이 많아질 경우 진행 상황을 시각적으로 확인할 수 있어 활용하기를 권장합니다.

 

2.2.1 RAG Reasoning 정의

RAG Reasoning은 Function Calling 형식의 엔진 호출을 제공하기 때문에 해당 형식에 따라 목적에 맞는 검색 함수들을 정의하여 작성합니다. 실제 서비스에서는 다양한 검색 목적에 맞춰 여러 개의 검색 함수를 정의하거나 검색이 아닌 다양한 목적의 함수들을 정의할 수 있지만, 본 예시에서는 시나리오의 이해를 돕기 위해 하나의 검색 함수만을 정의하였습니다.

RAG Reasoning은 Function calling 형식으로 정의된 함수들의 설명(function의 description)을 참고하고 스스로 판단하여 입력된 쿼리(query)에 필요한 (검색) 함수를 호출합니다. 호출된 검색 함수('ncloud_cs_retrieval')는 2.1.2에서 구현한 검색 API를 통해 입력 쿼리와 관련된 문서 검색을 실행합니다. 그 결과는 리랭커에 의해 정제되어 'document_list'에 저장됩니다. RAG Reasoning은 'document_list'를 확인한 후, 검색 함수 호출 대신 'document_list'를 'search_result' 형태로 변환하여 최종 답변 생성에 활용합니다.

def build_reasoning(query=None, document_list=None, sugquery_list=None, messages=None, tool_calls=None):
    # 1단계: 메시지 리스트 초기화
    # 멀티 턴 대화인지 싱글 턴 인지에 따라 메시지 구성이 달라짐
    if messages is not None:
        msg_list = messages  # 멀티 턴: 기존 대화 히스토리 사용
    else:
        msg_list = [  # 싱글 턴: 새로운 대화 시작
            {"content": query, "role": "user"}
        ]
    
    # 2단계: 기본 페이로드 구성
    payload = {
        "messages": msg_list,
    }
    
    # 3단계: 문서 존재 여부에 따른 조건 분기
    # Case A: 문서가 없는 경우 → 검색 함수 호출 (초기 검색 또는 재검색)
    if document_list is None:
        # 검색 함수 정의: RAG reasoning 모델이 필요시 검색을 수행할 수 있도록 함
        payload.update({
            "tools": [{
                "function": {
                    "description": "NCloud 관련 검색을 할 때 사용하는 도구입니다.\n나누어 질문해야 하는 경우 쿼리를 쪼개 나누어서 도구를 사용합니다.\n정보를 찾을 수 없었던 경우, 최종 답을 하지 않고 sugquery_list를 참고하여 도구를 다시 사용할 수 있습니다.",
                    "name": "ncloud_cs_retrieval",
                    "parameters": {
                        "properties": {
                            "query": {
                                "description": "사용자의 검색어를 정제해서 넣으세요.",
                                "type": "string"
                            },
                            "url": {
                                "description": "검색 API 서버의 엔드포인트 URL을 입력하세요.",
                                "type": "string"
                            }
                        },
                        "required": ["query", "url"],
                        "type": "object"
                    }
                },
                "type": "function"
            }],
            "toolChoice": "auto"  # 모델이 모델이 도구 호출 여부를 자율적으로 결정
        })
        
        # 재검색 케이스: 이전 검색에서 문서를 찾지 못했을 때 추천 검색어 제공
        if sugquery_list:
            system_message = f"이전 검색에서 관련 문서를 찾지 못했습니다. 다음 추천 검색어들을 참고하여 다시 검색해보세요: {', '.join(sugquery_list)}"
            msg_list.insert(0, {
                "content": system_message,
                "role": "system"
            })
            payload["messages"] = msg_list
    
    # Case B: 리랭커 결과인 문서가 있는 경우 → 최종 답변 생성 
    else:
        # 최종 문서 리스트를 tool content 형태로 변환 (doc- 접두사 추가)
        search_result = [{"id": f"doc-{doc['id']}", "doc": doc["doc"]} for doc in document_list]
        tool_content = json.dumps({"search_result": search_result}, ensure_ascii=False)
        
        # 대화 히스토리 형태로 구성: user → assistant(toolCalls) → tool
        # 첫 번째 user 메시지에서 쿼리 추출
        user_query = None
        for msg in msg_list:
            if msg.get("role") == "user":
                user_query = msg.get("content")
                break
        
        # 모든 tool_calls 포함 대화 히스토리 구성
        assistant_tool_calls = []
        tool_messages = []
        
        # 모든 tool_calls를 assistant 메시지에 포함
        if tool_calls:
            for tool_call in tool_calls:
                assistant_tool_calls.append({
                    "id": tool_call.get('id'),
                    "type": "function", 
                    "function": {
                        "name": "ncloud_cs_retrieval",
                        "arguments": tool_call['function']['arguments']
                    }
                })
                
                # 각 tool_call에 대한 tool 메시지 생성 (동일한 검색 결과 사용)
                tool_messages.append({
                    "role": "tool",
                    "name": "ncloud_cs_retrieval", 
                    "content": tool_content,
                    "toolCallId": tool_call.get('id')
                })
        
        msg_list = [
            {"content": user_query, "role": "user"},
            {
                "role": "assistant",
                "content": "",
                "toolCalls": assistant_tool_calls
            }
        ]
        
        # 모든 tool 메시지 추가
        msg_list.extend(tool_messages)
        payload["messages"] = msg_list
        
        # tools 정의 추가
        payload["tools"] = [{
            "function": {
                "description": "NCloud 관련 검색을 할 때 사용하는 도구입니다.\n나누어 질문해야 하는 경우 쿼리를 쪼개 나누어서 도구를 사용합니다.\n정보를 찾을 수 없었던 경우, 최종 답을 하지 않고 suggested_queries를 참고하여 도구를 다시 사용할 수 있습니다.",
                "name": "ncloud_cs_retrieval",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": "사용자의 검색어를 정제해서 넣으세요."
                        }
                    },
                    "required": ["query"]
                }
            },
            "type": "function"
        }]
    
    return payload
  • build_reasoning 함수는 RAG Reasoning에 전달할 payload를 생성하는 함수입니다. 'query'는 단일 유저 쿼리이고, 'messages'는 멀티 턴 대화 히스토리입니다.
  • 'document_list'가 있는 경우 리랭커에서 선별된 'citedDocuments'를 'search_result'로 구성하여 모델이 답변 생성 시 직접 참고할 수 있도록 제공합니다.
  • 'sugquery_list'가 있는 경우 리랭커에서 적절한 문서를 찾지 못했을 때 제공되는 'suggestedQueries'를 참고하여 재검색 가이드를 제공합니다.
  • 'toolChoice'는 auto로 설정하여 모델이 도구 호출 여부를 자율적으로 결정하게 합니다.
  • 요청과 응답을 포함한 RAG Reasoning API에 대한 자세한 가이드는 [API 가이드] RAG Reasoning를 참고해 주세요.

 

RAG Reasoning 코드의 요청(Request) 예시는 다음과 같습니다.

Quote
import requests
import json

# API 설정
reasoning_endpoint = "https://clovastudio.stream.ntruss.com/v1/api-tools/rag-reasoning"
api_key = "API_KEY"

user_query = "임베딩 v2 모델 입력이 너무 길 때 어떻게 처리해야 하는지 알려주세요."

def build_reasoning(query=None, document_list=None, sugquery_list=None, messages=None, tool_calls=None):
    # 멀티 턴 대화인지 싱글 턴 인지에 따라 메시지 구성이 달라짐
    if messages is not None:
        msg_list = messages  # 멀티 턴: 기존 대화 히스토리 사용
    else:
        msg_list = [  # 싱글 턴: 새로운 대화 시작
            {"content": query, "role": "user"}
        ]
    
    # 기본 페이로드 구성
    payload = {
        "messages": msg_list,
    }
    
    # 문서 존재 여부에 따른 조건 분기
    if document_list is None:
        # 검색 함수 정의
        payload.update({
            "tools": [{
                "function": {
                    "description": "NCloud 관련 검색을 할 때 사용하는 도구입니다.\n나누어 질문해야 하는 경우 쿼리를 쪼개 나누어서 도구를 사용합니다.\n정보를 찾을 수 없었던 경우, 최종 답을 하지 않고 sugquery_list를 참고하여 도구를 다시 사용할 수 있습니다.",
                    "name": "ncloud_cs_retrieval",
                    "parameters": {
                        "properties": {
                            "query": {
                                "description": "사용자의 검색어를 정제해서 넣으세요.",
                                "type": "string"
                            },
                            "url": {
                                "description": "검색 API 서버의 엔드포인트 URL을 입력하세요.",
                                "type": "string"
                            }
                        },
                        "required": ["query", "url"],
                        "type": "object"
                    }
                },
                "type": "function"
            }],
            "toolChoice": "auto"  
        })
        
        # 재검색 케이스
        if sugquery_list:
            system_message = f"이전 검색에서 관련 문서를 찾지 못했습니다. 다음 추천 검색어들을 참고하여 다시 검색해보세요: {', '.join(sugquery_list)}"
            msg_list.insert(0, {
                "content": system_message,
                "role": "system"
            })
            payload["messages"] = msg_list
    else:
        # 최종 문서 리스트를 tool content 형태로 변환 (doc- 접두사 추가)
        search_result = [{"id": f"doc-{doc['id']}", "doc": doc["doc"]} for doc in document_list]
        tool_content = json.dumps({"search_result": search_result}, ensure_ascii=False)
        
        # 대화 히스토리 재구성 (모든 tool_calls 포함)
        assistant_tool_calls = []
        tool_messages = []
        
        # 모든 tool_calls를 assistant 메시지에 포함
        if tool_calls:
            for tool_call in tool_calls:
                assistant_tool_calls.append({
                    "id": tool_call.get('id'),
                    "type": "function", 
                    "function": {
                        "name": "ncloud_cs_retrieval",
                        "arguments": tool_call['function']['arguments']
                    }
                })
                
                # 각 tool_call에 대한 tool 메시지 생성 (동일한 검색 결과 사용)
                tool_messages.append({
                    "role": "tool",
                    "name": "ncloud_cs_retrieval", 
                    "content": tool_content,
                    "toolCallId": tool_call.get('id')
                })
        
        msg_list = [
            {"content": user_query, "role": "user"},
            {
                "role": "assistant",
                "content": "",
                "toolCalls": assistant_tool_calls
            }
        ]
        
        # 모든 tool 메시지 추가
        msg_list.extend(tool_messages)
        payload["messages"] = msg_list
        
        # tools 정의 추가
        payload["tools"] = [{
            "function": {
                "description": "NCloud 관련 검색을 할 때 사용하는 도구입니다.\n나누어 질문해야 하는 경우 쿼리를 쪼개 나누어서 도구를 사용합니다.\n정보를 찾을 수 없었던 경우, 최종 답을 하지 않고 suggested_queries를 참고하여 도구를 다시 사용할 수 있습니다.",
                "name": "ncloud_cs_retrieval",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": "사용자의 검색어를 정제해서 넣으세요."
                        }
                    },
                    "required": ["query"]
                }
            },
            "type": "function"
        }]
    
    return payload

# 실행 
print("🔧 1단계: build_reasoning 함수 호출")
payload = build_reasoning(query=user_query)
print(json.dumps(payload, indent=2, ensure_ascii=False))

print("\n🌐 RAG Reasoning API 호출")
headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json"
}

response = requests.post(reasoning_endpoint, headers=headers, json=payload)
response.raise_for_status()
reasoning_result = response.json()

print(json.dumps(reasoning_result, indent=2, ensure_ascii=False)) 

 

위 코드에 대한 RAG Reasoning의 응답(Response) 예시는 다음과 같습니다.

Quote
🔧 1단계: build_reasoning 함수 호출
{
  "messages": [
    {
      "content": "임베딩 v2 모델 입력이 너무 길 때 어떻게 처리해야 하는지 알려주세요.",
      "role": "user"
    }
  ],
  "tools": [
    {
      "function": {
        "description": "NCloud 관련 검색을 할 때 사용하는 도구입니다.\n나누어 질문해야 하는 경우 쿼리를 쪼개 나누어서 도구를 사용합니다.\n정보를 찾을 수 없었던 경우, 최종 답을 하지 않고 sugquery_list를 참고하여 도구를 다시 사용할 수 있습니다.",
        "name": "ncloud_cs_retrieval",
        "parameters": {
          "properties": {
            "query": {
              "description": "사용자의 검색어를 정제해서 넣으세요.",
              "type": "string"
            },
            "url": {
              "description": "검색 API 서버의 엔드포인트 URL을 입력하세요.",
              "type": "string"
            }
          },
          "required": [
            "query",
            "url"
          ],
          "type": "object"
        }
      },
      "type": "function"
    }
  ],
  "toolChoice": "auto"
}

🌐 RAG Reasoning API 호출
{
  "status": {
    "code": "20000",
    "message": "OK"
  },
  "result": {
    "message": {
      "role": "assistant",
      "content": "",
      "thinkingContent": "사용자가 \"임베딩 v2 모델 입력이 너무 길 때 어떻게 처리해야 하는지\"에 대해 문의했습니다. 이 질문에 대한 답변을 찾기 위해 'ncloud_cs_retrieval' 도구를 사용하여 관련 정보를 검색할 필요가 있습니다.",
      "toolCalls": [
        {
          "id": "call_qfvgT7btHULeNVNOAQN1bKJA",
          "type": "function",
          "function": {
            "name": "ncloud_cs_retrieval",
            "arguments": {
              "query": "임베딩 v2 모델 입력이 너무 길 때 처리 방법",
              "url": "https://ncloud-docs-kr.storage.googleapis.com/ko_kr/ai-service/nlp/ai-nlp-guide/overview.html"
            }
          }
        }
      ]
    },
    "usage": {
      "promptTokens": 180,
      "completionTokens": 138,
      "totalTokens": 318
    }
  }
}

 

2.2.2 검색 API 호출

검색 함수는 2.1.2에서 구현한 검색 API를 통해 유저 쿼리와 관련된 상위 문서들을 검색하여 반환합니다. 

def ncloud_cs_retrieval(query, search_api_url):
    headers = {
        'accept': 'application/json',
        'Content-Type': 'application/json',
    }
    json_data = {
        'query': query,
        'top_k': 5,
    }
    response = requests.post(search_api_url, headers=headers, json=json_data)
    response.raise_for_status()
    return response.json()['result']
  • 'query'는  처음 RAG Reasoning에 인풋으로 넣은 유저의 쿼리 전문이 아닌 ncloud_cs_retrieval 함수가 정제한 검색어 입니다.   
  • 'top_k'는 출력할 검색 결과 상위 문서 설정 개수를 의미합니다. 본 코드에서는 편의상 5로 설정하였으나, 필요에 따라 자유롭게 조정이 가능합니다.

 

검색 API 요청(Request) 예시는 다음과 같습니다.

Quote
import requests
import json

# API 설정
search_api_url = "http://127.0.0.1:8000/search"

# 1단계 build_reasoning 함수에서 추출된 검색 쿼리 
search_query = "임베딩 v2 모델 입력 길이 제한 처리 방법"

def ncloud_cs_retrieval(query, search_api_url):
    headers = {
        'accept': 'application/json',
        'Content-Type': 'application/json',
    }
    json_data = {
        'query': query,
        'top_k': 5,
    }
    response = requests.post(search_api_url, headers=headers, json=json_data)
    response.raise_for_status()
    return response.json()['result']

# 2단계 실행
print("🔍 2단계: ncloud_cs_retrieval 함수 호출")

search_results = ncloud_cs_retrieval(search_query, search_api_url)
print(json.dumps(search_results, indent=2, ensure_ascii=False))

 

위 코드에 대한 검색 API 응답(Response) 예시는 다음과 같습니다.

Quote
🔍 2단계: ncloud_cs_retrieval 함수 호출
[
  {
    "id": "68",
    "content": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 사용 > 익스플로러 > API 활용]\r\n\r\n입력 텍스트를 숫자 형태의 벡터 값으로 변환할 수 있습니다. 사용자의 작업과 목적에 따라 세가지 모델 중 원하는 방식을 선택할 수 있습니다. 각 모델은 동일한 문장 쌍에 대해 서로 다른 유사도 결과를 반환합니다.\r\n\r\n| 도구명   | 모델명            | 최대 토큰 수 | 벡터 차원 | 권장 거리 지표 (distance metric) | 비고            |\r\n|----------|-------------------|--------------|-----------|----------------------------------|-----------------|\r\n| 임베딩   | clir-emb-dolphin  | 500 토큰     | 1024      | IP (Inner/Dot/Scalar Product; 내적) |                 |\r\n| 임베딩   | clir-sts-dolphin  | 500 토큰     | 1024      | Cosine Similarity (코사인 유사도) |                 |\r\n| 임베딩v2 | bge-m3            | 8,192 토큰   | 1024      | Cosine Similarity (코사인 유사도) | 오픈 소스 모델* |\r\n\r\n임베딩 API의 모델별 특징은 다음과 같습니다.\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-explorer_embedding1_ko.png?sv=2022-11-02&spr=https&st=2024-10-16T06%3A00%3A12Z&se=2024-10-16T06%3A10%3A12Z&sr=c&sp=r&sig=sFUvGX6%2FZLqlPGRqH3QNt0GC41kwiSKysybHHosukzg%3D)>\r\n\r\n참고\r\n임베딩 v2의 출력을 오픈소스로 제공되는 bge-m3 모델 출력과 동일하게 얻기 위해서는 아래 사항을 고려해주십시오.\r\n\r\n  - 임베딩 v2는 bge-m3 모델의 총 3가지 방식 (sparse, dense, multi-dense/colbert) 출력 중 dense 에 해당하는 값을 반환합니다.\r\n  - 임베딩 v2는 FP16 및 정규화(normalization) 적용이 되어있지 않습니다.\r\n\r\n\r\n임베딩 API는 다음과 같은 작업에 활용할 수 있습니다.\r\n\r\n  - 문장 간의 벡터 유사도를 계산하여 검색 기능을 개선할 수 있습니다. 예를 들어, 사용자가 입력한 검색어와 문서의 벡터 유사도를 측정하여 가장 관련성이 높은 문서를 반환할 수 있습니다.\r\n  - 두 문장 간의 유사도를 계산해 관련 문서의 유사성을 판단하거나 문장 간의 의미적 유사성을 비교할 수 있습니다.\r\n  - 비슷한 특성을 가진 문서를 클러스터로 그룹화할 수 있습니다.\r\n  - 문서를 분류할 수 있습니다. 벡터화된 텍스트 데이터를 학습된 모델에 사용하여 텍스트의 주제나 감성에 따라 분류하는 등 다양한 분류 작업에 활용할 수 있습니다.\r\n\r\n일반적인 임베딩의 작업 과정은 데이터 준비, 임베딩 수행, 벡터 결과물 저장, API 개발 및 결과물 호출로 구성됩니다. 이 과정을 통해 임베딩된 결과물을 저장하고 데이터베이스에 활용할 수 있습니다.\r\n\r\n임베딩 작업 과정\r\n  1. 임베딩할 파일을 Text로 변환합니다.\r\n  2. 텍스트 유형과 목적에 따라 문단 나누기 또는 요약 API 이용하여 Text를 적절하게 나눕니다.\r\n  3. 적합한 임베딩 모델을 선택 후, Text를 벡터로 변환하여 임베딩 작업을 수행합니다.\r\n  4. 임베딩된 결과물과 원본 Text를 벡터 DB에 함께 저장합니다.\r\n  5. 사용자 입력 쿼리를 벡터 변환하여 API화 → DB에 저장된 벡터와의 유사도를 비교하여 일치하는 벡터를 찾아 매핑된 원본 텍스트를 호출하여 최종 결과를 생성합니다.\r\n  6. API 결과를 prompt에 넣어 사용자가 원하는 적절한 형식의 응답으로 만들어 최종 결과물을 출력하기 위해 Chat Completions API를 사용하실 수 있습니다.\r\n\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-explorer_embedding2_ko.png?sv=2022-11-02&spr=https&st=2024-10-16T06%3A00%3A12Z&se=2024-10-16T06%3A10%3A12Z&sr=c&sp=r&sig=sFUvGX6%2FZLqlPGRqH3QNt0GC41kwiSKysybHHosukzg%3D)>\r\n\r\n\r\n임베딩 API가 한 번에 처리 가능한 최대 텍스트 길이는 500 토큰(clir-emb-dolphin, clir-sts-dolphin) 또는 8,192 토큰(v2, bge-m3)입니다. 텍스트 토큰 수 제한으로 임베딩이 어려운 경우, 긴 텍스트를 적절하게 나누기 위해 청크 방식을 사용하는 것을 권장합니다. 텍스트를 나눌 때에는 올바른 정보를 추출하기 위해 텍스트를 의미 단위로 적절하게 구분하는 것이 중요합니다. 청크는 텍스트를 작은 조각으로 나누는 작업으로 기본 텍스트 분할, 문단 나누기, 요약이 있습니다.\r\n\r\n청크 방식의 종류와 각 방식의 장단점은 다음과 같습니다.\r\n\r\n| 방식             | 설명                                           | 장점                                                                 | 단점                                                                 |\r\n|------------------|----------------------------------------------|--------------------------------------------------------------------|--------------------------------------------------------------------|\r\n| 기본 텍스트 분할 | 텍스트를 일정한 길이의 단위로 나누는 방식               | 텍스트를 세세하게 나누어 질의에 대한 정답을 추출하기 용이함                        | 텍스트를 의미 단위가 아닌 길이 단위로 자르기 때문에 텍스트의 처음과 끝 처리가 미흡하고 전체 텍스트의 의미 파악이 어려움 |\r\n| 문단             | 텍스트를 문맥에 맞게 의미 있는 문단으로 분리             | 텍스트가 의미 있는 단위로 묶여 임베딩 성능을 높일 수 있음                          | 긴 문단의 경우, 질의에 대한 정답이 있는 영역을 특정하기 어려움                            |\r\n| 요약             | 주요 내용에 초점을 두고 긴 텍스트를 짧게 요약            | 문단 나누기보다 더 긴 맥락의 텍스트를 요약할 수 있어 문서 단위로 임베딩하기에 용이함         | 긴 문단의 경우, 질의에 대한 정답이 있는 영역을 특정하기 어려움                            |\r\n\r\n참고\r\nCLOVA Studio에서는 토큰 계산기(임베딩 v2) API, 문단 나누기 API와 요약 API를 제공합니다. 자세한 정보는 토큰 계산기 API[토큰 계산기 API](주소: https://guide.ncloud-docs.com/docs/clovastudio-explorer03#%ED%86%A0%ED%81%B0%EA%B3%84%EC%82%B0%EA%B8%B0API), 문단 나누기 API[문단 나누기 API](주소: https://guide.ncloud-docs.com/docs/clovastudio-explorer03#%EB%AC%B8%EB%8B%A8%EB%82%98%EB%88%84%EA%B8%B0API)와 요약 API[요약 API](주소: https://guide.ncloud-docs.com/docs/clovastudio-explorer03#%EC%9A%94%EC%95%BDAPI)를 참고해 주십시오.\r\n\r\n\r\nbge-m3 모델 인용 정보는 다음과 같습니다.\r\n\r\n@misc{bge-m3,\r\n    title={BGE M3-Embedding: Multi-Lingual, Multi-Functionality, Multi-Granularity Text Embeddings Through Self-Knowledge Distillation},\r\n    author={Jianlv Chen and Shitao Xiao and Peitian Zhang and Kun Luo and Defu Lian and Zheng Liu},\r\n    year={2024},\r\n    eprint={2402.03216},\r\n    archivePrefix={arXiv},\r\n    primaryClass={cs.CL}\r\n}\r\nPlain text\r\n"
  },
  {
    "id": "117",
    "content": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 이용량 제어 정책]\r\n\r\nCLOVA Studio에서 이용량이 제한되는 수치는 분당 요청 횟수인 QPM과 분당 처리 토큰 수를 의미하는 TPM입니다. QPM과 TPM이 모두 적용되는 경우, 두 개의 값 중 하나라도 최댓값에 먼저 도달하면 오류 코드가 반환됩니다.\r\nQPM과 TPM에 대한 설명은 다음과 같습니다.\r\n\r\n| 구분 | 설명 |\r\n|------|------|\r\n| QPM (Queries per Minute) | 1분 동안 모델 및 도구에 작업을 요청한 횟수 |\r\n| TPM (Tokens per Minute) | 1분 동안 처리할 토큰 수 |\r\n| - 처리할 토큰 수 | 입력된 토큰 수 + 결괏값 생성 시 사용할 최대 토큰 수(maxTokens) | \r\n\r\n참고\r\n2024년 5월 9일 기준으로 지원 가능한 최대 이용량으로, 추후 변동될 수 있습니다.\r\n\r\n\r\nCLOVA Studio 플레이그라운드와 테스트 앱에서 이용 가능한 최대 QPM, TPM을 설명합니다.\r\n\r\n\r\n모델별로 이용 가능한 최대 QPM, TPM은 다음과 같습니다.\r\n\r\n| 구분            | QPM | TPM   |\r\n|-----------------|-----|-------|\r\n| HCX-003         | 60  | 10,000 |\r\n| HCX-DASH-001    | 100 | 20,000 |\r\n| LK-D2           | 60  | 10,000 |\r\n| LK-B            | 60  | 10,000 |\r\n- 튜닝 모델: 튜닝 시 사용된 모델에 대한 요청으로 간주.\r\n\r\n\r\n익스플로러 도구별로 이용 가능한 최대 QPM, TPM은 다음과 같습니다.\r\n\r\n| 구분         | QPM  | TPM    | 비고                        |\r\n|--------------|------|--------|-----------------------------|\r\n| 요약         | 30   | 30,000 | TPM 계산 시 입력값만 포함   |\r\n| 문단 나누기  | 120  | -      |                             |\r\n| 임베딩       | 60   | -      |                             |\r\n| 임베딩 v2    | 60   | 40,000 | TPM 계산 시 입력값만 포함   |\r\n\r\n\r\n서비스 앱에서 모델별, 도구별로 이용 가능한 최대 QPM과 TPM을 설명합니다.\r\n서비스 앱의 이용 가능한 최대 QPM, TPM 계산 시 웹 및 테스트 앱의 최대 이용량도 포함됩니다. 따라서 명시된 최댓값으로 이용하려면 서비스 앱과 테스트 앱을 동시에 운영해야 하며, 서비스 앱 단독 운영 시에는 테스트 앱의 최대 이용량을 뺀 값이 적용됩니다.\r\n\r\n\r\n모델별로 이용 가능한 최대 QPM, TPM은 다음과 같습니다.\r\n\r\n| 모델            | QPM  | TPM    |\r\n|-----------------|------|--------|\r\n| HCX-003         | 300  | 60,000 |\r\n| HCX-DASH-001    | 400  | 80,000 |\r\n| LK-D2           | 420  | 60,000 |\r\n| LK-B            | 360  | 60,000 |\r\n- 튜닝 모델: 튜닝 시 사용한 모델에 대한 요청으로 간주됩니다.\r\n\r\n\r\n익스플로러 도구별로 이용 가능한 최대 QPM, TPM은 다음과 같습니다.\r\n\r\n| 구분         | QPM  | TPM     | 비고                        |\r\n|--------------|------|---------|-----------------------------|\r\n| 요약         | 90   | 90,000  | TPM 계산 시 입력값만 포함   |\r\n| 문단 나누기  | 480  | -       |                             |\r\n| 임베딩       | 300  | -       |                             |\r\n| 임베딩 v2    | 360  | 240,000 | TPM 계산 시 입력값만 포함   |"
  },
  {
    "id": "102",
    "content": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 사용 > 스킬 트레이너 > 스킬 트레이너 FAQ]\r\n\r\nQ. 데이터 수집 시 최대 몇 번의 스킬 호출을 수행할 수 있나요?\r\nA. 데이터 수집 시 최대 5번의 스킬 호출이 가능합니다.\r\n\r\nQ. 액션 입력 생성 결과가 의도한 대로 나오지 않습니다. 의도한 대로 모델이 액션 입력을 생성하게 하려면 어떻게 해야 하나요??\r\nA. API Spec에서 파라미터의 'description'에 예시를 추가해 주십시오. 예시가 있으면 언어 모델이 API Spec을 기반으로 유저 쿼리를 이해하여 올바른 파라미터를 갖춘 URL을 생성할 수 있습니다.\r\n<작성 예시>\r\n\r\n{\r\n    \"keyword\": {\r\n        \"type\": \"string\",\r\n        \"description\": \"수식어를 의미하며, 관형어와 부사어의 형태입니다. <예시> 친절한, 만족스러운, 좋은\",\r\n    },\r\n    \"review\": {\r\n        \"type\": \"string\",\r\n        \"description\": \"긍부정 선호도. <예시> pos, neg 둘 중 하나의 값\"\r\n    }\r\n}\r\nJSON\r\n\r\nQ. 데이터 수집 시 '플래닝 가능한 스킬이 없습니다'라는 오류 메시지가 나타납니다. 참고로 스킬은 '완료'로 저장된 상태입니다.\r\nA. 모델이 스킬의 API Spec 또는 Manifest를 제대로 이해하지 못한 경우 발생하는 오류입니다. API Spec 및 Manifest 작성 가이드에 따라 모델이 충분히 이해할 수 있도록 다시 작성해 주십시오. 문제가 계속 발생할 경우, 포럼[포럼](주소: https://www.ncloud-forums.com/) 문의해 주십시오.\r\n\r\nQ. 데이터 수집 시 '토큰 수 초과' 오류 메시지가 나타납니다. 토큰이 얼마나 초과되었는 지 확인할 수 있나요?\r\nA. 1개의 데이터 수집에 제한된 토큰 수는 4,096 토큰입니다. 유저 쿼리, 모델의 사고 과정(생각, 액션 등), 스킬 정보(API Spec, Manifest) 등이 모두 합산됩니다. 토큰 수를 계산하려면 CLOVA Studio의 익스플로러 > 토큰 계산기(HCX)[토큰 계산기(HCX)](주소: https://guide.ncloud-docs.com/docs/clovastudio-explorer)를 이용해 주십시오.\r\n\r\nQ. 호출 옵션은 데이터 수집할 때마다 매번 초기화되나요?\r\nA. 호출 옵션을 적용하여 데이터 수집을 진행한 후, 다음 데이터 수집을 위해 [초기화] 버튼을 클릭하더라도 호출 옵션은 그대로 유지됩니다. 단, 데이터 수집 화면에서 벗어난 후에 데이터 수집 화면에 다시 접속한 경우에는 호출 옵션이 초기화됩니다. 이런 경우 불러오기 기능으로 임시저장한 작업을 불러오면 이전에 사용한 호출 옵션을 함께 불러올 수 있습니다.\r\n\r\n용도에 따른 호출 옵션 작성 예시가 궁금합니다.\r\nA. 스킬셋 내에 등록된 모든 API에 대해 호출 옵션을 일괄 적용하고자 할 때 baseOperation 필드를 활용할 수 있습니다. 작성 예시는 다음과 같습니다.\r\n\r\n{\r\n  \"baseOperation\": {\r\n    \"query\": {\r\n      \"serviceKey\": \"value\"\r\n    }\r\n  }\r\n}\r\nPlain text\r\n\r\n특정 Operation ID에 한하여 호출 옵션을 적용하고자 할 때 operations 필드를 활용할 수 있습니다. 작성 예시는 다음과 같습니다.\r\n\r\n{\r\n  \"operations\": [\r\n    {\r\n      \"operationId\": \"filterBakeryProducts\",\r\n      \"query\": {\r\n        \"serviceKey\": \"value\"\r\n      }\r\n    }\r\n  ]\r\n}\r\nPlain text\r\n\r\nbaseOperation 필드와 operations 필드를 함께 적용하는 것도 가능합니다. 작성 예시는 다음과 같습니다.\r\n\r\n{\r\n  \"baseOperation\": {\r\n    \"header\": {\r\n      \"baseHeader\": \"baseHeaderValue\"\r\n    },\r\n    \"query\": {\r\n      \"baseQuery\": \"baseQueryValue\"\r\n    },\r\n    \"requestBody\": {\r\n      \"baseBody\": \"baseBodyValue\"\r\n    }\r\n  },\r\n  \"operations\": [\r\n    {\r\n      \"operationId\": \"testOperationId1\",\r\n      \"header\": {\r\n        \"header1\": \"headerValue1\"\r\n      },\r\n      \"query\": {\r\n        \"query1\": \"queryValue1\"\r\n      },\r\n      \"requestBody\": {\r\n        \"body1\": \"bodyValue1\"\r\n      }\r\n    },\r\n    {\r\n      \"operationId\": \"testOperationId2\",\r\n      \"header\": {\r\n        \"header2\": \"headerValue2\"\r\n      },\r\n      \"query\": {\r\n        \"query2\": \"queryValue2\"\r\n      },\r\n      \"requestBody\": {\r\n        \"body2\": \"bodyValue2\"\r\n      }\r\n    }\r\n  ]\r\n}\r\nPlain text\r\n\r\nQ. 데이터 수집 작업 중에 오류가 발생했습니다. 로그를 어떻게 확인할 수 있나요?\r\nA. 작업 중 오류가 발생하면 다음 정보와 함께 문의를 남겨 주십시오. 해결 방법을 안내해 드리겠습니다.\r\n\r\n  - 오류 화면(스크린숏)\r\n  - 스킬셋\r\n  - 스킬 정보\r\n\r\nQ. 유저 쿼리에 필수 파라미터가 누락되었을 때 모델이 누락된 파라미터에 대해 재질의 하는 답변을 생성할 수 있나요?\r\nA. 네 가능합니다. API 설계 시 필수 파라미터가 누락된 경우, 오류 코드(ex. HTTP 상태코드 400)를 반환하지 않고 정상 코드(ex. HTTP 상태코드 200)로 반환하도록 해야 합니다. 대신 누락된 파라미터를 알려주는 메시지를 함께 반환하는 것이 좋습니다. 이렇게 하면 최종 답변 영역에서 모델이 누락된 파라미터를 요청하는 질문을 생성하고, 원하는 형식으로 튜닝 및 학습도 진행할 수 있습니다.\r\n\r\n<데이터 수집 예시>\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-faq_scenario_ko.png?sv=2022-11-02&spr=https&st=2024-10-17T01%3A46%3A15Z&se=2024-10-17T01%3A56%3A15Z&sr=c&sp=r&sig=XHxjTOMXoLQUDphjKfg0RVwW%2BD0Zy%2F55J7x4FWkGeMk%3D)>\r\n\r\n참고\r\n다음 주의 사항을 참고해 주십시오.\r\n\r\n  - 유저 쿼리에 필수 파라미터가 누락되었을 때 모델이 자체적으로 API 호출 단계(스킬 호출의 Step 2)를 수행하지 않고 생략할 수도 있습니다. 이 경우 스킬셋 답변 생성 API 호출 시에는 54020 에러가 발생되어 최종 답변이 생성되지 않습니다. 따라서 필수 파라미터가 누락된 케이스일지라도 API 호출 단계를 생략하지 않도록 유의하여 데이터를 수집해 주시기 바랍니다."
  },
  {
    "id": "41",
    "content": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 사용 > 튜닝 > Instruction 데이터셋 준비]\r\n\r\nInstruction 데이터셋의 파일 규격은 다음과 같습니다.\r\n\r\n| 항목            | 설명                                |\r\n|-----------------|-----------------------------------|\r\n| 파일 확장자       | .csv 또는 .jsonl                   |\r\n| 파일 인코딩 형식   | UTF-8                             |\r\n| 최소 데이터       | - 최소: 100행 - 권장: 1,000행 ~ 100,000행 |\r\n| 파일 용량         | 1TB 이하 (NCP Object storage 가능 범위) |"
  },
  {
    "id": "119",
    "content": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 이용량 제어 정책]\r\n\r\nCLOVA Studio 서비스 이용 시 요청할 수 있는 최댓값이 초과될 경우, HTTP 429 코드[HTTP 429 코드](주소: https://api.ncloud-docs.com/docs/ai-naver-clovastudio-summary#%EC%9D%91%EB%8B%B5%EC%83%81%ED%83%9C%EC%BD%94%EB%93%9C)가 반환되거나 오류 메시지가 나타납니다. 최대 이용량을 초과하지 않기 위해 수행할 수 있는 작업을 설명합니다.\r\n\r\n참고\r\nCLOVA Studio는 안정적이고 원활한 서비스를 제공하기 위해 최선을 다하고 있으나 최대 요청량 이내로 서비스를 이용할 경우에도 일부 처리 지연이 발생하거나 실패할 수 있습니다.\r\n\r\n\r\nQPM 수치를 미리 확인한 후 최댓값 이내로 요청해 주십시오.\r\n별도의 API 호출 제어 기능(rate limit)을 직접 구현 및 추가해 주십시오.\r\n요청 사이에 일정 시간 지연 기능(time sleep)을 별도로 추가해 주십시오.\r\nHTTP 429 코드 반환 및 해당 오류 메시지 응답 시 예외 처리를 통해 일정 시간 지연 이후 재요청해 주십시오.\r\n\r\n\r\n  - TPM 수치를 미리 확인하여 작업을 위해 입력하는 토큰 수와 결괏값 생성 시 사용할 최대 토큰 수를 실제로 필요한 정도만 설정해 주십시오.\r\n  - API 호출 시 입력한 문자열의 토큰 수를 확인하려면 익스플로러 메뉴의 토큰 계산기 API를 활용해 주십시오.\r\n  - API 호출 시 결괏값에 사용할 최대 토큰 수를 조정하려면 maxTokens 필드의 값을 수정해 주십시오.\r\n  - 플레이그라운드 메뉴에서 입력된 문자열의 토큰 수를 확인하려면 플레이그라운드 화면 상단의 계산 아이콘을 클릭해 주십시오.\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-ratelimiting_tpm-mitigation01_ko.png?sv=2022-11-02&spr=https&st=2024-10-17T07%3A04%3A02Z&se=2024-10-17T07%3A14%3A02Z&sr=c&sp=r&sig=9r5vWPhp0VEufhmk5OjQQB4jATO84QuSlHVvWzPdBRU%3D)>\r\n  - 플레이그라운드 메뉴에서 결괏값 생성 시 사용할 최대 토큰 수를 조정하려면 플레이그라운드 화면 왼쪽에 있는 Maximum tokens 필드를 수정해 주십시오.\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-ratelimiting_tpm-mitigation02_ko.png?sv=2022-11-02&spr=https&st=2024-10-17T07%3A04%3A02Z&se=2024-10-17T07%3A14%3A02Z&sr=c&sp=r&sig=9r5vWPhp0VEufhmk5OjQQB4jATO84QuSlHVvWzPdBRU%3D)>"
  }
]

 

2.2.3 리랭커 API 호출

검색 단계에서 가져온 문서들은 관련성이 떨어지는 내용이 포함될 수 있습니다. 리랭커 API는 이러한 문서들 중에서 유저 쿼리와 가장 적합한 핵심적인 문서를 선별하여 RAG Reasoning이 최종 답변을 생성할 때 참조할 수 있도록 돕습니다.

def reranker_function(query, documents, api_url, api_key):
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json',
    }
    
    # 유효한 문서만 필터링 (빈 내용, None, NaN 제외)
    indexed_documents = [
        {"id": str(doc["id"]), "doc": doc["content"]}
        for doc in documents
        if doc.get("content") not in [None, "", "None", "nan", "NaN"]
    ]
    
    json_data = {
        "query": query,
        "documents": indexed_documents
    }
    response = requests.post(api_url, headers=headers, json=json_data)
    response.raise_for_status()
    
    result = response.json()
    # 리랭커 결과에서 관련 문서와 추천 검색어만 추출하여 document_list와 sugquery_list에 저장
    document_list = result.get('result', {}).get('citedDocuments', [])
    sugquery_list = result.get('result', {}).get('suggestedQueries', [])
    
    return document_list, sugquery_list
  • 검색 API의 결과값인 문서와 고유 id를 입력받습니다. 리랭커 함수는 RAG Reasoning에 전달하기 위해 응답 파라미터로 'citedDocuments' 값을 반환합니다.
  • 리랭커의 답변인 'result'가 아닌 'citedDocuments'만을 RAG Reasoning에 전달하는 이유는 불필요한 부가 정보를 제외하고 실제 답변 생성에 필요한 문서 원문만을 전달함으로써 토큰 낭비를 줄이며, 리랭커가 선별한 문서를 기반으로 RAG Reasoning 이 한번 더 생각/처리하여 최종 답변을 생성내도록 하기 위함입니다.
  • 요청과 응답을 포함한 리랭커 API에 대한 더욱 자세한 가이드는 [API 가이드] 리랭커를 참고해 주세요.

 

리랭커 API 요청(Request) 예시는 다음과 같습니다.

Quote
import requests
import json

# API 설정
reranker_api_url = "https://clovastudio.stream.ntruss.com/v1/api-tools/reranker"
api_key = "API_KEY"

# 2단계 검색 결과 
search_query = "임베딩 v2 모델 입력 길이 제한 처리 방법"
search_results = [
    {
        "id": "68",
        "content": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 사용 > 익스플로러 > API 활용]\r\n\r\n입력 텍스트를 숫자 형태의 벡터 값으로 변환할 수 있습니다. 사용자의 작업과 목적에 따라 세가지 모델 중 원하는 방식을 선택할 수 있습니다. 각 모델은 동일한 문장 쌍에 대해 서로 다른 유사도 결과를 반환합니다.\r\n\r\n| 도구명   | 모델명            | 최대 토큰 수 | 벡터 차원 | 권장 거리 지표 (distance metric) | 비고            |\r\n|----------|-------------------|--------------|-----------|----------------------------------|-----------------|\r\n| 임베딩   | clir-emb-dolphin  | 500 토큰     | 1024      | IP (Inner/Dot/Scalar Product; 내적) |                 |\r\n| 임베딩   | clir-sts-dolphin  | 500 토큰     | 1024      | Cosine Similarity (코사인 유사도) |                 |\r\n| 임베딩v2 | bge-m3            | 8,192 토큰   | 1024      | Cosine Similarity (코사인 유사도) | 오픈 소스 모델* |\r\n\r\n임베딩 API의 모델별 특징은 다음과 같습니다.\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-explorer_embedding1_ko.png?sv=2022-11-02&spr=https&st=2024-10-16T06%3A00%3A12Z&se=2024-10-16T06%3A10%3A12Z&sr=c&sp=r&sig=sFUvGX6%2FZLqlPGRqH3QNt0GC41kwiSKysybHHosukzg%3D)>\r\n\r\n참고\r\n임베딩 v2의 출력을 오픈소스로 제공되는 bge-m3 모델 출력과 동일하게 얻기 위해서는 아래 사항을 고려해주십시오.\r\n\r\n  - 임베딩 v2는 bge-m3 모델의 총 3가지 방식 (sparse, dense, multi-dense/colbert) 출력 중 dense 에 해당하는 값을 반환합니다.\r\n  - 임베딩 v2는 FP16 및 정규화(normalization) 적용이 되어있지 않습니다.\r\n\r\n\r\n임베딩 API는 다음과 같은 작업에 활용할 수 있습니다.\r\n\r\n  - 문장 간의 벡터 유사도를 계산하여 검색 기능을 개선할 수 있습니다. 예를 들어, 사용자가 입력한 검색어와 문서의 벡터 유사도를 측정하여 가장 관련성이 높은 문서를 반환할 수 있습니다.\r\n  - 두 문장 간의 유사도를 계산해 관련 문서의 유사성을 판단하거나 문장 간의 의미적 유사성을 비교할 수 있습니다.\r\n  - 비슷한 특성을 가진 문서를 클러스터로 그룹화할 수 있습니다.\r\n  - 문서를 분류할 수 있습니다. 벡터화된 텍스트 데이터를 학습된 모델에 사용하여 텍스트의 주제나 감성에 따라 분류하는 등 다양한 분류 작업에 활용할 수 있습니다.\r\n\r\n일반적인 임베딩의 작업 과정은 데이터 준비, 임베딩 수행, 벡터 결과물 저장, API 개발 및 결과물 호출로 구성됩니다. 이 과정을 통해 임베딩된 결과물을 저장하고 데이터베이스에 활용할 수 있습니다.\r\n\r\n임베딩 작업 과정\r\n  1. 임베딩할 파일을 Text로 변환합니다.\r\n  2. 텍스트 유형과 목적에 따라 문단 나누기 또는 요약 API 이용하여 Text를 적절하게 나눕니다.\r\n  3. 적합한 임베딩 모델을 선택 후, Text를 벡터로 변환하여 임베딩 작업을 수행합니다.\r\n  4. 임베딩된 결과물과 원본 Text를 벡터 DB에 함께 저장합니다.\r\n  5. 사용자 입력 쿼리를 벡터 변환하여 API화 → DB에 저장된 벡터와의 유사도를 비교하여 일치하는 벡터를 찾아 매핑된 원본 텍스트를 호출하여 최종 결과를 생성합니다.\r\n  6. API 결과를 prompt에 넣어 사용자가 원하는 적절한 형식의 응답으로 만들어 최종 결과물을 출력하기 위해 Chat Completions API를 사용하실 수 있습니다.\r\n\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-explorer_embedding2_ko.png?sv=2022-11-02&spr=https&st=2024-10-16T06%3A00%3A12Z&se=2024-10-16T06%3A10%3A12Z&sr=c&sp=r&sig=sFUvGX6%2FZLqlPGRqH3QNt0GC41kwiSKysybHHosukzg%3D)>\r\n\r\n\r\n임베딩 API가 한 번에 처리 가능한 최대 텍스트 길이는 500 토큰(clir-emb-dolphin, clir-sts-dolphin) 또는 8,192 토큰(v2, bge-m3)입니다. 텍스트 토큰 수 제한으로 임베딩이 어려운 경우, 긴 텍스트를 적절하게 나누기 위해 청크 방식을 사용하는 것을 권장합니다. 텍스트를 나눌 때에는 올바른 정보를 추출하기 위해 텍스트를 의미 단위로 적절하게 구분하는 것이 중요합니다. 청크는 텍스트를 작은 조각으로 나누는 작업으로 기본 텍스트 분할, 문단 나누기, 요약이 있습니다.\r\n\r\n청크 방식의 종류와 각 방식의 장단점은 다음과 같습니다.\r\n\r\n| 방식             | 설명                                           | 장점                                                                 | 단점                                                                 |\r\n|------------------|----------------------------------------------|--------------------------------------------------------------------|--------------------------------------------------------------------|\r\n| 기본 텍스트 분할 | 텍스트를 일정한 길이의 단위로 나누는 방식               | 텍스트를 세세하게 나누어 질의에 대한 정답을 추출하기 용이함                        | 텍스트를 의미 단위가 아닌 길이 단위로 자르기 때문에 텍스트의 처음과 끝 처리가 미흡하고 전체 텍스트의 의미 파악이 어려움 |\r\n| 문단             | 텍스트를 문맥에 맞게 의미 있는 문단으로 분리             | 텍스트가 의미 있는 단위로 묶여 임베딩 성능을 높일 수 있음                          | 긴 문단의 경우, 질의에 대한 정답이 있는 영역을 특정하기 어려움                            |\r\n| 요약             | 주요 내용에 초점을 두고 긴 텍스트를 짧게 요약            | 문단 나누기보다 더 긴 맥락의 텍스트를 요약할 수 있어 문서 단위로 임베딩하기에 용이함         | 긴 문단의 경우, 질의에 대한 정답이 있는 영역을 특정하기 어려움                            |\r\n\r\n참고\r\nCLOVA Studio에서는 토큰 계산기(임베딩 v2) API, 문단 나누기 API와 요약 API를 제공합니다. 자세한 정보는 토큰 계산기 API[토큰 계산기 API](주소: https://guide.ncloud-docs.com/docs/clovastudio-explorer03#%ED%86%A0%ED%81%B0%EA%B3%84%EC%82%B0%EA%B8%B0API), 문단 나누기 API[문단 나누기 API](주소: https://guide.ncloud-docs.com/docs/clovastudio-explorer03#%EB%AC%B8%EB%8B%A8%EB%82%98%EB%88%84%EA%B8%B0API)와 요약 API[요약 API](주소: https://guide.ncloud-docs.com/docs/clovastudio-explorer03#%EC%9A%94%EC%95%BDAPI)를 참고해 주십시오.\r\n\r\n\r\nbge-m3 모델 인용 정보는 다음과 같습니다.\r\n\r\n@misc{bge-m3,\r\n    title={BGE M3-Embedding: Multi-Lingual, Multi-Functionality, Multi-Granularity Text Embeddings Through Self-Knowledge Distillation},\r\n    author={Jianlv Chen and Shitao Xiao and Peitian Zhang and Kun Luo and Defu Lian and Zheng Liu},\r\n    year={2024},\r\n    eprint={2402.03216},\r\n    archivePrefix={arXiv},\r\n    primaryClass={cs.CL}\r\n}\r\nPlain text\r\n"
    },
    {
        "id": "117",
        "content": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 이용량 제어 정책]\r\n\r\nCLOVA Studio에서 이용량이 제한되는 수치는 분당 요청 횟수인 QPM과 분당 처리 토큰 수를 의미하는 TPM입니다. QPM과 TPM이 모두 적용되는 경우, 두 개의 값 중 하나라도 최댓값에 먼저 도달하면 오류 코드가 반환됩니다.\r\nQPM과 TPM에 대한 설명은 다음과 같습니다.\r\n\r\n| 구분 | 설명 |\r\n|------|------|\r\n| QPM (Queries per Minute) | 1분 동안 모델 및 도구에 작업을 요청한 횟수 |\r\n| TPM (Tokens per Minute) | 1분 동안 처리할 토큰 수 |\r\n| - 처리할 토큰 수 | 입력된 토큰 수 + 결괏값 생성 시 사용할 최대 토큰 수(maxTokens) | \r\n\r\n참고\r\n2024년 5월 9일 기준으로 지원 가능한 최대 이용량으로, 추후 변동될 수 있습니다.\r\n\r\n\r\nCLOVA Studio 플레이그라운드와 테스트 앱에서 이용 가능한 최대 QPM, TPM을 설명합니다.\r\n\r\n\r\n모델별로 이용 가능한 최대 QPM, TPM은 다음과 같습니다.\r\n\r\n| 구분            | QPM | TPM   |\r\n|-----------------|-----|-------|\r\n| HCX-003         | 60  | 10,000 |\r\n| HCX-DASH-001    | 100 | 20,000 |\r\n| LK-D2           | 60  | 10,000 |\r\n| LK-B            | 60  | 10,000 |\r\n- 튜닝 모델: 튜닝 시 사용된 모델에 대한 요청으로 간주.\r\n\r\n\r\n익스플로러 도구별로 이용 가능한 최대 QPM, TPM은 다음과 같습니다.\r\n\r\n| 구분         | QPM  | TPM    | 비고                        |\r\n|--------------|------|--------|-----------------------------|\r\n| 요약         | 30   | 30,000 | TPM 계산 시 입력값만 포함   |\r\n| 문단 나누기  | 120  | -      |                             |\r\n| 임베딩       | 60   | -      |                             |\r\n| 임베딩 v2    | 60   | 40,000 | TPM 계산 시 입력값만 포함   |\r\n\r\n\r\n서비스 앱에서 모델별, 도구별로 이용 가능한 최대 QPM과 TPM을 설명합니다.\r\n서비스 앱의 이용 가능한 최대 QPM, TPM 계산 시 웹 및 테스트 앱의 최대 이용량도 포함됩니다. 따라서 명시된 최댓값으로 이용하려면 서비스 앱과 테스트 앱을 동시에 운영해야 하며, 서비스 앱 단독 운영 시에는 테스트 앱의 최대 이용량을 뺀 값이 적용됩니다.\r\n\r\n\r\n모델별로 이용 가능한 최대 QPM, TPM은 다음과 같습니다.\r\n\r\n| 모델            | QPM  | TPM    |\r\n|-----------------|------|--------|\r\n| HCX-003         | 300  | 60,000 |\r\n| HCX-DASH-001    | 400  | 80,000 |\r\n| LK-D2           | 420  | 60,000 |\r\n| LK-B            | 360  | 60,000 |\r\n- 튜닝 모델: 튜닝 시 사용한 모델에 대한 요청으로 간주됩니다.\r\n\r\n\r\n익스플로러 도구별로 이용 가능한 최대 QPM, TPM은 다음과 같습니다.\r\n\r\n| 구분         | QPM  | TPM     | 비고                        |\r\n|--------------|------|---------|-----------------------------|\r\n| 요약         | 90   | 90,000  | TPM 계산 시 입력값만 포함   |\r\n| 문단 나누기  | 480  | -       |                             |\r\n| 임베딩       | 300  | -       |                             |\r\n| 임베딩 v2    | 360  | 240,000 | TPM 계산 시 입력값만 포함   |"
    },
    {
        "id": "102",
        "content": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 사용 > 스킬 트레이너 > 스킬 트레이너 FAQ]\r\n\r\nQ. 데이터 수집 시 최대 몇 번의 스킬 호출을 수행할 수 있나요?\r\nA. 데이터 수집 시 최대 5번의 스킬 호출이 가능합니다.\r\n\r\nQ. 액션 입력 생성 결과가 의도한 대로 나오지 않습니다. 의도한 대로 모델이 액션 입력을 생성하게 하려면 어떻게 해야 하나요??\r\nA. API Spec에서 파라미터의 'description'에 예시를 추가해 주십시오. 예시가 있으면 언어 모델이 API Spec을 기반으로 유저 쿼리를 이해하여 올바른 파라미터를 갖춘 URL을 생성할 수 있습니다.\r\n<작성 예시>\r\n\r\n{\r\n    \"keyword\": {\r\n        \"type\": \"string\",\r\n        \"description\": \"수식어를 의미하며, 관형어와 부사어의 형태입니다. <예시> 친절한, 만족스러운, 좋은\",\r\n    },\r\n    \"review\": {\r\n        \"type\": \"string\",\r\n        \"description\": \"긍부정 선호도. <예시> pos, neg 둘 중 하나의 값\"\r\n    }\r\n}\r\nJSON\r\n\r\nQ. 데이터 수집 시 '플래닝 가능한 스킬이 없습니다'라는 오류 메시지가 나타납니다. 참고로 스킬은 '완료'로 저장된 상태입니다.\r\nA. 모델이 스킬의 API Spec 또는 Manifest를 제대로 이해하지 못한 경우 발생하는 오류입니다. API Spec 및 Manifest 작성 가이드에 따라 모델이 충분히 이해할 수 있도록 다시 작성해 주십시오. 문제가 계속 발생할 경우, 포럼[포럼](주소: https://www.ncloud-forums.com/) 문의해 주십시오.\r\n\r\nQ. 데이터 수집 시 '토큰 수 초과' 오류 메시지가 나타납니다. 토큰이 얼마나 초과되었는 지 확인할 수 있나요?\r\nA. 1개의 데이터 수집에 제한된 토큰 수는 4,096 토큰입니다. 유저 쿼리, 모델의 사고 과정(생각, 액션 등), 스킬 정보(API Spec, Manifest) 등이 모두 합산됩니다. 토큰 수를 계산하려면 CLOVA Studio의 익스플로러 > 토큰 계산기(HCX)[토큰 계산기(HCX)](주소: https://guide.ncloud-docs.com/docs/clovastudio-explorer)를 이용해 주십시오.\r\n\r\nQ. 호출 옵션은 데이터 수집할 때마다 매번 초기화되나요?\r\nA. 호출 옵션을 적용하여 데이터 수집을 진행한 후, 다음 데이터 수집을 위해 [초기화] 버튼을 클릭하더라도 호출 옵션은 그대로 유지됩니다. 단, 데이터 수집 화면에서 벗어난 후에 데이터 수집 화면에 다시 접속한 경우에는 호출 옵션이 초기화됩니다. 이런 경우 불러오기 기능으로 임시저장한 작업을 불러오면 이전에 사용한 호출 옵션을 함께 불러올 수 있습니다.\r\n\r\n용도에 따른 호출 옵션 작성 예시가 궁금합니다.\r\nA. 스킬셋 내에 등록된 모든 API에 대해 호출 옵션을 일괄 적용하고자 할 때 baseOperation 필드를 활용할 수 있습니다. 작성 예시는 다음과 같습니다.\r\n\r\n{\r\n  \"baseOperation\": {\r\n    \"query\": {\r\n      \"serviceKey\": \"value\"\r\n    }\r\n  }\r\n}\r\nPlain text\r\n\r\n특정 Operation ID에 한하여 호출 옵션을 적용하고자 할 때 operations 필드를 활용할 수 있습니다. 작성 예시는 다음과 같습니다.\r\n\r\n{\r\n  \"operations\": [\r\n    {\r\n      \"operationId\": \"filterBakeryProducts\",\r\n      \"query\": {\r\n        \"serviceKey\": \"value\"\r\n      }\r\n    }\r\n  ]\r\n}\r\nPlain text\r\n\r\nbaseOperation 필드와 operations 필드를 함께 적용하는 것도 가능합니다. 작성 예시는 다음과 같습니다.\r\n\r\n{\r\n  \"baseOperation\": {\r\n    \"header\": {\r\n      \"baseHeader\": \"baseHeaderValue\"\r\n    },\r\n    \"query\": {\r\n      \"baseQuery\": \"baseQueryValue\"\r\n    },\r\n    \"requestBody\": {\r\n      \"baseBody\": \"baseBodyValue\"\r\n    }\r\n  },\r\n  \"operations\": [\r\n    {\r\n      \"operationId\": \"testOperationId1\",\r\n      \"header\": {\r\n        \"header1\": \"headerValue1\"\r\n      },\r\n      \"query\": {\r\n        \"query1\": \"queryValue1\"\r\n      },\r\n      \"requestBody\": {\r\n        \"body1\": \"bodyValue1\"\r\n      }\r\n    },\r\n    {\r\n      \"operationId\": \"testOperationId2\",\r\n      \"header\": {\r\n        \"header2\": \"headerValue2\"\r\n      },\r\n      \"query\": {\r\n        \"query2\": \"queryValue2\"\r\n      },\r\n      \"requestBody\": {\r\n        \"body2\": \"bodyValue2\"\r\n      }\r\n    }\r\n  ]\r\n}\r\nPlain text\r\n\r\nQ. 데이터 수집 작업 중에 오류가 발생했습니다. 로그를 어떻게 확인할 수 있나요?\r\nA. 작업 중 오류가 발생하면 다음 정보와 함께 문의를 남겨 주십시오. 해결 방법을 안내해 드리겠습니다.\r\n\r\n  - 오류 화면(스크린숏)\r\n  - 스킬셋\r\n  - 스킬 정보\r\n\r\nQ. 유저 쿼리에 필수 파라미터가 누락되었을 때 모델이 누락된 파라미터에 대해 재질의 하는 답변을 생성할 수 있나요?\r\nA. 네 가능합니다. API 설계 시 필수 파라미터가 누락된 경우, 오류 코드(ex. HTTP 상태코드 400)를 반환하지 않고 정상 코드(ex. HTTP 상태코드 200)로 반환하도록 해야 합니다. 대신 누락된 파라미터를 알려주는 메시지를 함께 반환하는 것이 좋습니다. 이렇게 하면 최종 답변 영역에서 모델이 누락된 파라미터를 요청하는 질문을 생성하고, 원하는 형식으로 튜닝 및 학습도 진행할 수 있습니다.\r\n\r\n<데이터 수집 예시>\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-faq_scenario_ko.png?sv=2022-11-02&spr=https&st=2024-10-17T01%3A46%3A15Z&se=2024-10-17T01%3A56%3A15Z&sr=c&sp=r&sig=XHxjTOMXoLQUDphjKfg0RVwW%2BD0Zy%2F55J7x4FWkGeMk%3D)>\r\n\r\n참고\r\n다음 주의 사항을 참고해 주십시오.\r\n\r\n  - 유저 쿼리에 필수 파라미터가 누락되었을 때 모델이 자체적으로 API 호출 단계(스킬 호출의 Step 2)를 수행하지 않고 생략할 수도 있습니다. 이 경우 스킬셋 답변 생성 API 호출 시에는 54020 에러가 발생되어 최종 답변이 생성되지 않습니다. 따라서 필수 파라미터가 누락된 케이스일지라도 API 호출 단계를 생략하지 않도록 유의하여 데이터를 수집해 주시기 바랍니다."
    },
    {
        "id": "41",
        "content": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 사용 > 튜닝 > Instruction 데이터셋 준비]\r\n\r\nInstruction 데이터셋의 파일 규격은 다음과 같습니다.\r\n\r\n| 항목            | 설명                                |\r\n|-----------------|-----------------------------------|\r\n| 파일 확장자       | .csv 또는 .jsonl                   |\r\n| 파일 인코딩 형식   | UTF-8                             |\r\n| 최소 데이터       | - 최소: 100행 - 권장: 1,000행 ~ 100,000행 |\r\n| 파일 용량         | 1TB 이하 (NCP Object storage 가능 범위) |"
    },
    {
        "id": "119",
        "content": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 이용량 제어 정책]\r\n\r\nCLOVA Studio 서비스 이용 시 요청할 수 있는 최댓값이 초과될 경우, HTTP 429 코드[HTTP 429 코드](주소: https://api.ncloud-docs.com/docs/ai-naver-clovastudio-summary#%EC%9D%91%EB%8B%B5%EC%83%81%ED%83%9C%EC%BD%94%EB%93%9C)가 반환되거나 오류 메시지가 나타납니다. 최대 이용량을 초과하지 않기 위해 수행할 수 있는 작업을 설명합니다.\r\n\r\n참고\r\nCLOVA Studio는 안정적이고 원활한 서비스를 제공하기 위해 최선을 다하고 있으나 최대 요청량 이내로 서비스를 이용할 경우에도 일부 처리 지연이 발생하거나 실패할 수 있습니다.\r\n\r\n\r\nQPM 수치를 미리 확인한 후 최댓값 이내로 요청해 주십시오.\r\n별도의 API 호출 제어 기능(rate limit)을 직접 구현 및 추가해 주십시오.\r\n요청 사이에 일정 시간 지연 기능(time sleep)을 별도로 추가해 주십시오.\r\nHTTP 429 코드 반환 및 해당 오류 메시지 응답 시 예외 처리를 통해 일정 시간 지연 이후 재요청해 주십시오.\r\n\r\n\r\n  - TPM 수치를 미리 확인하여 작업을 위해 입력하는 토큰 수와 결괏값 생성 시 사용할 최대 토큰 수를 실제로 필요한 정도만 설정해 주십시오.\r\n  - API 호출 시 입력한 문자열의 토큰 수를 확인하려면 익스플로러 메뉴의 토큰 계산기 API를 활용해 주십시오.\r\n  - API 호출 시 결괏값에 사용할 최대 토큰 수를 조정하려면 maxTokens 필드의 값을 수정해 주십시오.\r\n  - 플레이그라운드 메뉴에서 입력된 문자열의 토큰 수를 확인하려면 플레이그라운드 화면 상단의 계산 아이콘을 클릭해 주십시오.\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-ratelimiting_tpm-mitigation01_ko.png?sv=2022-11-02&spr=https&st=2024-10-17T07%3A04%3A02Z&se=2024-10-17T07%3A14%3A02Z&sr=c&sp=r&sig=9r5vWPhp0VEufhmk5OjQQB4jATO84QuSlHVvWzPdBRU%3D)>\r\n  - 플레이그라운드 메뉴에서 결괏값 생성 시 사용할 최대 토큰 수를 조정하려면 플레이그라운드 화면 왼쪽에 있는 Maximum tokens 필드를 수정해 주십시오.\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-ratelimiting_tpm-mitigation02_ko.png?sv=2022-11-02&spr=https&st=2024-10-17T07%3A04%3A02Z&se=2024-10-17T07%3A14%3A02Z&sr=c&sp=r&sig=9r5vWPhp0VEufhmk5OjQQB4jATO84QuSlHVvWzPdBRU%3D)>"
    }
]

def reranker_function(query, documents):
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json',
    }
    
    # 유효한 문서만 필터링 (빈 내용, None, NaN 제외)
    indexed_documents = [
        {"id": str(doc["id"]), "doc": doc["content"]}
        for doc in documents
        if doc.get("content") not in [None, "", "None", "nan", "NaN"]
    ]
    
    json_data = {
        "query": query,
        "documents": indexed_documents
    }
    
    response = requests.post(reranker_api_url, headers=headers, json=json_data)
    response.raise_for_status()
    
    result = response.json()
    return result

# 3단계 실행
print("🎯 3단계: reranker_function 함수 호출")

result = reranker_function(search_query, search_results)
print(json.dumps(result, indent=2, ensure_ascii=False))

 

위 코드에 대한 리랭커 API 응답(Response) 예시는 다음과 같습니다.

Quote
🎯 3단계: reranker_function 함수 호출
{
  "status": {
    "code": "20000",
    "message": "OK"
  },
  "result": {
    "result": "답변: 임베딩 v2 모델의 입력 길이 제한을 처리하는 방법에 대해 설명드리겠습니다. <doc1>임베딩 API가 한 번에 처리 가능한 최대 텍스트 길이는 500 토큰(clir-emb-dolphin, clir-sts-dolphin) 또는 8,192 토큰(v2, bge-m3)입니다.</doc1> 만약 텍스트 토큰 수 제한으로 임베딩이 어려운 경우, <doc1>긴 텍스트를 적절하게 나누기 위해 청크 방식을 사용하는 것을 권장합니다.</doc1> 이때, <doc1>텍스트를 나눌 때에는 올바른 정보를 추출하기 위해 텍스트를 의미 단위로 적절하게 구분하는 것이 중요합니다.</doc1> 청크 방식의 종류로는 <doc1>기본 텍스트 분할, 문단 나누기, 요약</doc1>이 있으며, 각각의 장단점을 고려하여 적절한 방식을 선택하시면 됩니다.\n\n또한, <doc5>TPM 수치를 미리 확인하여 작업을 위해 입력하는 토큰 수와 결괏값 생성 시 사용할 최대 토큰 수를 실제로 필요한 정도만 설정하는 것이 중요합니다.</doc5> <doc5>API 호출 시 입력한 문자열의 토큰 수를 확인하려면 익스플로러 메뉴의 토큰 계산기 API를 활용할 수 있습니다.</doc5> 이를 통해 <doc5>최대 이용량을 초과하지 않도록 조정할 수 있습니다.</doc5>",
    "citedDocuments": [
      {
        "id": "68",
        "doc": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 사용 > 익스플로러 > API 활용]\r\n\r\n입력 텍스트를 숫자 형태의 벡터 값으로 변환할 수 있습니다. 사용자의 작업과 목적에 따라 세가지 모델 중 원하는 방식을 선택할 수 있습니다. 각 모델은 동일한 문장 쌍에 대해 서로 다른 유사도 결과를 반환합니다.\r\n\r\n| 도구명   | 모델명            | 최대 토큰 수 | 벡터 차원 | 권장 거리 지표 (distance metric) | 비고            |\r\n|----------|-------------------|--------------|-----------|----------------------------------|-----------------|\r\n| 임베딩   | clir-emb-dolphin  | 500 토큰     | 1024      | IP (Inner/Dot/Scalar Product; 내적) |                 |\r\n| 임베딩   | clir-sts-dolphin  | 500 토큰     | 1024      | Cosine Similarity (코사인 유사도) |                 |\r\n| 임베딩v2 | bge-m3            | 8,192 토큰   | 1024      | Cosine Similarity (코사인 유사도) | 오픈 소스 모델* |\r\n\r\n임베딩 API의 모델별 특징은 다음과 같습니다.\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-explorer_embedding1_ko.png?sv=2022-11-02&spr=https&st=2024-10-16T06%3A00%3A12Z&se=2024-10-16T06%3A10%3A12Z&sr=c&sp=r&sig=sFUvGX6%2FZLqlPGRqH3QNt0GC41kwiSKysybHHosukzg%3D)>\r\n\r\n참고\r\n임베딩 v2의 출력을 오픈소스로 제공되는 bge-m3 모델 출력과 동일하게 얻기 위해서는 아래 사항을 고려해주십시오.\r\n\r\n  - 임베딩 v2는 bge-m3 모델의 총 3가지 방식 (sparse, dense, multi-dense/colbert) 출력 중 dense 에 해당하는 값을 반환합니다.\r\n  - 임베딩 v2는 FP16 및 정규화(normalization) 적용이 되어있지 않습니다.\r\n\r\n\r\n임베딩 API는 다음과 같은 작업에 활용할 수 있습니다.\r\n\r\n  - 문장 간의 벡터 유사도를 계산하여 검색 기능을 개선할 수 있습니다. 예를 들어, 사용자가 입력한 검색어와 문서의 벡터 유사도를 측정하여 가장 관련성이 높은 문서를 반환할 수 있습니다.\r\n  - 두 문장 간의 유사도를 계산해 관련 문서의 유사성을 판단하거나 문장 간의 의미적 유사성을 비교할 수 있습니다.\r\n  - 비슷한 특성을 가진 문서를 클러스터로 그룹화할 수 있습니다.\r\n  - 문서를 분류할 수 있습니다. 벡터화된 텍스트 데이터를 학습된 모델에 사용하여 텍스트의 주제나 감성에 따라 분류하는 등 다양한 분류 작업에 활용할 수 있습니다.\r\n\r\n일반적인 임베딩의 작업 과정은 데이터 준비, 임베딩 수행, 벡터 결과물 저장, API 개발 및 결과물 호출로 구성됩니다. 이 과정을 통해 임베딩된 결과물을 저장하고 데이터베이스에 활용할 수 있습니다.\r\n\r\n임베딩 작업 과정\r\n  1. 임베딩할 파일을 Text로 변환합니다.\r\n  2. 텍스트 유형과 목적에 따라 문단 나누기 또는 요약 API 이용하여 Text를 적절하게 나눕니다.\r\n  3. 적합한 임베딩 모델을 선택 후, Text를 벡터로 변환하여 임베딩 작업을 수행합니다.\r\n  4. 임베딩된 결과물과 원본 Text를 벡터 DB에 함께 저장합니다.\r\n  5. 사용자 입력 쿼리를 벡터 변환하여 API화 → DB에 저장된 벡터와의 유사도를 비교하여 일치하는 벡터를 찾아 매핑된 원본 텍스트를 호출하여 최종 결과를 생성합니다.\r\n  6. API 결과를 prompt에 넣어 사용자가 원하는 적절한 형식의 응답으로 만들어 최종 결과물을 출력하기 위해 Chat Completions API를 사용하실 수 있습니다.\r\n\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-explorer_embedding2_ko.png?sv=2022-11-02&spr=https&st=2024-10-16T06%3A00%3A12Z&se=2024-10-16T06%3A10%3A12Z&sr=c&sp=r&sig=sFUvGX6%2FZLqlPGRqH3QNt0GC41kwiSKysybHHosukzg%3D)>\r\n\r\n\r\n임베딩 API가 한 번에 처리 가능한 최대 텍스트 길이는 500 토큰(clir-emb-dolphin, clir-sts-dolphin) 또는 8,192 토큰(v2, bge-m3)입니다. 텍스트 토큰 수 제한으로 임베딩이 어려운 경우, 긴 텍스트를 적절하게 나누기 위해 청크 방식을 사용하는 것을 권장합니다. 텍스트를 나눌 때에는 올바른 정보를 추출하기 위해 텍스트를 의미 단위로 적절하게 구분하는 것이 중요합니다. 청크는 텍스트를 작은 조각으로 나누는 작업으로 기본 텍스트 분할, 문단 나누기, 요약이 있습니다.\r\n\r\n청크 방식의 종류와 각 방식의 장단점은 다음과 같습니다.\r\n\r\n| 방식             | 설명                                           | 장점                                                                 | 단점                                                                 |\r\n|------------------|----------------------------------------------|--------------------------------------------------------------------|--------------------------------------------------------------------|\r\n| 기본 텍스트 분할 | 텍스트를 일정한 길이의 단위로 나누는 방식               | 텍스트를 세세하게 나누어 질의에 대한 정답을 추출하기 용이함                        | 텍스트를 의미 단위가 아닌 길이 단위로 자르기 때문에 텍스트의 처음과 끝 처리가 미흡하고 전체 텍스트의 의미 파악이 어려움 |\r\n| 문단             | 텍스트를 문맥에 맞게 의미 있는 문단으로 분리             | 텍스트가 의미 있는 단위로 묶여 임베딩 성능을 높일 수 있음                          | 긴 문단의 경우, 질의에 대한 정답이 있는 영역을 특정하기 어려움                            |\r\n| 요약             | 주요 내용에 초점을 두고 긴 텍스트를 짧게 요약            | 문단 나누기보다 더 긴 맥락의 텍스트를 요약할 수 있어 문서 단위로 임베딩하기에 용이함         | 긴 문단의 경우, 질의에 대한 정답이 있는 영역을 특정하기 어려움                            |\r\n\r\n참고\r\nCLOVA Studio에서는 토큰 계산기(임베딩 v2) API, 문단 나누기 API와 요약 API를 제공합니다. 자세한 정보는 토큰 계산기 API[토큰 계산기 API](주소: https://guide.ncloud-docs.com/docs/clovastudio-explorer03#%ED%86%A0%ED%81%B0%EA%B3%84%EC%82%B0%EA%B8%B0API), 문단 나누기 API[문단 나누기 API](주소: https://guide.ncloud-docs.com/docs/clovastudio-explorer03#%EB%AC%B8%EB%8B%A8%EB%82%98%EB%88%84%EA%B8%B0API)와 요약 API[요약 API](주소: https://guide.ncloud-docs.com/docs/clovastudio-explorer03#%EC%9A%94%EC%95%BDAPI)를 참고해 주십시오.\r\n\r\n\r\nbge-m3 모델 인용 정보는 다음과 같습니다.\r\n\r\n@misc{bge-m3,\r\n    title={BGE M3-Embedding: Multi-Lingual, Multi-Functionality, Multi-Granularity Text Embeddings Through Self-Knowledge Distillation},\r\n    author={Jianlv Chen and Shitao Xiao and Peitian Zhang and Kun Luo and Defu Lian and Zheng Liu},\r\n    year={2024},\r\n    eprint={2402.03216},\r\n    archivePrefix={arXiv},\r\n    primaryClass={cs.CL}\r\n}\r\nPlain text\r\n"
      },
      {
        "id": "119",
        "doc": "이 글은 다음의 주제와 관련된 글입니다: [AI services > CLOVA Studio > CLOVA Studio 이용량 제어 정책]\r\n\r\nCLOVA Studio 서비스 이용 시 요청할 수 있는 최댓값이 초과될 경우, HTTP 429 코드[HTTP 429 코드](주소: https://api.ncloud-docs.com/docs/ai-naver-clovastudio-summary#%EC%9D%91%EB%8B%B5%EC%83%81%ED%83%9C%EC%BD%94%EB%93%9C)가 반환되거나 오류 메시지가 나타납니다. 최대 이용량을 초과하지 않기 위해 수행할 수 있는 작업을 설명합니다.\r\n\r\n참고\r\nCLOVA Studio는 안정적이고 원활한 서비스를 제공하기 위해 최선을 다하고 있으나 최대 요청량 이내로 서비스를 이용할 경우에도 일부 처리 지연이 발생하거나 실패할 수 있습니다.\r\n\r\n\r\nQPM 수치를 미리 확인한 후 최댓값 이내로 요청해 주십시오.\r\n별도의 API 호출 제어 기능(rate limit)을 직접 구현 및 추가해 주십시오.\r\n요청 사이에 일정 시간 지연 기능(time sleep)을 별도로 추가해 주십시오.\r\nHTTP 429 코드 반환 및 해당 오류 메시지 응답 시 예외 처리를 통해 일정 시간 지연 이후 재요청해 주십시오.\r\n\r\n\r\n  - TPM 수치를 미리 확인하여 작업을 위해 입력하는 토큰 수와 결괏값 생성 시 사용할 최대 토큰 수를 실제로 필요한 정도만 설정해 주십시오.\r\n  - API 호출 시 입력한 문자열의 토큰 수를 확인하려면 익스플로러 메뉴의 토큰 계산기 API를 활용해 주십시오.\r\n  - API 호출 시 결괏값에 사용할 최대 토큰 수를 조정하려면 maxTokens 필드의 값을 수정해 주십시오.\r\n  - 플레이그라운드 메뉴에서 입력된 문자열의 토큰 수를 확인하려면 플레이그라운드 화면 상단의 계산 아이콘을 클릭해 주십시오.\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-ratelimiting_tpm-mitigation01_ko.png?sv=2022-11-02&spr=https&st=2024-10-17T07%3A04%3A02Z&se=2024-10-17T07%3A14%3A02Z&sr=c&sp=r&sig=9r5vWPhp0VEufhmk5OjQQB4jATO84QuSlHVvWzPdBRU%3D)>\r\n  - 플레이그라운드 메뉴에서 결괏값 생성 시 사용할 최대 토큰 수를 조정하려면 플레이그라운드 화면 왼쪽에 있는 Maximum tokens 필드를 수정해 주십시오.\r\n<이미지(https://cdn.document360.io/6998976f-9d95-4df8-b847-d375892b92c2/Images/Documentation/clovastudio-ratelimiting_tpm-mitigation02_ko.png?sv=2022-11-02&spr=https&st=2024-10-17T07%3A04%3A02Z&se=2024-10-17T07%3A14%3A02Z&sr=c&sp=r&sig=9r5vWPhp0VEufhmk5OjQQB4jATO84QuSlHVvWzPdBRU%3D)>"
      }
    ],
    "suggestedQueries": [],
    "usage": {
      "promptTokens": 10169,
      "completionTokens": 320,
      "totalTokens": 10489
    }
  }
}

 

2.2.4 전체 워크 플로우 실행

앞서 정의한 펑션 콜링, 검색 함수, 리랭커 함수들을 통합하고 최종 답변을 생성하는 전체 RAG 체이닝 워크플로우를 구현합니다. RAG Reasoning이 유저 쿼리를 분석하고, 필요시 검색 함수를 호출하여 관련 문서를 수집한 후 리랭커로 문서를 선별하는 과정을 반복하여, 최종적으로 핵심 문서 기반 정확한 답변 생성을 목적으로 합니다.

Quote

전체 동작 흐름은 다음과 같습니다.

  • 유저 쿼리를 RAG Reasoning에 전달하여 함수 호출을 유도합니다.
  • RAG Reasoning이 생성한 검색 쿼리를 호출한 'ncloud_cs_retireval'에 입력하고 그 결과를 'reranker function'에 활용하여 문서를 선별합니다.
  • 리랭커가 선별한 문서들은 'all_document_list'에 누적됩니다.
  • 더 이상 호출하지 않거나 최대 반복 횟수에 도달하면, 누적된 'all_document_list'를 RAG Reasoning에 요청을 보내 최종 답변을 생성합니다.

 

def run_reasoning(user_query_or_messages, search_api_url, reranker_api_url, api_key, reasoning_endpoint):
    all_document_list = []  
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    
    # 최대 3번까지 반복 (무한 루프 방지)
    for iteration in range(3):
        # Step 1: 페이로드 생성 
        if iteration == 0:
            # 첫 번째 호출: 검색 (문서 없음)
            if isinstance(user_query_or_messages, list):  
                # 멀티 턴 경우: 대화 히스토리 전체 전달
                payload = build_reasoning(messages=user_query_or_messages)
            else:  
                # 싱글 턴 경우 : 단일 쿼리만 전달
                payload = build_reasoning(query=user_query_or_messages)
        else:
            # 두 번째 이후 호출: 답변 생성 (문서 있음)
            if isinstance(user_query_or_messages, list):
                payload = build_reasoning(messages=user_query_or_messages, document_list=all_document_list)
            else:
                payload = build_reasoning(query=user_query_or_messages, document_list=all_document_list)
        
        # Step 2: RAG Reasoning API 호출
        response = requests.post(reasoning_endpoint, headers=headers, json=payload)
        response.raise_for_status()
        reasoning_result = response.json()
        
        # Step 3: 응답 분석
        message = reasoning_result.get("result", {}).get("message", {})
        tool_calls = message.get("toolCalls", [])  # 검색 도구 호출 여부
        content = message.get("content", "")        # 최종 답변 내용
        
        # Case A: tool_calls가 없음 → 모델이 더 이상 검색할 필요가 없다고 판단
        # 검색 함수 호출 X
        if not tool_calls:
            return content if content else "답변을 생성할 수 없습니다."
        
        # Case B: tool_calls가 있음 → Step 4로 이동하여 검색 실행
        all_sugquery_list = []  # 검색 실패 시 추천 검색어 수집용
        
        # Step 4: 검색 함수 호출에 대해 검색-리랭커 실행
        for i, tool_call in enumerate(tool_calls):
            search_query = tool_call['function']['arguments']['query']
            
            # Step 4-1: 검색 실행
            search_results = ncloud_cs_retrieval(search_query, search_api_url) # search_results는 검색 함수의 결과 값으로 RAG Reasoning의 입력 파라미터인 'search_result'와는 다른 값입니다.
            
            # Step 4-2: 리랭커 실행
            document_list, sugquery_list = reranker_function(search_query, search_results, reranker_api_url, api_key)
            
            # Step 4-3: 결과 처리
            if document_list:
                # 리랭커의 결과물인 document_list가 있는 경우: 중복 제거하여 누적
                for doc in document_list:
                    # 중복 문서 체크 
                    if not any(existing_doc['id'] == doc['id'] for existing_doc in all_document_list):
                        all_document_list.append(doc)
            elif sugquery_list:
                # 관련 문서를 찾지 못하여 document_list가 없는 경우: 추천 검색어를 재검색용으로 수집
                all_sugquery_list.extend(sugquery_list)
        
        # Step 5: 수집된 문서로 최종 답변 생성 시도
        if all_document_list:
            # 문서가 있으므로 최종 답변 생성 (모든 tool_calls 정보 사용)
            if isinstance(user_query_or_messages, list):
                payload = build_reasoning(messages=user_query_or_messages, document_list=all_document_list, 
                                        tool_calls=tool_calls)
            else:
                payload = build_reasoning(query=user_query_or_messages, document_list=all_document_list,
                                        tool_calls=tool_calls)
            
            final_response = requests.post(reasoning_endpoint, headers=headers, json=payload)
            final_response.raise_for_status()
            final_result = final_response.json()
            
            final_content = final_result.get("result", {}).get("message", {}).get("content", "")
            return final_content if final_content else "답변을 생성할 수 없습니다."
        
        # Step 6: 재검색 실행 (document_list가 없지만 추천 검색어가 있는 경우)
        if all_sugquery_list:
            # 추천 검색어를 사용하여 다음 iteration에서 재검색
            if isinstance(user_query_or_messages, list):
                payload = build_reasoning(messages=user_query_or_messages, sugquery_list=all_sugquery_list)
            else:
                payload = build_reasoning(query=user_query_or_messages, sugquery_list=all_sugquery_list)
            continue  # 다음 반복으로 이동
        
        # 문서도 없고 추천 검색어도 없는 경우 → 검색 중단
        break
    
    # 3번 반복 후에도 결과가 없는 경우
    return "검색된 문서가 없어 답변을 생성할 수 없습니다."
  • 여러 번의 검색 및 리랭커 API 활용 과정에서 선별된 모든 문서를 누적하여, 최종 답변 생성 시 RAG Reasoning이 더 광범위한 정보를 참조할 수 있도록 설계합니다.
  • RAG Reasoning이 도구 호출을 무한 반복하지 않도록, 'max_iterations'=3으로 제한합니다.

 

전체 체이닝 코드의 실행을 위한 요청(Request) 예시는 다음과 같습니다.

Quote
import requests
import json

# API 설정
search_api_url = "http://127.0.0.1:8000/search/"  
reranker_api_url = "https://clovastudio.stream.ntruss.com/v1/api-tools/reranker"  
reasoning_endpoint = "https://clovastudio.stream.ntruss.com/v1/api-tools/rag-reasoning"  
api_key = "API_KEY"  

user_query = "임베딩 v2 모델 입력이 너무 길 때 어떻게 처리해야 하는지 알려주세요."
    
def build_reasoning(query=None, document_list=None, sugquery_list=None, messages=None, tool_calls=None):
    # 1단계: 메시지 리스트 초기화
    # 멀티 턴 대화인지 싱글 턴 인지에 따라 메시지 구성이 달라짐
    if messages is not None:
        msg_list = messages  # 멀티 턴: 기존 대화 히스토리 사용
    else:
        msg_list = [  # 싱글 턴: 새로운 대화 시작
            {"content": query, "role": "user"}
        ]
    
    # 2단계: 기본 페이로드 구성
    payload = {
        "messages": msg_list,
    }
    
    # 3단계: 문서 존재 여부에 따른 조건 분기
    # Case A: 문서가 없는 경우 → 검색 함수 호출 (초기 검색 또는 재검색)
    if document_list is None:
        # 검색 함수 정의: RAG reasoning 모델이 필요시 검색을 수행할 수 있도록 함
        payload.update({
            "tools": [{
                "function": {
                    "description": "NCloud 관련 검색을 할 때 사용하는 도구입니다.\n나누어 질문해야 하는 경우 쿼리를 쪼개 나누어서 도구를 사용합니다.\n정보를 찾을 수 없었던 경우, 최종 답을 하지 않고 sugquery_list를 참고하여 도구를 다시 사용할 수 있습니다.",
                    "name": "ncloud_cs_retrieval",
                    "parameters": {
                        "properties": {
                            "query": {
                                "description": "사용자의 검색어를 정제해서 넣으세요.",
                                "type": "string"
                            },
                            "url": {
                                "description": "검색 API 서버의 엔드포인트 URL을 입력하세요.",
                                "type": "string"
                            }
                        },
                        "required": ["query", "url"],
                        "type": "object"
                    }
                },
                "type": "function"
            }],
            "toolChoice": "auto"  # 모델이 모델이 도구 호출 여부를 자율적으로 결정
        })
        
        # 재검색 케이스: 이전 검색에서 문서를 찾지 못했을 때 추천 검색어 제공
        if sugquery_list:
            system_message = f"이전 검색에서 관련 문서를 찾지 못했습니다. 다음 추천 검색어들을 참고하여 다시 검색해보세요: {', '.join(sugquery_list)}"
            msg_list.insert(0, {
                "content": system_message,
                "role": "system"
            })
            payload["messages"] = msg_list
    
    # Case B: 리랭커 결과인 문서가 있는 경우 → 최종 답변 생성 (대화 히스토리 형태)
    else:
        # 최종 문서 리스트를 tool content 형태로 변환 (doc- 접두사 추가)
        search_result = [{"id": f"doc-{doc['id']}", "doc": doc["doc"]} for doc in document_list]
        tool_content = json.dumps({"search_result": search_result}, ensure_ascii=False)
        
        # 대화 히스토리 형태로 구성: user → assistant(toolCalls) → tool
        # 첫 번째 user 메시지에서 쿼리 추출
        user_query = None
        for msg in msg_list:
            if msg.get("role") == "user":
                user_query = msg.get("content")
                break
        
        # 대화 히스토리 재구성 (모든 tool_calls 포함)
        assistant_tool_calls = []
        tool_messages = []
        
        # 모든 tool_calls를 assistant 메시지에 포함
        if tool_calls:
            for tool_call in tool_calls:
                assistant_tool_calls.append({
                    "id": tool_call.get('id'),
                    "type": "function", 
                    "function": {
                        "name": "ncloud_cs_retrieval",
                        "arguments": tool_call['function']['arguments']
                    }
                })
                
                # 각 tool_call에 대한 tool 메시지 생성 (동일한 검색 결과 사용)
                tool_messages.append({
                    "role": "tool",
                    "name": "ncloud_cs_retrieval", 
                    "content": tool_content,
                    "toolCallId": tool_call.get('id')
                })
        
        msg_list = [
            {"content": user_query, "role": "user"},
            {
                "role": "assistant",
                "content": "",
                "toolCalls": assistant_tool_calls
            }
        ]
        
        # 모든 tool 메시지 추가
        msg_list.extend(tool_messages)
        payload["messages"] = msg_list
        
        # tools 정의 추가
        payload["tools"] = [{
            "function": {
                "description": "NCloud 관련 검색을 할 때 사용하는 도구입니다.\n나누어 질문해야 하는 경우 쿼리를 쪼개 나누어서 도구를 사용합니다.\n정보를 찾을 수 없었던 경우, 최종 답을 하지 않고 suggested_queries를 참고하여 도구를 다시 사용할 수 있습니다.",
                "name": "ncloud_cs_retrieval",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": "사용자의 검색어를 정제해서 넣으세요."
                        }
                    },
                    "required": ["query"]
                }
            },
            "type": "function"
        }]
    
    return payload

def ncloud_cs_retrieval(query, search_api_url):
    headers = {
        'accept': 'application/json',
        'Content-Type': 'application/json',
    }
    json_data = {
        'query': query,
        'top_k': 5,
    }
    response = requests.post(search_api_url, headers=headers, json=json_data)
    response.raise_for_status()
    return response.json()['result']

def reranker_function(query, documents, api_url, api_key):
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json',
    }
    
    # 유효한 문서만 필터링 (빈 내용, None, NaN 제외)
    indexed_documents = [
        {"id": str(doc["id"]), "doc": doc["content"]}
        for doc in documents
        if doc.get("content") not in [None, "", "None", "nan", "NaN"]
    ]
    
    json_data = {
        "query": query,
        "documents": indexed_documents
    }
    response = requests.post(api_url, headers=headers, json=json_data)
    response.raise_for_status()
    
    result = response.json()
    # 리랭커 결과에서 관련 문서와 추천 검색어만 추출하여 document_list와 sugquery_list에 저장
    document_list = result.get('result', {}).get('citedDocuments', [])
    sugquery_list = result.get('result', {}).get('suggestedQueries', [])
    
    return document_list, sugquery_list

def run_reasoning(user_query_or_messages, search_api_url, reranker_api_url, api_key, reasoning_endpoint):
    all_document_list = []  
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    
    # 최대 3번까지 반복 (무한 루프 방지)
    for iteration in range(3):
        # Step 1: 페이로드 생성 
        if iteration == 0:
            # 첫 번째 호출: 검색 (문서 없음)
            if isinstance(user_query_or_messages, list):  
                # 멀티 턴 경우: 대화 히스토리 전체 전달
                payload = build_reasoning(messages=user_query_or_messages)
            else:  
                # 싱글 턴 경우 : 단일 쿼리만 전달
                payload = build_reasoning(query=user_query_or_messages)
        else:
            # 두 번째 이후 호출: 답변 생성 (문서 있음)
            if isinstance(user_query_or_messages, list):
                payload = build_reasoning(messages=user_query_or_messages, document_list=all_document_list)
            else:
                payload = build_reasoning(query=user_query_or_messages, document_list=all_document_list)
        
        # Step 2: RAG Reasoning API 호출
        response = requests.post(reasoning_endpoint, headers=headers, json=payload)
        response.raise_for_status()
        reasoning_result = response.json()
        
        # Step 3: 응답 분석
        message = reasoning_result.get("result", {}).get("message", {})
        tool_calls = message.get("toolCalls", [])  # 검색 도구 호출 여부
        content = message.get("content", "")        # 최종 답변 내용
        
        # Case A: tool_calls가 없음 → 모델이 더 이상 검색할 필요가 없다고 판단
        # 검색 함수 호출 X
        if not tool_calls:
            return content if content else "답변을 생성할 수 없습니다."
        
        # Case B: tool_calls가 있음 → Step 4로 이동하여 검색 실행
        all_sugquery_list = []  # 검색 실패 시 추천 검색어 수집용
        
        # Step 4: 검색 함수 호출에 대해 검색-리랭커 실행
        for i, tool_call in enumerate(tool_calls):
            search_query = tool_call['function']['arguments']['query']
            
            # Step 4-1: 검색 실행
            search_results = ncloud_cs_retrieval(search_query, search_api_url) # search_results는 검색 함수의 결과 값으로 RAG Reasoning의 입력 파라미터인 'search_result'와는 다른 값입니다.
            
            # Step 4-2: 리랭커 실행
            document_list, sugquery_list = reranker_function(search_query, search_results, reranker_api_url, api_key)
            
            # Step 4-3: 결과 처리
            if document_list:
                # 리랭커의 결과물인 document_list가 있는 경우: 중복 제거하여 누적
                for doc in document_list:
                    # 중복 문서 체크 
                    if not any(existing_doc['id'] == doc['id'] for existing_doc in all_document_list):
                        all_document_list.append(doc)
            elif sugquery_list:
                # 관련 문서를 찾지 못하여 document_list가 없는 경우: 추천 검색어를 재검색용으로 수집
                all_sugquery_list.extend(sugquery_list)
        
        # Step 5: 수집된 문서로 최종 답변 생성 시도
        if all_document_list:
            # 문서가 있으므로 최종 답변 생성 (모든 tool_calls 정보 사용)
            if isinstance(user_query_or_messages, list):
                payload = build_reasoning(messages=user_query_or_messages, document_list=all_document_list, 
                                        tool_calls=tool_calls)
            else:
                payload = build_reasoning(query=user_query_or_messages, document_list=all_document_list,
                                        tool_calls=tool_calls)
            
            final_response = requests.post(reasoning_endpoint, headers=headers, json=payload)
            final_response.raise_for_status()
            final_result = final_response.json()
            
            return final_result
        
        # Step 6: 재검색 실행 (document_list가 없지만 추천 검색어가 있는 경우)
        if all_sugquery_list:
            # 추천 검색어를 사용하여 다음 iteration에서 재검색
            if isinstance(user_query_or_messages, list):
                payload = build_reasoning(messages=user_query_or_messages, sugquery_list=all_sugquery_list)
            else:
                payload = build_reasoning(query=user_query_or_messages, sugquery_list=all_sugquery_list)
            continue  # 다음 반복으로 이동
        
        # 문서도 없고 추천 검색어도 없는 경우 → 검색 중단
        break
    
    # 3번 반복 후에도 결과가 없는 경우
    return "검색된 문서가 없어 답변을 생성할 수 없습니다."

# 전체 플로우 실행
print("🔧 4단계: run_reasoning 함수 호출 (최종 답변 생성)")
result = run_reasoning(user_query, search_api_url, reranker_api_url, api_key, reasoning_endpoint)
print(json.dumps(result, indent=2, ensure_ascii=False))

 

위 코드를 실행한 응답 (Request) 예시는 다음과 같습니다.

Quote
🔧 4단계: run_reasoning 함수 호출 (최종 답변 생성)
{
  "status": {
    "code": "20000",
    "message": "OK"
  },
  "result": {
    "message": {
      "role": "assistant",
      "content": "임베딩 v2 모델 입력이 너무 길 때 처리하는 방법은 <doc-68>임베딩 API가 한 번에 처리 가능한 최대 텍스트 길이는 500 토큰(clir-emb-dolphin, clir-sts-dolphin) 또는 8,192 토큰(v2, bge-m3)입니다. 텍스트 토큰 수 제한으로 임베딩이 어려운 경우, 긴 텍스트를 적절하게 나누기 위해 청크 방식을 사용하는 것을 권장합니다.</doc-68> 청크 방식의 종류에는 <doc-68>기본 텍스트 분할, 문단 나누기, 요약이 있으며, 각각의 방식은 장단점이 있습니다.</doc-68> 예를 들어, <doc-68>기본 텍스트 분할은 텍스트를 일정한 길이의 단위로 나누는 방식으로, 텍스트를 세세하게 나누어 질의에 대한 정답을 추출하기 용이하지만, 텍스트를 의미 단위가 아닌 길이 단위로 자르기 때문에 텍스트의 처음과 끝 처리가 미흡하고 전체 텍스트의 의미 파악이 어려울 수 있습니다.</doc-68> 반면에 <doc-68>문단 나누기는 텍스트를 문맥에 맞게 의미 있는 문단으로 분리하는 방식으로, 텍스트가 의미 있는 단위로 묶여 임베딩 성능을 높일 수 있지만, 긴 문단의 경우 질의에 대한 정답이 있는 영역을 특정하기 어려울 수 있습니다.</doc-68> 마지막으로 <doc-68>요약 방식은 주요 내용에 초점을 두고 긴 텍스트를 짧게 요약하는 방식으로, 문단 나누기보다 더 긴 맥락의 텍스트를 요약할 수 있어 문서 단위로 임베딩하기에 용이하지만, 긴 문단의 경우 질의에 대한 정답이 있는 영역을 특정하기 어려울 수 있습니다.</doc-68> 이러한 청크 방식을 활용하여 <doc-68>텍스트를 나눌 때에는 올바른 정보를 추출하기 위해 텍스트를 의미 단위로 적절하게 구분하는 것이 중요합니다.</doc-68>"
    },
    "usage": {
      "promptTokens": 2163,
      "completionTokens": 433,
      "totalTokens": 2596
    }
  }
}

 

3. 유저 쿼리 실행 예시


3.1 멀티 쿼리 실행 예시

실제 RAG 시스템을 실행하고 동작을 확인하는 부분입니다. 멀티 쿼리에 대해 RAG Reasoning, 리랭커 활용 체이닝 파이프라인이 어떻게 작동하여 최종 답변을 생성하는지 예시로 소개해 보고자 합니다.

if __name__ == "__main__":
    search_api_url = "http://127.0.0.1:8000/search/"
    reranker_api_url = "https://clovastudio.stream.ntruss.com/v1/api-tools/reranker"
    reasoning_endpoint = "https://clovastudio.stream.ntruss.com/v1/api-tools/rag-reasoning"
	api_key = "API_KEY"

    user_query = "스킬 트레이너가 제공하는 스킬셋을 삭제하는 방법은 뭐야? 그리고 하나의 스킬셋에 몇 개의 스킬을 생성할 수 있는지 알려줘" #멀티 쿼리 예시

    print("=== RAG 체이닝 시스템 실행 ===")
    print(f"질문: {user_query}")

    result = run_reasoning(user_query, search_api_url, reranker_api_url, api_key, reasoning_endpoint)
    print(f"\n=== 최종 답변 ===\n{result}") 
  • 실제 실행시에는 발급받은 api_key와 목적에 맞는 유저 쿼리를 입력합니다.

 

입력된 유저 쿼리(스킬 트레이너가 제공하는 스킬셋을 삭제하는 방법은 뭐야? 그리고 하나의 스킬셋에 몇 개의 스킬을 생성할 수 있는지 알려줘)에 대한 출력 결과는 다음과 같습니다.

스킬 트레이너가 제공하는 스킬셋을 삭제하는 방법은 <doc-76>네이버 클라우드 플랫폼 콘솔에서 Services > AI Services > CLOVA Studio 메뉴를 차례대로 클릭한 후, My Product 메뉴에서 [CLOVA Studio 바로가기] 버튼을 클릭하고, CLOVA Studio에서 스킬 트레이너 메뉴를 클릭한 다음, 스킬셋의 삭제 메뉴를 클릭하고, 스킬셋 삭제 창이 나타나면 [삭제] 버튼을 클릭하는 것입니다.</doc-76> 하나의 스킬셋에는 <doc-100>10개까지 스킬을 생성할 수 있습니다.</doc-100>

 

3.2 멀티 턴 실행 예시

앞서 다룬 예시는 멀티 쿼리 시나리오의 전체 프로세스를 보여주었습니다. 이 체이닝 시스템은 싱글 쿼리에도 효과적으로 작동할 수 있습니다. 지금부터는  RAG 시스템이 이전 대화의 맥락을 이해하고 후속 질문에 답변하는 '멀티 턴' 시나리오를 어떻게 처리하는지 보여주는 예시를 실행해보고자 합니다. 첫 번째 질문에 대한 답변 후, 사용자가 그 답변과 관련된 추가 질문을 했을 때, 시스템이 이전 대화 기록을 참고하여 자연스럽고 정확한 응답을 생성하는 과정을 보여줍니다.

if __name__ == "__main__":
    search_api_url = "http://127.0.0.1:8000/search/"   # 앞서 구현한 검색 API 기본 주소
    reranker_api_url = "https://clovastudio.stream.ntruss.com/v1/api-tools/reranker"
    reasoning_endpoint = "https://clovastudio.stream.ntruss.com/v1/api-tools/rag-reasoning"
	api_key = "API_KEY"
   
    
    # 첫 번째 질문
    user_query = "플레이그라운드가 뭐야?" 
    answer1 = run_reasoning(user_query, search_api_url, reranker_api_url, api_key, reasoning_endpoint)
    print("[답변 1]", answer1)

	# 두 번째 질문
    user_query2 = "챗 모드는 어떻게 사용해??"
    messages = [									# 메세지 리스트 형태로 제공
        {"role": "user", "content": user_query},
        {"role": "assistant", "content": answer1},
        {"role": "user", "content": user_query2}
    ]

    answer2 = run_reasoning(messages, search_api_url, reranker_api_url, api_key, reasoning_endpoint)
    print("\n[답변 2]", answer2)
  • 멀티 턴 예시의 핵심은 두 번째 질문에서 전달되는 'messages' 리스트 입니다. 리스트에는 해당 시점의 유저 쿼리와 함께 이전 대화 기록(앞선 유저 쿼리와 그에 대한 답변을 의미)을 순서대로 담아 전달합니다.
    • 해당 과정을 통해 RAG Reasoning은 이전 대화 내용을 기반으로 현재 질문의 맥락과 자연스러운 답변 생성을 할 수 있게 됩니다.

 

예시 코드에서 입력한 값을 바탕으로 출력한 결과는 다음과 같습니다.

[답변 1] 플레이그라운드는 <doc-22>프롬프트 입력과 파라미터 설정을 이용해 HyperCLOVA X 모델을 활용할 수 있는 공간</doc-22>으로, <doc-22>챗 모드와 일반 모드를 제공하며, 다양한 기능을 통해 텍스트 생성 작업을 관리하고 실행할 수 있는 환경을 제공합니다.</doc-22> 사용자는 <doc-22>플레이그라운드에서 작업을 저장하고, 공유할 수 있으며, 테스트 앱을 생성할 수도 있습니다.</doc-22> 또한, <doc-31>네이버 AI 윤리 준칙 준수의 관점에서 사용자는 AI Filter 기능을 설정할 수 있으며, 이 기능은 플레이그라운드에서 생성된 테스트 앱으로부터 부적절한 결과물이 출력되는 것을 감지하여 사용자에게 알려주는 기능입니다.</doc-31>

[답변 2] 플레이그라운드는 <doc-23>네이버 클라우드 플랫폼 콘솔에서 Services > AI Services > CLOVA Studio 메뉴를 통해 접근할 수 있는 기능</doc-23>으로, <doc-23>챗 모드를 사용하여 사용자와 어시스턴트 간의 대화를 생성하고 관리할 수 있는 작업 관리 도구</doc-23>입니다. 이 도구를 사용하기 위해서는 <doc-23>파라미터를 설정하고, 작업 제목을 입력한 후, 시스템 영역에 모델이 수행할 작업에 대한 구체적인 지시문 또는 예제를 입력해야 합니다.</doc-23> 또한, <doc-23>필요한 경우 대화 턴을 추가하고 실행 버튼을 클릭하여 작업을 실행할 수 있으며, 작업 저장, 작업 공유, 테스트 앱 생성, 서비스 앱 신청 등의 추가 절차를 진행할 수 있습니다.</doc-23> 

챗 모드에서는 <doc-65>사용자와 어시스턴트의 대화가 HyperCLOVA X 언어 모델이 처리 가능한 최대 토큰 수를 초과할 경우, 더 이상 새로운 대화를 생성하지 못하도록 설정되어 있습니다.</doc-65> 이를 해결하기 위해 <doc-65>슬라이딩 윈도우 API는 사용자와 어시스턴트의 대화 내역 중 가장 오래된 대화 턴을 삭제하여 대화가 끊기지 않고 진행되도록 도와줍니다.</doc-65> 슬라이딩 윈도우 API를 사용하면 <doc-65>전체 대화 내용의 토큰 수를 별도로 조절할 필요 없이 Chat Completions API를 연속적으로 사용할 수 있습니다.</doc-65>

 

4. 결론


이번 쿡북을 통해 CLOVA Studio의 리랭커 API와 RAG 리즈닝 API를 연동하여 복잡한 CS 문의에 대응하는 RAG 시스템을 구축하는 과정을 살펴보았습니다. 이러한 리랭커 - RAG Reasoning 체이닝 구조는 CS 문의 대응 뿐 만 아니라, 다양한 산업 영역의 복합적인 문제 해결에 폭넓게 활용될 수 있습니다. 실제 서비스 환경에 최적화된 RAG 시스템을 구축하는 데 Clova Studio의 기능과 본 쿡북이 실질적인 지침이 될 수 있기를 바라면서, 여러분의 창의성을 맘껏 발휘하여 업무 환경에 맞는 강력한 RAG 시스템을 자유롭게 설계하고 확장해 나가시길 바랍니다!
 

 

stdbtn.png.764965d52559e5babbbf11a1d12a136c.png

 

링크 복사
다른 사이트에 공유하기

게시글 및 댓글을 작성하려면 로그인 해주세요.



로그인
×
×
  • Create New...