首页 / 文章 / 《实用笔记》:超越基础RAG——构建生产级法律研究系统

《实用笔记》:超越基础RAG——构建生产级法律研究系统

《实用笔记》操作指南:超越基础RAG——构建生产级法律研究系统:适用于采用该模式的团队的合同、校验功能及可插入代码模块。

3979 词

本指南将逐步构建从原始材料到可运行系统的完整流程,内容来自《超越基础RAG:打造生产级法律研究助手(第一部分)》。重点在于可操作的步骤、明确的检查点,以及可直接放入代码仓库的代码,无需猜测其用途。 在概览阶段,应在修改代码之前明确输入内容、各步骤的负责人以及完成标准。操作人员应能够从已知的检查点重新运行相应步骤,而无需推测隐藏状态。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理都是产品不可或缺的部分,而非后续需要补充的功能。

1. 数据导入:将法律PDF转换为结构化内容

在处理“1次数据摄入并转换为一个阶段”的任务时,首先需明确相关规范:所需的输入参数、成功信号,以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改始终符合预期。 相比冗长的脚本,应优先选择小型且可测试的单元。当某个步骤失败时,故障应能指向具体的责任模块,而非整个复杂的处理流程。 在调整提示词之前,需先使用固定的问题集来评估检索的覆盖率。仅仅更换提示词往往无法解决检索效果不佳的问题。

!mineru -p c.pdf -o output --dump-content-list -b pipeline -l en

2.分块处理

在处理“分块”阶段的时,首先写下契约:所需的输入、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 将这一阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,杜绝无声的半完成状态。 在调整提示词之前,先使用固定的问题集来测试召回率。仅仅更换提示词很难改善较差的检索效果。

Structured document
        ↓
Heading-aware parent chunks
        ↓
Semantic child chunks

为何两级策略很有用

在制定两级策略的“原因分析”阶段时,首先需明确相关约定:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在系统从演示环境过渡到共享环境时出现意外费用。 在调整提示词之前,先使用固定的问题集测试系统的召回率。仅仅更换提示词往往无法改善较差的检索效果。 在制定两级策略的“原因分析”阶段时,首先需明确相关约定:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 需同时记录正常流程和故障恢复流程。重试机制、人工审核环节以及错误处理措施都是产品本身的组成部分,而非后续需要补充的功能。

Query
  ↓
Retrieve focused child
  ↓
Read parent_id
  ↓
Return complete parent context

创建具备标题意识的父块

将“创建具备标题意识的父块”这一阶段视为可衡量的工作面会更为有效。在扩大范围之前,先记录一份理想的转录样本、一个失败案例以及回滚说明。 优先选择小型且可测试的单元,而非庞大的脚本。当某个步骤出现故障时,故障应指向单一责任点,而非复杂的流程链。 将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应迫使重新编写另一项。

data/processed/cr.md
data/extracted/auto/blockpage.json
code/chunking_parent.py

步骤1:读取两种提取结果

第一步中的“阅读两个阶段内容”这一操作,若将其视为可度量的指标会更为有效。在扩大范围之前,先记录一个成功的案例、一个失败的案例以及回滚说明。 将这一阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,绝不允许出现悄无声息的半完成状态。 应将分块策略与检索策略分开。当质量指标发生变化时,调整其中一项不应强制要求重新编写另一项。

md_text = MARKDOWN_PATH.read_text(encoding="utf-8")
with BLOCKPAGE_PATH.open("r", encoding="utf-8") as file:
    blockpage = json.load(file)

第二步:将 Markdown 分割成块

将“第二步:划分阶段”视为可度量的对象来处理时效果最佳。在扩大范围之前,需记录一个理想案例、一个失败案例以及回滚说明。在功能结果旁还需记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在系统从演示环境过渡到共享环境时出现意外费用。应将分块策略与检索策略分开,当质量指标发生变化时,修改其中一项不应迫使重新编写另一项。将“第二步:划分阶段”视为可度量的对象来处理时效果最佳。在扩大范围之前,需记录一个理想案例、一个失败案例以及回滚说明。需同时记录正常流程与恢复流程,重试机制、人工审核环节以及死信处理都属于产品功能的一部分,而非后续需要补充的内容。

