본문 바로가기
NLP

[Agent Study] 에이전트를 활용하여 멀티 DB 연결 구축하기

by AI미남홀란드 2024. 12. 20.
728x90

요즘 Agent 관련 게시글들이 정말 많이 나오면서 참고를 할만한 글들이 너무 많아서 좋습니다. 문득 작년 봄, 여름 ChatGPT를 활용해 ChatBot 을 만들어야 한다는 미션과 함께, RAG가 처음 얘기가 막 나올때였는데요. 어떻게 하나 막 엄청 돌아다니고, LLaMA가 나오고 ChatGPT 4로 성능이 개선되고, 토큰얼 어쩌니 뭐니 할 때 였는데, 지금 돌이켜보면 시간이 해결을 해주는 것 같네요. 

 

WIZnet Official - Leading Internet Connectivity Solutions

Discover WIZnet's leading role in providing innovative internet connectivity solutions, enhancing networks globally.

wiznet.io

 

이걸 만들기 위해 정말 데이터 전처리 노가다부터 고생했던 기억이 나는데요. 그 당시 RAG를 막 나와서 LangChain 에 처음 적용해보는 수준이었는데 다양한 고민을 했던것으로 기억납니다. 결국 위즈네트의 많은 제품에 관련된 정보를 잘 답변하기 위해서는 카테고리컬로 DB를 구성하고 그에 맞는 질문이 들어오면 답변을 하는 방식을 생각은 했었는데, 에이전트 라던지 쿼리에 대한 벡터디비로 라우팅을 하기 위해선 그당시 쿼리를 Classification 해서 벡터디비를 연결시켜야할지 LLM 을 통해서 한번 더 추론할지 그에 대한 비용은? 등 많은 고민을 했던것 같습니다. 이전 부터 Advanced RAG 를 통해서도 해결을 하겠다 생각은 했었는데 이제 Agent 로 별다른 큰 노력을 하지 않아도 구현 가능하겠다 생각이 들었습니다.

 

cookbook/mistral/rag/RAG_via_function_calling.ipynb at main · mistralai/cookbook

Contribute to mistralai/cookbook development by creating an account on GitHub.

github.com

몇일전 링크드인에 multi Vector Database 를 에이전트를 통해 적절한 답변을 만드는 방법에 대한 포스팅을 보고 호기심에 코드를 들어가봤는데 비교적 Function Calling 의 형태로 간단히 구현이 되어있었습니다. 코드를 천천히 하나씩 읽어보겠습니다. 저희 회사 관련된 정보를 Gemini 에게 물어보고 이걸 통해 합성데이터를 통해서 HR, Product , Finance 로 데이터를 구분해서 만들고, 테스트를 해보았습니다.

 

첫번째로 기존의 LLM 을 통해서 단순 체이닝 방식, 순차적으로 구현하는 방법부터 테스트를 해보았습니다.

 

현재 kt ds 관련된 내용을 토대로 구성해보았습니다.

 

 

def generate_test_questions() -> List[Tuple[str, str]]:
    """테스트용 질문 생성"""
    prompt = """
    KT DS(케이티디에스)의 내부 질문/답변 시스템을 위한 테스트 질문을 10개 만들어주세요.
    
    다음 세 분야의 질문이 고르게 포함되어야 합니다:
    - HR: 휴가, 복리후생, 급여, 사무실, 교육 등
    - Product: 클라우드, AI, DX 서비스, 보안, IT 플랫폼 등
    - Finance: 매출, 투자, 프로젝트, 파트너십, 전략 등
    
    다음과 같은 JSON 형식으로 답변해주세요:
    {"questions": [{"question": "질문내용", "category": "분류(HR/Product/Finance)"}]}
    질문은 실제 직원이 물어볼 법한 자연스러운 질문으로 작성해주세요.
    """
    
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "JSON 형식으로 응답해주세요."},
            {"role": "user", "content": prompt}
        ],
        temperature=0.7
    )
    
    try:
        result = json.loads(response.choices[0].message.content)
        return [(q["question"], q["category"]) for q in result["questions"]]
    except json.JSONDecodeError:
        # JSON 파싱 실패시 기본 테스트 질문 반환
        return [
            ("KT DS의 연차 휴가 정책은 어떻게 되나요?", "HR"),
            ("클라우드 서비스의 종류에 대해 알려주세요", "Product"),
            ("작년 매출과 영업이익은 얼마인가요?", "Finance")
        ]

 

