对数据进行结构化处理,可提高检索的准确性
在Neo4j AuraDB中创建免费的图数据库实例
检索增强生成(RAG)已成为一种强大的技术,能够为大型语言模型(LLM)补充外部知识。但在处理复杂、相互关联的信息时,传统的基于向量的RAG方法正日益显现出其局限性。简单的语义相似性搜索往往无法捕捉实体间细微的关联,难以完成多跳推理,还可能遗漏跨多个文档的关键上下文。
尽管有多种方法可应对这些挑战,但其中一个极具前景的解决方案是通过数据结构化,来实现更复杂的检索与推理能力。将非结构化文档转换为结构化知识表示后,我们能够执行复杂的图遍历、关联查询和上下文推理——这些能力远超简单的相似性匹配。
而LlamaCloud这类工具的价值便在于此:它具备强大的解析和提取能力,可将原始文档转换为结构化数据。Neo4j则作为知识图谱表示的核心支撑,构成了GraphRAG架构的基础。该架构不仅能识别信息的存在,还能理解所有信息之间的关联方式。
法律领域是结构化数据RAG方法最具说服力的应用场景之一——在该领域,信息检索的准确性和精确性会对现实世界产生重大影响。法律文档本身具有内在关联性,案例、法令、法规和判例之间存在复杂的引用网络,而传统向量搜索往往无法有效捕捉这些关联。法律信息的层级特性,加之理解实体、条款与法律概念之间关联的关键重要性,使得结构化知识图谱在提高检索准确性方面具有特殊价值。
为展示这种潜力,我们将通过一个完整的法律文档处理示例,详细介绍下文所示的整个流程。
从法律文档中提取信息并将其表示为图谱的处理流程
处理流程如下:
-
使用LlamaParse解析PDF文档并提取可读取文本。
-
利用LLM对合同类型进行分类,以实现上下文感知处理。
-
基于分类结果,借助LlamaExtract为每个特定合同类别提取相应的相关属性集。
-
将所有结构化信息存储到Neo4j知识图谱中,形成丰富的可查询表示——该表示既能捕捉法律文档的内容,也能体现文档内部复杂的关联。
环境设置
在运行此代码之前,你需要从LlamaCloud和OpenAI获取并设置API密钥。对于Neo4j,最简单的方法是创建一个免费的Aura数据库实例。
使用 LlamaParse 进行 OCR 处理
在本教程中,我们将分析来自合同理解阿提库斯数据集(CUAD)的一份商业合同样本。
现在,我们将使用 LlamaParse 解析合同文档,以提取其文本内容:
# 以指定模式初始化解析器
parser = LlamaParse(
api_key=llama_api_key,
parse_mode="parse_page_without_llm"
)
pdf_path = "CybergyHoldingsInc_Affliate Agreement.pdf"
results = await parser.aparse(pdf_path)
这段代码创建了一个 LlamaParse 实例,并处理我们的合同样本 PDF。
文档分类
在从合同中提取相关信息之前,我们需要确定所处理的合同类型。不同类型的合同具有不同的条款结构和法律信息,因此我们需要根据合同类型动态选择合适的提取 schema。
openai_client = AsyncOpenAI(api_key=openai_api_key)
classification_prompt = """你是一名法律文档分类助手。
你的任务是根据合同前10页的内容,确定最可能的合同类型。
说明:
阅读下面的合同节选。
查看可能的合同类型列表。
从列表中选择唯一最合适的合同类型。
仅根据节选中的信息,简要说明分类理由。
合同节选:
{contract_text}
可能的合同类型:
{contract_type_list}
输出格式:
<Reason>简要理由</Reason>
<ContractType>从列表中选择的类型</ContractType>
"""
async def classify_contract(contract_text: str, contract_types: list[str]) -> dict:
prompt = classification_prompt.format(
contract_text=file_content,
contract_type_list=contract_types
)
history = [{"role": "user", "content": prompt}]
response = await openai_client.responses.create(
input=history,
model="gpt-4o-mini",
store=False,
)
return extract_reason_and_contract_type(response.output[0].content[0].text)
这段代码设置了一个 OpenAI 客户端,并创建了一个分类系统,该系统包含两部分:一个提示模板(用于指示 LLM 如何对合同进行分类)和一个处理实际分类过程的函数。
现在,我们使用解析后的合同数据执行分类过程:
contract_types = ["Affiliate_Agreements", "Co_Branding", "Development"]
# 仅取前10页作为合同分类的输入
file_content = " ".join([el.text for el in results.pages[:10]])
classification = await classify_contract(file_content, contract_types)
使用 LlamaExtract 进行提取
LlamaExtract 是一种云服务,它利用基于 schema 的 AI 提取技术,将非结构化文档转换为结构化数据。
定义 Schema
这里我们定义了两个 Pydantic 模型:Location 用于捕捉结构化的地址信息,包含可选的国家、州和地址字段;Party 用于表示合同参与方,包含必填的名称和可选的位置详情。字段描述通过告知 LLM 每个字段具体需要查找哪些信息,来辅助提取过程。
class Location(BaseModel):
"""包含结构化地址组件的位置信息。"""
country: Optional[str] = Field(None, description="国家")
state: Optional[str] = Field(None, description="州或省")
address: Optional[str] = Field(None, description="街道地址或城市")
class Party(BaseModel):
"""包含名称和位置的参与方信息。"""
name: str = Field(description="参与方名称")
location: Optional[Location] = Field(None, description="参与方位置详情")
请记住,我们有多种合同类型,因此需要为每种类型定义特定的提取 schema,并创建一个映射系统,根据分类结果动态选择合适的 schema。
class BaseContract(BaseModel):
"""包含通用字段的基础合同类。"""
parties: Optional[List[Party]] = Field(None, description="所有合同参与方")
agreement_date: Optional[str] = Field(None, description="合同签署日期。使用YYYY-MM-DD格式")
effective_date: Optional[str] = Field(None, description="合同生效日期。使用YYYY-MM-DD格式")
expiration_date: Optional[str] = Field(None, description="合同到期日期。使用YYYY-MM-DD格式")
governing_law: Optional[str] = Field(None, description="管辖法律所属司法管辖区")
termination_for_convenience: Optional[bool] = Field(None, description="是否可以无理由终止")
anti_assignment: Optional[bool] = Field(None, description="是否限制向第三方转让")
cap_on_liability: Optional[str] = Field(None, description="责任限额金额")
class AffiliateAgreement(BaseContract):
"""附属协议提取内容。"""
exclusivity: Optional[str] = Field(None, description="独家区域或市场权利")
non_compete: Optional[str] = Field(None, description="竞业禁止限制")
revenue_profit_sharing: Optional[str] = Field(None, description="佣金或收入分成")
minimum_commitment: Optional[str] = Field(None, description="最低销售目标")
class CoBrandingAgreement(BaseContract):
"""联合品牌协议提取内容。"""
exclusivity: Optional[str] = Field(None, description="独家联合品牌权利")
ip_ownership_assignment: Optional[str] = Field(None, description="知识产权所有权分配")
license_grant: Optional[str] = Field(None, description="品牌/商标许可")
revenue_profit_sharing: Optional[str] = Field(None, description="收入分成条款")
mapping = {
"Affiliate_Agreements": AffiliateAgreement,
"Co_Branding": CoBrandingAgreement,
}
这种 schema 设计既包含结构化的提取字段(如参与方名称、日期和布尔标志),也包含更类似摘要的字段(用于处理复杂条款,如独家条款、收入分成安排和知识产权所有权详情)。
现在,我们可以将定义的 schema 与解析后的合同文本结合使用,以提取结构化信息。LlamaExtract 将分析整个文档内容,并提取我们定义的特定字段。
extractor = LlamaExtract(api_key=llama_api_key)
agent = extractor.create_agent(
name=f"extraction_workflow_import_{uuid.uuid4()}",
data_schema=mapping[classification['contract_type']],
config=ExtractConfig(
extraction_mode=ExtractMode.BALANCED,
),
)
result = await agent.aextract(
files=SourceText(
text_content=" ".join([el.text for el in results.pages]),
filename=pdf_path
),
)
Neo4j 知识图谱
最后一步是利用我们提取到的结构化信息,构建一个能够表示合同实体间关系的知识图谱。我们需要定义一个图谱模型,明确合同数据在 Neo4j 中应如何以“节点(node)”和“关系(relationship)”的形式组织。
法律图谱模型(Legal graph model)
我们的图谱模型包含三种主要节点类型:
-
Contract 节点:存储协议核心信息,包括日期、条款和法律条文。
-
Party 节点:表示签约实体及其名称。
-
Location 节点:记录包含地址组成部分的地理信息。
接下来,我们将根据定义的图谱模型,把提取到的合同数据导入 Neo4j:
import_query = """WITH $contract AS contract
MERGE (c:Contract {path: $path})
SET c += apoc.map.clean(contract, ["parties", "agreement_date", "effective_date", "expiration_date"], [])
// 转换为日期格式
SET c.agreement_date = date(contract.agreement_date),
c.effective_date = date(contract.effective_date),
c.expiration_date = date(contract.expiration_date)
// 创建参与方(Party)及其位置(Location)
WITH c, contract
UNWIND coalesce(contract.parties, []) AS party
MERGE (p:Party {name: party.name})
MERGE (c)-[:HAS_PARTY]->(p)
// 创建位置节点并关联到参与方
WITH p, party
WHERE party.location IS NOT NULL
MERGE (p)-[:HAS_LOCATION]->(l:Location)
SET l += party.location
"""
response = await neo4j_driver.execute_query(import_query, contract=result.data, path=pdf_path)
response.summary.counters
数据导入后,你的 Neo4j 图谱应如下所示:
提取的信息以图谱形式呈现
在工作流中整合所有环节
最后,我们可以将所有逻辑整合到一个可执行的智能体工作流(agentic workflow)中。设计该工作流时,我们使其能够通过接收单个 PDF 文件来运行,并且每次运行都会向 Neo4j 图谱中添加新条目。
提取工作流
该工作流的设计遵循简洁原则,你只需一条命令即可处理任意文档:
knowledge_graph_builder = KnowledgeGraphBuilder(
parser=parser,
affiliate_extract_agent=affiliage_extraction_agent,
branding_extract_agent=cobranding_extraction_agent,
classification_prompt=classification_prompt,
timeout=None,
verbose=True,
)
response = await knowledge_graph_builder.run(
pdf_path="CybergyHoldingsInc_Affliate Agreement.pdf"
)