for raw_block in md_text.split("\n\n"):
heading
paragraph
list
table
superscript
blank
HEADING_RE = re.compile(r"^(#{1,6})\s+")

第三步:跟踪当前处理流程

在第三步“跟踪阶段”中,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。相比冗长的脚本,更应采用小型且可测试的单元。当某个步骤失败时,故障原因应指向单一责任模块,而非复杂的流程链。必须引用实际作为答案依据的段落;没有引用的话,操作人员就无法区分是虚假信息还是索引缺失所致。

def find_heading(heading_text: str):
    nonlocal search_pos
    for index in range(search_pos, len(blockpage)):
        block = blockpage[index]        if (
            block.get("type") == "text"
            and block.get("text") == heading_text
            and "text_level" in block
        ):
            search_pos = index + 1
            return block

第四步:收集相关内容

在第四步“收集相关数据”阶段,应在修改代码之前明确输入内容、该步骤的负责人以及完成标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将此阶段视为输入与已验证输出之间的契约。为相关成果命名,定义成功判定标准,并拒绝默许的半完成状态。 需引用实际作为答案依据的段落。没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。

Heading
  ├── Paragraph
  ├── Clause
  ├── List
  └── Table
        ↓
     Parent chunk

第五步:分配稳定元数据

在第五步“分配稳定阶段”中,修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。应在功能结果旁记录执行时间以及令牌或查询成本。提前显示成本信息,可避免在流程从演示环境转向共享环境时出现意外费用。需注明实际作为答案依据的段落;没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。在第五步“分配稳定阶段”中,修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理措施都是产品本身的组成部分,而非后续需要补充的内容。

{
    "parent_id": f"parent_{len(chunks):04d}",
    "doc_id": DOC_ID,
    "heading_path": heading_path.copy(),
    "text": "\n\n".join(stack),
}
{
  "parent_id": "parent_0164",
  "doc_id": "constitution_of_india",
  "heading_path": ["PART XII"],
  "text": "# PART XII\n\nFINANCE, PROPERTY, CONTRACTS AND SUITS..."
}
PART XII — Finance
Article 264 — Interpretation
Article 265 — Taxes not to be imposed without authority of law
Article 266 — Consolidated Funds and public accounts
Article 267 — Contingency Fund

创建语义子块

在“创建语义子块”阶段工作时,首先列出相关规范:所需输入、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 优先选择小型、可测试的单元,而非冗长的脚本。当某个步骤失败时,故障应指向单一责任点,而非复杂的流程链。 在调整提示词之前,先使用固定的问题集来测试召回率。频繁更换提示词往往无法改善较差的检索效果。

code/semantic-children.ipynb

步骤1:将父内容拆分为结构化块

在完成“第一步:解析”阶段时,首先需写下相关契约:所需的输入参数、成功信号,以及部分失败时的处理方式。这样的清单能确保后续的代码修改始终符合约定。 将此阶段视为输入与验证后输出之间的契约。为相关成果命名,明确成功判定标准,杜绝无声的半完成状态。 在调整提示词之前,先使用固定的问题集来测试召回率。仅仅更换提示词往往无法解决检索效果不佳的问题。

table_pattern = re.compile(
    r"(<table[\s\S]*?</table>)",
    re.IGNORECASE
)

第二步:为内容添加标题

在完成“第2步:添加标题”这一阶段时,首先需写下相关规范:所需的输入参数、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改始终符合既定要求。 在功能结果旁记录处理时间以及令牌或查询成本。提前了解成本情况,可避免在系统从演示环境过渡到共享环境时出现意外费用。 在调整提示词之前,先使用固定的问题集测试系统的召回率。仅仅更换提示词往往无法改善较差的检索效果。 在完成“第2步:添加标题”这一阶段时,首先需写下相关规范:所需的输入参数、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改始终符合既定要求。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理措施都是产品本身的组成部分,而非后续需要补充的功能。

