반응형

질문 기반 추출 접근 방식

이번 블로그에서는 Graph Retrieval-Augmented Generation(GraphRAG)을 사용해서 상업 계약 데이터를 수집하고 Q&A 에이전트를 구축하는 과정을 얼마나 간소화할 수 있는지 알아볼 거예요.

이 접근 방식은 기존의 Retrieval-Augmented Generation(RAG) 방식과는 조금 다른데요. 기존 RAG는 문서 전체를 무작위로 잘라서 벡터화하는 데 비해, 여기서는 데이터 추출 효율성을 훨씬 중요하게 생각하거든요.

기존 RAG에서는 모든 문서를 잘게 나누고 검색을 위해 벡터화하기 때문에, 불필요하게 많은 양의 데이터가 쪼개져서 벡터 index에 저장될 수 있어요. 하지만 저희는 특정 사용 사례, 즉 상업 계약 검토를 위해 계약에서 가장 관련성이 높은 정보만 뽑아내는 데 집중할 거예요. 이렇게 추출된 데이터는 핵심 엔터티와 관계로 이루어진 Knowledge Graph로 구성되고, Cypher 쿼리 및 벡터 검색을 통해 훨씬 정확한 그래프 데이터 검색이 가능해지죠.

벡터화되는 콘텐츠 양을 최소화하고 관련성 높은 지식 추출에 집중함으로써, Q&A 에이전트의 정확성과 성능을 높일 수 있어요. 복잡하고 특정 분야에 특화된 질문을 처리하는 데 아주 적합하답니다.

저희가 사용할 4단계 접근 방식은 Knowledge Graph(LLM + Neo4j) 생성을 위한 타겟 정보 추출(LLM + Prompt Engineering)과 간단한 그래프 데이터 검색 기능 세트(Cypher, Text2Cypher, 벡터 검색)를 포함해요. 마지막으로, 이러한 데이터 검색 기능을 활용하는 Q&A 에이전트는 Microsoft Semantic Kernel을 사용합니다.

아래 다이어그램에서 이 접근 방식을 한눈에 볼 수 있어요.

The four-stage GraphRAG approach

4단계 GraphRAG 접근 방식: 질문 기반 추출 > 지식 그래프 모델 > GraphRAG 검색 > Q&A 에이전트. Sebastian Nilsson @ Neo4j의 이미지는 저자의 허가를 받아 여기에 재현되었습니다.

본격적으로 시작하기 전에, 상법에 익숙하지 않은 분들을 위해 계약 검토라는 과제가 무엇인지 간단하게 소개할게요.

계약 검토 및 Large Language Model

상업 계약 검토는 법률 보조원이나 주니어 변호사가 계약서에서 중요한 정보를 꼼꼼하게 찾아내는, 시간과 노력이 많이 필요한 작업이에요.

“계약 검토는 계약서에 서명하는 개인이나 회사의 권리와 의무를 이해하고 그에 따른 영향을 평가하기 위해 계약서를 철저히 읽는 과정입니다.”
-Hendrycks, Burns 외, NeurIPS 2021, in
법적 계약 검토를 위한 전문가 주석이 달린 NLP 데이터세트인 CUAD

계약 검토의 첫 번째 단계는 수백 페이지에 달하는 계약서를 샅샅이 훑으면서 관련 조항이나 의무를 찾아내는 거예요. 계약 검토자는 관련 조항이 있는지, 있다면 어떤 내용인지, 그리고 해당 조항이 어디에 있는지 확인해야 하죠.

예를 들어 계약이 3년 계약인지 1년 계약인지 판단해야 해요. 계약 종료일도 알아내야 하고요. 또, 조항이 양도 금지 조항인지, 아니면 독점 조항인지도 판단해야 하죠…
-Hendrycks, Burns 외, NeurIPS 2021, in
법적 계약 검토를 위한 전문가 주석이 달린 NLP 데이터세트인 CUAD

꼼꼼함이 필수적인 작업이지만, 종종 비효율적인 부분이 많아서 Large Language Model에게 아주 적합한 분야라고 할 수 있어요.

첫 번째 단계를 마치면, 숙련된 법률 전문가가 계약서의 약점과 위험 요소를 검토하기 시작할 수 있어요. 이 부분이 바로 LLM 기반으로 작동하고 Knowledge Graph에 저장된 정보를 활용하는 Q&A 에이전트가 법률 전문가의 훌륭한 조력자가 될 수 있는 영역이죠.

LLM, 함수 호출 및 GraphRAG를 사용하여 상업 계약 검토 에이전트를 구축하는 4단계 접근 방식

이제 이 블로그의 나머지 부분에서는 위에서 언급한 프로세스의 각 단계를 자세히 설명할 거예요. 주요 아이디어를 더 명확하게 보여드리기 위해 코드 조각도 함께 사용할 거고요.

네 단계는 다음과 같아요:

  1. 계약서에서 관련 정보 추출 (LLM + Contract)
  2. 추출된 정보를 Knowledge Graph로 저장 (Neo4j)
  3. 간단한 Knowledge Graph 데이터 검색 기능 개발 (Python)
  4. 복잡한 질문을 처리하는 Q&A 에이전트 구축 (Semantic Kernel, LLM, Neo4j)

데이터 세트

The CUAD (계약 이해 Atticus 데이터 세트)는 510개의 법적 계약에 걸쳐 전문가가 라벨을 붙인 13,000개 이상의 조항으로 구성된 CC BY 4.0 라이선스 및 공개적으로 사용 가능한 데이터세트에요. 계약 검토를 위한 AI 모델을 구축하는 데 도움이 되도록 설계되었죠. 계약 분석에 중요한 기밀 유지, 해지, 면책 등 중요한 법적 조항을 광범위하게 다루고 있어요.

이 데이터 세트의 세 가지 계약을 사용해서 주요 법률 정보를 효과적으로 추출 및 분석하고, Knowledge Graph를 구축하고, 정확하고 복잡한 질문 답변을 위해 이를 활용하는 접근 방식을 보여줄 거예요.

3개의 계약서를 합치면 총 95페이지나 되네요!

1단계. 계약서에서 관련 정보 추출

LLM이 계약에서 정확한 정보를 추출하고 계약의 관련 정보를 나타내는 JSON 출력을 생성하도록 요청하는 건 비교적 간단해요.

상업 리뷰에서는 위에서 언급한 각 주요 요소 (당사자, 날짜, 조항)를 찾아 기계가 읽을 수 있는 JSON 파일에 깔끔하게 요약하는 Prompt 초안을 작성할 수 있어요.

