由ChatGPT生成
在每一个业务中,法律合同都是确定各方之间关系、义务和责任的基础文件。无论是合伙协议(partnership agreement)、保密协议(NDA)还是供应商合同,这些文件通常包含关键信息,对决策制定、风险管理及合规性都有推动作用。然而,从这些合同中导航并提取信息可能既复杂又耗时。
在这篇文章中,我们将探讨如何通过实施端到端的解决方案来简化理解和处理法律合同的过程。我认为GraphRAG是任何从知识图中的信息检索或推理的方法的总称,这使得回复更加结构化和情境感知。
通过将法律合同组织为 Neo4j 中的知识图谱,我们可以构建一个强大且易于查询和分析的信息库。之后,我们将创建一个 LangGraph 代理,让使用者能对这些合同进行特定查询,从而快速发现新的洞察。
示例应用程序的响应
代码可在 GitHub 上找到。
为什么结构化数据很重要一些领域与简单的RAG配合得很好,但法律合同却面临着特别的难题。
从不相关的合同中提取信息,利用基于向量的RAG方法
如上所述,仅仅依靠向量索引来检索相关的文本段落可能会带来风险,例如提取与合同无关的信息。这是因为法律语言高度结构化,不同协议中的相似措辞可能会导致检索结果不准确或误导。这些限制凸显了采用更结构化的方法,如GraphRAG,以确保检索结果的准确性和上下文相关性。
为了实现GraphRAG,我们首先需要搭建知识图。
包含结构化和非结构化信息的法律知识图
为了构建法律合同的知识图谱,我们需要一种从文档中提取结构化信息并将其与原始文本一起存储的方法。大规模语言模型可以通过阅读合同并识别关键细节,如当事人、日期、合同类型和重要条款来提供帮助。我们不再将合同仅视为一段文本,而是将其分解为反映其法律意义的结构化组件。例如,大语言模型可以识别到“ACME Inc. 同意从2024年1月1日起每月支付10,000美元”既包含了支付义务也包含了开始日期,然后我们可以将这些信息以结构化的方式存储起来。
一旦我们有了这些结构化数据,我们会将其存储在一个知识图谱中,在那里公司、协议和条款等实体及其关系都得到了表示。虽然非结构化的文本仍然可用,但现在我们可以利用结构化层来优化搜索并使检索更加精确。我们不仅可以根据相关性获取文本片段,还可以根据合同的属性进行筛选。这意味着我们可以回答那些基础的检索方法难以应对的问题,例如上个月签订了多少份合同,或者我们与某个特定公司是否有活跃协议。这些问题需要聚合和筛选,而仅靠标准的基于向量的检索无法实现这些功能。
通过整合结构化和非结构化数据,我们还让检索更具上下文相关性。如果用户询问某个合同的付款条款,我们会确保搜索仅限于正确的协议,而不是依赖于文本相似度,这可能从无关的合同中提取错误信息。这种混合方法克服了简单检索和生成(RAG)的局限性,使得法律文件的深入和可靠分析更有可能。
图构建我们将利用大型语言模型(LLM)从法律文件中提取结构化的信息,使用的是合同理解 Atticus 数据集(CUAD),一个广泛使用的合同分析基准数据集,该数据集遵循 CC BY 4.0 许可协议。CUAD 数据集包含超过 500 份合同,非常适合用来评估我们的结构化提取流程。
合同的词频分布如下图所示。
CUAD的合同中Token的数量分布情况
这个数据集中大多数合同都相对较短,token 数低于 10,000。但也有一些合同非常长,有几份甚至达到了 80,000 token。这些长合同很少见,而较短的合同占了大多数。分布显示了一个陡峭的下降,意味着长合同是特例,而非常态。
我们用Gemini 2.0 Flash来提取数据,它的输入限制是100万个token,处理这些合同对我们来说不是问题。即使是最长的合同(大约80,000个token),也能很好地适应模型。大多数合同要短得多,所以我们不需要担心截断或拆分文档。
结构化数据提取大多数商用大型语言模型都可以使用Pydantic对象来定义输出格式。比如,对于位置的定义:
class Location(BaseModel):
"""
表示一个物理位置,包括地址、城市、州/省以及国家。
"""
address: Optional[str] = Field(
..., description="位置的街道地址。如果没有提供,则使用 None"
)
city: Optional[str] = Field(
..., description="位置的城市。如果没有提供,则使用 None"
)
state: Optional[str] = Field(
..., description="位置的州或省。如果没有提供,则使用 None"
)
country: str = Field(
...,
description="位置的国家。使用两个字母的 ISO 标准缩写。",
)
在使用大语言模型(LLM)进行结构化输出时,Pydantic 有助于通过指定属性的类型并提供描述来定义一个清晰的结构,这些描述可以指导模型如何响应。每个字段都有一个类型,例如 str
或 Optional[str]
,以及一个描述,告诉 LLM 如何格式化输出。
例如,在一个 Location
模型中,我们定义了一些关键属性,例如 address
、city
、state
和 country
,指定了期望的数据类型及其结构。例如,country
字段采用两个字母的国家代码标准,如 "US"
、"FR"
或 "JP"
,而不是不一致的变体,比如 "United States" 或 "USA"。这一原则同样适用于其他结构化数据,ISO 8601 也使用标准格式 (YYYY-MM-DD
) 等来表示日期。
通过Pydantic定义结构化输出,这使LLM的响应更加可靠且易于机器读取,更易于集成到数据库或API。清晰的字段描述进一步帮助模型生成格式正确的数据,减少了后续处理的需要。
Pydantic模式可以更加复杂化,比如如下的Contract模型,该模型捕捉法律协议的关键要素,确保提取的数据符合标准结构。
class Contract(BaseModel):
"""
表示合同的关键信息。
"""
summary: str = Field(
...,
description=("提供合同的全面概要,包括所有相关事实和细节。"
"不要使用任何代词"),
)
contract_type: str = Field(
...,
description="正在进入的合同类型。",
enum=CONTRACT_TYPES,
)
parties: List[Organization] = Field(
...,
description="合同中涉及各方的列表及其角色。",
)
effective_date: str = Field(
...,
description=(
"输入合同生效日期,格式为 yyyy-MM-dd。"
"如果只知年份(例如,2015),则使用2015-01-01作为默认日期。"
"填写完整日期,切勿省略"
),
)
contract_scope: str = Field(
...,
description="合同的范围描述,包括权利、义务和任何限制。",
)
duration: Optional[str] = Field(
None,
description=(
"协议的期限,包括续期或终止的规定。"
"使用ISO 8601持续时间格式"
),
)
end_date: Optional[str] = Field(
None,
description=(
"合同到期日期,格式为 yyyy-MM-dd。"
"如果只知年份(例如,2015),则使用2015-01-01作为默认日期。"
"填写完整日期,切勿省略"
),
)
total_amount: Optional[float] = Field(
None, description="合同的总金额。"
)
governing_law: Optional[Location] = Field(
None, description="管辖合同的法律。"
)
clauses: Optional[List[Clause]] = Field(
None, description=f"""条款类型的摘要。允许的条款类型为 {CLAUSE_TYPES}"""
)
这个合同架构以结构化方式组织法律协议的关键细节,使其更易于大型语言模型分析。它包括各种条款,如保密条款或终止条款,每个条款都附有简短摘要。参与方以姓名、地点和角色列出,而合同细节则包括开始和结束日期、总价值以及适用法律等信息。某些属性,比如适用法律,可以通过嵌套模型定义,从而实现更详细和复杂的描述。
使用嵌套对象的方法与某些处理复杂的数据关联的AI模型配合得相当好,而其他模型可能难以处理这种深层嵌套的细节。
我们可以用这个例子来测试我们的方法,我们使用LangChain框架来调度LLMs。
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
llm.with_structured_output(合同).invoke(
"Tomaz从2017年开始就在Neo4j工作,并计划在2030年前赚取10亿美元"
"合同是在拉斯维加斯签订的"
)
这会输出:
合同(
摘要="Tomaz 自 2017 年起在 Neo4j 工作,预计到 2030 年将为公司创造十亿美元的价值。",
合同类型="服务",
各方=[
机构(
名称="Tomaz",
地点=地点(
地址=None,
城市="拉斯维加斯",
州=None,
国家="美国"
),
身份="雇员"
),
公司(
名称="Neo4j",
地点=地点(
地址=None,
城市=None,
州=None,
国家="美国"
),
身份="雇主"
)
],
生效日期="2017-01-01",
工作内容="Tomaz 将与 Neo4j 共同工作",
结束日期="2030-01-01",
总金额=1_000_000_000.0,
法律适用=None,
条款细则=None
)
现在我们的合同数据已经以结构化的格式存在了,我们可以定义将数据导入Neo4j所需的Cypher查询,将实体、关系以及关键语句映射到图结构。这一步将原始提取的数据转换成可查询的知识图,从而实现合同信息的高效查询和获取。
UNWIND $data AS row
MERGE (c:Contract {file_id: row.file_id})
SET c.summary = row.summary,
c.contract_type = row.contract_type,
c.effective_date = date(row.effective_date),
c.contract_scope = row.contract_scope,
c.duration = row.duration,
c.end_date = CASE WHEN row.end_date IS NOT NULL THEN date(row.end_date) ELSE NULL END,
c.total_amount = row.total_amount
WITH c, row
CALL (c, row) {
WITH c, row
WHERE row.governing_law IS NOT NULL
MERGE (c)-[:HAS_GOVERNING_LAW]->(l:Location)
SET l += row.governing_law
}
FOREACH (party IN row.parties |
MERGE (p:Party {name: party.name})
MERGE (p)-[:HAS_LOCATION]->(pl:Location)
SET pl += party.location
MERGE (p)-[pr:PARTY_TO]->(c)
SET pr.role = party.role
)
FOREACH (clause IN row.clauses |
MERGE (c)-[:HAS_CLAUSE]->(cl:Clause {type: clause.clause_type})
SET cl.summary = clause.summary
)
这个 Cypher 查询通过将结构化的合同数据导入 Neo4j 数据库,并创建具有如 summary
(摘要)、contract_type
(合同类型)、effective_date
(生效日期)、duration
(期限)和 total_amount
(总金额)等属性的 Contract
节点。如果指定了适用法律,则将合同链接到一个 Location
节点。合同中的各方作为 Party
节点被存储,每个当事人与一个 Location
节点相连,并在合同中分配一个角色。查询还对条款进行处理,创建 Clause
节点并将它们与合同相连,同时记录它们的类型和摘要。
在处理并导入合同之后,生成的图表遵循以下的图表模式。
进口的法律图表模式
我们也再来看看一个合同。
单个合同图表
此图表示一个合同结构,其中合同(橙色节点)连接到各种条款(红色的节点)、各方(蓝色节点)和地点(紫色节点)。合同包含三个条款:续签与终止、责任与赔偿、以及保密及不披露条款。涉及两个当事人,分别是Modus Media International和Dragon Systems, Inc.,每个当事人都与其相应的地点链接,分别是荷兰(荷兰)和美国(美国)。该合同受美国法律约束。合同节点还包含其他元数据,如日期等其他相关信息。
包含CUAD法律合同的可供公众查看的只读实例可以使用以下凭据查看,如下所述:
以下是连接Neo4j数据库所需的信息:
URI: neo4j+s://demo.neo4jlabs.com
用户名: legalcontracts
密码: legalcontracts
数据库: legalcontracts
请注意不要泄露用户名和密码。
实体识别
在法律合同中,实体识别颇具挑战性,因为公司名称、个人名称和地点名称的引用方式多种多样。一家公司在一个合同中可能被称作“Acme Inc.”,而在另一个合同中则可能被称为“Acme Corporation”,这就需要一个流程来判断这些引用是否是指同一个实体。
一种方法是通过文本嵌入或像莱文斯坦距离这样的字符串距离度量生成候选匹配。嵌入捕捉语义相似性,而字符串距离测量字符级别的差异。一旦找到候选对象,还需要进行进一步的评估,比如比较地址或税号之类的元数据,分析图表中共享的关系,或者在关键情况下进行人工审核。
为了大规模地解决实体解析问题,开源解决方案例如Dedupe和商业工具如Senzing都提供了自动化的手段。因此,选择合适的方法取决于数据质量、准确性需求以及是否可以进行人工审核。
在法律图构建完成后,我们就可以实现代理GraphRAG了。
Agentic RAG代理架构在复杂性、模块化和推理能力上存在很大差异。在核心部分,这些架构都包含一个语言模型(LM)作为中央推理引擎,通常会配备如工具、内存和编排机制等辅助设备。关键的区别在于语言模型在做决策时的自主程度以及它与外部系统交互的方式和结构。
其中一个最简单且最有效的设计,特别是对于类似聊天机器人的实现,是直接使用带有工具的LLM方式。在这种情况下,LLM充当决策者,根据需要动态选择要调用的工具(如果有的话),必要时重试操作,并按顺序执行多个工具以完成复杂的任务。
语言图谱代理体系架构
这幅图展示了一个简单的LangGraph代理工作流程。它从__start__
开始,移动到assistant
节点,在这里,LLM处理用户输入。从那里,助手可以调用tools
来获取相关信息,或者直接跳转到__end__
来结束对话。当使用工具时,助手会处理返回的信息,然后决定是否调用另一个工具或直接结束会话。这种结构使代理能够自主决定何时需要调用外部信息来回应用户。
这种方法特别适合像Gemini或GPT-4这样的商业模型,这些模型在推理和自我纠正方面特别出色。
工具篇大型语言模型是强大的推理引擎,但它们的有效性往往取决于如何配备外部工具。这些工具包括数据库查询、API或搜索功能,使大模型能够检索事实、执行计算或与结构化信息互动。
大型语言模型工具包
设计既能应对多样查询又足够精确以返回有意义结果的工具,更多的是艺术而非科学。我们真正构建的是LLM和底层数据之间的语义层,抽象掉了这些复杂细节,而不是定义一些工具,使LLM不必理解Neo4j知识图谱或数据库模式的具体结构。
通过这种方法,LLM无需关心合同信息是通过图节点还是纯文本存储。它只需调用合适的工具,根据用户的问题获取相关数据。
在我们的情况下,这种语义接口充当关键角色。当用户询问合同条款、义务或相关方信息时,LLM调用结构化查询工具,将其转换为数据库查询,检索相关信息,并以LLM能够理解和总结的格式呈现。这使得一个灵活且与模型无关的系统得以创建,不同的LLM可以与合同数据进行交互,而无需直接了解其存储方式或结构。
没有适用于所有模型的通用标准来设计理想的工具集。适合一种模型的方法可能对另一种模型不适用。一些模型能优雅地处理模糊指令,而另一些则在处理复杂参数上挣扎,或者需要明确的指引。在通用性和特定任务效率之间的权衡,意味着工具设计需要不断地迭代、测试和针对所使用的大规模语言模型进行微调。
对于合同分析,一个有效的工具应该能检索合同并总结关键条款,而无需用户用固定格式提问。实现这种灵活性取决于精心设计的提示、稳健的设计架构以及适应不同大型语言模型的能力。随着模型的不断进步,让工具变得更直观和有效的策略也在不断变化。
在这一部分,我们看看不同的工具实施的途径,比较它们的灵活性、有效性以及与各种大型语言模型的兼容性。
我的首选方法是动态并确定地构建一个Cypher查询,并将其执行在数据库上。这种方法确保了查询生成的一致性和可预测性,同时保持了实现的灵活性。这样结构查询,我们强化了语义层,使得用户输入能够无缝转换成数据库检索。这使LLM专注于检索相关信息,而不是理解底层数据模型。
我们的工具旨在识别相关的合同内容,所以我们需要为LLM提供搜索合同的选项,这些选项可以根据各种属性。输入描述再次以Pydantic对象的形式给出。
class ContractInput(BaseModel):
min_effective_date: Optional[str] = Field(
None, description="合同最早生效日期(YYYY-MM-DD)"
)
max_effective_date: Optional[str] = Field(
None, description="合同最晚的生效日期(YYYY-MM-DD)"
)
min_end_date: Optional[str] = Field(
None, description="合同最早结束日期(YYYY-MM-DD)"
)
max_end_date: Optional[str] = Field(
None, description="合同最晚的结束日期(YYYY-MM-DD)"
)
contract_type: Optional[str] = Field(
None, description=f"合同类型;有效类型包括:{CONTRACT_TYPES}"
)
parties: Optional[List[str]] = Field(
None, description="合同涉及各方的名单"
)
summary_search: Optional[str] = Field(
None, description="查看合同摘要"
)
country: Optional[str] = Field(
None, description="合同适用的国家。使用两字母的ISO标准。"
)
active: Optional[bool] = Field(None, description="合同是否有效")
monetary_value: Optional[MonetaryValue] = Field(
None, description="合同的总金额或价值(如适用)"
)
使用LLM工具,属性可以以多种形式存在,根据它们的用途。有些字段是简单的字符串,例如contract_type
和country
,只存储一个值。另一个例子是parties
,它包含一个字符串列表,可以包含多个条目,比如合同中的多个相关方。
除了基本的数据类型之外,属性还可以表示复杂的对象。例如,monetary_value
使用了一个 MonetaryValue
对象,该对象包括如货币种类和操作符类型之类的结构化数据。虽然带有嵌套对象的属性可以清晰地呈现数据,但模型在处理这些数据时往往不太擅长处理,因此我们应该尽量保持它们简单。
在这个项目中,我们正在尝试一个额外的cypher_aggregation
属性,为LLM增加灵活性,尤其是在需要特定过滤或聚合功能的场景中,使语句更加清晰。
cypher_aggregation: Optional[str] = Field(
None,
description="""自定义Cypher语句,用于高级聚合和分析。
此语句将附加到基础查询中:
MATCH (c:Contract)
<根据其他参数进行过滤>
WITH c, summary, contract_type, contract_scope, effective_date, end_date, parties, active, monetary_value, contract_id, countries
<请在此处添加您的Cypher语句>
```
示例:
-
按类型统计合同数量:
RETURN contract_type, count(*) AS count 按 count 降序排列
-
按类型计算合同平均时长:
WITH contract_type, effective_date, end_date WHERE effective_date IS NOT NULL AND end_date IS NOT NULL WITH contract_type, duration.between(effective_date, end_date).days AS duration RETURN contract_type, avg(duration) AS avg_duration 按 avg_duration 降序排列
-
按生效日期的年份计算合同数量:
RETURN effective_date.year AS year, count(*) AS count 按 year 排序
- 统计拥有最多活跃合同的当事人方:
UNWIND parties AS party WITH party.name AS party_name, active, count(*) AS contract_count WHERE active = true RETURN party_name, contract_count 按 contract_count 降序排列 限制为1
"""
cypher_aggregation
属性让 LLMs 定义自定义 Cypher 语句以进行更复杂的聚合和分析。它通过扩展基础查询以包含用户指定的聚合逻辑,从而实现更灵活的过滤和计算。
此功能支持多种应用场景,例如按类型统计合同、计算平均合同时长、分析合同随时间的分布以及识别关键合同方。通过利用此属性,LLM可以动态生成针对具体分析需求的见解,而无需预先设定查询结构。
虽然这种灵活性很有价值,但它需要仔细评估,因为更高的灵活性是以牺牲一致性和稳定性为代价的,这主要是由于操作复杂性增加。
在向大型语言模型展示函数时,我们应当明确地定义函数的名字和描述。这样的描述能帮助模型正确使用函数,确保模型明白函数的目的、预期输入和输出。这减少了歧义,也增强了模型生成有效查询的能力。
class ContractSearchTool(BaseTool):
name: str = "ContractSearch"
description: str = "这个工具非常有用,当你需要回答关于任何合同的问题时。"
args_schema: Type[BaseModel] = ContractInput
最后,我们需要创建一个函数来处理这些事情,该函数处理给定的输入数据,构建相应的Cypher语句并高效执行。
功能的核心逻辑在于构建Cypher语句的过程。我们首先从匹配合约开始,作为查询的基础:
cypher语句 = "匹配 (c:Contract) "
接下来,我们需要实现处理输入参数的函数。在本例中,我们主要使用属性来根据给定的条件过滤合约。
简单的属性筛选
比如说,contract_type
属性用来进行简单的节点属性筛选:
如果有 contract_type:
filters.append("c.contract_type = $contract_type")
params["contract_type"] = contract_type
此代码为 contract_type
添加了一个 Cypher 过滤器,并使用查询参数传递值,以防止查询注入的安全问题,更符合技术领域的专业术语。
这里列出了可能的合同类型选项。
contract_type: Optional[str] = Field(
None, description=f"类型;有效类型包括:{CONTRACT_TYPES}"
)
我们不必担心将输入值映射到有效的合同类型上,因为大型语言模型会处理这个问题。
推断的属性过滤
我们正在构建工具,让大模型可以与知识图谱互动,这些工具作为一个抽象层,位于结构化查询之上。一个关键特性是在运行时使用推断出的属性,类似于动态计算的本体。这种特性使得大模型在处理知识图谱时更加灵活。
if active is not None:
operator = ">=" if active else "<"
filters.append(f"c.end_date {operator} date()")
在这里,active
作为运行时的一个分类标志,确定合同是否处于活跃状态(>= date()
)或已过期(< date()
)。这种逻辑通过按需计算属性来扩展结构化查询,从而实现更加灵活的LLM推理。通过在工具内部处理这类逻辑,我们确保LLM专注于更简单直观的操作,而不是构建查询,使其能够专注于推理而不是构建查询。
邻居筛选
有时过滤会基于相邻节点,比如只展示涉及特定方的合同。如果有提供,提供的parties
属性是可选的列表,确保只考虑与这些实体相关的合同:
如果有 parties:
parties_filter = []
for i, party in enumerate(parties):
party_param_name = f"party_{i}"
parties_filter.append(
f"""EXISTS {{
MATCH (c)<-[:PARTY_TO]-(party)
WHERE lower(party.name) CONTAINS ${{party_param_name}}
}}"""
)
params[party_param_name] = party.name.lower()
这段代码根据关联各方筛选合同,并使用逻辑 AND,这意味着只有所有条件都满足,合同才会被包含。它会遍历提供的 parties
列表中的每个元素,并构建一个查询,其中每个条件都必须成立。
为每个当事方生成一个唯一的参数名以避免冲突。EXISTS
子句确保合约与名称包含指定值的当事方之间存在 PARTY_TO
关系。名称转换为小写以实现大小写不敏感匹配。每个条件分别添加,隐式地将它们用 AND 连接起来。
如果需要更复杂的逻辑,例如支持OR逻辑条件或允许不同的匹配标准,则输入需要更改。输入将不再仅限于简单的政党名称列表,而是需要一种指定操作符的结构化输入格式,例如。
通过处理拼写和格式上的细微差别来改善用户体验,我们还可以实现一个容许一些小错误的匹配聚会的方法。
自定义操作过滤
为了增强灵活性,我们可以引入一个作为嵌套属性的操作符对象,从而更好地管理过滤逻辑。而不是直接硬编码比较,我们定义一个操作符枚举并动态应用这个枚举。
例如,对于货币金额,合同可能需要根据总额是否大于、小于或等于指定金额来筛选。而不是假设固定的比较逻辑,我们定义一个枚举来表示这些可能的操作符。
class NumberOperator(str, Enum):
等于 = "="
大于 = ">"
小于 = "<"
class MonetaryValue(BaseModel): # 合同的总金额或价值描述
"""合同的总金额或价值描述"""
value: float
operator: NumberOperator # BaseModel 是基础模型类
if monetary_value: # monetary_value 是货币值变量
filters.append(f"c.total_amount {monetary_value.operator.value} $total_value") # 添加过滤器
params["total_value"] = monetary_value.value # 设置参数
这种方法使系统更具表现力。工具界面允许LLM不仅指定一个值,还可以指定如何进行比较,避免了僵化的过滤规则,这使处理更广泛的查询变得更加简单,同时保持LLM交互的简单性和声明性。
某些大型语言模型在处理作为输入的嵌套对象时会遇到困难,使得基于操作符的结构化筛选更加困难。添加一个_between_操作符会增加额外的复杂性,因为它需要两个独立的值,这可能在解析和输入验证时造成歧义。
最小值属性和最大值属性
为了简化,我更喜欢使用 min
和 max
属性来处理日期相关的操作,因为这自然支持范围筛选,并使得 between 逻辑更直接。
如果 min_effective_date 存在:
filters.append("c.effective_date >= date($min_effective_date)")
params["min_effective_date"] = min_effective_date
如果 max_effective_date 存在:
filters.append("c.effective_date <= date($max_effective_date)")
params["max_effective_date"] = max_effective_date
当提供了 min_effective_date
和 max_effective_date
时,此功能会添加可选的日期范围条件,根据合同的生效日期范围来过滤,确保只包括在指定日期范围内的合同。
语义搜索(语义搜索)
属性还可以用于语义搜索,在这种类型的搜索中,我们不是依赖预设的向量索引,而是采用元数据的后过滤方法。首先,应用结构化的过滤器,例如日期范围、货币值或相关方,以缩小候选范围。然后,在经过筛选的子集中进行向量搜索,根据语义的相似性来排名。
如果 summary_search 存在,
cypher_statement += (
"WITH c, vector.similarity.cosine(c.embedding, $embedding) "
"AS score ORDER BY score DESC WITH c, score WHERE score > 0.9 "
) # 设置阈值限制
params["embedding"] = embeddings.embed_query(summary_search)
否则, # 否则我们按最新日期排序
cypher_statement += "WITH c ORDER BY c.effective_date DESC "
当提供 summary_search
时,此代码执行语义搜索,计算合同嵌入与查询嵌入之间的余弦相似度,按相关性排序结果,并使用 0.9 作为阈值,过滤掉得分较低的匹配项。否则,默认按照最近的 effective_date
对合同进行排序。
动态搜索
Cypher聚合功能是我想要测试的一个实验,它赋予了LLM一定的Text2Cypher能力,使它能够在最初的结构化过滤基础上动态生成聚合。这种方法允许LLM在需要时指定计数、平均值或分组摘要等计算,而不是预先定义所有可能的聚合方式,从而使查询更加灵活和更具表达力。然而,由于这将更多的查询逻辑转移到了LLM上,确保所有生成的查询正确执行变得更具挑战性,因为错误的格式或不兼容的Cypher语句可能导致执行失败。这种灵活性与可靠性的权衡在系统设计时是关键考虑因素。
if cypher_aggregation:
cypher_statement += """WITH c, c.summary AS summary, c.contract_type AS contract_type,
c.contract_scope AS contract_scope, c.effective_date AS effective_date, c.end_date AS end_date,
[(c)<-[r:PARTY_TO]-(party) | {party: party.name, role: r.role}] AS parties, c.end_date >= 当前日期() AS active, c.total_amount as 金额, c.file_id AS 合同ID,
apoc.coll.toSet([(c)<-[:PARTY_TO]-(party)-[:LOCATED_IN]->(country) | country.name]) AS countries """
cypher_statement += cypher_aggregation
如果没有提供密文聚合信息,我们会返回识别到的合同总数和五个示例合同,以避免使结果过载。妥善处理大量数据非常重要,因为大型语言模型在处理大量数据时会显得力不从心。此外,大型语言模型生成包含100个合同标题的答案对用户来说也不是很好的体验。
cypher_statement += """WITH collect(c) AS nodes
RETURN {
total_count_of_contracts: size(nodes),
example_values: [
el in nodes[..5] |
{summary: el.summary, contract_type: el.contract_type,
contract_scope: el.contract_scope, file_id: el.file_id,
effective_date: el.effective_date, end_date: el.end_date,
monetary_value: el.total_amount, contract_id: el.file_id,
parties: [(el)<-[r:PARTY_TO]-(party) | {name: party.name, role: r.role}],
countries: apoc.coll.toSet([(el)<-[:PARTY_TO]-()-[:LOCATED_IN]->(country) | country.name])}
]
} AS output"""
此Cypher语句将所有匹配的合同汇总到一个列表中。它返回总数以及最多五个示例合同的关键属性,包括摘要、类型、范围、日期、金额,以及相关方的角色和相关的国家。
现在我们已经建成了合同搜索工具,然后我们将它交给LLM,就这样,我们实现了具有代理功能的GraphRAG。
代理基准如果你认真对待实施代理型GraphRAG系统,你需要一个评估数据集,这不仅仅是一个基准,更是整个项目的基础。一个好的数据集有助于界定系统应处理的内容范围,确保系统开发与实际应用场景相符。除此之外,它还成为评估性能的宝贵手段,让你能够衡量LLM与图的交互、信息检索和推理应用的能力。它在提示工程优化方面也至关重要,使你可以通过清晰的反馈而非猜测来迭代地优化查询、工具使用和响应格式。没有结构化的数据集,改进会变得难以量化,错误会更难以被发现。
基准测试代码可以在这里找到:GitHub。
我列出了22个问题,我们将用这些问题来评价系统的效果。除此之外,我们还将引入一个新的指标称为answer_satisfaction
,我们将提供一个定制的提示。
answer_satisfaction = AspectCritic(
name="answer_satisfaction",
definition="""你将评估一个针对法律问题的回答,根据提供的解决方案。
请在0到1的范围内评分,其中:
- 0 = 错误、内容严重不全或误导
- 1 = 正确且内容足够充分
请考虑以下评估标准:
1. 事实正确性优先 - 回答不得与解决方案相矛盾
2. 回答必须涵盖解决方案的关键要素
3. 超出解决方案的额外相关信息也是可以的,并可能增强回答
4. 如果解决方案中包含技术法律术语,应适当使用
5. 对于定量分析,必须提供准确的数字
+fewshots
"""
许多查询可以返回大量信息。例如,询问2020年前签订的合同可能会返回数百个结果。由于LLM接收到的不仅包括总数,还包括几个示例条目,因此我们的评估应侧重于总数,而不是LLM选择显示的具体示例。
基准测试结果
结果显示,所有被评估的模型(Gemini 1.5 Pro、Gemini 2.0 Flash 和 GPT-4o)在大多数工具调用上的表现相似,其中 GPT-4o 的表现稍优于 Gemini 模型(0.82 对比 0.77)。明显的差异主要出现在使用部分 Text2Cypher 功能时,特别是在执行各种聚合操作时。
请注意,这只有22个比较简单的问题,所以我们并没有真正地去测试大型语言模型的推理能力方面。
此外,我还见过一些项目,在这些项目中通过使用Python进行数据聚合,能够显著提升准确性,因为LLM通常在生成和执行Python代码方面比直接生成复杂的Cypher查询更胜一筹。
Web应用我们还建立了一个简单的React web应用程序,基于LangGraph,并托管在FastAPI上,它直接将响应流式传输到前端。特别感谢Anej Gorkic开发了这个网络应用程序。
你可以使用以下命令运行整个栈:
请在这里输入具体的命令:
运行 docker compose up
命令
然后导航到 localhost:5173
。
网页应用程序
摘要随着大型语言模型(LLMs)推理能力越来越强,当与合适的工具结合时,它们可以成为处理如法律合同这类复杂的事务的强大代理。在这里,我们只是稍微触碰了一下皮毛,只关注核心合同属性,而几乎没有涉及实际协议中那些多样化的条款。从扩展条款覆盖面到优化工具设计和交互策略,还有不小的发展空间。
代码可以在 GitHub 上找到,网址是 GitHub。
图片:本文中的所有图片均由作者绘制。
共同学习,写下你的评论
评论加载中...
作者其他优质文章