商业合同审查是一项劳动密集型的工作,通常需要律师助理和初级律师仔细识别合同中的关键信息。
“合同审查是指通过仔细阅读合同,了解签署方的权利和义务,并评估其相关影响的过程。”——Hendrycks, Burns 等,《NeurIPS 2021》,在《CUAD:用于法律合同审查的专家注释 NLP 数据集》中。
合同审查的第一阶段涉及审阅数百页的合同,以找到相关条款或义务。审查人员需要确定相关条款是否存在,如果存在,它们的内容是什么,并记录这些条款的位置。
例如,他们需要判断合同是三年期还是一年期;确定合同的终止日期;判断某条款是否属于反转让条款或排他性条款等。”——Hendrycks, Burns 等,《NeurIPS 2021》,在《CUAD:用于法律合同审查的专家注释 NLP 数据集》中。
这是一项需要极大细致程度的任务,但往往效率低下,同时非常适合大语言模型的应用。
完成第一阶段后,高级法律从业者可以开始审查合同中的弱点和风险。这正是一个由大语言模型驱动、以知识图谱中的信息为基础的问答智能体可以成为法律专家得力助手的地方。
接下来的内容将描述这一过程的每一步。过程中,我将通过代码片段来说明主要思想。
四个步骤如下:
1.从合同中提取相关信息(LLM + 合同)
2.将提取的信息存储到知识图谱中(Neo4j)
3.开发简单的知识图谱数据检索功能(Python)
4.构建一个能够处理复杂问题的问答智能体(Semantic Kernel、LLM、Neo4j)
CUAD(Contract Understanding Atticus Dataset)是一个依据 CC BY 4.0 许可发布的公开数据集,包含 510 份法律合同中超过 13,000 条专家标注的条款,用于帮助构建用于合同审查的 AI 模型。它涵盖了许多重要的法律条款,如保密性、终止条款和赔偿条款,这些条款对于合同分析至关重要。
我们将使用该数据集中的三份合同,展示如何有效提取和分析关键法律信息,构建知识图谱并利用其进行精确的复杂问题回答。
这三份合同的总页数为 95 页。
通过提示词让 LLM 从合同中提取精确信息并生成 JSON 输出,表示合同中的相关信息,这是一个相对简单的任务。
在商业审查中,可以设计提示词定位上述每个关键要素——当事方、日期、条款——并将它们整齐地总结为机器可读的 JSON 文件。
提取提示词(简化版)
仅使用此合同[Contract.pdf]中的信息回答以下问题:
1)这是什么类型的合同?2)合同的当事方及其角色是什么?它们在哪注册成立?请列出州和国家(使用 ISO 3166 国家名称)。3)协议日期是什么?4)生效日期是什么?
对于以下每种合同条款类型,提取两条信息:a)一个是/否,指示您是否认为该条款存在于合同中;b)一个列出指示该条款类型存在的摘录的列表。
合同条款类型:竞争限制例外、竞业禁止条款、排他性条款、禁止招揽客户、禁止招揽员工、不得诋毁条款、方便终止条款、优先购买权条款、控制权变更条款、反转让条款、不设上限的责任条款、责任上限条款。
请将最终答案以 JSON 文档的形式提供。
请注意,上述部分展示的是提取提示词的简化版本。完整版本可以在此处查看。提示词的最后部分指定了 JSON 文档的格式,这对于确保一致的 JSON 模式输出非常有用。
在 Python 中,这项任务相对简单。以下main()函数旨在通过提取提示词(extraction_prompt),利用 OpenAI GPT-4o 从一组 PDF 合同文件中提取相关法律信息并将结果保存为 JSON 格式:
defmain():
pdf_files=[filenameforfilenameinos.listdir('./data/input/')iffilename.endswith('.pdf')]
forpdf_filenameinpdf_files:
print('Processing'+pdf_filename+'...')
#ExtractcontentfromPDFusingtheassistant
complete_response=process_pdf('./data/input/'+pdf_filename)
#Logthecompleteresponsetodebug
save_json_string_to_file(complete_response,'./data/debug/complete_response_'+pdf_filename+'.json')
“_process_pdf_”函数使用 OpenAI GPT-4o,通过提取提示词从合同中提取知识:
defprocess_pdf(pdf_filename):
#CreateOpenAImessagethread
thread=client.beta.threads.create()
#UploadPDFfiletothethread
file=client.files.create(file=open(pdf_filename,"rb"),purpose="assistants")
#Createmessagewithcontractasattachmentandextraction_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,
)
#Runthemessagethread
run=client.beta.threads.runs.create_and_poll(
thread_id=thread.id,assistant_id=pdf_assistant.id,timeout=1000)
#Retrievemessages
messages_cursor=client.beta.threads.messages.list(thread_id=thread.id)
messages=[messageformessageinmessages_cursor]
#ReturnlastmessageinThread
returnmessages[0].content[0].text.value对于每份合同,“process_pdf”返回的消息如下所示:
{
"agreement":{
"agreement_name":"MarketingAffiliateAgreement",
"agreement_type":"MarketingAffiliateAgreement",
"effective_date":"May8,2014",
"expiration_date":"December31,2014",
"renewal_term":"1year",
"Notice_period_to_Terminate_Renewal":"30days",
"parties":[
{
"role":"Company",
"name":"BirchFirstGlobalInvestmentsInc.",
"incorporation_country":"UnitedStatesVirginIslands",
"incorporation_state":"N/A"
},
{
"role":"MarketingAffiliate",
"name":"MountKnowledgeHoldingsInc.",
"incorporation_country":"UnitedStates",
"incorporation_state":"Nevada"
}
],
"governing_law":{
"country":"UnitedStates",
"state":"Nevada",
"most_favored_country":"UnitedStates"
},
"clauses":[
{
"clause_type":"CompetitiveRestrictionException",
"exists":false,
"excerpts":[]
},
{
"clause_type":"Exclusivity",
"exists":true,
"excerpts":[
"CompanyherebygrantstoMAtherighttoadvertise,marketandselltocorporateusers,governmentagenciesandeducationalfacilitiesfortheirowninternalpurposesonly,notforremarketingorredistribution."
]
},
{
"clause_type":"Non-Disparagement",
"exists":true,
"excerpts":[
"MAagreestoconductbusinessinamannerthatreflectsfavorablyatalltimesontheTechnologysoldandthegoodname,goodwillandreputationofCompany."
]
},
{
"clause_type":"TerminationForConvenience",
"exists":true,
"excerpts":[
"ThisAgreementmaybeterminatedbyeitherpartyattheexpirationofitstermoranyrenewaltermuponthirty(30)dayswrittennoticetotheotherparty."
]
},
{
"clause_type":"Anti-Assignment",
"exists":true,
"excerpts":[
"MAmaynotassign,sell,leaseorotherwisetransferinwholeorinpartanyoftherightsgrantedpursuanttothisAgreementwithoutpriorwrittenapprovalofCompany."
]
},
{
"clause_type":"PriceRestrictions",
"exists":true,
"excerpts":[
"Companyreservestherighttochangeitspricesand/orfees,fromtimetotime,initssoleandabsolutediscretion."
]
},
{
"clause_type":"MinimumCommitment",
"exists":true,
"excerpts":[
"MAcommitstopurchaseaminimumof100UnitsinaggregatewithintheTerritorywithinthefirstsixmonthsoftermofthisAgreement."
]
},
{
"clause_type":"IPOwnershipAssignment",
"exists":true,
"excerpts":[
"TitletotheTechnologyandallcopyrightsinTechnologyshallremainwithCompanyand/oritsAffiliates."
]
},
{
"clause_type":"Licensegrant",
"exists":true,
"excerpts":[
"CompanyherebygrantstoMAtherighttoadvertise,marketandselltheTechnologylistedinScheduleAofthisAgreement."
]
},
{
"clause_type":"Non-TransferableLicense",
"exists":true,
"excerpts":[
"MAacknowledgesthatMAanditsClientsreceivenotitletotheTechnologycontainedontheTechnology."
]
},
{
"clause_type":"CapOnLiability",
"exists":true,
"excerpts":[
"InnoeventshallCompanybeliabletoMA,itsClients,oranythirdpartyforanytortorcontractdamagesorindirect,special,general,incidentalorconsequentialdamages."
]
},
{
"clause_type":"WarrantyDuration",
"exists":true,
"excerpts":[
"Company'ssoleandexclusiveliabilityforthewarrantyprovidedshallbetocorrecttheTechnologytooperateinsubstantialaccordancewithitsthencurrentspecifications."
]
}
]
}
}现在,每份合同都已经转为 JSON 文件,下一步是将其创建为 Neo4j 中的知识图谱。
此时,花些时间设计数据模型是很有必要的。您需要考虑以下关键问题:
•图谱中的节点和关系分别代表什么?
•每个节点和关系的主要属性是什么?
•是否需要对某些属性建立索引?
•哪些属性需要向量嵌入以实现语义相似性搜索?
在我们的案例中,一个合适的设计(模式)包括以下主要实体:协议(合同)、其条款、作为协议方的组织以及它们之间的关系。
下图展示了该模式的可视化表示。
Nodeproperties:
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}
Relationshipproperties:
IS_PARTY_TO{role:STRING}
GOVERNED_BY_LAW{state:STRING}
HAS_CLAUSE{type:STRING}
INCORPORATED_IN{state:STRING}只有摘录——即在第一步中由 LLM 识别的短文本片段——需要文本嵌入。这种方法显著减少了表示每份合同所需的向量数量和向量索引的大小,使流程更高效、可扩展。
将每个 JSON 加载到具有上述模式的知识图谱中的 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=[filenameforfilenameinos.listdir(JSON_CONTRACT_FOLDER)iffilename.endswith('.json')]
forjson_contractinjson_contracts:
withopen(JSON_CONTRACT_FOLDER+json_contract,'r')asfile:
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("GeneratingEmbeddingsforContractExcerpts...")
driver.execute_query(EMBEDDINGS_STATEMENT,token=OPENAI_API_KEY)其中,唯一复杂的部分是“CREATE_GRAPH_STATEMENT”。这是一个 Cypher 语句,用于将合同 JSON 映射到知识图谱中的节点和关系。
完整的 Cypher 语句如下:
CREATE_GRAPH_STATEMENT="""
WITH$dataASdata
WITHdata.agreementasa
MERGE(agreement:Agreement{contract_id:a.contract_id})
ONCREATESET
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)
SETgbl.state=a.governing_law.state
FOREACH(partyINa.parties|
//todoproperglobalidfortheparty
MERGE(p:Organization{name:party.name})
MERGE(p)-[ipt:IS_PARTY_TO]->(agreement)
SETipt.role=party.role
MERGE(country_of_incorporation:Country{name:party.incorporation_country})
MERGE(p)-[incorporated:INCORPORATED_IN]->(country_of_incorporation)
SETincorporated.state=party.incorporation_state
)
WITHa,agreement,[clauseINa.clausesWHEREclause.exists=true]ASvalid_clauses
FOREACH(clauseINvalid_clauses|
CREATE(cl:ContractClause{type:clause.clause_type})
MERGE(agreement)-[clt:HAS_CLAUSE]->(cl)
SETclt.type=clause.clause_type
//ONCREATESETc.excerpts=clause.excerpts
FOREACH(excerptINclause.excerpts|
MERGE(cl)-[:HAS_EXCERPT]->(e:Excerpt{text:excerpt})
)
//linkclausestoaClauseTypelabel
MERGE(clType:ClauseType{name:clause.clause_type})
MERGE(cl)-[:HAS_TYPE]->(clType)
)"""以下是该语句的分解。
WITH$dataASdata
WITHdata.agreementasa•$data是以 JSON 格式传递到查询中的输入数据,包含有关协议(合同)的信息。
•第二行将data.agreement分配给别名a,以便在后续查询中引用合同详细信息。
MERGE(agreement:Agreement{contract_id:a.contract_id})
ONCREATESET
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尝试查找具有指定 contract_id 的现有Agreement节点。如果不存在,则创建一个。
•ON CREATE SET 子句设置新创建的Agreement节点上的各种属性,例如contract_id、agreement_name、effective_date以及 JSON 输入中的其他协议相关字段。
MERGE(gl_country:Country{name:a.governing_law.country})
MERGE(agreement)-[gbl:GOVERNED_BY_LAW]->(gl_country)
SETgbl.state=a.governing_law.state•这会为与协议相关的适用法律国家创建或合并一个Country节点。
•然后,在Agreement和Country之间创建或合并一个GOVERNED_BY_LAW关系。
•它还设置了GOVERNED_BY_LAW关系的state属性。
FOREACH(partyINa.parties|
MERGE(p:Organization{name:party.name})
MERGE(p)-[ipt:IS_PARTY_TO]->(agreement)
SETipt.role=party.role
MERGE(country_of_incorporation:Country{name:party.incorporation_country})
MERGE(p)-[incorporated:INCORPORATED_IN]->(country_of_incorporation)
SETincorporated.state=party.incorporation_state
)对于合同中的每个当事方(a.parties):
•合并(或插入)一个代表当事方的Organization节点。
•在Organization和Agreement之间创建一个IS_PARTY_TO关系,设置当事方的角色(例如:买方、卖方)。
•为组织注册地的国家合并一个Country节点。
•在组织和注册地国家之间创建一个INCORPORATED_IN关系,并设置注册地的州。
WITHa,agreement,[clauseINa.clausesWHEREclause.exists=true]ASvalid_clauses
FOREACH(clauseINvalid_clauses|
CREATE(cl:ContractClause{type:clause.clause_type})
MERGE(agreement)-[clt:HAS_CLAUSE]->(cl)
SETclt.type=clause.clause_type
FOREACH(excerptINclause.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的条款(即在第一步中由 LLM 识别出摘录的条款)。
对于每个条款:
•创建一个ContractClause节点,其name和type分别对应于条款类型。
•在Agreement和ContractClause之间建立一个HAS_CLAUSE关系。
•对于与条款相关的每个excerpt,创建一个Excerpt节点,并通过HAS_EXCERPT关系将其链接到ContractClause。
•最后,为条款类型创建(或合并)一个ClauseType节点,并通过HAS_TYPE关系将ContractClause链接到ClauseType。
运行导入脚本后,可以在 Neo4j 中将单个合同可视化为知识图谱。
知识图谱中这三份合同仅需要一个小型图谱(少于 100 个节点和 200 个关系)。最重要的是,仅需要 40-50 个摘录的向量嵌入。这个带有少量向量的知识图谱现在可以用来支持一个功能强大的问答智能体。
合同现在已被结构化为知识图谱,下一步是创建一小组图数据检索功能。这些功能是开发问答智能体的核心构建块。
让我们定义一些基本的数据检索功能:
1.检索合同的基本信息(通过合同 ID)。
2.查找涉及特定组织的合同(通过组织名称的部分匹配)。
3.查找不包含特定条款类型的合同。
4.查找包含特定条款类型的合同。
5.根据条款中的文本(摘录)与输入文本的语义相似性查找合同(例如:提到“禁止物品”的合同)。
6.针对数据库中的所有合同运行自然语言查询——例如,统计“满足某些条件的合同数量”的聚合查询。
在第四步中,我们将使用 Microsoft Semantic Kernel 库构建一个问答智能体。该库简化了智能体的构建过程。开发者可以定义智能体可用的功能和工具,以便回答问题。
为了简化 Neo4j 与 Semantic Kernel 库之间的集成,我们定义了一个ContractPlugin,其中包含每个数据检索功能的“签名”。请注意每个功能的@kernel_function装饰器,以及为每个功能提供的类型信息和描述。
Semantic Kernel 使用“插件”类的概念来封装智能体可用的一组功能。它会使用这些装饰过的功能、类型信息和文档,向 LLM 函数调用功能提供智能体可用的功能信息:
fromtypingimportList,Optional,Annotated
fromAgreementSchemaimportAgreement,ClauseType
fromsemantic_kernel.functionsimportkernel_function
fromContractServiceimportContractSearchService
classContractPlugin:
def__init__(self,contract_search_service:ContractSearchService):
self.contract_search_service=contract_search_service
@kernel_function
asyncdefget_contract(self,contract_id:int)->Annotated[Agreement,"Acontract"]:
"""Getsdetailsaboutacontractwiththegivenid."""
returnawaitself.contract_search_service.get_contract(contract_id)
@kernel_function
asyncdefget_contracts(self,organization_name:str)->Annotated[List[Agreement],"Alistofcontracts"]:
"""Getsbasicdetailsaboutallcontractswhereoneofthepartieshasanamesimilartothegivenorganizationname."""
returnawaitself.contract_search_service.get_contracts(organization_name)
@kernel_function
asyncdefget_contracts_without_clause(self,clause_type:ClauseType)->Annotated[List[Agreement],"Alistofcontracts"]:
"""Getsbasicdetailsfromcontractswithoutaclauseofthegiventype."""
returnawaitself.contract_search_service.get_contracts_without_clause(clause_type=clause_type)
@kernel_function
asyncdefget_contracts_with_clause_type(self,clause_type:ClauseType)->Annotated[List[Agreement],"Alistofcontracts"]:
"""Getsbasicdetailsfromcontractswithaclauseofthegiventype."""
returnawaitself.contract_search_service.get_contracts_with_clause_type(clause_type=clause_type)
@kernel_function
asyncdefget_contracts_similar_text(self,clause_text:str)->Annotated[List[Agreement],"Alistofcontractswithsimilartextinoneoftheirclauses"]:
"""Getsbasicdetailsfromcontractshavingsemanticallysimilartextinoneoftheirclausestothetothe'clause_text'provided."""
returnawaitself.contract_search_service.get_contracts_similar_text(clause_text=clause_text)
@kernel_function
asyncdefanswer_aggregation_question(self,user_question:str)->Annotated[str,"Ananswertouser_question"]:
"""Answerobtainedbyturninguser_questionintoaCYPHERquery"""
returnawaitself.contract_search_service.answer_aggregation_question(user_question=user_question)建议探索包含上述功能实现的ContractService类。每个功能展示了不同的 GraphRAG 数据检索技术和模式。
让我们逐步了解这些功能的实现,因为它们展示了不同的 GraphRAG 数据检索技术和模式。
get_contract(self, contract_id: int)是一个异步方法,旨在使用 Cypher 查询从 Neo4j 数据库检索特定合同(Agreement)的详细信息。该功能返回一个Agreement对象,其中包含有关协议、条款、当事方及其关系的信息。
以下是该功能的实现:
asyncdefget_contract(self,contract_id:int)->Agreement:
GET_CONTRACT_BY_ID_QUERY="""
MATCH(a:Agreement{contract_id
contract_id})-[:HAS_CLAUSE]->(clause:ContractClause)
WITHa,collect(clause)asclauses
MATCH(country:Country)-[i:INCORPORATED_IN]-(p:Organization)-[r:IS_PARTY_TO]-(a)
WITHa,clauses,collect(p)asparties,collect(country)ascountries,collect(r)asroles,collect(i)asstates
RETURNaasagreement,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')
returnawaitself._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
)最重要的组件是**GET_CONTRACT_BY_ID_QUERY**中的 Cypher 查询。此查询使用作为输入参数提供的 contract_id 执行。输出是匹配的协议及其条款和相关的当事方(每个当事方具有角色和注册地的国家/州)。
数据随后传递给_get_agreement实用函数,该函数将数据映射到“Agreement”。Agreement 是一个 TypedDict,定义如下:
classAgreement(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
ist[Party]
clauses
ist[ContractClause]此功能展示了知识图谱的一项强大功能,即测试关系的不存在。
get_contracts_without_clause()功能从 Neo4j 数据库中检索所有不包含特定类型条款的合同(Agreements)。该功能以ClauseType作为输入,并返回符合条件的Agreement对象列表。
此类数据检索信息无法通过向量搜索轻松实现。完整实现如下:
asyncdefget_contracts_without_clause(self,clause_type:ClauseType)->List[Agreement]:
GET_CONTRACT_WITHOUT_CLAUSE_TYPE_QUERY="""
MATCH(a:Agreement)
OPTIONALMATCH(a)-[:HAS_CLAUSE]->(cc:ContractClause{type
clause_type})
WITHa,cc
WHEREccisNULL
WITHa
MATCH(country:Country)-[i:INCORPORATED_IN]-(p:Organization)-[r:IS_PARTY_TO]-(a)
RETURNaasagreement,collect(p)asparties,collect(r)asroles,collect(country)ascountries,collect(i)asstates
"""
#runtheCypherquery
records,_,_=self._driver.execute_query(GET_CONTRACT_WITHOUT_CLAUSE_TYPE_QUERY,{'clause_type':clause_type.value})
all_agreements=[]
forrowinrecords:
agreement_node=row['agreement']
party_list=row['parties']
role_list=row['roles']
country_list=row['countries']
state_list=row['states']
agreement:Agreement=awaitself._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)
returnall_agreements格式与前一个功能类似。Cypher 查询**GET_CONTRACTS_WITHOUT_CLAUSE_TYPE_QUERY**定义了要匹配的节点和关系模式。它执行一个可选匹配,以过滤掉包含特定条款类型的合同,并收集有关协议的相关数据,例如涉及的当事方及其详细信息。
然后,该功能构建并返回一个Agreement对象列表,其中封装了每个匹配协议的所有相关信息。
get_contracts_similar_text()功能旨在查找包含与提供的clause_text文本相似条款的协议(合同)。它使用语义向量搜索来识别相关摘录,并遍历图谱以返回有关相应协议和条款的信息以及这些摘录的来源。
此功能利用定义在每个摘录“text”属性上的向量索引。它使用最近发布的 Neo4j GraphRAG 包来简化运行语义搜索+图遍历代码所需的 Cypher 代码:
asyncdefget_contracts_similar_text(self,clause_text:str)->List[Agreement]:
#Cyphertotraversefromthesemanticallysimilarexcerptsbacktotheagreement
EXCERPT_TO_AGREEMENT_TRAVERSAL_QUERY="""
MATCH(a:Agreement)-[:HAS_CLAUSE]->(cc:ContractClause)-[:HAS_EXCERPT]-(node)
RETURNa.nameasagreement_name,a.contract_idascontract_id,cc.typeasclause_type,node.textasexcerpt
"""
#SetupvectorCypherretriever
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
)
#runvectorsearchqueryonexcerptsandgetresultscontainingtherelevantagreementandclause
retriever_result=retriever.search(query_text=clause_text,top_k=3)
#setupListofAgreements(withpartialdata)tobereturned
agreements=[]
foriteminretriever_result.items:
//extractinformationfromreturneditemsandappendagreementtoresults
//fullcodenotshownherebutavailableontheGithubrepo
returnagreements让我们了解此数据检索功能的主要组件:
•Neo4j GraphRAG VectorCypherRetriever 允许开发者在向量索引上执行语义相似性。在我们的案例中,对于每个通过语义相似性找到的“摘录”节点,使用额外的 Cypher 表达式来获取与该节点相关的其他节点。
•VectorCypherRetriever 的参数相对简单。index_name是运行语义相似性的向量索引。embedder 为文本生成向量嵌入。driver只是一个 Neo4j Python 驱动程序实例。retrieval_query指定了与每个“摘录”节点相关的其他节点和关系。
•EXCERPT_TO_AGREEMENT_TRAVERSAL_QUERY指定了要检索的其他节点。在这种情况下,对于每个摘录,我们检索其相关的合同条款及对应的协议。
EXCERPT_TO_AGREEMENT_TRAVERSAL_QUERY="""
MATCH(a:Agreement)-[:HAS_CLAUSE]->(cc:ContractClause)-[:HAS_EXCERPT]-(node)
RETURNa.nameasagreement_name,a.contract_idascontract_id,cc.typeasclause_type,node.textasexcerpt
"""answer_aggregation_question()功能利用 Neo4j GraphRAG 包的 Text2CypherRetriever 来回答自然语言中的问题。Text2CypherRetriever 使用 LLM 将用户问题转换为 Cypher 查询并在 Neo4j 数据库中运行。
该功能利用 GPT-4o 生成所需的 Cypher 查询。让我们逐步了解此数据检索功能的主要组件:
asyncdefanswer_aggregation_question(self,user_question)->str:
answer=""
NEO4J_SCHEMA="""
omittedforbrevity(seebelowforthefullvalue)
"""
#Initializetheretriever
retriever=Text2CypherRetriever(
driver=self._driver,
llm=self._llm,
neo4j_schema=NEO4J_SCHEMA
)
#GenerateaCypherqueryusingtheLLM,sendittotheNeo4jdatabase,andreturntheresults
retriever_result=retriever.search(query_text=user_question)
foriteminretriever_result.items:
content=str(item.content)
ifcontent:
answer+=content+'\n\n'
returnanswer此功能利用 Neo4j GraphRAG 包的 Text2CypherRetriever。它使用 LLM——在本例中是 OpenAI LLM——将用户问题(自然语言)转换为在数据库上执行的 Cypher 查询。查询结果将被返回。
确保 LLM 生成的查询使用数据库中定义的节点、关系和属性的关键要素是为 LLM 提供数据模型的文本描述。
在我们的案例中,使用以下数据模型表示法即可满足需求:
NEO4J_SCHEMA="""
Nodeproperties:
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}
Relationshipproperties:
IS_PARTY_TO{role:STRING}
GOVERNED_BY_LAW{state:STRING}
HAS_CLAUSE{type:STRING}
INCORPORATED_IN{state:STRING}
Therelationships:
(: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)
"""通过我们的知识图谱数据检索功能,我们已经准备好构建一个由 GraphRAG 支持的智能体。
让我们设置一个聊天机器人智能体,能够使用 GPT-4o、我们的数据检索功能以及 Neo4j 支持的知识图谱回答用户关于合同的问题。
我们将使用 Microsoft Semantic Kernel,一个允许开发者将 LLM 函数调用与现有 API 和数据检索功能集成的框架。
该框架使用“插件”概念来表示内核可以执行的特定功能。在我们的案例中,所有在“ContractPlugin”中定义的数据检索功能都可以被 LLM 用来回答问题。
该框架使用“内存”概念来存储用户和智能体之间的所有交互,以及执行的功能和检索到的数据。
一个极其简单的基于终端的智能体可以通过几行代码实现。以下代码片段展示了智能体的主要部分(省略了导入和环境变量):
logging.basicConfig(level=logging.INFO)
#Initializethekernel
kernel=Kernel()
#AddtheContractSearchplugintothekernel
contract_search_neo4j=ContractSearchService(NEO4J_URI,NEO4J_USER,NEO4J_PASSWORD)
kernel.add_plugin(ContractPlugin(contract_search_service=contract_search_neo4j),plugin_name="contract_search")
#AddtheOpenAIchatcompletionservicetotheKernel
kernel.add_service(OpenAIChatCompletion(ai_model_id="gpt-4o",api_key=OPENAI_KEY,service_id=service_id))
#Enableautomaticfunctioncalling
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"]})
#Createahistoryoftheconversation
history=ChatHistory()
asyncdefbasic_agent():
userInput=None
whileTrue:
#Collectuserinput
userInput=input("User>")
#Terminatetheloopiftheusersays"exit"
ifuserInput=="exit":
break
#Adduserinputtothehistory
history.add_user_message(userInput)
#3.GettheresponsefromtheAIwithautomaticfunctioncalling
chat_completion:OpenAIChatCompletion=kernel.get_service(type=ChatCompletionClientBase)
result=(awaitchat_completion.get_chat_message_contents(
chat_history=history,
settings=settings,
kernel=kernel,
arguments=KernelArguments(),
))[0]
#Printtheresults
print("Assistant>"+str(result))
#Addthemessagefromtheagenttothechathistory
history.add_message(result)
if__name__=="__main__":
asyncio.run(basic_agent())让我们向智能体提出一些非平凡的问题。
1. 找到包含价格限制但没有保险条款的合同:
Assistant>IfoundthefollowingcontractthatcontainsaPriceRestrictionsclausebutdoesnotcontainanInsuranceclause:
-**Name:**BirchFirstGlobalInvestmentsInc.andMountKnowledgeHoldingsInc.MARKETINGAFFILIATEAGREEMENT
-**AgreementType:**MARKETINGAFFILIATEAGREEMENT
-**Parties:**
-**MOUNTKNOWLEDGEHOLDINGSINC.**(MarketingAffiliate),incorporatedinNevada,USA
-**BIRCHFIRSTGLOBALINVESTMENTSINC.**(Company),incorporatedintheVirginIslands(VIR)
Wouldyouliketoknowmoredetailsaboutthiscontract?在这里,Semantic Agent 规划器审查了内核中可用的功能,并确定回答此问题需要调用两个功能,而单独一个功能无法提供完整答案。
首先,它执行“获取包含[价格限制]条款的合同”功能,然后执行“获取没有[保险]条款的合同”功能。规划器随后结合两个功能的结果以识别交集,最终得出正确答案。
如果查看调试信息,可以确认 LLM 执行的函数调用序列,以及每次调用消耗的 token 数量。注意两个数据检索功能的调用。
2. 是的,请提供更多细节:
Assistant>Herearethedetailedinformationaboutthecontract:
###ContractDetails:
-**Name:**BirchFirstGlobalInvestmentsInc.andMountKnowledgeHoldingsInc.MARKETINGAFFILIATEAGREEMENT
-**AgreementType:**MARKETINGAFFILIATEAGREEMENT
-**ExpirationDate:**December31,2014
-**RenewalTerm:**One(1)year
###PartiesInvolved:
1.**MOUNTKNOWLEDGEHOLDINGSINC.**
-**Role:**MarketingAffiliate
-**IncorporationCountry:**USA
-**IncorporationState:**Nevada
2.**BIRCHFIRSTGLOBALINVESTMENTSINC.**
-**Role:**Company
-**IncorporationCountry:**VirginIslands(VIR)
###ClausesIncluded:
-No-SolicitOfEmployees
-ThirdPartyBeneficiary
-PriceRestrictions
-AffiliateLicense-Licensee
-AffiliateLicense-Licensor
-CapOnLiability
-AuditRights
-WarrantyDuration
-Post-TerminationServices
-Non-TransferableLicense
-Licensegrant
-IPOwnershipAssignment
-MinimumCommitment
-Anti-Assignment
-Non-Disparagement
###ClausesNotIncluded:
-Insurance
Isthereanythingspecificyouwouldliketoknoworanyparticularclausedetailsyouareinterestedin?在这种情况下,LLM 在后台使用了“通过 ID 查找合同”。您可能注意到,显示给用户的输出并未包括合同的 ID。然而,Semantic Kernel 在会话内存中存储了从先前调用的函数返回的所有信息!
这种智能体功能调用、内存使用以及用户查询自动映射到具有正确参数的函数调用的强大组合,解释了使用 Semantic Kernel 构建的智能体背后的“奥秘”。
3. 是否有任何合同提到可以不时更改价格的能力?
Assistant>Yes,thereisacontractthatmentions
theabilitytomakepricechangesfromtimetotime.
Hereistherelevantexcerpt:
1.**BirchFirstGlobalInvestmentsInc.andMountKnowledgeHoldingsInc.MARKETINGAFFILIATEAGREEMENT**
-**ContractID:**1
-**ClauseType:**PriceRestrictions
-**Excerpt:**
```
Companyreservestherighttochangeitsprocessand/orfees,fromtimetotime,initssoleandabsolutediscretion.Intheeventofapriceand/orfeechangeforTechnology,CompanyshallnotifyMAinwritingninety(90)dayspriortotheeffectivedateofanysuchchange.Allotherpricesand/orfeesmaybeadjustedwithoutpriornoticetoMA.
```
Wouldyoulikemoreinformationaboutthiscontractoranyotherdetails?为了回答这个问题,Semantic Kernel 正在执行get_contract_similar_text 数据检索功能。
User>anycontractmentionstheabilitytomakepricechangesfromtimetotime
INFO:semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base:processing1toolcallsinparallel.
INFO:semantic_kernel.kernel:Callingcontract_search-get_contracts_similar_text
functionwithargs:{"clause_text":"pricechangesfromtimetotime"}
Assistant>Yes,thereisacontractthatmentionstheabilitytomakepricechangesfromtimetotime.Hereistherelevantexcerpt:
1.**BirchFirstGlobalInvestmentsInc.andMountKnowledgeHoldingsInc.MARKETINGAFFILIATEAGREEMENT**
-**ContractID:**1
-**ClauseType:**PriceRestrictions
-**Excerpt:**
```
Companyreservestherighttochangeitsprocessand/orfees,fromtimetotime,initssoleandabsolutediscretion.Intheeventofapriceand/orfeechangeforTechnology,CompanyshallnotifyMAinwritingninety(90)dayspriortotheeffectivedateofanysuchchange.Allotherpricesand/orfeesmaybeadjustedwithoutpriornoticetoMA.
```
如前所述,此功能使用向量搜索和 Cypher 查询,不仅检索语义相似的摘录,还检索包含这些摘录的相关合同和条款。
最后,让我们问数据库中有多少份合同。
4. 数据库中有多少份合同?
此问题需要 LLM 使用 Text2Cypher 数据检索器:
Assistant>Thereareatotalof3contractsinthedatabase.
调试信息确认 Semantic Kernel 使用“user_question” = “数据库中有多少份合同?”运行了 Text2Cypher 数据检索功能。
User>howmanycontractsarethereonthedatabase?
INFO:httpx:HTTPRequest
OSThttps://api.openai.com/v1/chat/completions"HTTP/1.1200OK"
INFO:semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion_base:processing1toolcallsinparallel.
INFO:semantic_kernel.kernel:Callingcontract_search-answer_aggregation_questionfunction
withargs:{"user_question":"Howmanycontractsarethereinthedatabase?"}
INFO:semantic_kernel.functions.kernel_function:Functioncompleted.Duration:0.588805s
INFO:semantic_kernel.connectors.ai.open_ai.services.open_ai_handler:OpenAIusage:CompletionUsage(completion_tokens=13,prompt_tokens=3328,total_tokens=3341,completion_tokens_details={'reasoning_tokens':0})
Assistant>Thereareatotalof3contractsinthedatabase.GitHub 仓库(https://github.com/neo4j-product-examples/graphrag-contract-review)包含一个提供更优雅智能体 UI 的 Streamlit 应用程序。鼓励您与智能体交互,并对 ContractPlugin 进行更改,以便您的智能体能够处理更多问题。
在本篇博文中,我们探索了一种 GraphRAG 方法,将商业合同审查的劳动密集型任务转变为更高效的 AI 驱动流程。
通过专注于使用 LLM 和提示词进行目标信息提取,构建基于 Neo4j 的结构化知识图谱,实施简单的数据检索功能,最终开发问答智能体,我们创建了一个能够有效处理复杂问题的智能解决方案。
这种方法减少了传统基于向量搜索的 RAG 中存在的低效问题,而是专注于提取相关信息,降低了对不必要向量嵌入的需求,同时简化了整体流程。希望从合同数据处理到交互式问答智能体的这一旅程能够激发您利用 GraphRAG 实现更高效、更智能的 AI 驱动决策。
立即开始构建您自己的商业合同审查智能体,亲身体验 GraphRAG 的强大功能吧!
| 欢迎光临 链载Ai (https://www.lianzai.com/) | Powered by Discuz! X3.5 |