추출 Prompt (단순화)

본 계약에만 있는 정보를 사용하여 다음 질문에 답하세요.
[계약서.pdf]

1) 이것은 어떤 유형의 계약입니까?
2) 당사자는 누구이며 그 역할은 무엇입니까? 그들은 어디에 통합되어 있습니까? 주 및 국가 이름 지정 (ISO 3166 국가 이름 사용)
3) 계약날짜는 언제인가요?
4) 발효일은 언제인가요?

다음 유형의 계약 조항 각각에 대해 두 가지 정보를 추출합니다.
a) 해당 조항이 본 계약서에 포함되어 있다고 생각하는지 여부를 나타내는 예/아니요
b) 이 조항 유형이 존재함을 나타내는 발췌 목록

계약 조항 유형: 경쟁 제한 예외, 비경쟁 조항, 독점, 고객 요청 금지, 직원 요청 금지, 비방 금지, 편의를 위한 종료, Rofr/Rofo/Rofn, 통제 변경, 할당 방지, 책임 제한 없음, 책임 한도

JSON 문서에 최종 답변을 제공하세요.

위 섹션에는 추출 Prompt의 단순화된 버전이 표시되어 있어요 (전체 버전은 에서 확인하세요!). Prompt의 마지막 부분에서는 JSON 문서의 원하는 형식을 지정하는데, 이는 일관된 JSON 스키마 출력을 보장하는 데 유용하답니다.

이 작업은 Python에서 비교적 간단해요. 아래 main() 함수는 OpenAI GPT-4o를 사용해서 관련 법률 정보 (extraction_prompt)를 추출하고 결과를 JSON 형식으로 저장해서 PDF 계약 파일 세트를 처리하도록 설계되었어요.

def main():
    pdf_files = [filename for filename in os.listdir('./data/input/') if filename.endswith('.pdf')]
    
    for pdf_filename in pdf_files:
        print('Processing ' + pdf_filename + '...')    
        # Extract content from PDF using the assistant
        complete_response = process_pdf('./data/input/' + pdf_filename)
        # Log the complete response to debug
        save_json_string_to_file(complete_response, './data/debug/complete_response_' + pdf_filename + '.json')

process_pdf” 기능은 OpenAI GPT-4o를 사용해서 추출 Prompt가 있는 계약에서 지식 추출을 수행해요.

 def process_pdf(pdf_filename):
    # Create OpenAI message thread
    thread = client.beta.threads.create()
    # Upload PDF file to the thread
    file = client.files.create(file=open(pdf_filename, "rb"), purpose="assistants")
    # Create message with contract as attachment and extraction_prompt
    client.beta.threads.messages.create(thread_id=thread.id,role="user",
        attachments=[
            Attachment(
                file_id=file.id, tools=[AttachmentToolFileSearch(type="file_search")])
        ],
        content=extraction_prompt,
    )
    # Run the message thread
    run = client.beta.threads.runs.create_and_poll(
        thread_id=thread.id, assistant_id=pdf_assistant.id, timeout=1000)
    # Retrieve messages
    messages_cursor = client.beta.threads.messages.list(thread_id=thread.id)
    messages = [message for message in messages_cursor]
    # Return last message in Thread 
    return messages[0].content[0].text.value

각 계약에 대해 "process_pdf"가 반환하는 메시지는 다음과 같아요.

{
    "agreement": {
        "agreement_name": "Marketing Affiliate Agreement",
        "agreement_type": "Marketing Affiliate Agreement",
        "effective_date": "May 8, 2014",
        "expiration_date": "December 31, 2014",
        "renewal_term": "1 year",
        "Notice_period_to_Terminate_Renewal": "30 days",
        "parties": [
            {
                "role": "Company",
                "name": "Birch First Global Investments Inc.",
                "incorporation_country": "United States Virgin Islands",
                "incorporation_state": "N/A"
            },
            {
                "role": "Marketing Affiliate",
                "name": "Mount Knowledge Holdings Inc.",
                "incorporation_country": "United States",
                "incorporation_state": "Nevada"
            }
        ],
        "governing_law": {
            "country": "United States",
            "state": "Nevada",
            "most_favored_country": "United States"
        },
        "clauses": [
            {
                "clause_type": "Competitive Restriction Exception",
                "exists": false,
                "excerpts": []
            },
            {
                "clause_type": "Exclusivity",
                "exists": true,
                "excerpts": [
                    "Company hereby grants to MA the right to advertise, market and sell to corporate users, government agencies and educational facilities for their own internal purposes only, not for remarketing or redistribution."
                ]
            },
            {
                "clause_type": "Non-Disparagement",
                "exists": true,
                "excerpts": [
                    "MA agrees to conduct business in a manner that reflects favorably at all times on the Technology sold and the good name, goodwill and reputation of Company."
                ]
            },
            {
                "clause_type": "Termination For Convenience",
                "exists": true,
                "excerpts": [
                    "This Agreement may be terminated by either party at the expiration of its term or any renewal term upon thirty (30) days written notice to the other party."
                ]
            },
            {
                "clause_type": "Anti-Assignment",
                "exists": true,
                "excerpts": [
                    "MA may not assign, sell, lease or otherwise transfer in whole or in part any of the rights granted pursuant to this Agreement without prior written approval of Company."
                ]
            },
            
            {
                "clause_type": "Price Restrictions",
                "exists": true,
                "excerpts": [
                    "Company reserves the right to change its prices and/or fees, from time to time, in its sole and absolute discretion."
                ]
            },
            {
                "clause_type": "Minimum Commitment",
                "exists": true,
                "excerpts": [
                    "MA commits to purchase a minimum of 100 Units in aggregate within the Territory within the first six months of term of this Agreement."
                ]
            },
            
            {
                "clause_type": "IP Ownership Assignment",
                "exists": true,
                "excerpts": [
                    "Title to the Technology and all copyrights in Technology shall remain with Company and/or its Affiliates."
                ]
            },
            
            {
                "clause_type": "License grant",
                "exists": true,
                "excerpts": [
                    "Company hereby grants to MA the right to advertise, market and sell the Technology listed in Schedule A of this Agreement."
                ]
            },
            {
                "clause_type": "Non-Transferable License",
                "exists": true,
                "excerpts": [
                    "MA acknowledges that MA and its Clients receive no title to the Technology contained on the Technology."
                ]
            },
            {
                "clause_type": "Cap On Liability",
                "exists": true,
                "excerpts": [
                    "In no event shall Company be liable to MA, its Clients, or any third party for any tort or contract damages or indirect, special, general, incidental or consequential damages."
                ]
            },
            
            {
                "clause_type": "Warranty Duration",
                "exists": true,
                "excerpts": [
                    "Company's sole and exclusive liability for the warranty provided shall be to correct the Technology to operate in substantial accordance with its then current specifications."
                ]
            }
            
            
        ]
    }
}

