Jump to content

CLOVA Studio 운영자

Administrators
  • 게시글

    279
  • 첫 방문

  • 최근 방문

  • Days Won

    51

Posts posted by CLOVA Studio 운영자

  1. 안녕하세요, @모바일님,

    시스템 프롬프트를 구체적이고 명료하게 작성하는 것이 좋습니다.

    예를들어, 답변과 이어지는 질문을 함께 출력하는 것이 중요한 과업이라면, 해당 요청에 대해서 정확하게 명시하는 것이 좋을것 같습니다.

    또한 답변 예시를 통해서 few-shot 형태로 프롬프팅 하는 것이 도움이 될 수 있습니다.

    아래는 DASH-001 모델과 HCX-003 모델로 수행한 결과입니다.

    DASH 모델을 가지고 튜닝을 해보시는 것도 접근해볼 수 있는 방안입니다.

    답변의 스타일을 커스텀할 수 있어서, 원하시는 형태를 더욱 잘 수행할 수 있을 것으로 예상합니다.

     

    <HCX-DASH-001>

    image.png.85eaadcf2a55c8cb99a44cda2ba40b9b.png

     

    <HCX-003>

    image.png.9e8f70a179da2ddb018459ff78306a2b.png

    • Like 1
  2. 안녕하세요, @Roadpia 님,

    Milvus가 2.4.x 버전으로 업데이트되면서 코드에 변화가 생겼습니다.

    이로 인해 collections를 만드는 형식도 변경되었습니다. 저희가 확인해보니, Requirements.txt에 기재된 2.4.x 버전은 실제로는 2.2.x 버전이었습니다.

    Milvus 2.4x 버전에서는 아래 코드로 정상 동작하는 것을 확인하였습니다. 따라서 이 방법으로 진행해 주시기 바랍니다. 본문의 코드 또한 수정하도록 하겠습니다.

    from pymilvus import connections, FieldSchema, CollectionSchema, DataType, Collection, utility
    
    fields = [
        FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
        FieldSchema(name="source", dtype=DataType.VARCHAR, max_length=3000),
        FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=9000),
        FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1024)
    ]
    
    schema = CollectionSchema(fields, description="컬렉션 설명 적어넣기")
    
    collection_name = "컬렉션 이름"
    collection = Collection(name=collection_name, schema=schema, using='default', shards_num=2)
    
    for item in chunked_html:
        source_list = [item['source']]
        text_list = [item['text']]
        embedding_list = [item['embedding']]
        
        entities = [
            source_list,
            text_list,
            embedding_list
        ]
        
        insert_result = collection.insert(entities)
        print("데이터 Insertion이 완료된 ID:", insert_result.primary_keys)
    
    print("데이터 Insertion이 전부 완료되었습니다")

     

    • Like 1
  3. 안녕하세요, @잔퀸신종님, 클로바 스튜디오 담당입니다.

    프롬프트, 튜닝, 스킬 트레이너 개별적으로 테스트 앱 신청 또는 서비스 앱 신청을 진행 부탁드립니다.

    각각 발급을 진행한 뒤에 서비스 로직에 따라 연동 진행하시면 될 것 같습니다.

    감사합니다.

  4. 안녕하세요, @곳간로지스님,클로바 스튜디오 담당입니다.

    서비스 신청시 업로드 하는 데이터는 서비스 심사를 위한 목적으로, 학습 엔진에 영향을 끼치지 않습니다. 🙂

    학습 데이터의 방향을 파악하기에 5개는 부족하여, 10MB 이하로 충분한 데이터 패턴이 포함되도록 업로드 부탁드립니다.

    감사합니다.

    • Haha 1
  5. 안녕하세요, @Eric.Yoo님,

    signature의 매개변수(page=&size=) 정의가 잘못되어 오류가 발생한 것으로 보입니다.

    아래와 같이 수정 후 요청 부탁드립니다.

    def _make_signature(self, page_no, size_cnt): 

    감사합니다.

  6. 지난 RAG 3부작 시리즈에서는 클로바 스튜디오를 활용하여 RAG를 구현하는 방법에 대해 자세히 설명했습니다.
    이번 포스팅에서는 LlamaIndex를 이용하여 MS 워드, PDF, HWP, txt 등 다양한 형식의 파일을 불러오는 방법을 소개하고자 합니다.
     
    RAG 3부작 시리즈
    ✔︎ (1부) RAG란 무엇인가 링크
    ✔︎ (2부) RAG 구현 단계 알아보기 링크
    ✔︎ (3부) CLOVA Studio를 이용해 RAG 구현하기 Cookbook 링크
     

    image.png.e0863244358b0336743c54c07979ad4f.png

     

    RAG(Retrieval-Augmented Generation)를 구현하기 위해서는 문서를 효과적으로 불러오고 처리할 수 있는 작업이 필요합니다. LlamaIndex는 이를 위해 MS 워드, PDF, HWP, txt 등 다양한 형식의 파일을 읽어들여 LLM 모델이 사용할 수 있는 형태로 변환해주는 Loader를 제공합니다. LlamaIndex는 불러올 파일의 개수에 따라 두 가지 방법을 제시합니다.

     

    1. 단일 파일만 불러오는 방식입니다. 데이터 유형에 맞는 parser를 단독으로 사용하여 개별 파일을 불러옵니다. 이 방식은 문서 자체에 대한 메타데이터를 포함하고 있어, 문서 내용뿐만 아니라 문서와 연계된 다른 작업에 용이합니다.

    2. 폴더 내 동일한 유형의 여러 파일을 한 번에 불러오는 방식입니다. SimpleDirectoryReader를 통해 폴더를 불러온 뒤, 해당 데이터 유형에 맞는 parser로 폴더 내 모든 파일을 처리할 수 있습니다. 이 방식은 여러 문서의 본문 내용을 빠르게 가져올 수 있어 로딩 속도와 양적인 면에서 강점이 있습니다.

    이처럼 LlamaIndex는 사용 목적에 맞게 데이터를 불러오고 처리할 수 있는 모듈들을 하나의 패키지로 제공함으로써, RAG 구현을 보다 쉽고 효율적으로 수행할 수 있게 만들어줍니다.

     

    필요 패키지 설치

    pip install llama-index-readers-file

    사용할 모듈 한 번에 불러오기

    from llama_index.core import SimpleDirectoryReader
    from llama_index.readers.file import (
        DocxReader,
        HWPReader,
        PyMuPDFReader,
    )
    from pathlib import Path

     

     

    1. 개별 문서 파일 불러오기

    MS Word 문서 불러오기 : DocxReader

    MS Word(워드) 파일을 불러올 때, 파일명은 "metadata" 변수에, 문서 전체 내용은 페이지 구분 없이 "text" 변수에 저장됩니다. 아래 실행 코드는 현재는 Deprecated된 이전 페이지와 폴더 단위 불러오기 방식을 참고해 작성했습니다. 각 파일에는 고유 ID가 할당되고, 문서 본문 전체가 "text"에 저장되며, 파일명은 "metadata"의 "file_name"에 저장됩니다. 문서 임베딩 결과는 "embedding" 변수에 정리할 수 있습니다. 이를 통해 Word 파일 내 표 형식과 표 안의 텍스트 정보도 성공적으로 불러올 수 있음을 아래 예시에서 확인할 수 있습니다.

    loader = DocxReader()
    documents = loader.load_data(file=Path("/Users/user/Desktop/connector/docx/table2.docx"))
     
    print(documents)

    ex_docx.png.81bb5cc8319cd72bbb02b75b98c92d22.png

     

    PDF 불러오기 : PyMuPDFReader

    PyMuPDFReader를 사용하면 단일 PDF 파일을 간편하게 불러올 수 있습니다. 다음은 LlamaIndex의 공식 깃허브에서 가져온 사용 예제입니다. 파일마다 고유한 ID가 할당되며, 문서의 본문 내용 전체는 "text" 변수에 저장됩니다. 또한, 파일의 이름은 "metadata"의 "file_name"으로 저장되고, 문서 임베딩 결과는 "embedding" 변수에 정리할 수 있습니다. 아래 예시를 통해 PyMuPDFReader가 그림 안의 텍스트뿐만 아니라 경계가 모호한 표 형식의 글자들도 성공적으로 추출할 수 있음을 알 수 있습니다.

    loader = PyMuPDFReader()
    documents = loader.load_data(file_path=Path("/Users/user/Desktop/connector/data/korean1.pdf"), metadata=True)
     
    print([doc for doc in documents if doc.metadata.get('source') == '8']) # 8쪽만 확인

    ex_pdf.png.0973397f90b93d0e83430c4cffe2f527.png

     

    한글 파일 불러오기: HWPReader

    아래의 사용 예제 코드는 LlamaIndex의 공식 깃허브의 사용 예제를 가져왔습니다. 파일마다 고유한 ID가 할당되고, 문서를 불러올 때 각 페이지마다 고유한 Doc ID와 해당 페이지의 내용인 Text가 지정되지만, 이 외에 문서에 대한 추가적인 메타데이터는 별도로 제공되지 않습니다. 아래 예시를 통해 HWPReader가 hwp 파일 내의 표나 박스 형식 안에 있는 텍스트를 효과적으로 추출할 수 있으며, 머릿말과 같은 본문 외의 텍스트도 빠짐없이 가져올 수 있음을 확인할 수 있습니다.

    hwp_path = Path("/Users/user/Desktop/connector/hwp/report1.hwp")
    reader = HWPReader()
    documents = reader.load_data(file=hwp_path)
     
    print(documents)

    ex_hwp.png.ad04b08581971a68bde919f2c8fd526a.png

    Quote

    HWP 문서에서는 글자 여백에 null 문자가 이스케이프 시퀀스('\x00') 형태로 나타나는 문제가 있습니다. 이 문제를 해결하려면 문서에서 해당 문자를 제거하는 추가 작업이 필요합니다.

     

     

    2. 동일한 유형의 txt 파일만 포함된 폴더 전체를 불러오기 : SimpleDirectoryReader

    LlamaIndex에는 txt 파일 전용 로더(parser)가 별도로 제공되지 않습니다. 그러나 SimpleDirectoryReader를 사용하여 폴더 내의 모든 파일을 한 번에 불러오는 방식으로 txt 파일을 로딩할 수 있습니다. 만약 특정 txt 파일들만 선택적으로 불러오고 싶다면, 해당 파일들을 별도의 폴더에 모아 저장한 후 SimpleDirectoryReader로 그 폴더를 지정하여 불러오면 됩니다. SimpleDirectoryReader의 자세한 사용 방법은 LlamaIndex 홈페이지의 가이드에서 확인할 수 있습니다. SimpleDirectoryReader를 사용하여 txt 파일을 불러올 경우, 메타데이터의 구성이 Parser(docx, pdf, hwp 등)를 사용했을 때와는 차이가 있습니다. 메타데이터에는 파일 저장 경로, 파일 이름, 파일의 형태(text), 파일 크기, 제작 일자, 마지막 수정일이 포함되며, 각 문서마다 고유한 ID가 생성됩니다.

    reader = SimpleDirectoryReader(input_dir="txt파일이있는/폴더/경로/입력")
    documents = reader.load_data()
     
    print(documents)

    ex_txt.png.ca0a7321a6a021905ed665c653fc4347.png

     

     

    3. 폴더에서 특정 문서 통째로 불러오기

    폴더에서 특정 파일만을 통째로 불러올 수 있습니다. 문서를 불러올 때, 각 페이지마다 고유한 Doc ID와 해당 페이지의 내용인 Text가 지정됩니다. 이 외에 문서에 대한 추가적인 메타데이터는 별도로 제공되지 않습니다.

    parser = DocxReader()
    file_extractor = {".docx": parser}
    documents = SimpleDirectoryReader(
        "폴더/경로/입력", file_extractor=file_extractor
    ).load_data()
     
    for doc in documents:
        print(doc)

    ex_pick.png.88d2e00ef7a431fb30afd0e41a3b7a0e.png

     

     

    맺음말

    이상으로 LlamaIndex가 제공하는 다양한 문서 로더(Loader)와 그 사용 방법에 대해 알아보았습니다. LlamaIndex는 단일 파일을 불러올 때는 해당 데이터 유형에 맞는 parser를 사용하고, 여러 파일을 한 번에 불러올 때는 SimpleDirectoryReader와 parser를 조합하여 사용할 수 있도록 합니다. 이를 통해 사용자는 문서의 내용뿐만 아니라 메타데이터까지 효과적으로 추출할 수 있으며, 대량의 문서를 빠르게 처리할 수 있습니다.

    LlamaIndex의 이러한 기능은 RAG 모델 구현에 필수적인 요소로, 사용자가 보다 쉽고 편리하게 문서를 불러와 모델에 활용할 수 있도록 돕습니다. 앞으로도 LlamaIndex는 다양한 형식의 데이터를 처리할 수 있는 새로운 Loader를 지속해서 개발할 것으로 기대되며, 이는 RAG 모델의 활용 범위를 더욱 확장시킬 것입니다.

     

    image.png.9fa441f51bb41b4e6e37c93868108b2b.png

    • Haha 1
  7. 안녕하세요, @데이터 빅님,

    아시다시피, 모델마다 성능 차이가 있을 수 있으며,

    프롬프트 작업이나 튜닝을 통해, 성능의 전후 평가가 필요합니다.

    모델이 다양한 상황에 잘 대응할 수 있도록 프롬프트를 정의할 때는 Few-shot prompting 방식을 활용하는 것이 도움될 수 있습니다.

    즉, 프롬프트에 적절한 예시들을 포함시켜 모델이 원하는 방향으로 동작하도록 유도하는 것입니다.

    아래는 같은 문장이 주어졌을 때 모델이 어떻게 답변해야 하는지를 예시로 보여주는 프롬프트의 결과물입니다.

    감사합니다.

    __.png.5bafb7b37fab354e6779a8301e3e861b.png

     

    • Like 1
  8. 안녕하세요, @ak68님,

    본 Cookbook에서 활용된 html을 로딩하기 위해선 ‘—user-agent=Mozilla/5.0’ 을 추가하면 정상적으로 불러올 수 있습니다.

    대상 사이트에서 사용하는 User-Agent 정보와 동일한 형태로 --user-agent 옵션을 설정하면, 요청 차단으로 인해 발생할 수 있는 에러를 방지할 수 있습니다.

    @hatiolab님, 공유 감사합니다.

    • Like 1
  9. 안녕하세요, @데이터 빅님,

    말씀하신 내용을 확인하고 재현해 본 결과, 해당 부분은 현재 모델의 부족한 영역으로 판단됩니다.

    이를 위해, 시스템 프롬프트에 올바른 문장 부호를 사용해야 한다는 규칙을 추가하였고, 그 결과 모델의 성능이 개선되는 것을 확인하였습니다.

    앞으로도 지속적인 모델 개선을 통해 더 좋은 서비스를 제공할 수 있도록 노력하겠습니다.

    감사합니다.

    image.png.6c63b30c0fc0abd76e188a8286fe4a04.png

    • Like 1
    • Haha 1
  10. 안녕하세요, @Roadpia님, 

    튜닝한 모델에 추가로 튜닝을 진행하실 수는 없으며 새로 튜닝을 진행하셔야 합니다.

    튜닝 가이드의 학습 조회/학습 생성 URI가 잘못 기재된 오류를 확인하여, 수정하도록 하겠습니다.

    학습 생성은 POST: /tuning/v2/tasks 학습 조회는 GET: /tuning/v2/tasks/{taskId} 로 진행하실 수 있으며,

    가이드 수정 전까지는 우선 튜닝 API의 상세 페이지로 확인 부탁드립니다.

    https://hyperclova.navercorp.com/tuning/api/create

    감사합니다.

  11. 이 cookbook은 네이버 클라우드 플랫폼에서 CLOVA Studio의 기능을 활용하여 RAG(Retrieval Augmented Generation)를 구현하는 방법을 설명합니다. RAG에 사용된 데이터는 HTML 형식이며, CLOVA Studio의 문단 나누기 API, 임베딩 API, Chat Completion API를 활용했습니다. HyperCLOVA X를 연계하여 주어진 데이터를 기반으로 대화를 진행하는 서비스 구현이 가능합니다.

     

    RAG 3부작 시리즈
    ✔︎ (1부) RAG란 무엇인가 링크
    ✔︎ (2부) RAG 구현 단계 알아보기 링크
    ✔︎ (3부) CLOVA Studio를 이용해 RAG 구현하기 Cookbook

     

    image.png.46abc5c291e0255c259cf79115a8261f.png

    Quote

    Python 3.12.2

    requirements.txt

     

    # Vector DB Milvus 관련된 모듈들은 모듈의 용도를 명확히 구분하기 위해 Vector DB 구축 단계에서 불러왔으며 해당 부분에서 코드를 확인하실  있습니다.
    import json
    import os
    import subprocess
    from langchain_community.document_loaders import UnstructuredHTMLLoader
    from pathlib import Path
    import base64
    import http.client
    from tqdm import tqdm
    import requests

     

    1. Raw Data → Connecting

    데이터를 추후 작업을 위해 가져오는 단계입니다. 직접 크롤링을 하지 않고, txt 파일(clovastudiourl.txt)에 사용하고 싶은 사이트 URL을 작성한 후, wget 라이브러리와 LangChain의 로더를 통해 작업할 수 있는 형태로 HTML 데이터를 변환했습니다. 또한, 로딩한 데이터 안에 사이트 URL을 넣어 추후 답변과 함께 사이트 주소가 출력되게 하여 질문한 사용자가 답변의 내용이 있는 실제 URL을 함께 참조할 수 있게 로딩된 데이터를 수정했습니다.

     

    txt → html 변환 및 원본 사이트 주소 mapping

    url_to_filename_map = {}
     
    with open("clovastudiourl.txt", "r") as file:
        urls = [url.strip() for url in file.readlines()]
     
    folder_path = "clovastudioguide"
     
    if not os.path.exists(folder_path):
        os.makedirs(folder_path)
     
    for url in urls:
        filename = url.split("/")[-1] + ".html"
        file_path = os.path.join(folder_path, filename)
        subprocess.run(["wget", "-O", file_path, url], check=True)
        url_to_filename_map[url] = filename
     
    with open("url_to_filename_map.json", "w") as map_file:
        json.dump(url_to_filename_map, map_file)

    Output

    --2024-04-11 17:51:59--  https://guide.ncloud-docs.com/docs/clovastudio-overview
    guide.ncloud-docs.com (guide.ncloud-docs.com) 해석 중... 104.18.7.159, 104.18.6.159
    다음으로 연결 중: guide.ncloud-docs.com (guide.ncloud-docs.com)|104.18.7.159|:443... 연결했습니다.
    HTTP 요청을 보냈습니다. 응답 기다리는 중... 200 OK
    길이: 지정하지 않음 [text/html]
    저장 위치: `clovastudioguide/clovastudio-overview.html'
     
         0K .......... .......... .......... .......... .......... 15.5M
        50K .......... .......... .......... ...                    158M=0.003s
     
    2024-04-11 17:52:00 (24.5 MB/s) - `clovastudioguide/clovastudio-overview.html' 저장함 [85968]
     
    --2024-04-11 17:52:00--  https://guide.ncloud-docs.com/docs/clovastudio-spec
    guide.ncloud-docs.com (guide.ncloud-docs.com) 해석 중... 104.18.6.159, 104.18.7.159
    다음으로 연결 중: guide.ncloud-docs.com (guide.ncloud-docs.com)|104.18.6.159|:443... 연결했습니다.
    HTTP 요청을 보냈습니다. 응답 기다리는 중... 200 OK
    길이: 지정하지 않음 [text/html]
    저장 위치: `clovastudioguide/clovastudio-spec.html'
     
         0K .......... .......... .......... .......... .......... 42.0M
        50K .......... .......... ......                           83.4M=0.001s
     
    2024-04-11 17:52:01 (50.8 MB/s) - `clovastudioguide/clovastudio-spec.html' 저장함 [78387]
     
    --2024-04-11 17:52:01--  https://guide.ncloud-docs.com/docs/clovastudio-info
    ...
        50K .......... .......... .......... .                     14.0M=0.7s
     
    2024-04-11 17:52:03 (110 KB/s) - `clovastudioguide/clovastudio-procedure.html' 저장함 [83208]
     
    Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...

     

    Quote

    txt 파일을 작성할 때는 한 줄에 하나의 URL을 작성해야 합니다. 이때, 각 URL은 줄바꿈으로 구분되어야 합니다.

    그리고 html 데이터를 로딩하기 전, robots.txt를 통해 데이터로 활용할 사이트의 접근 허용 여부를 확인해야합니다. NCP의 클로바스튜디오 사용 가이드의 경우 User-agent: * Allow:/ 로 접근이 가능합니다.

    예제에서 활용한 HTML은, 네이버클라우드 플랫폼(NCP)의 클로바스튜디오 사용 가이드중 CLOVA Studio("AI Services" → "CLOVA Studio")와 관련된 모든 안내 페이지를 데이터로 활용했습니다.

    LangChain을 활용해 로딩한 html은, 파일을 저장한 디렉토리의 주소를 metadata의 'source'로 가져오게 됩니다. 추후 답변 제공시, 디렉토리 주소가 아닌 실제 URL을 제공하기 위해, LangChain이 로딩한 데이터를 수정해야합니다.

    html을 로딩할 때 파일명을 URL로 할 경우, /와 : 기호로 인해 파일명이 깨지게 됩니다. 따라서 원본 URL과 로딩한 HTML 파일을 쌍으로 mapping하고, 이 정보를 json 형식으로 "url_to_filename_map"에 저장합니다.

    이후, html 파일의 저장 경로가 담긴 'source'를, 이 json 파일과 연결해 실제 URL로 변환해줍니다. 위 코드는, json 파일을 저장하는 단계까지입니다.

     

    LangChain 활용 HTML 로딩

    이전 단계에서 txt 파일을 HTML로 변환한 후, Langchain의 UnstructuredHTMLLoader를 사용하여 이후 문단 나누기와 임베딩을 위해 machine readable한 형태로 변환해 줍니다.

    # 폴더 이름에 맞게 수정
    html_files_dir = Path('/Users/user/Desktop/raghigh/rag_html/forwiki/clovastudioguide')
     
    html_files = list(html_files_dir.glob("*.html"))
     
    clovastudiodatas = []
     
    for html_file in html_files:
        loader = UnstructuredHTMLLoader(str(html_file))
        document_data = loader.load()
        clovastudiodatas.append(document_data)
        print(f"Processed {html_file}")

    Output

    Processed /Users/user/Desktop/raghigh/rag_html/forwiki/clovastudioguide/clovastudio-info.html
    Processed /Users/user/Desktop/raghigh/rag_html/forwiki/clovastudioguide/clovastudio-overview.html
    Processed /Users/user/Desktop/raghigh/rag_html/forwiki/clovastudioguide/clovastudio-procedure.html
    Processed /Users/user/Desktop/raghigh/rag_html/forwiki/clovastudioguide/clovastudio-spec.html
    ...
    Processed /Users/user/Desktop/raghigh/rag_html/forwiki/clovastudioguide/clovastudio-explorer03.html
    Processed /Users/user/Desktop/raghigh/rag_html/forwiki/clovastudioguide/clovastudio-dataset.html
    Processed /Users/user/Desktop/raghigh/rag_html/forwiki/clovastudioguide/clovastudio-procedure.html
    Processed /Users/user/Desktop/raghigh/rag_html/forwiki/clovastudioguide/clovastudio-spec.html

     

    # page_content는 길이가 긴 관계로 중략합니다.
     
    [Document(page_content="Login\n\nrelease/20240321 release/20240321  release/20240125  release/20231123  release/20230525\n\nKorean English  Ja - 日本語  Korean\n\nAPI 가이드\n\nCLI 가이드\n\n내용 \n\nHOME\n\n포털 및 콘솔\n\n네이버 클라우드 플랫폼 사용 환경\n\nCompute\n\nContainers\n\nStorage\n\nNetworking\n\nDatabase\n\nSecurity\n\nAI Services\n\nAI·NAVER API\n\nApplication Services ... Enter a valid password\n\nYour profile has been successfully updated.\n\nLogout", metadata={'source': '/Users/user/Desktop/raghigh/rag_html/forwiki/clovastudioguide/clovastudio-info.html'})]

     

    Quote

    각 HTML에는 page_content에 모든 텍스트가, metadata의 'source'에는 저장 경로가 기록되어 있습니다. 위는, 로딩된 데이터 중 하나인 clovastudio-info.html의 형태입니다. metadata의 'source'에 실제 URL이 아닌 html 파일이 저장된 디렉토리 경로가 담긴 것을 확인할 수 있습니다. Mapping을 통해 이 'source'를 실제 URL로 바꾸는 작업을 다음 단계에 진행하게 됩니다.

     

    Mapping 정보를 활용해 'source'를 실제 URL로 대체

    아래의 output은 앞선 예시인 clovastudio-info.html의 형태입니다. Mapping 정보를 활용하여 'source'의 값을 실제 URL로 변환한 것을 확인할 수 있습니다.

    with open("url_to_filename_map.json", "r") as map_file:
        url_to_filename_map = json.load(map_file)
     
    filename_to_url_map = {v: k for k, v in url_to_filename_map.items()}
     
    # clovastudiodatas 리스트의  Document 객체의 'source' 수정
    for doc_list in clovastudiodatas:
        for doc in doc_list:
            extracted_filename = doc.metadata["source"].split("/")[-1]
            if extracted_filename in filename_to_url_map:
                doc.metadata["source"] = filename_to_url_map[extracted_filename]
            else:
                print(f"Warning: {extracted_filename}에 해당하는 URL을 찾을 수 없습니다.")

     

    # page_content는 길이가 긴 관계로 중략합니다.
     
    [Document(page_content="Login\n\nrelease/20240321 release/20240321  release/20240125  release/20231123  release/20230525\n\nKorean English  Ja - 日本語  Korean\n\nAPI 가이드\n\nCLI 가이드\n\n내용 \n\nHOME\n\n포털 및 콘솔\n\n네이버 클라우드 플랫폼 사용 환경\n\nCompute\n\nContainers\n\nStorage\n\nNetworking\n\nDatabase\n\nSecurity\n\nAI Services\n\nAI·NAVER API\n\nApplication Services ... Enter a valid password\n\nYour profile has been successfully updated.\n\nLogout", metadata={'source': 'https://guide.ncloud-docs.com/docs/clovastudio-info'})]

     

    # 이중 리스트를 풀어서 하나의 리스트로 만드는 작업
    clovastudiodatas_flattened = [item for sublist in clovastudiodatas for item in sublist]

     

    2. Chunking

    임베딩 모델이 처리할 수 있는 적당한 크기로 raw data를 나누는 것은 매우 중요합니다. 이는 임베딩 모델마다 한 번에 처리할 수 있는 토큰 수의 한계가 있기 때문입니다. CLOVA Studio의 문단 나누기 API는 모델이 직접 문장들간의 의미 유사도를 찾아 최적의 chunk 개수와 사용자가 원하는 1개 chunk의 크기(글자 수)를 직접 설정하여 문단을 나눌 수도 있습니다. 추가로, 후처리(postProcess = True)를 통해 chunk당 글자 수의 상한선과 하한선을 postProcessMaxSize와 postProcessMinSize로 조절할 수도 있습니다.

    class SegmentationExecutor:
        def __init__(self, host, api_key, api_key_primary_val, request_id):
            self._host = host
            self._api_key = api_key
            self._api_key_primary_val = api_key_primary_val
            self._request_id = request_id
     
        def _send_request(self, completion_request):
            headers = {
                "Content-Type": "application/json; charset=utf-8",
                "X-NCP-CLOVASTUDIO-API-KEY": self._api_key,
                "X-NCP-APIGW-API-KEY": self._api_key_primary_val,
                "X-NCP-CLOVASTUDIO-REQUEST-ID": self._request_id
            }
     
            conn = http.client.HTTPSConnection(self._host)
            conn.request(
                "POST",
                "/testapp/v1/api-tools/segmentation/{app-id}", # If using Service App, change 'testapp' to 'serviceapp', and corresponding app id.
                json.dumps(completion_request),
                headers
            )
            response = conn.getresponse()
            result = json.loads(response.read().decode(encoding="utf-8"))
            conn.close()
            return result
     
        def execute(self, completion_request):
            res = self._send_request(completion_request)
            if res["status"]["code"] == "20000":
                return res["result"]["topicSeg"]
            else:
                raise ValueError(f"{res}")
     
     
    if __name__ == "__main__":
        segmentation_executor = SegmentationExecutor(
            host="clovastudio.apigw.ntruss.com",
            api_key='<api_key>',
            api_key_primary_val='<api_key_primary_val>',
            request_id='<request_id>'
        )
      
        chunked_html = []
      
        for htmldata in tqdm(clovastudiodatas_flattened):
            try:
                request_data = {
                    "postProcessMaxSize": 100,
                    "alpha": -100,
                    "segCnt": -1,
                    "postProcessMinSize": -1,
                    "text": htmldata.page_content,
                    "postProcess": True
                }
                 
                request_json_string = json.dumps(request_data)
                request_data = json.loads(request_json_string, strict=False)
                response_data = segmentation_executor.execute(request_data)
                result_data = [' '.join(segment) for segment in response_data]
     
            except json.JSONDecodeError as e:
                print(f"JSON decoding failed: {e}")
            except Exception as e:
                print(f"An error occurred: {e}")
             
            for paragraph in result_data:
                chunked_document = {
                    "source": htmldata.metadata["source"],
                    "text": paragraph
                }
                chunked_html.append(chunked_document)
      
    print(len(chunked_html))

    Output

    100%|██████████| 4/4 [00:08<00:00,  2.02s/it]
    525

     

    Quote

    파이썬 스펙을 확인하기 위해서는 테스트 앱을 발급받아야 합니다. 테스트 앱에는 고유한 식별자(코드)인 **{테스트앱 식별자}**가 부여됩니다.

    테스트 앱이 요청한 작업에는 <api_key>, <api_key_primary_val>, **<request_id>**와 같은 3가지 정보가 사용되며, 이는 사용자 계정마다 고유한 값입니다.

    파이썬 스펙에서는 필요한 모듈을 불러오는 부분이 상단에 위치하지만, 파이프라인에서는 초반에 번에 로딩하였습니다. 또한, 파이썬 스펙에서는 클래스 이름이 "CompletionExecutor" 명시되어 있으나, 다른 API와의 이름 중복을 피하기 위해 "SegmentationExecutor" 임의 변경하였습니다.

     

    3. Embedding

    문단 나누기 API를 통해 나누어진 525개의 chunk(chunked_html)을 CLOVA Studio의 임베딩 API를 사용해 1024차원의 벡터로 변환하는 과정입니다. clir-emb-dolphin의 경우, 임베딩 과정에서 유사도 판단을 위해 벡터의 내적(Inner Product, IP)을 거리 단위로 사용합니다. 반면, clir-sts-dolphin은 코사인 거리(Cosine)를 거리 단위로 사용합니다. 이후, 벡터의 인덱싱과 검색 과정에서 사용자는 어떤 거리 단위를 사용하여 데이터와 사용자의 쿼리 간 유사도를 판단할 것인지 선택할 수 있습니다.

    리 단위를 임베딩부터 인덱싱, 검색까지 일치시켜야 데이터의 품질이 향상되므로, 임베딩 과정에서 사용한 모델(emb / sts)을 기억하는 것이 중요합니다. 임베딩 클래스에 오류가 발생시 멈추게끔 하는 로직을 일부 추가해, 다량의 데이터를 처리할 때 오류 발생시 재실행의 부담을 줄입니다.

    class EmbeddingExecutor:
        def __init__(self, host, api_key, api_key_primary_val, request_id):
            self._host = host
            self._api_key = api_key
            self._api_key_primary_val = api_key_primary_val
            self._request_id = request_id
     
        def _send_request(self, completion_request):
            headers = {
                "Content-Type": "application/json; charset=utf-8",
                "X-NCP-CLOVASTUDIO-API-KEY": self._api_key,
                "X-NCP-APIGW-API-KEY": self._api_key_primary_val,
                "X-NCP-CLOVASTUDIO-REQUEST-ID": self._request_id
            }
     
            conn = http.client.HTTPSConnection(self._host)
            conn.request(
                "POST",
                "/testapp/v1/api-tools/embedding/clir-emb-dolphin/{app-id (앱 식별자)}", # If using Service App, change 'testapp' to 'serviceapp', and corresponding app id.
                json.dumps(completion_request),
                headers
            )
            response = conn.getresponse()
            result = json.loads(response.read().decode(encoding="utf-8"))
            conn.close()
            return result
     
        def execute(self, completion_request):
            res = self._send_request(completion_request)
            if res["status"]["code"] == "20000":
                return res["result"]["embedding"]
            else:
                error_code = res["status"]["code"]
                error_message = res.get("status", {}).get("message", "Unknown error")
                raise ValueError(f"오류 발생: {error_code}: {error_message}")
     
     
    if __name__ == "__main__":
        embedding_executor = EmbeddingExecutor(
            host="clovastudio.apigw.ntruss.com",
            api_key='<api_key>',
            api_key_primary_val='<api_key_primary_val>',
            request_id='<request_id>'
        )
     
        for i, chunked_document in enumerate(tqdm(chunked_html)):
            try:
                request_json = {
                    "text": chunked_document['text']
                }
                request_json_string = json.dumps(request_json)
                request_data = json.loads(request_json_string, strict=False)
                response_data = embedding_executor.execute(request_data)
            except ValueError as e:
                print(f"Embedding API Error. {e}")
            except Exception as e:
                print(f"Unexpected error: {e}")
             
            chunked_document["embedding"] = response_data

    Output

    100%|██████████| 525/525 [01:04<00:00,  8.08it/s]

     

    Quote

    이 예제에서는 clir-emb-dolphin을 임베딩 모델로 사용했으며, 이후 인덱싱과 검색에서도 거리 단위를 IP로 활용할 것입니다.
    clir-sts-dolphin, v2 (bge-m3) 를 임베딩 모델로 사용하고자 할 경우 해당 테스트 앱 또는 서비스 앱을 생성하여 이용하면 되며, 이 때는 인덱싱 및 검색 시 거리 단위를 Cosine Similarity (코사인 유사도) 로 활용하는 것을 권장합니다.

     

    dimension_set = set()
     
    for item in chunked_html:
        if "embedding" in item:
            dimension = len(item["embedding"])
            dimension_set.add(dimension)
     
    print("임베딩된 벡터들의 차원:", dimension_set)

    Output

    임베딩된 벡터들의 차원: {1024}

     

    chunked_html[400]

    Output

    {'source': 'https://guide.ncloud-docs.com/docs/clovastudio-procedure',
     'text': '4. 테스트 앱 생성',
     'embedding': [-0.4846521,
      -0.47464514,
      -0.47014025,
      1.5649279,
      -0.28591785,
      1.0830964,
    ...
      -0.09297561,
      -0.48607916,
      -0.4411355,
      -0.86348283,
      ...]}
    Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...

     

    4. Vector DB

    CLOVA Studio의 임베딩 API를 활용하여 모든 chunked_의 text를 1024차원의 벡터로 변환한 후, 'embedding'이라는 이름의 객체로 chunked_html에 추가해 주었습니다. 이제 이 데이터를 Vector DB에 저장하고, 인덱싱을 하는 단계입니다. Vector DB로는 Milvus를 사용하였으며, 대시보드는 Docker를 사용하여 DB를 켜고(Run) 끄는(Stop) 작업을 수행하였습니다.

    vectordb_1.png.9e15e7281a43ce5f4e4e93b19f39f9bf.png

     

    Milvus Set Up

    from pymilvus import connections, FieldSchema, CollectionSchema, DataType, Collection, utility

    MacOS 환경에서 Docker를 설치한 뒤, 터미널을 사용하여 git clone 명령어로 Milvus Standalone을 설치했습니다. Milvus Standalone 모드는 단일 노드 또는 서버에서 모든 기능을 제공하며, 필요한 모든 컴포넌트가 하나의 인스턴스나 컨테이너 안에서 작동합니다. 이는 주로 소규모 프로젝트나 개발 단계에 이상적입니다. 프로젝트에 파이썬을 사용함에 따라, Milvus를 효과적으로 활용하기 위해 파이썬용 SDK인 Pymilvus를 설치하고 필요한 라이브러리를 가져왔습니다.

     

    Collections & ID 생성

    connections.connect()
      
    fields = [
        FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
        FieldSchema(name="source", dtype=DataType.VARCHAR, max_length=3000),
        FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=9000),
        FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1024)
    ]
    
    schema = CollectionSchema(fields, description="컬렉션 설명 적어넣기")
    
    collection_name = "컬렉션 이름"
    collection = Collection(name=collection_name, schema=schema, using='default', shards_num=2)
    
    for item in chunked_html:
        source_list = [item['source']]
        text_list = [item['text']]
        embedding_list = [item['embedding']]
        
        entities = [
            source_list,
            text_list,
            embedding_list
        ]
        
        insert_result = collection.insert(entities)
        print("데이터 Insertion이 완료된 ID:", insert_result.primary_keys)
    
    print("데이터 Insertion이 전부 완료되었습니다")

    Output

    Data insertion complete. IDs: [448298454101600167, 448298454101600168, 448298454101600173, ..., 448298454101600687, 448298454101600688, 448298454101600689, 448298454101600690, 448298454101600691]
    

     

    Quote

    상단 Output의 경우, 길이가 긴 관계로 중략(...) 처리한 것이며, 실제로는 모든 ID가 출력됩니다.

     

    Quote
    • 활용 SDK가 업데이트 될 경우 코드에 변화가 발생할 수 있습니다.
    • 데이터 chunk가 에러가 발생하여 임베딩 값을 가지고 있지 않을 경우 오류가 발생할 수 있으니, 임베딩이 정상적으로 완료 되었는지 확인해야 합니다.

     

    Indexing

    앞서 임베딩 시 거리 단위를 IP로 설정했기 때문에, 인덱싱에서도 "metric_type"을 "IP"로 설정합니다. 인덱싱 유형은 다양한 알고리즘이 존재하며, 최근에는 HNSW 방식이 많이 활용됩니다. 하지만 선택한 Vector DB가 어떤 인덱싱 유형을 지원하는지 확인해야 합니다. "M"은 각 노드(데이터 포인트)가 유지할 수 있는 최대 엣지(연결)의 수로, 값이 높을수록 검색 정확도는 높아지지만 인덱스 생성 시간과 메모리 사용량도 증가하는 trade-off 관계를 가지고 있습니다.

    "efConstruction"은 인덱스 구축 시 만들 그래프 구조의 깊이와 너비로, 값이 높을수록 정확도는 올라가지만 인덱스 생성 시간과 메모리 사용량도 증가하는 trade-off 관계를 가지고 있습니다. "M" : 8, "efConstruction" : 200의 설정은 Milvus가 공개한 Milvus 2.2.0의 성능 벤치마크에서 실험 시 설정한 파라미터 값을 가져온 것으로, Cookbook도 Milvus 2.2.x 환경에서 진행되었습니다. 데이터의 양이 많을수록 인덱싱에 시간이 오래 소요될 수 있습니다. 데이터가 많은 경우, 인덱싱 생성 후 바로 결과를 확인하면 결과를 확인할 수 없으며, 인덱싱은 하나의 객체와 값으로 저장되는 것이 아니기 때문에 아래와 같은 실행 결과로 인덱싱이 완료되었는지 확인할 수 있습니다.

    index_params = {
        "metric_type": "IP"
        "index_type": "HNSW"
        "params": {
            "M": 8,
            "efConstruction": 200
        }
    }
     
    collection = Collection("htmlrag_forncp")
    collection.create_index(field_name="embedding", index_params=index_params)
    utility.index_building_progress("htmlrag_forncp")
     
    print([index.params for index in collection.indexes])

    Output

    [{'metric_type': 'IP', 'index_type': 'HNSW', 'params': {'M': 8, 'efConstruction': 200}}]

     

    Quote

    위와 같은 output이 나왔다는 것은 인덱싱 작업이 성공적으로 완료됐다는 의미입니다.

     

    재실행시 Milvus 로딩 방법

    작업 환경 종료 이후, RAG를 이용해 다시 질의응답을 하려면, Vector DB인 Milvus와 연결 상태여야 합니다. Docker를 Milvus의 대시보드로 사용할 때의 연결 방법은 다음과 같습니다. 질의응답은 html_chat() 함수를 통해 이전과 동일하게 진행할 수 있습니다.

    1. Docker를 실행한 후, collections가 저장되어 있는 컨테이너(milvus-standalone)를 실행시켜줍니다.

    docker_1.png.6d4ba7f72a8b1d02173139b6f8dd09fc.png

    2. 아래의 코드를 통해 connection을 실행하고, 만든 collection의 이름을 넣어 연결합니다. Milvus 및 Milvus-Standalone의 기본 환경은 host = "localhost", port="19530"입니다. 필요한 모듈과 라이브러리는 Milvus에 데이터를 저장할 때와 동일합니다.

    connections.connect("default", host="localhost", port="19530")
     
    # 불러올 collection 이름을 넣는 
    collection = Collection("htmlrag_forncp")
    utility.load_state("htmlrag_forncp")

    Output

    <LoadState: Loaded>

     

    5. Retrieval → HyperCLOVA X

    사용자의 질문을 임베딩하는 함수와, 임베딩한 사용자의 질문에 대응하는 Vector DB에 저장된 데이터를 검색하는 로직을 구성하는 단계입니다. 검색을 통해 사용자의 질문과 가장 유사한 데이터를 모은 "reference"를 HyperCLOVA X가 참조해 질문에 대한 답변을 생성합니다. Chat Completion API를 생성하여 답변 생성을 위한 LLM을 호출하며, 플레이그라운드와 동일하게 파라미터(Top P, Top K 등)와 시스템 프롬프트를 작성할 수 있습니다.

     

    Chat Completion API

    Chat Completion의 파이썬 스펙은 플레이그라운드의 테스트앱 발급을 통해 확인할 수 있습니다. 기본적으로 Chat Completion API의 실행 결과는, 아래와 같은 stream 형태로 출력됩니다.

    chatcompletions.png.7aaf9c5ceb45961005058f027555c778.png

    Chat Completion API의 파이썬 스펙을 약간 변형하여 full-length 최종 답변만을 추출할 수 있습니다. 이를 위해 두 가지 로직을 사용할 수 있는데, 첫째는 Stream 답변 중 가장 긴 길이의 답변을 선택하는 방법이고, 둘째는 제일 마지막 출력 결과인 "[DONE]" 바로 직전의 출력 결과를 선택하는 방법입니다. 각각의 방법에 대한 코드는 아래에서 확인하실 수 있습니다.

    1. 가장 긴 길이의 답변을 선택하는 경우

    class CompletionExecutor:
        def __init__(self, host, api_key, api_key_primary_val, request_id):
            self._host = host
            self._api_key = api_key
            self._api_key_primary_val = api_key_primary_val
            self._request_id = request_id
     
        def execute(self, completion_request, response_type="stream"):
            headers = {
                "X-NCP-CLOVASTUDIO-API-KEY": self._api_key,
                "X-NCP-APIGW-API-KEY": self._api_key_primary_val,
                "X-NCP-CLOVASTUDIO-REQUEST-ID": self._request_id,
                "Content-Type": "application/json; charset=utf-8",
                "Accept": "text/event-stream"
            }
     
            final_answer = ""
     
            with requests.post(
                self._host + "/testapp/v1/chat-completions/HCX-003",
                headers=headers,
                json=completion_request,
                stream=True
            ) as r:
                if response_type == "stream":
                    longest_line = ""
                    for line in r.iter_lines():
                        if line:
                            decoded_line = line.decode("utf-8")
                            if decoded_line.startswith("data:"):
                                event_data = json.loads(decoded_line[len("data:"):])
                                message_content = event_data.get("message", {}).get("content", "")
                                if len(message_content) > len(longest_line):
                                    longest_line = message_content
                    final_answer = longest_line
                elif response_type == "single":
                    final_answer = r.json()  # 가정: 단일 응답이 JSON 형태로 반환됨
     

    2. "[DONE]" 바로 직전의 출력 결과를 선택하는 경우

    class CompletionExecutor:
        def __init__(self, host, api_key, api_key_primary_val, request_id):
            self._host = host
            self._api_key = api_key
            self._api_key_primary_val = api_key_primary_val
            self._request_id = request_id
     
        def execute(self, completion_request):
            headers = {
                "X-NCP-CLOVASTUDIO-API-KEY": self._api_key,
                "X-NCP-APIGW-API-KEY": self._api_key_primary_val,
                "X-NCP-CLOVASTUDIO-REQUEST-ID": self._request_id,
                "Content-Type": "application/json; charset=utf-8",
                "Accept": "text/event-stream"
            }
     
            response = requests.post(
                self._host + "/testapp/v1/chat-completions/HCX-003",
                headers=headers,
                json=completion_request,
                stream=True
            )
     
            # 스트림에서 마지막 'data:' 라인을 찾기 위한 로직
            last_data_content = ""
     
            for line in response.iter_lines():
                if line:
                    decoded_line = line.decode("utf-8")
                    if '"data":"[DONE]"' in decoded_line:
                        break
                    if decoded_line.startswith("data:"):
                        last_data_content = json.loads(decoded_line[5:])["message"]["content"]
     
            return last_data_content

     

    Quote

    Stream 형태의 답변 구조에서는 다음과 같은 두 가지 로직을 사용하여 최종 full-length 답변을 추출할 수 있습니다.

    1. 최종 full-length 답변은 출력 결과들 중 가장 긴 답변이므로, 모든 출력 결과의 길이를 비교하여 가장 긴 답변을 선택합니다.
    2. Stream 답변 구조상 "[DONE]" 표시 직전의 출력 결과가 최종 full-length 답변이므로, "[DONE]" 표시 이전의 마지막 출력 결과를 선택합니다.

    Cookbook에서는 1번 로직을 사용하여 클래스를 정의했습니다.

     

    답변 생성 함수 정의

    # 사용자의 쿼리를 임베딩하는 함수를 먼저 정의
    def query_embed(text: str):
        request_data = {"text": text}
        response_data = embedding_executor.execute(request_data)
        return response_data

     

    def html_chat(realquery: str) -> str:
        # 사용자 쿼리 벡터화
        query_vector = query_embed(realquery)
     
        collection.load()
     
        search_params = {"metric_type": "IP", "params": {"ef": 64}}
        results = collection.search(
            data=[query_vector],  # 검색할 벡터 데이터
            anns_field="embedding",  # 검색을 수행할 벡터 필드 지정
            param=search_params,
            limit=10,
            output_fields=["source", "text"]
        )
     
        reference = []
     
        for hit in results[0]:
            distance = hit.distance
            source = hit.entity.get("source")
            text = hit.entity.get("text")
            reference.append({"distance": distance, "source": source, "text": text})
     
        completion_executor = CompletionExecutor(
            host="https://clovastudio.stream.ntruss.com",
            api_key='<api_key>',
            api_key_primary_val='<api_key_primary_val>',
            request_id='<request_id>'
        )
     
        preset_texts = [
            {
                "role": "system",
                "content": "- 너의 역할은 사용자의 질문에 reference를 바탕으로 답변하는거야. \n- 너가 가지고있는 지식은 모두 배제하고, 주어진 reference의 내용만을 바탕으로 답변해야해. \n- 답변의 출처가 되는 html의 내용인 'source'도 답변과 함께 {url:}의 형태로 제공해야해. \n- 만약 사용자의 질문이 reference와 관련이 없다면, {제가 가지고 있는 정보로는 답변할 수 없습니다.}라고만 반드시 말해야해."
            }
        ]
     
        for ref in reference:
            preset_texts.append(
                {
                    "role": "system",
                    "content": f"reference: {ref['text']}, url: {ref['source']}"
                }
            )
     
        preset_texts.append({"role": "user", "content": realquery})
     
        request_data = {
            "messages": preset_texts,
            "topP": 0.6,
            "topK": 0,
            "maxTokens": 1024,
            "temperature": 0.5,
            "repeatPenalty": 1.2,
            "stopBefore": [],
            "includeAiFilters": False
        }
     
        # LLM 생성 답변 반환
        response_data = completion_executor.execute(request_data)
     
        return response_data

     

    Quote

    - Search 역시 "metric_type" = "IP"로 설정해 임베딩과 인덱싱의 거리 단위와 일치시켜줍니다. 이때, "ef" 값을 64로 설정한 것은 Milvus 2.2.0의 성능 벤치마크에서 사용된 설정을 참고한 것입니다.

    - results 내 limit = 10의 경우 상위 10개의 문서를 가져온다는 의미인데, Milvus가 우수한 성능을 보여준 2023 ANN Benchmarks에서의 실험 세팅인 k = 10을 그대로 가져온 것입니다.

    - 이렇게 선정된 상위 10개의 문서를 'reference'라는 리스트로 만들고, 이를 바탕으로 HyperCLOVA X가 오로지 이 리스트를 참조하여 답변하도록 프롬프팅을 설정했습니다. 또한, 추가적인 프롬프팅을 통해 데이터를 기반으로 한 질문에만 답변하도록 하며, 정보가 없을 경우 이를 인정할 수 있는 지침을 부여했습니다.

     

    실행 결과 모음

    별도의 패키징 작업 없이, RAG 파이프라인을 사용하여 질문을 하고 HyperCLOVA X로부터 출력된 실행 결과입니다. 정확성을 위해 코드 실행 화면을 그대로 캡쳐해 이미지로 첨부했습니다.

    image.png.42963ec784f9ee309bd8e9d486100c85.png

    실제 원본 데이터인 NCP의 CLOVA Studio 사용 가이드에 포함된 내용에 대해 정확한 답변을 제공하는 것을 확인할 수 있습니다. 추가로, mapping을 통해 답변의 출처가 되는 데이터의 로컬 디렉토리 경로가 아닌, 접속 가능한 실제 URL로 답변하는 것을 확인할 수 있습니다.

    image.png.3e1aa9770c05bd6488d5da8971107a5a.png

    시스템 프롬프트에 입력된 지침에 따라, 참조 자료에서 답변을 찾을 수 없는 질문에 대해서는 시스템이 아는 듯이 답변하지 않는 것을 발견할 수 있습니다. 만약, 이 예제에서 사용하지 않은 다른 CLOVA Studio 사용 가이드 문서가 데이터 소스로 사용되었다면 "CLOVA X에서 스킬을 만들 수 있어?"라는 질문에 답변할 수 있었을 것입니다. 하지만, 데이터 소스로 사용된 NCP의 CLOVA Studio 사용 가이드에는 해당 내용이 포함되어 있지 않아, HyperCLOVA X가 답변을 회피하는 안정적인 결과를 보여주었습니다.

     

    맺음말

    이 가이드는 CLOVA Studio의 기능을 이용하여 간략하게 구현한 RAG 파이프라인입니다. 문단 나누기 API, 임베딩 API, Chat Completion API를 통해 HyperCLOVA X를 RAG에서 활용할 수 있습니다. RAG의 전체적인 구조와 흐름에 대해서만 다루었으며, Vector DB에 따라 적용할 수 있는 다양한 post-retrieval 방법론이나 retrieval의 평가에 대해서는 다루지 않았습니다. CLOVA Studio의 다양한 기능이 추가되고 업데이트될 예정이므로, 이를 활용하여 RAG를 더욱 발전시킬 수 있을 것입니다.

     

    RAG 3부작 시리즈
    ✔︎ (1부) RAG란 무엇인가 링크
    ✔︎ (2부) RAG 구현 단계 알아보기 링크
    ✔︎ (3부) CLOVA Studio를 이용해 RAG 구현하기 Cookbook

     

    studio_btn3.png.bc1e03e065c28273035c5ea9fbedeedc.png

     

    • Like 1
    • Thanks 1
  12. 안녕하세요, @추만득님,

    분류 작업의 경우 구체적인 지시문이나 예제를 가지고 프롬프트를 만드신 후 수행이 가능합니다.

    참고하실 수 있는 자료를 전달드립니다.

    감사합니다.

  13. 안녕하세요, @comes님, 

    저희쪽에서 확인했을때 정상적으로 응답되고 있어서, 가이드대로 인증 요청이 되어있는지 확인 부탁드립니다.

    보다 자세한 확인을 위해서는 요청 헤더에 대한 정보가 필요할 것 같아서

    아래 링크를 통해 자세히 알려주시면, 답변드릴 수 있도록 하겠습니다.

    https://www.ncloud.com/support/question/service

    감사합니다.

  14. @이철희님,

    네, 한글 문서의 프롬프트, 영문 문서의 프롬프트처럼 작업을 구분해서 이용하는 것이 성능상 안정적일 수 있을 것 같습니다.

    다만 현실적으로 모든 작업들을 이렇게 구분하는 것은 어렵기 때문에, 한글은 한글, 영문은 영문으로 해달라는 형태의 시스템 프롬프트를 추가해보면서 성능 확인이 필요할 것 같습니다.

    또는 프롬프트에 간단한 예제를 넣어주는 것도 도움이 될 수 있습니다.

    마지막으로 앞서 말씀드린대로, Repetition penalty 문구를 생성할 때 반복적인 결괏값을 생성하지 않도록 반복되는 토큰에는 감점 요소를 부여하는 값입니다.

    따라서 해당 값을 1.2 정도로 낮추면, 최대한 원문에 근접한 언어의 결과를 낼 수 있지 않을까 기대합니다.

    그 밖에 부족한 부분들은 지속적으로 모델 개선이 이루어질 수 있도록 관리하겠습니다.

    감사합니다.

     

  15. @이철희님, 공유주셔서 감사합니다.

    언어를 설정하는 옵션은 없으나, 이러한 문제는 의외로 간단하게 해결할 수 있습니다.

    전달해주신 시스템 프롬프트로는 아래와 같이 한국어 키워드가 추출되는 것을 확인했습니다.

     

    image.png.6fc9fcd4349ab0dbf3442bdfb5d132c3.png

     

    ▼ 작성하신 시스템 프롬프트에 '영문으로만 출력' 해달라는 지시를 입력해두었습니다.

    """
    ###
    지시사항
    - 문서에서 핵심 키워드 최대 5개를 추출합니다.
    - 키워드는 핵심 주제와 상응하는 우선순위로  json 형식으로 답변합니다.
    - 각각의 핵심 키워드는 2단어 이하로 조합해서 추출합니다.
    - 주어진 문서에서 핵심 키워드를 영문으로만 출력합니다.
    - 한글을 출력하지 않고 절대로 영문으로만 출력합니다.
    ###
    응답형식 {'keywords':['키워드1', '키워드2', '키워드3', '키워드4', '키워드5']}
    """

    ▼ 영문으로 출력되는 것을 확인할 수 있었습니다.

    image.png.ba87ed994051b9630b8ac2895ea34936.png

     

    또한 분류와 같은 작업에서 Repetition penalty 값은 1.2로 설정을 추천드립니다.

    감사합니다.

    • Haha 1
×
×
  • Create New...