合规检查在不少行业里仍然是“人肉 + 表格 + 会议评审”的活。一个稍微复杂一点的项目,可能要同时核对几十份法规、几百个条款,还要跨文档确认引用关系。传统规则引擎只能处理写死的“如果-那么”逻辑,遇到语义模糊的合规要求就完全失灵。大模型出现后,很多人尝试直接把法规文档丢给 LLM 做判断,结果很快发现两个问题:一是模型会一本正经地编造不存在的条款,二是无法向审查方证明“这个结论到底依据的是哪一条”。CTRAG 这个名字代表的框架,正是冲着这两个问题去的。它的核心思路不是让 LLM 自由发挥,而是先用检索把相关条款找出来,再让模型基于检索到的上下文做合规判断。
这篇博客会拆开讲清楚几个问题:自动化合规检查到底难在哪;LLM 用于合规判断为什么不能“裸奔”;CTRAG 这种 In-Context Retrieval 的框架如何工作;如果自己想在项目里搭一个类似的系统,应该怎么做、需要注意哪些坑。
需要先说明的是,由于手头只有论文标题和关键词,没有完整的论文正文,本文会基于框架命名、以及检索增强生成在合规场景下的通用工程实践来做拆解。这不会影响你理解 CTRAG 的思路,因为它的关键技术点——In-Context Retrieval、基于 LLM 的合规判断、引用溯源——都是可以落到代码里的。
1. 这篇文章真正要解决的问题
先回答一个最关键的问题:自动化合规检查(Automated Compliance Checking,ACC)为什么直到现在才被 AI 领域认真对待?
因为过去的大部分自动化方案都太“脆”了。早期做法是基于规则引擎:把法规要求翻译成结构化的条件表达式。比如“建筑高度不得超过 30 米”,翻译成代码里的if height > 30: fail。这种方式对数值型条款有效,但法规里大量存在“合理的”“充分的”“不得损害公共利益”这类模糊表达,规则引擎无法建模。后来也有基于逻辑推理的方案,比如用描述逻辑对规范进行形式化建模,但建模成本极高,维护一套适用于多行业的规则库会变成另一个无底洞。
LLM 的出现改变了一个关键点:语义理解能力。模型能读懂“如果项目位于饮用水水源保护区,则不得建设排放污染物的设施”这种自然语言条款,也能判断一个项目描述是否命中该条款。但直接让 LLM 做判断有两大硬伤。第一是幻觉,模型可能引用一条不存在的法规条款来支撑结论。第二是审计不通过,合规结论必须能够被追溯,审查方要看到模型依据的是哪一版法规的哪一条,而不是一句“大模型认为合规”。
CTRAG 要解决的就是这两个问题。它用“检索相关条款 → 放入上下文 → 让 LLM 基于条款判断”的流程,把 LLM 的自由生成限制在检索到的法规上下文之内,同时把检索到的条款作为引用来源输出。这种方式在工程上意味着三件事:你不需要重新训练模型;你可以在不修改模型的情况下更新法规库;你输出的结论天然带引用,可以直接放到审计报告里。
这篇文章适合哪类读者?如果你在做合规科技、建筑信息模型审查、金融合规、数据安全合规、医疗合规等方向的系统设计,或者你想在项目里引入 LLM 但不想接受“模型自由发挥”带来的风险,那么这篇博客会给你一个可参考的框架。看完之后,你能理解 CTRAG 的模块划分、能搭出一个最小可运行的检索增强合规检查原型、也能知道它在生产落地时要面对哪些真实问题。
2. 基础概念与核心原理
2.1 什么是 Automated Compliance Checking
自动化合规检查是建筑工程、金融、数据保护、环境评估等领域的传统研究课题。它的定义很简单:用计算机系统自动判断“某个待审查对象是否符合一组规范要求”。输出通常是一个合规结论、风险等级、不合规原因列表,以及对应的依据条款。
传统 ACC 系统高度依赖专家知识的形式化表达。瑞士、挪威、新加坡等国家在建筑规范审查上有不少研究积累,很多系统把规范条文转成逻辑规则,再与设计模型做匹配。这类系统的优点是结果可解释、可复现,缺点是规则库建设成本高、规则之间可能冲突、新法规发布后需要人工更新规则。另一个缺点是它只能做“条款命中”式检查,很难处理开放式的、需要语义推理的合规问题。例如“该数据存储方案是否满足最小化收集原则”,这类判断需要理解业务场景和法规意图,传统规则系统无能为力。
这是 LLM 和检索增强框架能够切入的空白地带。LLM 能做的不是替代传统规则引擎,而是把那些无法形式化、依赖语义理解的合规判断自动化。
2.2 LLM 做合规检查的两种路线
从工程角度看,用 LLM 做合规检查有两种路线。
第一种是“长上下文直接判断”。把整部法规文档塞进 LLM 的上下文窗口,然后让模型回答问题。优点是简单,直接调用 API 即可;缺点是上下文窗口有限,法规文档动辄几十上百页,超过窗口后只能截断,截断就可能丢掉关键条款。更麻烦的是,模型对长文本后段内容的注意力会下降,检索准确率和引用准确性都不可控。另一个隐性成本是:每增加一个待审查项目,都要把整份法规重新拼进 prompt,token 成本非常高。
第二种是“检索增强判断”。先根据待审查内容检索出最相关的法规条款,只把这几条放进上下文。这就是 RAG 的基本思路。它的优势在于:法规库再大,每次参与判断的只是小部分条款;模型不需要背下所有法规,只需要基于给定的条款做推理;条款来源可以被记录和验证。CTRAG 属于这一路线的演进版本,它的重点是在“检索”和“上下文组织”层面做了更针对合规场景的强化。
2.3 In-Context Retrieval 与普通 RAG 的差异
RAG 的通用流程是:文档切块 → 向量化 → 存入向量库 → 根据用户问题检索 Top-K 块 → 拼接 prompt → LLM 生成回答。这个流程在很多问答场景已经够用,但在合规检查场景有明显不足。
普通 RAG 检索的是“文本块”,但法规条款经常是嵌套的:一个条款里面包含多个子项,某个子项又引用了另一章的定义条款。如果只检索一块文本,模型可能只看到结论看不到上下文。In-Context Retrieval 的思路是在检索阶段就为每个条款保存更丰富的上下文信息,包括条款编号、所属章节、生效版本、关联条款引用等,并将其一起送入 LLM。这样模型判断时看到的不仅仅是孤立的一句话,而是一个“可引用的条款单元”。
另外,合规场景对召回要求苛刻。漏检一条关键条款,可能导致严重的合规风险。普通 RAG 用单次向量检索,Top-K 可能漏掉语义不相似但法规上相关的条款。In-Context Retrieval 通常会在向量检索之外叠加关键词检索、同义词扩展、同义条款匹配、甚至多轮检索,让检索结果覆盖更完整。这也是 CTRAG 这类框架与通用 RAG 的核心差异:检索不追求“看起来相关”,而是追求“不能漏”。
2.4 CTRAG 的定位
从标题可以判断,CTRAG 全称至少包含 Context、Retrieval、LLM、Compliance 这些关键词。更稳妥的理解是:Compliance-oriented Contextual Retrieval-Augmented Generation,即面向合规场景的上下文检索增强生成框架。
它与通用 RAG 的差异可以总结为四点:
| 对比维度 | 通用 RAG | CTRAG 类合规框架 |
|---|---|---|
| 检索对象 | 任意文本块 | 结构化法规条款及关联信息 |
| 上下文要求 | 语义相关即可 | 必须包含条款编号、版本、引用链 |
| 判断要求 | 生成自然语言回答 | 输出合规结论、风险等级、引用条款 |
| 审计要求 | 无强制追溯 | 结论必须可追溯到具体条款 |
| 更新频率 | 文档更新较少 | 法规频繁修订,知识库需持续维护 |
这张表是理解 CTRAG 的核心。它本质上不是一个新的通用模型,而是一种把信息检索、上下文工程和 LLM 推理组合起来的系统方案。
3. CTRAG 框架的核心模块与架构拆解
将 CTRAG 抽象成一个可实现的系统,至少需要五个核心模块。
3.1 法规知识库层
这是整个框架的地基。知识库中存储的不只是法规 PDF 原始文本,而是被处理成“条款单元”的结构化数据。
普通 RAG 的文档库是“文本块的集合”,而合规知识库应该是“条款的集合”。每个条款单元至少包含:条款编号、正文内容、所属法规、发布机构、生效日期、版本号、关联条文引用。有了这些元数据,后续的引用溯源才能成立。
做这一层最容易踩的坑是:直接把 PDF 全文本塞进向量库。这样会导致检索时返回的是大段无结构文本,LLM 无法判断这些文字来自哪一条法规。因此在构建知识库时,必须预留元数据字段,并把文档切分与“条款切分”结合起来。比如根据“第X条”或“Article X”做结构化切分,而不是按固定字符数硬切。
3.2 检索层
检索层负责根据待审查内容召回相关的法规条款。工程上通常采用混合检索策略。
向量检索负责语义匹配,解决“含义相近但用词不同”的问题。关键词检索负责精确匹配,解决条款编号、专业术语、专有名词的匹配问题。二者结果做去重和合并,再按相关性排序。对于法律合规领域,还需要做查询改写:把项目描述中的口语化表达改写为法规中常见的规范术语。例如“收集用户的手机定位”可以改写为“收集个人信息中的行踪轨迹”,才能在法规库中检索到对应条款。
3.3 上下文组装层
检索到条款之后,不能直接把 Top-K 个文本块拼起来,还要组装出 LLM 可以高效利用的上下文。组装规则包括:按法规优先级排序;对存在引用关系的条款进行递归展开;在每条条款前标注来源、效力等级、版本号;控制总 token 数量在模型窗口安全范围内。
这个层是 In-Context Retrieval 的关键。如果只是把条款简单拼接,模型很可能忽略掉重要的限定条件。比如某项目符合 A 条款的表面要求,但 A 条款的例外条款规定“以下情形除外”,此时如果检索结果里没有例外条款,模型就会判断错误。因此上下文组装不仅要管“放什么”,还要管“放全没有”。
3.4 推理与验证层
推理层调用 LLM 对组装好的上下文做合规判断。理想输出不是自由文本,而是结构化 JSON,包含是否合规、风险等级、理由列表、引用条款列表。
验证层则对 LLM 的输出做二次校验:检查引用的条款 ID 是否真实存在于知识库中;检查引用条是否真的被包含在送给模型的上下文里;检查模型是否试图引用上下文之外的条款。发现异常时,可以触发重新检索或标记“无法判断”。这一层在合规审计场景中不可或缺,它能拦住一大部分幻觉输出。
4. 环境准备与前置条件
要动手搭建一个最小 CTRAG 原型,需要一个 Python 环境、一个向量数据库、一个 LLM API。
本文示例代码采用以下环境,实际版本请以项目实际情况为准:
- 操作系统:Windows / macOS / Linux 均可
- Python 3.9 以上
- 包管理:pip 或 poetry
- 向量库:Chroma(本地轻量,适合原型验证)
- 嵌入模型:OpenAI text-embedding-3-small
- 大模型:OpenAI gpt-4o-mini 或同级别模型
- 文档解析:pypdf
- 框架:LangChain 社区版本
安装依赖:
pip install openai langchain langchain-community langchain-openai chromadb pypdf tiktoken如果使用 OpenAI 接口,需要配置环境变量:
export OPENAI_API_KEY="your-api-key"Windows 下使用:
set OPENAI_API_KEY=your-api-key原型阶段建议先用小规模法规文件测试,比如选一个只有几十页的规范作为知识库。不要一开始就导入整个法律体系,否则检索效果和调试成本都会失控。
5. 核心流程拆解
一个完整的 CTRAG 合规检查流程可以拆成以下步骤。
5.1 知识库数据准备
准备好法规 PDF 或 Markdown 文件,放在统一目录中。对于 PDF 文件,需要先解析文本,注意扫描版 PDF 需要 OCR,本文不展开。把法规文件按“条款”切分,而不是按固定长度切分。这一步很关键,直接决定后续检索粒度和引用准确性。如果法规是 Markdown 格式,可以直接按“第X条”标题切。
5.2 文档切分与向量化
对每个条款单元做切分后,保留元数据:来源文件名、条款编号、页码、生效版本。然后将条款文本通过嵌入模型转换成向量,写入向量库。这里的要点是不要只存文本,一定要把条款编号和来源写入 metadata。
5.3 检索与上下文构建
输入一个待审查的项目描述,先做查询改写,然后分别在向量库和倒排索引中检索相关条款。将两路结果合并、去重,按照相关度排序取 Top-K。再根据条款之间的引用关系,把被引用的关联条款补充进来。最终形成一个有结构的上下文文本。
5.4 LLM 推理与结构化输出
把上下文和项目描述一起放入 prompt,要求模型输出符合规定格式的 JSON。系统提示词需要明确角色、判断依据边界、输出字段、以及“只能引用给定条款,不得编造条款”的红线。温度设置为 0,减少随机性。
5.5 结果验证与报告
解析模型输出,校验引用字段。如果引用了知识库中不存在的条款,就判定为幻觉输出,重新检索或返回“无法判断”。最后将合规结论、风险等级、依据条款、判断理由汇总为报告。
6. 完整示例代码实现
下面给出一个最小可运行的 CTRAG 原型代码,用于演示从法规知识库构建到合规判断的完整链路。
6.1 示例 1:构建法规知识库索引
# build_index.py import os from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma DOC_DIR = "./regulations" DB_DIR = "./compliance_db" embeddings = OpenAIEmbeddings(model="text-embedding-3-small") text_splitter = RecursiveCharacterTextSplitter( chunk_size=800, chunk_overlap=120, separators=["\n\n", "\n", "。", ";", " ", ""] ) docs = [] for file in os.listdir(DOC_DIR): if not file.endswith(".pdf"): continue loader = PyPDFLoader(os.path.join(DOC_DIR, file)) pages = loader.load() chunks = text_splitter.split_documents(pages) for chunk in chunks: chunk.metadata["source_file"] = file docs.extend(chunks) vectorstore = Chroma.from_documents( documents=docs, embedding=embeddings, persist_directory=DB_DIR ) print(f"indexed chunks: {len(docs)}")这段代码的核心作用是完成“文档 → 切片 → 向量 → 入库”。chunk_size=800是经验值,法规条款如果较长,建议按条款粒度切分而不是等长切分。chunk_overlap在这里用于减少条款边界被切断的损失,但它不能替代基于章节的切分。
6.2 示例 2:检索与上下文组装
# retrieve.py from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma DB_DIR = "./compliance_db" embeddings = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = Chroma( persist_directory=DB_DIR, embedding_function=embeddings ) retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 5} ) def build_context(query: str) -> str: docs = retriever.invoke(query) parts = [] for i, doc in enumerate(docs, 1): source = doc.metadata.get("source_file", "unknown") page = doc.metadata.get("page", "unknown") parts.append( f"[条款 {i}]\n" f"来源: {source}\n" f"页号: {page}\n" f"内容: {doc.page_content}" ) return "\n\n".join(parts) if __name__ == "__main__": context = build_context("平台采集用户地理定位数据用于个性化推荐") print(context)这个示例展示的是“检索结果如何变成可追溯上下文”。每个条款都带上来源文件和页码,目的就是让后续 LLM 输出的引用能够回到原始文件。这里的检索方式还是最基础的相似度检索。生产系统里,建议叠加 BM25 关键词检索,再用Reciprocal Rank Fusion合并结果。
6.3 示例 3:LLM 合规判断与结果解析
# check.py import json from openai import OpenAI client = OpenAI() SYSTEM_PROMPT = """你是一名合规审查专家。请基于给定的法规条款,判断项目描述是否合规。 规则: 1. 只能引用用户提供的条款,不得编造或引用外部条款。 2. 输出必须是 JSON,包含以下字段: - is_compliant: boolean - risk_level: "LOW" | "MEDIUM" | "HIGH" - reasons: string[] - citations: string[] 3. 如果给定条款不足以下结论,将 is_compliant 设为 false,并在 reasons 中说明原因。""" def compliance_check(project_desc: str, context: str) -> dict: user_prompt = f"""项目描述: {project_desc} 当前检索到的相关法规条款: {context} 请结合上述条款,分析该项目是否合规,并给出依据。""" resp = client.chat.completions.create( model="gpt-4o-mini", temperature=0, response_format={"type": "json_object"}, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ], ) return json.loads(resp.choices[0].message.content)这段代码把 LLM 的输出限制成了 JSON 格式。response_format={"type": "json_object"}是为了提高格式稳定性。temperature=0是为了让每次判断尽量一致。系统提示词里强调“只能引用给定条款”,这是控制幻觉的最直接手段,但要注意它不是万能的,后续还需要验证层。
6.4 示例 4:主流程串联
# main.py import json from retrieve import build_context from check import compliance_check if __name__ == "__main__": query = "平台采集用户地理定位数据用于个性化推荐,是否合规?" context = build_context(query) result = compliance_check(query, context) print(json.dumps(result, ensure_ascii=False, indent=2))运行:
python main.py7. 运行结果与效果验证
如果知识库中有相关法规,模型应输出类似这样的结构化结果:
{ "is_compliant": false, "risk_level": "MEDIUM", "reasons": [ "平台收集用户地理定位数据属于处理个人信息中的行踪轨迹信息,属于敏感个人信息。", "收集前未说明是否存在单独同意机制,因此存在合规风险。" ], "citations": [ "个人信息保护法-第二十八条", "个人信息保护法-第二十九条" ] }注意,这个输出是示例性质,实际输出取决于知识库中加载的法规内容和项目描述。验证一个 CTRAG 系统,不能只靠“看起来对不对”,至少要看三个维度:
第一,检索质量。审查查询对应的真实相关条款是否出现在 Top-K 中。可以人工标注一批测试查询,统计召回率。第二,判断准确率。拿一批人工标注过合规结论的项目描述,对比系统判断与人工判断的一致性。第三,引用准确率。检查模型输出的 citations 是否真实存在于知识库、是否真的被检索进入上下文。如果 outputs 中的条款 ID 没有出现在上下文里,那基本可以判定为幻觉。
如果运行失败,第一步看错误位置:如果是 API 调用报错,看是否配置了环境变量;如果是向量库加载失败,看持久化目录和嵌入模型是否一致,不同嵌入模型生成的向量不能混用;如果输出不是合法 JSON,看模型是否支持json_object输出格式,或者系统提示词是否够明确。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 检索不到相关条款 | 嵌入模型对专业术语理解不足,或法规文档未正确切分 | 打印检索 Top-K 文本,人工检查相似度分数 | 改为混合检索,加入 BM25 关键词匹配;优化切分粒度,按条款切分 |
| 模型引用不存在的条款 | 模型在生成时编造引用 | 校验 citations 字段是否都在上下文中 | 在提示词中强调禁止外部引用;增加结果验证层,过滤幻觉引用 |
| 输出 JSON 格式不稳定 | 模型变体不支持结构化输出,或提示词约束不够 | 检查返回原始文本 | 使用支持 JSON mode 的模型;增加输出格式示例 |
| 上下文超出模型窗口 | 检索结果和关联条款过多 | 统计每次请求的 token 消耗 | 控制 Top-K 数量;对关联条款做摘要;优先选择上下文更长的模型 |
| 不同法规对同一事项要求冲突 | 知识库中法规之间冲突,检索同时返回 | 人工核对冲突条款 | 建立法规优先级和版本规则,在上下文中标注优先适用关系 |
| 向量库版本升级后无法加载 | Chroma 持久化格式变更 | 查看错误日志 | 重建索引,或固定向量库版本 |
| 同一问题多次判断结果不同 | 模型温度过高或上下文顺序不稳定 | 将 temperature 设为 0;固定条款排序规则 | 统一 prompt 模板和条款排序方式 |
9. 最佳实践与工程建议
从原型走向生产,CTRAG 系统还需要考虑很多工程问题。
第一个建议是建立法规知识库的版本管理。法规会更新,条款会废止,判断结果会随着法规版本变化而变化。生产系统必须记录每条判断使用的法规版本,否则审计时无法回答“当时为什么判定合规”。建议用一个简单的regulations_version字段记录版本号,或者用单独的元数据表维护。
第二个建议是认真设计切分策略。法规条文有内在结构,按字符数硬切会破坏条款完整性,导致检索片段残缺。更推荐的做法是先解析文档的标题层级,识别“第X条”边界,再按条款切分。如果条款过长,再在条款内部做二次切分,但每个分块必须保留条款编号。
第三个建议是引入验证层,不要完全信任 LLM 输出。验证层不只是检查 JSON 是否合法,还要检查引用是否存在、引用是否在当前上下文、风险等级是否与理由一致。这一步是审计合规系统的生命线。
第四个建议是控制成本。合规审查通常不是单次问答,而是成批审查。可以对检索结果做缓存,同一份法规如果已经向量化,不需要每次重新切分。对 LLM 调用,可以考虑使用缓存命中,完全相同的项目描述和上下文直接返回历史结果。还要注意 token 消耗,一次检索上下文里放入 10 条长条款,可能消耗几千 token,批量场景下成本会线性上升。
第五个建议是建设小规模标注评估集。随便写几个测试用例只能证明“能跑通”,不能证明“测得好”。建议挑 50 到 100 个真实项目描述,人工标注合规结论和应命中的条款,作为回归测试集。每次修改切分策略、prompt 模板或检索参数,都跑一遍回归测试,防止效果回退。
第六个建议是明确系统的辅助定位。在合规场景中,LLM 判断应该作为“预审”或“辅助审查”,而不是最终决策。系统输出高风险项目时,应由专业合规人员复核。这一点不仅是工程建议,也是责任边界的判断。
10. 总结与后续学习方向
CTRAG 这类框架的价值不在于用了多强的模型,而在于把一个高风险场景中的 LLM 应用,从“不可控的自由生成”变成了“可检索、可引用、可追溯的辅助判断”。它的核心是三个动作:把法规加工成结构化条款库;通过上下文检索把相关条款送到模型面前;用结构化输出和验证层把模型的回答限制在可审计范围内。这套思路可以迁移到任何“判断结果必须给依据”的领域,比如安全审计、招聘合规、信贷审核、技术标准符合性检查。
如果你要继续深入,建议按这个顺序学习:先熟悉 RAG 的基本流程和向量检索原理;然后研究混合检索和重排序算法,比如 BM25、Cross-Encoder Rerank;再深入法律文本的结构化解析,包括条款切分、引用关系抽取;最后关注 LLM 输出可靠性,包括结构化输出、幻觉检测和评估集建设。这些方向组合起来,才是 CTRAG 能在生产环境中真正落地的基础。