2단계. Knowledge Graph 만들기

이제 각 계약을 JSON 파일로 사용해서 Neo4j에 Knowledge Graph를 만들어볼 차례에요.

여기서 잠깐! 데이터 모델을 설계하는 데 시간을 투자하는 게 좋아요. 몇 가지 중요한 질문을 생각해봐야 하거든요.

  • 이 그래프의 Nodes와 Relationships는 뭘 나타낼까요?
  • 각 Node와 Relationship의 주요 속성은 뭘까요?
  • 속성에 Index를 만들어야 할까요?
  • Semantic Search를 활성화하려면 어떤 속성에 Vector Embedding이 필요할까요?

우리의 경우, 적합한 설계 (스키마)에는 계약 (Agreement), 해당 조항, 계약 당사자인 조직, 그리고 이들 간의 Relationships 같은 주요 엔터티가 포함될 거예요.

스키마를 그림으로 표현하면 이렇게 생겼어요.

A visual representation of the schema
Node properties:
Agreement {agreement_type: STRING, contract_id: INTEGER,
          effective_date: STRING, expiration_date: STRING,
          renewal_term: STRING, name: STRING}
ContractClause {name: STRING, type: STRING}
ClauseType {name: STRING}
Country {name: STRING}
Excerpt {text: STRING}
Organization {name: STRING}

Relationship properties:
IS_PARTY_TO {role: STRING}
GOVERNED_BY_LAW {state: STRING}
HAS_CLAUSE {type: STRING}
INCORPORATED_IN {state: STRING}

1단계에서 LLM이 식별한 짧은 텍스트 조각인 발췌문에만 텍스트 삽입이 필요하다는 점! 이 접근 방식은 각 계약을 나타내는 데 필요한 벡터 수와 벡터 Index의 크기를 확 줄여서 프로세스를 훨씬 효율적이고 확장 가능하게 만들어줘요.

위 스키마를 사용해서 각 JSON을 Knowledge Graph에 로드하는 Python 스크립트를 간단하게 만들어보면 이렇습니다.

NEO4J_URI=os.getenv('NEO4J_URI', 'bolt://localhost:7687')
NEO4J_USER=os.getenv('NEO4J_USERNAME', 'neo4j')
NEO4J_PASSWORD=os.getenv('NEO4J_PASSWORD')
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
JSON_CONTRACT_FOLDER = './data/output/'

driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))

contract_id = 1

json_contracts = [filename for filename in os.listdir(JSON_CONTRACT_FOLDER) if filename.endswith('.json')]
for json_contract in json_contracts:
  with open(JSON_CONTRACT_FOLDER + json_contract,'r') as file:
    json_string = file.read()
    json_data = json.loads(json_string)
    agreement = json_data['agreement']
    agreement['contract_id'] = contract_id
    driver.execute_query(CREATE_GRAPH_STATEMENT,  data=json_data)
    contract_id+=1

create_full_text_indices(driver)
driver.execute_query(CREATE_VECTOR_INDEX_STATEMENT)
print ("Generating Embeddings for Contract Excerpts...")
driver.execute_query(EMBEDDINGS_STATEMENT, token = OPENAI_API_KEY)

여기서 "CREATE_GRAPH_STATEMENT"가 유일하게 복잡한 부분이에요. 계약 JSON을 Knowledge Graph의 Nodes와 Relationships에 매핑하는 Cypher 문이거든요.

전체 Cypher 설명은 다음과 같아요:

CREATE_GRAPH_STATEMENT = """
WITH $data AS data
WITH data.agreement as a

MERGE (agreement:Agreement {contract_id: a.contract_id})
ON CREATE SET 
  agreement.contract_id  = a.contract_id,
  agreement.name = a.agreement_name,
  agreement.effective_date = a.effective_date,
  agreement.expiration_date = a.expiration_date,
  agreement.agreement_type = a.agreement_type,
  agreement.renewal_term = a.renewal_term,
  agreement.most_favored_country = a.governing_law.most_favored_country
  //agreement.Notice_period_to_Terminate_Renewal = a.Notice_period_to_Terminate_Renewal

MERGE (gl_country:Country {name: a.governing_law.country})
MERGE (agreement)-[gbl:GOVERNED_BY_LAW]->(gl_country)
SET gbl.state = a.governing_law.state


FOREACH (party IN a.parties |
  // todo proper global id for the party
  MERGE (p:Organization {name: party.name})
  MERGE (p)-[ipt:IS_PARTY_TO]->(agreement)
  SET ipt.role = party.role
  MERGE (country_of_incorporation:Country {name: party.incorporation_country})
  MERGE (p)-[incorporated:INCORPORATED_IN]->(country_of_incorporation)
  SET incorporated.state = party.incorporation_state
)

WITH a, agreement, [clause IN a.clauses WHERE clause.exists = true] AS valid_clauses
FOREACH (clause IN valid_clauses |
  CREATE (cl:ContractClause {type: clause.clause_type})
  MERGE (agreement)-[clt:HAS_CLAUSE]->(cl)
  SET clt.type = clause.clause_type
  // ON CREATE SET c.excerpts = clause.excerpts
  FOREACH (excerpt IN clause.excerpts |
    MERGE (cl)-[:HAS_EXCERPT]->(e:Excerpt {text: excerpt})
  )
  //link clauses to a Clause Type label
  MERGE (clType:ClauseType{name: clause.clause_type})
  MERGE (cl)-[:HAS_TYPE]->(clType)
)"""

다음은 성명서의 기능에 대한 분석이에요.

데이터 바인딩:

WITH $data AS data
WITH data.agreement as a
  • $data는 JSON 형식으로 쿼리에 전달되는 입력 데이터에요. 계약(agreement)에 관한 정보를 담고 있죠.
  • 두 번째 줄은 data.agreement를 별칭 a에 할당해서 후속 쿼리에서 계약 세부정보를 참조할 수 있게 해줘요.

계약 Node를 업데이트해요:

MERGE (agreement:Agreement {contract_id: a.contract_id})
ON CREATE SET 
  agreement.name = a.agreement_name,
  agreement.effective_date = a.effective_date,
  agreement.expiration_date = a.expiration_date,
  agreement.agreement_type = a.agreement_type,
  agreement.renewal_term = a.renewal_term,
  agreement.most_favored_country = a.governing_law.most_favored_country
  • MERGE는 기존의 Agreement Node를 지정된 contract_id로 찾으려고 시도해요. 해당 Node가 없으면 Node를 만들죠.
  • ON CREATE SET 절은 새로 생성된 Agreement Node에 contract_id, agreement_name, effective_date 및 JSON 입력의 기타 계약 관련 필드와 같은 다양한 속성을 설정하는 역할을 해요.

준거법 Relationship 생성:

 MERGE (gl_country:Country {name: a.governing_law.country})
MERGE (agreement)-[gbl:GOVERNED_BY_LAW]->(gl_country)
SET gbl.state = a.governing_law.state
  • 이렇게 하면 계약과 관련된 준거법 국가의 Country Node가 생성돼요.
  • 그런 다음 GOVERNED_BY_LAW Relationship을 AgreementCountry 사이에 생성하거나 병합하죠.
  • 또한 GOVERNED_BY_LAW Relationship의 state 속성도 설정해줘요.

당사자 및 법인 Relationship 생성:

 FOREACH (party IN a.parties |
  MERGE (p:Organization {name: party.name})
  MERGE (p)-[ipt:IS_PARTY_TO]->(agreement)
  SET ipt.role = party.role
  MERGE (country_of_incorporation:Country {name: party.incorporation_country})
  MERGE (p)-[incorporated:INCORPORATED_IN]->(country_of_incorporation)
  SET incorporated.state = party.incorporation_state
)

계약의 각 당사자(a.parties)에 대해 다음을 수행해요:

  • Organization 파티를 위한 Node를 upsert(병합)해요.
  • 생성합니다 IS_PARTY_TO 관계를 OrganizationAgreement 사이에 만들고, role을 당사자(예: 구매자, 판매자)로 설정해요.
  • Country 노드를 조직이 통합된 국가의 노드와 병합해요.
  • 생성합니다 INCORPORATED_IN 관계를 조직과 법인 설립 국가 간에 설정하고, 조직이 설립된 state를 지정해요.

계약 조항 및 발췌문 작성:

WITH a, agreement, [clause IN a.clauses WHERE clause.exists = true] AS valid_clauses
FOREACH (clause IN valid_clauses |
  CREATE (cl:ContractClause {type: clause.clause_type})
  MERGE (agreement)-[clt:HAS_CLAUSE]->(cl)
  SET clt.type = clause.clause_type
  FOREACH (excerpt IN clause.excerpts |
    MERGE (cl)-[:HAS_EXCERPT]->(e:Excerpt {text: excerpt})
  )
  MERGE (clType:ClauseType{name: clause.clause_type})
  MERGE (cl)-[:HAS_TYPE]->(clType)
)
  • 이 부분은 먼저 조항 목록 (a.clauses)을 필터링해서 clause.exists = true인 항목만 포함하도록 해요 (즉, 1단계에서 LLM이 식별한 발췌문이 있는 조항).

각 조항에 대해:

  • ContractClause 노드를 만들고 nametype을 조항 유형에 맞게 설정해요.
  • HAS_CLAUSE 관계를 AgreementContractClause 사이에 설정해요.
  • excerpt를 조항과 연결해서 Excerpt 노드를 만들고, ContractClauseHAS_EXCERPT 관계를 사용해 연결해요.
  • 마지막으로, ClauseType 노드를 조항 유형에 따라 생성(또는 병합)하고, ContractClauseHAS_TYPE 관계를 사용해 ClauseType에 연결해요.

가져오기 스크립트가 실행되면 Neo4j에서 단일 계약을 Knowledge Graph로 시각화할 수 있어요.

A knowledge graph representation of a single contract

단일 계약에 대한 Knowledge Graph 표현: 녹색은 당사자(조직), 파란색은 계약 조항, 연한 갈색은 발췌, 주황색은 국가입니다. 저자의 이미지.

Knowledge Graph의 세 가지 계약에는 작은 그래프(노드 100개 미만, 관계 200개 미만)만 필요했어요. 가장 중요한 것은 발췌문에 40~50개의 Vector Embedding만 필요하다는 것이죠. 이제 적은 수의 벡터가 포함된 이 Knowledge Graph를 사용해서 꽤 강력한 Q&A 에이전트를 만들 수 있어요.

3단계. GraphRAG용 데이터 검색 기능 개발

이제 계약이 Knowledge Graph로 구성되었으니, 다음 단계는 작은 그래프 데이터 검색 기능 세트를 만드는 거예요. 이러한 기능은 핵심 빌딩 블록 역할을 해서 4단계에서 Q&A 에이전트를 개발할 수 있게 해줘요.

몇 가지 기본 데이터 검색 기능을 정의해 볼까요?

  1. 계약에 대한 기본 세부 정보를 검색합니다 (계약 ID 제공).
  2. 특정 조직과 관련된 계약을 찾습니다 (일부 조직 이름 제공).
  3. 다음을 수행 특정 절 유형을 포함하는 계약을 찾아보세요.
  4. 계약에 특정 유형의 조항이 포함되어 있는지 확인하세요.
  5. 조항의 텍스트(발췌)와의 Semantic Search 유사성을 기반으로 계약을 찾습니다 (예: "금지 품목"의 사용을 언급하는 계약).
  6. 데이터베이스의 모든 계약에 대해 Natural Language 쿼리를 실행합니다. 예를 들어 "특정 조건을 충족하는 계약 수"를 계산하는 집계 쿼리를 실행합니다.

4단계에서는 Microsoft Semantic Kernel 라이브러리를 사용해서 Q&A를 작성할 거예요. 이 라이브러리는 에이전트 구축 프로세스를 단순화해줘요. 이를 통해 개발자는 에이전트가 질문에 답하기 위해 사용할 수 있는 기능과 도구를 정의할 수 있죠.

Neo4j와 Semantic Kernel 라이브러리 간의 통합을 단순화하기 위해 ContractPlugin을 만들었어요. 이는 각 데이터 검색 기능의 "서명"을 정의해요. @kernel_function 데코레이터와 각 기능에 제공되는 유형 정보 및 설명도 포함되어 있다는 점을 참고하세요.