if pending_heading:
    block.text = pending_heading + "\n\n" + block.text
    pending_heading = None

步骤3:比较相邻段落

在将步骤3视为可度量的对象时,比较相邻段落的方法效果最佳。在扩大范围之前,先记录一份理想的文本样本、一个失败案例以及回滚说明。 优先选择小型且可测试的单元,而非庞大的脚本。当某个步骤失败时,故障应指向单一责任点,而非复杂的流程链。 将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应迫使重新编写另一项。

embedder = SentenceTransformer(
    "BAAI/bge-base-en-v1.5"
)
similarity = cosine_similarity(
    emb1,
    emb2
)[0][0]
Similarity threshold: 0.40

步骤4:应用大小与结构约束

在将第四步“应用尺寸”视为可测量的表面时,其效果最佳。在扩大范围之前,先记录一个成功的案例、一个失败案例以及回滚说明。 将此阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,绝不允许出现悄无声息的半完成状态。 将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应强制要求重新编写另一项。

Preferred minimum size: 800 characters
Maximum combined size: 1,600 characters
Tiny-block threshold: 150 characters
if (
    current_type == BlockType.TABLE
    and block.type != BlockType.TABLE
) or (
    block.type == BlockType.TABLE
    and current_type != BlockType.TABLE
):
    is_under_min = False
Document structure
        +
Semantic similarity
        +
Minimum and maximum sizes

第五步:保持父子关系

将第5步“保留测试环境”视为可度量的对象来处理效果最佳。在扩大范围之前,需记录一个理想运行案例、一个故障案例以及回滚说明。 在功能结果旁同时记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。 应将分块策略与检索策略分开。当质量指标发生变化时,调整其中一个不应迫使重新编写另一个。 将第5步“保留测试环境”视为可度量的对象来处理效果最佳。在扩大范围之前,需记录一个理想运行案例、一个故障案例以及回滚说明。 需同时记录正常流程与恢复流程。重试机制、人工审核环节以及死信处理都是产品的一部分,而非后续需要补充的内容。

{
    "parent_id": parent["parent_id"],
    "child_id": f"{parent['parent_id']}_child_{index}",
    "doc_id": parent.get("doc_id", ""),
    "heading_path": parent.get("heading_path", []),
    "text": chunk["text"],
    "type": chunk.get("type", "mixed"),
    "embedding": encode_text(chunk["text"])
}
parent_0164
│
├── parent_0164_child_1
│   Article 264 and introductory Finance context
│
├── parent_0164_child_2
│   Articles 265 and 266 concerning taxation and public funds
│
└── parent_0164_child_3
    Article 267 concerning the Contingency Fund
{
  "parent_id": "parent_0164"
}
{
  "child_id": "parent_0164_child_2",
  "parent_id": "parent_0164",
  "doc_id": "constitution_of_india",
  "heading_path": ["PART XII"],
  "type": "list",
  "text": "265. Taxes not to be imposed save by authority of law...",
  "embedding": [0.012, -0.034, 0.021]
}

3. 混合检索:从查询到上下文

在采用混合检索方式时,应在修改代码之前明确输入内容、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 相比冗长的脚本,更应优先使用小型且可测试的单元。当某个步骤失败时,故障原因应能指向单一责任点,而非复杂的流程问题。 需引用实际作为答案依据的段落。没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。

为何仅一种检索方法不够

在“原因检索方法”阶段,修改代码之前需明确输入内容、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将此阶段视为输入与经过验证的输出之间的契约。为相关成果命名,定义成功判定标准,并拒绝默许的半完成状态。 需引用实际作为答案依据的段落。没有引用的话,操作人员就无法区分幻觉内容与索引缺失问题。

使用BM25进行稀疏检索

在修改代码之前,针对基于BM25的稀疏检索阶段,需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 除了功能结果外,还需记录处理时间以及令牌或查询成本。提前显示成本信息可以避免在系统从演示环境切换到共享环境时出现意外费用。 需注明实际用于生成答案的对应内容。如果没有引用依据,操作人员就无法区分是幻觉结果还是索引缺失导致的错误。 在修改代码之前,针对基于BM25的稀疏检索阶段,需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理方式都是产品功能的一部分,而非后续需要补充的内容。