위코드를 활용해서 Key : Value 형식으로 json 형태로 데이터를 만들었습니다.

 

# KTDS 목업 데이터베이스
MOCK_DB = {
    "HR": {
        "holidays": "기본 연차 15일, 근속 연수별 추가 휴가, 리프레시 휴가(3/5/7/10년 차), 명절/창립기념일 휴가",
        "salary": "신입 연봉 4000만원 수준, PS/EVA 성과급, 직무/역량 수당",
        "benefits": "복지포인트 연 200만원, 자기계발비, 의료비, 건강검진, 단체보험, 통신비 지원",
        "office": "서울시 서초구 방배로 107 KT DS빌딩",
        "education": "IT 전문가 육성 프로그램, 클라우드/AI 교육, 해외연수 기회"
    },
    "Product": {
        "cloud": "KT Cloud, 프라이빗 클라우드, 하이브리드 클라우드 솔루션",
        "ai": "AI 컨설팅, AI 플랫폼 개발, 빅데이터 분석 서비스",
        "dx": "디지털 트랜스포메이션 컨설팅, SI/SM 서비스, RPA 솔루션",
        "security": "통합보안관제, 클라우드 보안, 정보보안 컨설팅",
        "platform": "IT 인프라 통합 관리, MSP 서비스, DevOps 플랫폼"
    },
    "Finance": {
        "revenue_2023": "매출액 7000억원 수준, 영업이익 500억원 이상",
        "investments": "클라우드/AI 기술 투자 확대, 디지털 혁신 사업 강화",
        "projects": "KT 그룹사 IT 시스템 운영, 대외 SI/SM 사업 확장",
        "partners": "글로벌 클라우드/SW 기업들과 전략적 파트너십",
        "strategy": "AI/DX 전문기업으로 사업 확장, 클라우드 사업 강화"
    }
}

 

Gemini 가 추천해준 데이터를 목업형태의 DB로 구성을 해두고 활용할 예정입니다.

 

ef get_db_category(question: str) -> str:
    """질문을 분석하여 적절한 데이터베이스 카테고리 반환"""
    prompt = f"""
    다음 질문이 어떤 카테고리에 속하는지 판단해주세요:
    질문: {question}
    
    가능한 카테고리:
    - HR: 인사, 복리후생, 휴가, 급여, 교육 관련
    - Product: 클라우드, AI, DX 서비스, 보안, IT 플랫폼 관련
    - Finance: 매출, 투자, 프로젝트, 파트너십, 전략 관련
    
    다음 형식으로 답변: {{"category": "HR" 또는 "Product" 또는 "Finance"}}
    """
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "JSON 형식으로 응답해주세요."},
            {"role": "user", "content": prompt}
        ]
    )
    
    try:
        result = json.loads(response.choices[0].message.content)
        return result["category"]
    except json.JSONDecodeError:
        # 기본값으로 'HR' 반환
        return "HR"

def get_answer_from_db(question: str, category: str) -> str:
    """목업 데이터베이스에서 관련 정보 검색"""
    db_content = " ".join(MOCK_DB[category].values())
    
    prompt = f"""
    다음 KT DS 데이터베이스 내용을 바탕으로 질문에 답변해주세요:
    
    데이터베이스 내용: {db_content}
    질문: {question}
    
    IT 서비스 기업의 전문성이 느껴지도록 답변해주세요.
    기술 용어가 포함된 경우 간단한 설명을 덧붙여주세요.
    """
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "KT DS의 공식 답변을 작성합니다."},
            {"role": "user", "content": prompt}
        ]
    )
    
    return response.choices[0].message.content