Semantic Kernel은 "플러그인" 클래스 개념을 사용해서 에이전트가 사용할 수 있는 기능 그룹을 캡슐화해요. 데코레이팅된 함수, 타입 정보, 문서를 사용해서 사용 가능한 함수에 대한 LLM 함수 호출 기능을 알려주는 거죠.

from typing import List, Optional, Annotated
from AgreementSchema import Agreement, ClauseType
from semantic_kernel.functions import kernel_function
from ContractService import  ContractSearchService

class ContractPlugin:
    def __init__(self, contract_search_service: ContractSearchService ):
        self.contract_search_service = contract_search_service
    
    @kernel_function
    async def get_contract(self, contract_id: int) -> Annotated[Agreement, "A contract"]:
        """Gets details about a contract with the given id."""
        return await self.contract_search_service.get_contract(contract_id)

    @kernel_function
    async def get_contracts(self, organization_name: str) -> Annotated[List[Agreement], "A list of contracts"]:
        """Gets basic details about all contracts where one of the parties has a name similar to the given organization name."""
        return await self.contract_search_service.get_contracts(organization_name)
    
    @kernel_function
    async def get_contracts_without_clause(self, clause_type: ClauseType) -> Annotated[List[Agreement], "A list of contracts"]:
        """Gets basic details from contracts without a clause of the given type."""
        return await self.contract_search_service.get_contracts_without_clause(clause_type=clause_type)
    
    @kernel_function
    async def get_contracts_with_clause_type(self, clause_type: ClauseType) -> Annotated[List[Agreement], "A list of contracts"]:
        """Gets basic details from contracts with a clause of the given type."""
        return await self.contract_search_service.get_contracts_with_clause_type(clause_type=clause_type)

    @kernel_function
    async def get_contracts_similar_text(self, clause_text: str) -> Annotated[List[Agreement], "A list of contracts with similar text in one of their clauses"]:
        """Gets basic details from contracts having semantically similar text in one of their clauses to the to the 'clause_text' provided."""
        return await self.contract_search_service.get_contracts_similar_text(clause_text=clause_text)
    
    @kernel_function
    async def answer_aggregation_question(self, user_question: str) -> Annotated[str, "An answer to user_question"]:
        """Answer obtained by turning user_question into a CYPHER query"""
        return await self.contract_search_service.answer_aggregation_question(user_question=user_question)

한번 ContractService를 살펴보시는 걸 추천드려요. 여기에는 위에 나온 각 함수 구현을 포함하는 클래스가 들어있는데, 각 기능은 서로 다른 데이터 검색 기술을 사용하고 있어요.

다양한 GraphRAG 데이터 검색 기술과 패턴을 보여주는 기능들의 구현을 좀 더 자세히 살펴볼까요?

계약 가져오기 (계약 ID에서) — Cypher 기반 검색 기능

get_contract(self, contract_id: int)는 특정 계약 (Agreement)에 대한 세부 정보를 검색하도록 설계된 비동기식 메서드에요. Cypher 쿼리를 사용해서 Neo4j 데이터베이스에서 정보를 가져오죠. 이 함수는 계약, 조항, 당사자 및 이들의 관계에 대한 정보로 채워진 Agreement 객체를 반환해요.

이 함수의 구현은 다음과 같아요:

async def get_contract(self, contract_id: int) -> Agreement:
        
        GET_CONTRACT_BY_ID_QUERY = """
            MATCH (a:Agreement {contract_id: $contract_id})-[:HAS_CLAUSE]->(clause:ContractClause)
            WITH a, collect(clause) as clauses
            MATCH (country:Country)-[i:INCORPORATED_IN]-(p:Organization)-[r:IS_PARTY_TO]-(a)
            WITH a, clauses, collect(p) as parties, collect(country) as countries, collect(r) as roles, collect(i) as states
            RETURN a as agreement, clauses, parties, countries, roles, states
        """
        
        agreement_node = {}
        
        records, _, _  = self._driver.execute_query(GET_CONTRACT_BY_ID_QUERY,{'contract_id':contract_id})

        if (len(records)==1):
            agreement_node =    records[0].get('agreement')
            party_list =        records[0].get('parties')
            role_list =         records[0].get('roles')
            country_list =      records[0].get('countries')
            state_list =        records[0].get('states')
            clause_list =       records[0].get('clauses')
        
        return await self._get_agreement(
            agreement_node, format="long",
            party_list=party_list, role_list=role_list,
            country_list=country_list,state_list=state_list,
            clause_list=clause_list
        )

가장 중요한 부분은 바로 이 Cypher 쿼리, GET_CONTRACT_BY_ID_QUERY이에요. 이 쿼리는 입력 파라미터로 주어진 `contract_id`를 사용해서 실행되죠. 결과는 매칭되는 계약, 그 계약의 조항들, 그리고 관련된 당사자들인데, 각 당사자는 역할과 법인 설립 국가/주를 가지고 있어요.

그 다음, 데이터는 _get_agreement로 전달되는데, 이건 데이터를 "계약"에 매핑하는 간단한 유틸리티 함수에요. 계약은 다음과 같이 정의된 TypedDict랍니다:

class Agreement(TypedDict):  
    contract_id: int
    agreement_name: str
    agreement_type: str
    effective_date: str
    expiration_date: str
    renewal_term: str
    notice_period_to_terminate_Renewal: str
    parties: List[Party]
    clauses: List[ContractClause]

조항 유형 없이 계약 가져오기 - 또 다른 Cypher 검색 기능

이 함수는 관계가 없는지 테스트하는 Knowledge Graph의 강력한 기능을 보여주는 예시입니다.

get_contracts_without_clause() 함수는 Neo4j 데이터베이스에서 특정 유형의 조항을 *가지고 있지 않은* 모든 `Agreements`를 검색해요. 이 함수는 입력으로 `ClauseType`을 받아서 조건에 맞는 `Agreement` 객체들의 리스트를 반환하죠.

이런 유형의 데이터 검색은 Vector Search로는 쉽게 구현하기 어려워요. 전체 구현은 다음과 같습니다:

async def get_contracts_without_clause(self, clause_type: ClauseType) -> List[Agreement]:
        GET_CONTRACT_WITHOUT_CLAUSE_TYPE_QUERY = """
            MATCH (a:Agreement)
            OPTIONAL MATCH (a)-[:HAS_CLAUSE]->(cc:ContractClause {type: $clause_type})
            WITH a,cc
            WHERE cc is NULL
            WITH a
            MATCH (country:Country)-[i:INCORPORATED_IN]-(p:Organization)-[r:IS_PARTY_TO]-(a)
            RETURN a as agreement, collect(p) as parties, collect(r) as roles, collect(country) as countries, collect(i) as states
        """
       
        #run the Cypher query
        records, _ , _ = self._driver.execute_query(GET_CONTRACT_WITHOUT_CLAUSE_TYPE_QUERY,{'clause_type':clause_type.value})

        all_agreements = []
        for row in records:
            agreement_node =  row['agreement']
            party_list =  row['parties']
            role_list =  row['roles']
            country_list = row['countries']
            state_list = row['states']
            agreement : Agreement = await self._get_agreement(
                format="short",
                agreement_node=agreement_node,
                party_list=party_list,
                role_list=role_list,
                country_list=country_list,
                state_list=state_list
            )
            all_agreements.append(agreement)
        return all_agreements

다시 말씀드리지만, 형식은 이전 기능과 유사해요. Cypher 쿼리 GET_CONTRACTS_WITHOUT_CLAUSE_TYPE_QUERY는 매칭할 Node와 Relationship 패턴을 정의하죠. Clause Type을 포함하는 계약을 필터링하기 위해 선택적 매칭을 수행하고, 관련 당사자 및 해당 세부 정보와 같은 계약에 대한 관련 데이터를 수집해요.

그런 다음 함수는 다음 목록을 구성하고 반환합니다. Agreement는 각 매칭된 계약에 대한 모든 관련 정보를 캡슐화하는 객체에요.

의미상 유사한 텍스트로 계약 체결 — Vector Search + Graph Data 검색 기능

get_contracts_similar_text() 기능은 제공된 clause_text와 유사한 텍스트가 포함된 Clause를 포함하는 계약을 찾도록 설계되었어요. Semantic Vector Search를 사용하여 관련 발췌문을 식별하고 Graph를 탐색하여 해당 계약 및 Clause에 대한 정보와 해당 발췌문의 출처에 대한 정보를 반환하죠.

async def get_contracts_similar_text(self, clause_text: str) -> List[Agreement]:

        #Cypher to traverse from the semantically similar excerpts back to the agreement
        EXCERPT_TO_AGREEMENT_TRAVERSAL_QUERY="""
            MATCH (a:Agreement)-[:HAS_CLAUSE]->(cc:ContractClause)-[:HAS_EXCERPT]-(node) 
            RETURN a.name as agreement_name, a.contract_id as contract_id, cc.type as clause_type, node.text as excerpt
        """
        
        #Set up vector Cypher retriever
        retriever = VectorCypherRetriever(
            driver= self._driver,  
            index_name="excerpt_embedding",
            embedder=self._openai_embedder, 
            retrieval_query=EXCERPT_TO_AGREEMENT_TRAVERSAL_QUERY,
            result_formatter=my_vector_search_excerpt_record_formatter
        )
        
        # run vector search query on excerpts and get results containing the relevant agreement and clause 
        retriever_result = retriever.search(query_text=clause_text, top_k=3)

        #set up List of Agreements (with partial data) to be returned
        agreements = []
        for item in retriever_result.items:
            //extract information from returned items and append agreement to results
            // full code not shown here but available on the Github repo
            

        return agreements

이 Data 검색 기능의 주요 구성 요소를 한번 살펴볼까요?

  • Neo4j GraphRAG VectorCypherRetriever는 개발자가 Vector Index에 대해 Semantic 유사성을 수행할 수 있도록 도와줘요. 우리의 경우, 의미상 유사한 각 발췌 "Node"가 발견되면 추가 Cypher 표현식을 사용하여 해당 Node와 관련된 Graph에서 추가 Node를 가져오게 되죠.
  • VectorCypherRetriever의 매개변수는 꽤 간단해요. index_name은 Semantic 유사성을 실행할 Vector Index이고, Embedder는 텍스트 조각에 대한 Vector Embedding을 생성해요. driver는 Neo4j Python Driver의 인스턴스일 뿐이고요. retrieval_query는 Semantic 유사성으로 식별되는 모든 "발췌" Node와 연결된 추가 Node 및 Relationship을 지정합니다.
  • EXCERPT_TO_AGREEMENT_TRAVERSAL_QUERY는 검색할 추가 Node를 지정해요. 이 경우 모든 발췌문에 대해 관련 계약 Clause 및 해당 계약을 검색하죠.
 EXCERPT_TO_AGREEMENT_TRAVERSAL_QUERY="""
  MATCH (a:Agreement)-[:HAS_CLAUSE]->(cc:ContractClause)-[:HAS_EXCERPT]-(node) 
  RETURN a.name as agreement_name, a.contract_id as contract_id, cc.type as clause_type, node.text as excerpt
"""

자연어 쿼리 실행 — Text2Cypher Data 검색 기능

answer_aggregation_question() 기능은 Neo4j GraphRAG 패키지 Text2CypherRetriever를 활용하여 자연어로 질문에 답해요. Text2CypherRetriever는 LLM을 사용하여 사용자 질문을 Cypher 쿼리로 변환하고 이를 Neo4j Database에 대해 실행하죠.

이 기능은 GPT-4o를 활용하여 필요한 Cypher 쿼리를 생성해요. 이 Data 검색 기능의 주요 구성 요소를 한번 살펴볼까요?

 async def answer_aggregation_question(self, user_question) -> str:
        answer = ""


        NEO4J_SCHEMA = """
            omitted for brevity (see below for the full value)
        """

        # Initialize the retriever
        retriever = Text2CypherRetriever(
            driver=self._driver,
            llm=self._llm,
            neo4j_schema=NEO4J_SCHEMA
        )

        # Generate a Cypher query using the LLM, send it to the Neo4j database, and return the results
        retriever_result = retriever.search(query_text=user_question)

        for item in retriever_result.items:
            content = str(item.content)
            if content:
                answer += content + 'nn'

        return answer

이 기능은 Neo4j GraphRAG 패키지의 Text2CypherRetriever를 활용해요. LLM을 사용하는데, 이 경우 OpenAI LLM은 사용자 질문(자연어)을 데이터베이스에 대해 실행되는 Cypher 쿼리로 바꾸는 데 사용돼요. 이 쿼리의 결과가 반환되는 거죠.