在处理基于BGE的密集检索阶段时,首先明确相关约定:所需的输入参数、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 优先选择小型且易于测试的模块,而非结构复杂的脚本。当某个步骤出现故障时,故障应指向单一责任点,而非错综复杂的流程。 在调整提示词之前,先使用固定的问题集来评估召回率。仅仅更换提示词很难改善较差的检索效果。

embedding_text = heading_path + child_text

通过评估选择嵌入模型

在“选择嵌入模型”阶段工作时,首先列出相关约定:所需输入、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 将此阶段视为输入与验证后输出之间的契约。为相关成果命名,明确成功判定标准,杜绝无声的半完成状态。 缓存稳定的系统指令和工具架构。重复发送相同的开头信息是造成资源浪费的常见原因。

query_text = (
    "Represent this sentence for searching relevant passages: "
    + user_query
)

构建 FAISS 索引

在构建 FAISS 索引的阶段,首先需明确相关约定:所需的输入参数、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 在记录功能结果的同时,还需标注执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。 在调整提示词之前,应先使用固定的问题集测试召回率。仅仅更换提示词很难改善较差的检索效果。 在构建 FAISS 索引的阶段,首先需明确相关约定:所需的输入参数、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 需同时记录正常流程和故障恢复流程。重试机制、人工审核环节以及错误处理措施都是产品不可或缺的部分,而非后续需要补充的内容。

index = faiss.IndexHNSWFlat(
    embedding_dimension,
    32,
    faiss.METRIC_INNER_PRODUCT,
)
index.hnsw.efConstruction = 200
index.hnsw.efSearch = 64
index.add(embeddings)

结合两种排名方式

将“结合两种排名方式”这一阶段视为可度量的工作面最为有效。在扩大范围之前,先记录一个成功的案例、一个失败案例以及回滚说明。相比庞大的脚本,应优先选择小型且可测试的单元。当某个步骤出现故障时,故障原因应能明确指向某个具体的责任主体,而非复杂的流程链。应将分块策略与检索策略分开处理;当质量指标发生变化时,修改其中一项不应迫使另一项也必须重新编写。

RRF score = Σ 1 / (k + rank + 1)
sparse_results = bm25_search(query)
dense_results = dense_search(query)
fused_candidates = reciprocal_rank_fusion(
    sparse_results,
    dense_results,
)

使用交叉编码器进行重新排名

将带有交叉编码器的重排阶段视为可度量的流程时效果最佳。在扩大范围之前,先收集一份理想的转录文本、一个失败案例以及回滚说明。把这一阶段视为输入与经过验证的输出之间的契约,为相关成果命名,明确成功标准,杜绝默许的半完成状态。应将分块策略与检索策略分开,当质量指标发生变化时,修改其中一项不应强制要求重写另一项。

pairs = [
    (query, child_text),
    ...
]
scores = reranker.predict(pairs)
candidates = fused_candidates[:30]
reranked_children = cross_encoder.rank(query, candidates)
selected_children = [
    child
    for child in reranked_children[:20]
    if child.score >= 0.30
]

将选定的子项扩展到父上下文中

将“将选定的子项扩展到测试阶段”这一流程视为可度量的对象时,效果最佳。在扩大范围之前,需记录一个理想案例、一个失败案例以及回滚说明。 在功能结果旁同时记录处理时间以及令牌或查询成本。提前了解成本情况,可避免在流程从演示环境转向共享环境时出现意外费用。 应将分块策略与检索策略分开。当质量指标发生变化时,调整其中一个不应强制要求重新编写另一个。 将“将选定的子项扩展到测试阶段”这一流程视为可度量的对象时,效果最佳。在扩大范围之前,需记录一个理想案例、一个失败案例以及回滚说明。 需同时记录正常流程与故障恢复流程。重试机制、人工审核环节以及死信处理都是产品本身的组成部分,而非后续需要补充的功能。