def process_question(question: str) -> Dict:
    """질문 처리 전체 프로세스"""
    category = get_db_category(question)
    answer = get_answer_from_db(question, category)
    
    return {
        "question": question,
        "category": category,
        "answer": answer
    }

 

 

질문이 먼저 카테고리를 판단하고 그 후에, 카테고리 안에 있는 데이터를 LLM 이 보고 답변을 하는 방식이 됩니다. 그래서 이부분은 충분히 VectorDB 로 대체가 가능해보입니다.

 

아래의 코드를 툴로 사용해서 펑션콜에 툴에 적용해주고 그럼 LLM 은 유저의 질문에 관련된 vector DB 카테고리를 반환하게 됩니다.

 

# 1. Function 정의
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_in_database",  # 함수 이름
            "description": "search_answer_in",  # 함수 설명
            "parameters": {  # 함수 파라미터 정의
                "type": "object",
                "properties": {
                    "question": {
                        "type": "string",
                        "description": "The question asked by user",
                    },
                    "source": {
                        "type": "string",
                        "description": "Source to use to answer the question",
                        "enum": ["HR", "Product", "Finance"]  # 가능한 카테고리 제한
                    }
                },
                "required": ["source", "question"],  # 필수 파라미터
            }
        }
    }
]

# 2. OpenAI API 호출
chat_response = client.chat.completions.create(
    model="gpt-4o-mini",
    temperature=0.1,
    messages=chat_history,
    tools=tools,  # 정의한 함수 전달
    tool_choice="auto"  # GPT가 자동으로 함수 호출 결정
)

# 3. 함수 호출 결과 파싱
db_category = json.loads(chat_response.choices[0].message.tool_calls[0].function.arguments)['source']

 

question 에 대해서 3개의 답변을 출력시키고 이 걸 스트링을 활용해서, 이제 벡터디비를 연결해서 사용하면 되는 구조입니다.

 

def process_query(question: str, vector_dbs: Dict[str, VectorDB]) -> Dict:
    """전체 질의응답 프로세스"""
    # 1. Get category using function calling
    category = get_db_category(question)
    
    # 2. Search in vector DB
    relevant_docs = vector_dbs[category].search(question)
    
    # 3. Generate answer
    answer = get_answer(question, relevant_docs)
    
    return {
        "question": question,
        "category": category,
        "answer": answer,
        "sources": [
            {"content": doc.content, "metadata": doc.metadata}
            for doc in relevant_docs
        ]
    }

 

벡터디비 관련 정보를 스키마에 입력을 해주고 클래스 형태로 구현해두었습니다. 질문과 벡터디비의 정보를 인자값으로 받습니다. 

1. 펑션 콜링한 결과가 Category 를 출력을 합니다.  -> HR

2. 출력된 카테고리를 통하여 벡터디비 정보 인덱싱 값에 맞게 벨류값에 저장된 벡터디비로 접근 하고 질문을 유사도 검색을 진행해서 답변을 출력합니다.

3. 시각화를 위해 질문, 답변, 카테고리, 참고소스를 만듭니다.

 

테스트 결과

정말 쉽게 간단하게 펑션콜링으로 에이전트 형태의 챗봇을 구축하였습니다. 3개의 답변을 받는 시간도 11초 정도 걸렸고 충분히 쓸 수 있을것같다고 생각합니다. 여기서 tip 은 Advanced RAG의 팁들을 최대한 활용하는 것입니다. 결국 Agent 도 방대한 정보를 통해서 추론하는 것이기 때문에, 힌트 즉 주석 / 디스크립션을 최대한 많이 활용해서 힌트를 주는 개념으로 접근하면 모델이 서치도 잘하고, 답변도 잘 해주는 모습입니다.

 

아마 멀티에이전트를 구축하는 과정에서 멀티 벡터DB를 활용하지 않을까 조심스럽게 생각합니다. 다음 포스팅으로 돌아오겠습니다,

728x90