LLM이 데이터베이스에 정의된 Node, Relationship, 그리고 Property를 사용하는 쿼리를 생성하도록 하는 핵심 요소는 LLM에 Schema에 대한 텍스트 설명을 제공하는 것이에요.

우리의 경우에는 다음과 같은 데이터 모델 표현으로 충분하답니다.

 NEO4J_SCHEMA = """
Node properties:
Agreement {agreement_type: STRING, contract_id: INTEGER,effective_date: STRING,renewal_term: STRING, name: STRING}
ContractClause {name: STRING, type: STRING}
ClauseType {name: STRING}
Country {name: STRING}
Excerpt {text: STRING}
Organization {name: STRING}

Relationship properties:
IS_PARTY_TO {role: STRING}
GOVERNED_BY_LAW {state: STRING}
HAS_CLAUSE {type: STRING}
INCORPORATED_IN {state: STRING}

The relationships:
(:Agreement)-[:HAS_CLAUSE]->(:ContractClause)
(:ContractClause)-[:HAS_EXCERPT]->(:Excerpt)
(:ContractClause)-[:HAS_TYPE]->(:ClauseType)
(:Agreement)-[:GOVERNED_BY_LAW]->(:Country)
(:Organization)-[:IS_PARTY_TO]->(:Agreement)
(:Organization)-[:INCORPORATED_IN]->(:Country)
  """

4단계. Q&A 에이전트 구축

Knowledge Graph 데이터 검색 기능으로 무장했으니, GraphRAG 기반 에이전트를 구축할 준비가 되었어요.

GPT-4o, 데이터 검색 기능, 그리고 Neo4j 기반 Knowledge Graph의 조합을 사용해서 계약에 대한 사용자 Query에 응답할 수 있는 챗봇 에이전트를 설정해 볼 거예요.

Microsoft Semantic Kernel이라는 프레임워크를 사용할 건데요, 개발자가 LLM 함수 호출을 기존 API 및 데이터 검색 기능과 통합할 수 있게 도와주죠.

이 프레임워크는 "플러그인"이라는 개념을 사용해서 커널이 수행할 수 있는 특정 기능을 나타내요. 우리의 경우 "ContractPlugin"에 정의된 모든 데이터 검색 기능은 LLM에서 질문에 답하는 데 사용될 수 있답니다.

프레임워크는 "메모리" 개념을 사용해서 사용자와 에이전트 간의 모든 상호 작용은 물론, 실행된 기능과 검색된 데이터를 유지해요.

몇 줄의 코드만으로 아주 간단한 터미널 기반 에이전트를 구현할 수 있어요. 아래 스니펫은 에이전트의 주요 부분을 보여줍니다 (import 및 환경 변수는 제거했어요).

logging.basicConfig(level=logging.INFO)

# Initialize the kernel
kernel = Kernel()

# Add the Contract Search plugin to the kernel
contract_search_neo4j = ContractSearchService(NEO4J_URI,NEO4J_USER,NEO4J_PASSWORD)
kernel.add_plugin(ContractPlugin(contract_search_service=contract_search_neo4j),plugin_name="contract_search")

# Add the OpenAI chat completion service to the Kernel
kernel.add_service(OpenAIChatCompletion(ai_model_id="gpt-4o",api_key=OPENAI_KEY, service_id=service_id))

# Enable automatic function calling
settings: OpenAIChatPromptExecutionSettings = kernel.get_prompt_execution_settings_from_service_id(service_id=service_id)
settings.function_choice_behavior = FunctionChoiceBehavior.Auto(filters={"included_plugins": ["contract_search"]})

# Create a history of the conversation
history = ChatHistory()

async def basic_agent() :
    userInput = None
    while True:
        # Collect user input
        userInput = input("User > ")

        # Terminate the loop if the user says "exit"
        if userInput == "exit":
            break

        # Add user input to the history
        history.add_user_message(userInput)

        # 3. Get the response from the AI with automatic function calling
        chat_completion : OpenAIChatCompletion = kernel.get_service(type=ChatCompletionClientBase)
        result = (await chat_completion.get_chat_message_contents(
            chat_history=history,
            settings=settings,
            kernel=kernel,
            arguments=KernelArguments(),
        ))[0]

        # Print the results
        print("Assistant > " + str(result))

        # Add the message from the agent to the chat history
        history.add_message(result)

if __name__ == "__main__":
    asyncio.run(basic_agent())

에이전트에게 몇 가지 중요한 질문을 던져볼게요.

1. 가격 제한은 있지만 보험 조항은 없는 계약을 찾아줘.

Assistant > I found the following contract that contains a Price Restrictions clause but does not contain an Insurance clause:

- **Name:** Birch First Global Investments Inc. and Mount Knowledge Holdings Inc. MARKETING AFFILIATE AGREEMENT
- **Agreement Type:** MARKETING AFFILIATE AGREEMENT
- **Parties:**
  - **MOUNT KNOWLEDGE HOLDINGS INC.** (Marketing Affiliate), incorporated in Nevada, USA
  - **BIRCH FIRST GLOBAL INVESTMENTS INC.** (Company), incorporated in the Virgin Islands (VIR)

Would you like to know more details about this contract?

여기서 Semantic Agent 플래너는 커널에서 사용 가능한 기능을 살펴보고, 이 질문에 답하려면 자체적으로 완전한 답을 제공하지 않는 두 가지 기능을 호출해야 한다고 판단했어요.

먼저 "[가격 제한] clause가 있는 계약 받기" 기능을 실행한 다음, "[보험] clause 없이 계약 받기" 기능을 실행하는 거죠. 그런 다음 플래너는 두 기능의 결과를 합쳐서 교차점을 찾고, 결국 정답을 찾아낸답니다.

디버깅 정보를 보면 LLM이 수행한 호출 순서와 호출에 사용된 토큰 수를 확인할 수 있어요. 두 가지 데이터 검색 함수를 사용한 호출을 확인해 보세요.

Debugging information

2. 네, 부탁해요. 자세한 내용을 알려주세요.

Assistant > Here are the detailed information about the contract:

### Contract Details:
- **Name:** Birch First Global Investments Inc. and Mount Knowledge Holdings Inc. MARKETING AFFILIATE AGREEMENT
- **Agreement Type:** MARKETING AFFILIATE AGREEMENT
- **Expiration Date:** December 31, 2014
- **Renewal Term:** One (1) year

### Parties Involved:
1. **MOUNT KNOWLEDGE HOLDINGS INC.** 
   - **Role:** Marketing Affiliate
   - **Incorporation Country:** USA
   - **Incorporation State:** Nevada