{
  "child_id": "parent_0082_child_1",
  "parent_id": "parent_0082",
  "text": "23. Prohibition of traffic in human beings and forced labour..."
}
parent_id = selected_child["parent_id"]
parent = parent_lookup[parent_id]
Selected child 1 ──┐
Selected child 2 ──┼── parent_0082
Selected child 3 ──┘

4. 查询扩展与多跳查询

在处理查询扩展阶段时,应在修改代码之前明确输入内容、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。相比冗长的脚本,更应采用小型且可测试的单元。当某个步骤失败时,故障原因应能指向具体的责任主体,而非复杂的流程问题。必须引用那些真正作为答案依据的段落;没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。

Kaggle评估揭示了什么

在Kaggle评估阶段,应在修改代码之前明确输入内容、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将这一阶段视为输入与经过验证的输出之间的契约。为相关成果命名,定义成功判定标准,并拒绝默许的半完成状态。 需引用实际作为答案依据的段落。没有引用的话,操作人员就无法区分幻觉内容与索引缺失问题。

在不改变查询意图的情况下扩展查询

对于“无阶段扩展查询”功能,应在修改代码之前明确输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 在功能结果旁记录执行时间以及令牌或查询成本。提前显示成本可避免在流程从演示环境转向共享环境时出现意外费用。 需注明实际作为答案依据的段落。没有引用的话,操作人员就无法区分是幻觉内容还是索引缺失导致的错误。 对于“无阶段扩展查询”功能,应在修改代码之前明确输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理措施都是产品本身的组成部分,而非后续需要补充的内容。

查询扩展与子查询生成并非同一概念

在处理查询扩展阶段时,首先需明确相关约定:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。相比庞大的脚本,应优先选择小型且可测试的单元。当某个步骤出现故障时,故障点应指向单一责任模块,而非复杂的流程链。在调整提示词之前,需先使用固定的问题集来评估召回率——仅仅更换提示词往往无法改善较差的检索效果。

最终的查询策略

在处理“生成的查询策略”阶段时,首先写下相关契约:所需的输入参数、成功标志,以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 将这一阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功判定标准,杜绝无声的半完成状态。 在调整提示词之前,先使用固定的问题集来衡量召回率。仅仅更换提示词很难改善较差的检索效果。

总结

在进入总结阶段时,首先写下接口规范:所需的输入参数、成功标志以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前明确成本信息,可避免从演示环境过渡到共享环境时出现意外费用。 在调整提示词之前,先用固定的问题集测试系统的召回率。仅仅更换提示词很难改善较差的检索效果。 在进入总结阶段时,首先写下接口规范:所需的输入参数、成功标志以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理措施都是产品本身的组成部分,而非后续需要补充的功能。

操作检查清单

将操作检查清单视为可度量的指标,这样会更有效。在扩大范围之前,先记录一份最佳实践案例、一个故障实例以及回滚说明。

将配置与应用程序代码分开。环境文件、密钥存储和功能开关应集中存放,以便操作人员无需查看全部内容即可进行审计。

将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应迫使重新编写另一项。

在预算允许的情况下,使用测试数据而非真实的付费 API,在持续集成过程中添加用于检测关键路径的冒烟测试。

同时记录正常流程和恢复流程。重试机制、人工审核环节以及错误处理都是产品的一部分,而非后续需要补充的内容。

应将分块策略与检索策略分开。当质量指标发生变化时,修改其中一项不应强制重新编写另一项。

在升级该技术栈之前,先冻结版本,为关键流程保存标准转录文本,并明确回滚步骤。共享环境需要设置速率限制、租户验证机制,以及明确的密钥轮换负责人。与其追求花哨的一次性演示,不如注重扎实的可靠性。

关于8a4b50ff4af2的批注:不要将提供商密钥放入代码仓库,为每个会话设置令牌上限,并将转录文本与评估用文件存放在同一位置,以便后续模型更换时保持数据可比性。