GraphRAG智能体的SHACL TDD:一条可阻止不良行为的执行上限规则
将限制责任比例在30%以内的高管审批政策编码为SHACL格式,使用pytest进行验证,然后在价值230万美元的合同演示中观察本体防火墙如何阻止该智能体的操作。
阅读架构说明与实际部署治理规则并非同一回事。本文在针对一份价值230万美元的样本协议对普通RAG、GraphRAG以及结合OWL/SHACL/policy技术的GraphRAG进行了三方比较之后,重点探讨了一项可迁移的技能:将某项业务规则编码为特定结构,通过自动化检查来验证它,进而让智能体拒绝继续执行操作。
Ontology RAG Firewall仓库中包含了此处使用的cont:词汇表、结构文件以及离线演示版本。请克隆该仓库,确认main分支上的所有内容均为正常状态,之后可根据需要回放旧版本的提交记录,亲身体验红绿状态循环的过程。
确认main分支的基准状态
git clone https://github.com/cloudbadal007/ontology-rag-firewall
cd ontology-rag-firewall
pip install -e ".[dev]"
pytest -q # 18 passed (full suite)
python examples/demo_offline.py
如果文章中最后验证过的提示更为重要,可选择固定版本号为6318929;因为main分支的提示可能已经更新了。
正常的运行结果显示18项已通过。离线演示应在赔偿条款部分显示针对管理层的警告,内容大致如下:
Safe to act: 🚫 NO
- Flagged: 5
...
⚠️ EXECUTIVE APPROVAL: Liability cap is below 30% of contract value. Cap ratio: 25.00%. Agent action requires executive sign-off.
...
AGENT ACTION: HALTED. Routed to human review queue.
Total value protected: $2,300,000
这一警告正是新格式所引入的。其余部分则说明了它是如何通过以测试为先的开发方式实现的。
正在被编码的策略
目前已有11种节点格式存在于contract_domain_shacl.ttl中,涵盖支付条款、通知期限、正常运行时间服务级别协议、低可信度数据提取、补救措施缺失问题、自动续订、缺失的赔偿条款表述、直接损失审核、10%的价值上限比例、高价值但绝对上限较低的情况,以及本演示中重点介绍的管理层比例格式。
在演示交易中,57.5万美元的上限是230万美元的25%,高于10%的下限,因此旧的比率规则不会起作用。而高级规则则弥补了高价协议中的这一缺陷。
用通俗的话来说,采购部门还有额外要求:
只要协议金额至少为50万美元,且赔偿上限低于该金额的30%,代理人就必须在采取行动前获得高级审批。
这条规则会体现为ExecutiveCapRatioShape以及对应的两个pytest测试用例。
第一步——先让测试失败
始终要在TTL之前编写断言代码。
在当前的main分支中,这些检查已经通过。若想体验失败情况,请切换到bbeb15e(规则生成前的版本),插入测试用例,看到红色提示后加入第二步中的规则,最后再回到main分支。
在 tests/test_shacl_constraints.py 中添加或比较此案例:
def test_liability_cap_below_30_percent_on_high_value_contract() -> None:
"""
25% cap on a $2.3M contract must trigger ExecutiveCapRatioShape.
Existing shapes (10% ratio, $100K absolute) do not catch 575K / 2.3M.
"""
clause = ExtractedClause(
"test-cap-ratio",
"LiabilityClause",
"text",
{"liabilityCap": 575_000, "liabilityScope": "DirectDamagesOnly"},
0.9,
1,
)
graph = ClauseRDFBuilder().build(clause, 2_300_000)
conforms, violations, _ = SHACLContractValidator().validate(graph)
assert not conforms
assert any(
"30%" in v or "executive" in v.lower() for v in violations
), violations
def test_liability_cap_at_32_percent_no_executive_flag() -> None:
"""32.6% cap on $2.3M should not trigger the 30% executive rule."""
clause = ExtractedClause(
"test-cap-ratio-ok",
"LiabilityClause",
"text",
{"liabilityCap": 750_000, "liabilityScope": "FullDamages"},
0.9,
1,
)
graph = ClauseRDFBuilder().build(clause, 2_300_000)
_, violations, _ = SHACLContractValidator().validate(graph)
cap_ratio_hits = [
v for v in violations if "30%" in v or "executive" in v.lower()
]
assert len(cap_ratio_hits) == 0, cap_ratio_hits
执行操作:
pytest tests/test_shacl_constraints.py::test_liability_cap_below_30_percent_on_high_value_contract -v
红色的样子
在形状存在之前(例如在 bbeb15e 上):
FAILED tests/test_shacl_constraints.py::test_liability_cap_below_30_percent_on_high_value_contract
AssertionError: ... executive ...
在当前的 main 版本中,相同的调用会显示为绿色。接下来是 TTL 本身——它已被合并到上游代码中,且经过复现以便该模式可重复使用。
步骤 2 — 创建形状的元数据
将其添加到 ontologies/contract_domain_shacl.ttl 中。通过原始的 GitHub IRI 保持 cont: 指向 OWL 命名空间(避免创建无法解析的 /contract# 路径):
https://raw.githubusercontent.com/cloudbadal007/ontology-rag-firewall/main/ontologies/contract_domain_owl.ttl#
实例构建器会在相同的基础路径下生成 URI(…#instance/)。
要重新使用 bbeb15e 版本吗?请直接复用该版本 SHACL 文件中已存在的前缀 URI。在 main 主线中,建议使用原始 IRI,这样本体、形状和测试才能保持一致。
cont:ExecutiveCapRatioShape a sh:NodeShape ;
sh:targetClass cont:LiabilityClause ;
sh:severity sh:Warning ;
sh:message "⚠️ EXECUTIVE APPROVAL: Liability cap is below 30% of contract value. Cap ratio: {?capRatio}%. Agent action requires executive sign-off." ;
sh:sparql [
a sh:SPARQLConstraint ;
sh:select """
PREFIX cont: <https://raw.githubusercontent.com/cloudbadal007/ontology-rag-firewall/main/ontologies/contract_domain_owl.ttl#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT $this ?capRatio WHERE {
?contract cont:hasLiabilityClause $this ;
cont:contractValue ?v .
$this cont:liabilityCap ?cap .
BIND((xsd:decimal(?cap) / xsd:decimal(?v) * 100) AS ?capRatio)
FILTER (xsd:decimal(?v) >= 500000)
FILTER (?capRatio < 30)
}
""" ;
] .
以下三种设计选择都是经过深思熟虑的:
FILTER中设定的值要求为>= 500000,旨在将规则应用到高价值交易上;同样的百分比在 5 万美元的合同中所代表的含义则不同。- 在人工消息中嵌入
{?capRatio},能让审核人员看到具体的百分比数值,而非模糊的警告信息。 - 30% 的阈值是组织内部的政策规定——如果法务部门要求改为 40%,则直接修改该数值即可。该文件本身就是政策依据。
第 3 步 —— 将违规项对应到可理解的条款
这些结构会引发机器违规;防火墙会将关键词匹配结果转换为条款级的报告行。在main中,EXECUTIVE早已被列在firewall.py中的责任标记之中:
"LiabilityClause": ["LIABILITY", "LOW CONFIDENCE", "LEGAL REVIEW", "HIGH-VALUE", "EXECUTIVE"],
在重新运行bbeb15e时,需将该标记与对应结构一同添加——否则演示程序可能会在未将其关联到赔偿条款的情况下计算出违规情况。
第4步 — 重新运行测试套件与演示
pytest -q # 18 passed (entire repo)
pytest tests/test_shacl_constraints.py -v # 8 passed (this file)
python examples/demo_offline.py
整个测试套件应在几秒钟内完成。演示报告会在赔偿条款部分新增专门的执行行(当多个违规情况共享同一条款时,标记数量可能保持不变):
⚠️ EXECUTIVE APPROVAL: Liability cap is below 30% of contract value. Cap ratio: 25.00%. Agent action requires executive sign-off.
AGENT ACTION: HALTED. Routed to human review queue.
Total value protected: $2,300,000
一项策略已被编码、验证且可见。正是这个循环使得其能够扩展到下一个领域规则。
为何不能“直接提示它”?
将同样的30%指导原则硬塞进系统提示语中,会在律师重新表述条款、指令淹没在冗长上下文中、有人在不了解合规背景的情况下编辑提示语,或是审计人员询问某天使用了哪条规则时失效。
在类型化的RDF中,结构是确定性的,它处于版本控制之下,配有回归测试,能够输出包含测量比例在内的结构化证据,并且不会因为有人追求其他方面的流畅度而消失。
对于代理系统而言,除了检索和生成功能外,还需要正式的约束机制。政策体现在结构中;证据存在于测试中;违规记录则是审计结果。
可重复的扩展方法
docs/extending.md中有详细说明;简述如下:
- 用合规负责人能理解的语言表述规则。
目标状态:每个形状都有对应的测试;每项测试都能映射到具体的业务影响。法律条款设置有效期限;持续集成系统负责执行验证;整体架构始终处于可审查状态。
超越单一协议的路线图
目前的防火墙仅支持单一协议路径。仍存在两个待解决的生产缺陷:一是跨整个产品组合的批量汇总功能(examples/demo_batch_processing.py可作为示例),二是当属性图建立后所需的供应商多跳上下文功能(用于存储相关数据和事件),而非临时的URL链接。
在此之前,坚持循环实践:编写形状定义、进行验证、观察结果,再将相同模式应用到下一个领域。
为每种数据结构维护一个辅助账本,记录其所有者、生效日期、来源备忘录编号以及pytest节点编号,这样git blame输出就能转化为完整的审计轨迹。当阈值发生变化时,需对相关消息字符串进行版本控制,并添加边界测试,防止旧的截止标准再次悄悄生效。应将关键词与子句的映射关系视为API接口:在CI环境中截取演示输出,确保在进行代码重构时,若删除了赔偿条款列表中的EXECUTIVE关键字,测试会立即失败。相比修改共享的SPARQL数据块,更应采用可叠加的数据结构;这样在政策实验失败时,各个独立的数据结构能够干净地恢复原状。最后,应在所有面向用户的警告信息中公布实测比例——审核人员更愿意相信那些可以从RDF数据重新计算得出的数字,而非笼统的“需审批”提示。