合同搜索的难点通常不在于“能不能找到相关文字”,而在于能不能找到适用的合同、正确的法律语境,以及当前用户有权访问的内容。一个看似合理的搜索结果,如果来自错误的地区、合同版本或业务实体,实际使用价值可能接近于零。
AIDA 的思路是把合同检索从单纯的语义相似度匹配,提升为“语义搜索 + 元数据过滤 + 访问边界控制”。在 Amazon Bedrock Knowledge Bases 中,这通常通过隐式过滤、显式过滤和元数据增强的分块策略共同实现。
为什么只靠语义搜索不够
假设用户搜索“提前终止条款”,知识库中可能同时存在:
- 美国和欧洲不同法域下的合同;
- 主合同、补充协议和历史版本;
- 不同客户、部门或业务实体的文件;
- 用户无权查看的合同内容;
- 同一个合同中适用于不同产品或地区的条款。
向量检索可以找到语义相近的段落,却未必知道哪个结果适用于当前问题。搜索系统需要把用户问题转换成更明确的检索条件,例如法域、合同类型、生效状态、客户标识和访问范围。
这正是自动生成过滤器的价值:过滤条件不必全部依赖用户手工填写,而是可以从问题、用户上下文和系统策略中推断出来,再与用户明确指定的条件组合使用。
三层检索约束
1. 显式过滤:尊重用户明确指定的条件
用户可能会直接提出“查找 2024 年生效的德国供应商合同”。其中的“2024 年”“德国”和“供应商合同”都可以映射为结构化元数据过滤条件。
显式过滤的特点是意图清晰、可审计。系统应当保留这些条件,并在最终检索请求中明确传递,而不是只把它们留在自然语言查询里。
2. 隐式过滤:补充用户没有说出的上下文
用户往往不会在每个问题中重复说明自己的组织、角色和访问范围。例如,用户只问“这个客户的赔偿责任上限是多少”,但系统还需要根据登录身份补充:
- 当前用户所属的客户或业务实体;
- 用户被授予的合同集合;
- 当前工作区或地区;
- 允许检索的敏感级别;
- 当前生效版本,而不是已归档版本。
这些条件可以由应用层根据身份系统、合同目录和授权策略生成。它们不应完全交给模型决定,因为访问控制属于系统策略,必须能够稳定执行和审计。
3. 元数据增强分块:让每个片段带上法律上下文
合同分块不能只保存一段正文。每个 chunk 最好同时带有足以支撑过滤和解释的元数据,例如:
{
"contract_id": "CTR-1042",
"contract_type": "supplier",
"governing_law": "Germany",
"effective_date": "2024-01-01",
"status": "active",
"business_entity": "eu-procurement",
"access_group": "legal-eu",
"section": "Limitation of Liability",
"version": "3"
}
这样做有两个直接收益。第一,检索系统可以在向量相似度之外进行精确筛选。第二,生成模型看到的上下文更完整,能够减少把一个法域或版本中的条款误用于另一个法域的风险。
一个可改造的检索请求示例
下面的 Python 示例展示了应用层如何把用户明确条件和系统生成的隐式条件合并,再调用 Amazon Bedrock Knowledge Bases 的检索接口。示例中的字段名称需要根据实际导入的元数据文件和授权系统进行调整。
运行前需要配置 AWS 凭证,并设置 KNOWLEDGE_BASE_ID 与 AWS_REGION 环境变量。示例假设 Knowledge Base 使用的元数据字段支持 equals 和 in 等过滤操作。
import os
import boto3
region = os.environ.get("AWS_REGION", "us-east-1")
knowledge_base_id = os.environ["KNOWLEDGE_BASE_ID"]
client = boto3.client("bedrock-agent-runtime", region_name=region)
user_query = "What is the liability cap for this customer?"
# 这些条件来自身份系统和业务授权策略,不应由模型自由修改。
implicit_filter = {
"andAll": [
{"equals": {"key": "status", "value": "active"}},
{"equals": {"key": "business_entity", "value": "eu-procurement"}},
{"in": {"key": "access_group", "value": ["legal-eu", "procurement-eu"]}}
]
}
# 这些条件可以来自用户的明确要求,也可以由一个受约束的查询解析器生成。
explicit_filter = {
"andAll": [
{"equals": {"key": "contract_type", "value": "supplier"}},
{"equals": {"key": "governing_law", "value": "Germany"}}
]
}
filters = {"andAll": [implicit_filter, explicit_filter]}
response = client.retrieve(
knowledgeBaseId=knowledge_base_id,
retrievalQuery={"text": user_query},
retrievalConfiguration={
"vectorSearchConfiguration": {
"numberOfResults": 8,
"filter": filters
}
}
)
for result in response.get("retrievalResults", []):
score = result.get("score")
text = result.get("content", {}).get("text", "")
metadata = {
item.get("key"): item.get("value", {}).get("stringValue")
for item in result.get("metadata", [])
}
print(f"score={score:.3f} section={metadata.get('section')}")
print(text[:500])
print("-")
这个流程的关键不在于过滤器数量,而在于过滤器来源是否可靠。可以让模型负责从自然语言中提取“德国”“供应商合同”等用户意图,但身份、权限和合同状态应当由可信系统提供,并在检索前进行校验。
设计自动生成过滤器时要注意什么
保留过滤器的来源
每个过滤条件都应记录来源,例如 user_explicit、identity_policy 或 query_inference。当结果为空或用户质疑结果时,工程团队可以快速判断是用户条件过严、权限策略生效,还是查询解析出现问题。
区分“没有结果”和“没有权限”
如果系统把无权限合同直接当成不存在,用户体验更简单,但排障和审计会更困难。对最终用户可以返回统一的安全提示;对审计日志则应记录过滤器命中了哪些访问策略,以及结果数量如何变化。
控制模型推断的边界
自动生成过滤器很容易把不确定的推测伪装成确定条件。例如,用户提到“欧洲合同”,并不一定意味着适用德国法。对于高风险字段,可以要求模型输出候选值和置信度,再由规则或用户确认;对于访问权限字段,则应禁止模型自行推断。
评估过滤前后的检索质量
不要只看向量搜索的相关性分数。可以建立一组包含法域、版本、权限和合同类型的评估问题,比较以下指标:
- 正确合同是否出现在前几条结果中;
- 是否返回了错误法域或过期版本;
- 是否出现越权内容;
- 用户是否能根据元数据理解结果适用范围;
- 过滤过严导致的空结果比例。
落地检查清单
- 为每个合同 chunk 设计稳定、可过滤的元数据字段;
- 将用户明确条件与系统隐式条件分开管理;
- 从身份和授权系统生成访问过滤器;
- 对法域、合同版本和生效状态设置明确规则;
- 记录过滤器来源、检索请求和审计信息;
- 为错误法域、历史版本和越权访问建立专门测试集;
- 对空结果提供可诊断的内部信息,并对外保持安全的提示。
自动生成过滤器并不能替代合同专家或授权系统。它真正解决的是检索范围定义问题:让语义搜索在正确的法律语境和访问边界内工作。对于合同这类高风险知识库,元数据质量、权限策略和评估集往往与嵌入模型本身同样重要。