2. **BIRCH FIRST GLOBAL INVESTMENTS INC.**
   - **Role:** Company
   - **Incorporation Country:** Virgin Islands (VIR)

### Clauses Included:
- No-Solicit Of Employees
- Third Party Beneficiary
- Price Restrictions
- Affiliate License-Licensee
- Affiliate License-Licensor
- Cap On Liability
- Audit Rights
- Warranty Duration
- Post-Termination Services
- Non-Transferable License
- License grant
- IP Ownership Assignment
- Minimum Commitment
- Anti-Assignment
- Non-Disparagement

### Clauses Not Included:
- Insurance

Is there anything specific you would like to know or any particular clause details you are interested in?

이 경우, LLM은 뒤에서 'ID로 계약 찾기'를 사용했어요. 사용자에게 표시되는 출력에는 계약 ID가 포함되지 않은 것을 알 수 있죠. 하지만 Semantic Kernel은 이전 호출의 함수에서 반환된 모든 정보를 대화 메모리에 저장했다는 점!

에이전트가 호출할 함수, 메모리 사용, 그리고 사용자 쿼리를 올바른 매개변수를 사용하여 함수 호출로 자동 마샬링하도록 하는 이러한 강력한 조합은 Semantic Kernel로 구축된 에이전트 뒤에 숨겨진 마법을 설명해줘요.

3. 때때로 가격을 변경할 수 있는 능력을 언급하는 계약이 있나요?

Assistant > Yes, there is a contract that mentions 
the ability to make price changes from time to time. 
Here is the relevant excerpt:

1. **Birch First Global Investments Inc. and Mount Knowledge Holdings Inc. MARKETING AFFILIATE AGREEMENT**
   - **Contract ID:** 1
   - **Clause Type:** Price Restrictions
   - **Excerpt:**
     ```
     Company reserves the right to change its process and/or fees, from time to time, in its sole and absolute discretion. In the event of a price and/or fee change for Technology, Company shall notify MA in writing ninety (90) days prior to the effective date of any such change. All other prices and/or fees may be adjusted without prior notice to MA.
     ```

Would you like more information about this contract or any other details?

이 질문에 답하기 위해 Semantic Kernel은 get_contract_similar_text 데이터 검색 기능을 사용했어요.

User > any contract mentions the ability to make price changes from time to time
INFO:semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base:processing 1 tool calls in parallel.

INFO:semantic_kernel.kernel:Calling contract_search-get_contracts_similar_text
 function with args: {"clause_text":"price changes from time to time"}


Assistant > Yes, there is a contract that mentions the ability to make price changes from time to time. Here is the relevant excerpt:

1. **Birch First Global Investments Inc. and Mount Knowledge Holdings Inc. MARKETING AFFILIATE AGREEMENT**
   - **Contract ID:** 1
   - **Clause Type:** Price Restrictions
   - **Excerpt:**
     ```
     Company reserves the right to change its process and/or fees, from time to time, in its sole and absolute discretion. In the event of a price and/or fee change for Technology, Company shall notify MA in writing ninety (90) days prior to the effective date of any such change. All other prices and/or fees may be adjusted without prior notice to MA.
     ```

앞서 설명했듯이 이 기능은 Vector Search와 Cypher 쿼리를 사용하여 의미상 유사한 발췌문뿐만 아니라 이를 포함하는 관련 계약서 및 조항도 검색해요.

마지막으로 데이터베이스에 몇 개의 계약이 있는지 물어볼게요.

4. 데이터베이스에는 몇 개의 계약이 있나요?

이 질문에서는 LLM이 Text2Cypher 데이터 검색기를 사용해야 해요.

Assistant > There are a total of 3 contracts in the database.

디버그 정보에서 Semantic Kernel이 "user_question" = "데이터베이스에 몇 개의 계약이 있습니까?"로 Text2Cypher 데이터 검색 기능을 실행한 것을 확인할 수 있어요.

User >  how many contracts are there on the database?
INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
INFO:semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base:processing 1 tool calls in parallel.

INFO:semantic_kernel.kernel:Calling contract_search-answer_aggregation_question function 
with args: {"user_question":"How many contracts are there in the database?"}


INFO:semantic_kernel.functions.kernel_function:Function completed. Duration: 0.588805s

INFO:semantic_kernel.connectors.ai.open_ai.services.open_ai_handler:OpenAI usage: CompletionUsage(completion_tokens=13, prompt_tokens=3328, total_tokens=3341, completion_tokens_details={'reasoning_tokens': 0})

Assistant > There are a total of 3 contracts in the database.

직접 시도해 보세요

GitHub 저장소에는 더 멋진 에이전트 UI를 제공하는 Streamlit 앱이 포함되어 있어요. 에이전트가 더 많은 질문을 처리할 수 있도록 에이전트와 상호 작용하고 ContractPlugin을 변경해 보는 걸 추천해요.

결론

이번 블로그에서는 노동 집약적인 상업 계약 검토 작업을 더 효율적인 AI 기반 프로세스로 전환하기 위한 GraphRAG 접근 방식을 살펴봤어요.

LLM과 프롬프트를 활용한 타겟 정보 추출에 집중하고, Neo4j로 구조화된 Knowledge Graph를 구축하고, 간단한 데이터 검색 기능을 구현하고, 궁극적으로 Q&A 에이전트를 개발함으로써 복잡한 질문을 효과적으로 처리하는 지능형 솔루션을 만들 수 있었죠.

이 접근 방식은 기존 벡터 검색 기반 RAG에서 발견되는 비효율성을 최소화하고, 대신 관련 정보만 추출하는 데 중점을 둬서 불필요한 Vector Embedding의 필요성을 줄이고 전체 프로세스를 단순화해요. 계약 수집에서 대화형 Q&A 에이전트까지의 여정을 통해 여러분의 프로젝트에서 GraphRAG를 활용하여 효율성을 높이고 더 스마트한 AI 기반 의사 결정을 내릴 수 있기를 바라요.

지금 바로 상업 계약 검토 에이전트 구축을 시작하고 GraphRAG의 강력한 기능을 직접 경험해보세요!

  • GraphRAG
  • RAG

에이치시스템즈LogTree는 Neo4j 기반 GraphRAG 플랫폼으로, 데이터를 자동으로 지식그래프화하고 자연어 질의로 즉시 답을 제공합니다.

👉 에이치시스템즈 홈페이지

반응형

+ Recent posts