【开源chunk】超越chonkie 达到人工级chunk效果

1.背景

chunk是rag等流程的第一步,chunk的好坏,直接决定了rag、图谱提取等方法的质量。结构合理的chunk,能提供更完整的信息,同时提供兄弟节点扩展、父节点寻找等功能。

在使用chonkie的过程中,我发现chonkie的绝大部分方法都不靠谱,对于国家标准等结构化文档,都无法做到合理划分 。chonkie中的大模型切分效果稍微好一点,但也不尽如人意。当使用8b等小模型时,原始chonkie的处理效果非常差,几乎没有完整、合理的分段

因此,我自己实现了一个基于qwen的方法,大概意思就是"给段落添加或识别标题,利用标题完成切分" ,这个方法可以显著提升处理效果,同时实现对结构化文档和非结构化文档的处理。对于结构化文档,可以将其中已有的标题作为分段边界(也可以参照非结构化文档添加标题,在一些情况下效果可能更好),对于结构化文档,可以向其中添加原本不存在的标题。

我的方法可以保证8b模型,也能得到较为满意的结果。当使用plus等模型且采用第7章中的代码,效果几乎可以达到真人水平。


2.结构化文档处理效果

对于结构化文档(我把两篇关于保险的文章拼到了一起,分界线就在"4000元智驾保险是否存在"那里),使用qwen-plus模型的初步的效果如下。可以看到"[level1] # 4000元智驾保险是否存在?"这一句,正确识别到了语义变化,其余的level2和level3,效果也不错。

现在使用的是第6章中的代码,如果需要更好的处理效果,可以也参照第7章的代码 ,将结构化文本和非结构化文本,按照同样的逻辑进行处理,在一些情况下效果可能更好

python 复制代码
[level2] ## 建议

[level3] 1. 发生智驾事故时,应立即报警并通知保险公司(95590),按常规交通事故流程处理
[level3] 2. 保留车辆智能驾驶系统的数据记录,作为责任认定的辅助证据
[level3] 3. 如怀疑是车辆智能系统故障导致事故,除保险理赔外,可向车辆制造商追责
[level3] 4. 关注保险行业对智能驾驶保险条款的更新,未来可能会有更针对性的保障产品

[level1] # 4000元智驾保险是否存在?

经过仔细检查您提供的所有保险文件,我没有发现任何关于"4000元智驾保险"的具体内容。以下是详细分析:

[level3] 1. 交强险保单(PDFA25110104240000003124):
   - 保险费合计950元
   - 为标准交强险,无任何智驾相关特殊条款或特别约定

[level3] 2. 商业险保单(PDEN25110104240000001456):
   - 保险费合计4,943.99元,包含:
     - 新能源汽车损失保险(保费3,228.60元)
     - 新能源汽车第三者责任保险(保费1,564.28元)

使用qwen3-8b的时候,处理效果如下,有个别地方出错了。chonkie中,即便使用qwen-plus模型,效果也无法与下面的结果相比。

python 复制代码
[level2] ## 建议

1. 发生智驾事故时,应立即报警并通知保险公司(95590),按常规交通事故流程处理
2. 保留车辆智能驾驶系统的数据记录,作为责任认定的辅助证据
3. 如怀疑是车辆智能系统故障导致事故,除保险理赔外,可向车辆制造商追责
4. 关注保险行业对智能驾驶保险条款的更新,未来可能会有更针对性的保障产品

[level2] # 4000元智驾保险是否存在?

经过仔细检查您提供的所有保险文件,我没有发现任何关于"4000元智驾保险"的具体内容。以下是详细分析:

[level3] 1. 交强险保单(PDFA25110104240000003124):
   - 保险费合计950元
   - 为标准交强险,无任何智驾相关特殊条款或特别约定

[level3] 2. 商业险保单(PDEN25110104240000001456):
   - 保险费合计4,943.99元,包含:
     - 新能源汽车损失保险(保费3,228.60元)
     - 新能源汽车第三者责任保险(保费1,564.28元)

3.非结构化文档处理效果

使用qwen-plus模型的处理效果如下。这里的level1、2、3,都是模型自己添加的。

python 复制代码
[level1] 反无人机系统的使用争议
上半年有次饭局,听一位老师在劝另一位朋友,不要上反无人机系统。

他说现在技术很成熟,反无人机系统的效果也很好,审批也不算特别麻烦,你只要有足够的理由报部委,就能拿到安装许可。供货就更简单了,现在多数反无人机系统都是民品,你拿到批文就有人上门来给你推销。

但还是不推荐你装。

[level2] 反无人机系统的两种技术路线
现在的反无人机系统有两条技术路线。

[level3] 卫星定位干扰系统
一是卫星定位干扰,一定范围内屏蔽卫星定位信号,无人机飞过来就丢导航,要么炸机,要么原路返回起飞点。

这种干扰型反无人机系统的麻烦在哪呢?它是无差别屏蔽卫星信号的,无人机丢导航,你的手机也一样要丢,也就是说只要反无人机系统开机,在这机器周围多少公里范围内,开车导航会乱,外卖找不到地方,连你 骑共享单车来上班都找不到停车的地方。

你愿意在这种地方上班?

[level3] 主动击落系统
二是主动击落系统,也就是九三大阅里面你们看到的光棱炮。这玩意儿我知道的分三档,阅兵的是舰载型,装重卡上也行,电池体积很大,激光功率强,防御范围多少公里我就不给你们说了。然后是轻便型,装在越 野吉普车上的,功率小一些,防御范围也小一些。最后是单兵型,一个大包把电池组装里面背上,拎起光棱抢就到处跑那种,功率最小,烧穿钢板最薄,射程虽然最短,那也是以公里来计算的。

4.细节与改进

模型可能无法理解标题与正文的关系,有可能把比较短的正文当成标题,需要在提示词中着重强调

模型无法理解1级标题下需要包含2级标题,不能直接包含3级标题,需要着重强调;

把上一轮对话中提取到的目录结构信息,作为上下文,供当前轮次使用;这个技巧是否有用,可以自行分析取舍

可以先识别一级标题,发文章分成几个大块,然后再每个块中,识别2级、3级


5.方法详解

5.1方法概述

Auto Title Chunker(智能标题分块器)是一种基于大语言模型的文本切分方法。它的核心思想是:通过为文本生成标题来建立结构,再根据标题层级进行分块

该方法针对两类文档采用不同的策略,但最终目标一致:让每一块文本都有清晰的标题标识。

核心思想

给文本"加标题",按标题"切分块"

无论你的原文是什么样子,方法11都会做两件事:

  1. 加标题:为原文添加层级化的标题标记
  2. 切分块:按照最高层级标题(level1)将文本分成若干块

针对两类文档的处理策略

策略一:结构化文档 → "提取现有标题"

适用对象:包含章节编号、标题标记、列表编号的文档,例如国家标准、论文等

处理方式

  • 原文:第一章 绪论 → 处理后:[level1] 第一章 绪论
  • 原文:1.1 研究背景 → 处理后:[level2] 1.1 研究背景
  • 原文:(一)理论框架 → 处理后:[level3] (一)理论框架

核心逻辑:模型识别原文中已有的标题行,为其添加层级标记(level1/level2/level3),不做内容修改。

策略二:非结构化文档 → "智能生成标题"

适用对象:没有明显结构标记的普通文章、叙事文本

处理方式

  • 原文:连续的段落描述

    复制代码
    牛肉面是成都著名小吃,选用上等牛肉...
    鳝鱼面则是另一道美味,选用新鲜鳝鱼...
  • 处理后:在话题转换处插入标题

    复制代码
    [level2] 牛肉面
    牛肉面是成都著名小吃,选用上等牛肉...
    
    [level2] 鳝鱼面
    鳝鱼面则是另一道美味,选用新鲜鳝鱼...

核心逻辑:模型识别语义边界(话题转换、描述对象变化、时间跳跃),在边界处插入合适的标题。


5.2 方法流程

整体流程图

复制代码
输入文本
   ↓
【步骤1】判断文本类型(结构化 vs 非结构化)
   ↓
   ├─→ 结构化文本 → 【步骤2a】识别现有标题并添加层级标记
   ↓
   └─→ 非结构化文本 → 【步骤2b】智能生成标题并插入
   ↓
【步骤3】生成带标题层级的完整文本
   ↓
【步骤4】根据标题层级(level1)进行分块
   ↓
输出分块结果

5.3 针对不同文章的处理逻辑

5.3.1 结构化文本处理流程

适用场景:包含章节编号、标题标记、列表编号等明显结构化符号的文本

处理步骤
  1. 识别特征:文本中包含以下特征之一即判定为结构化

    • 章节编号:第一章1.1第2节Chapter 1
    • 标题标记:# ## 一、(一)
    • 列表编号:1.(1)A.
    • 明显分隔:---===***
  2. 提取标题并标注层级

    • 将文本按行分割,每批处理 30 行(可配置)
    • 构造提示词,要求模型识别标题行并标注层级
    • 为每个标题添加层级标记,如 [level1][level2][level3]
    • 将已识别的标题作为上下文传递给下一批次
  3. 示例

    text 复制代码
    原文:
    第一章 绪论
    这是第一章的内容
    1.1 背景
    研究背景介绍
    ---------------------------------------
    处理后:
    [level1] 第一章 绪论
    这是第一章的内容
    [level2] 1.1 背景
    研究背景介绍
提示词设计
text 复制代码
【任务】分析以下文本,识别出所有标题行,并为每个标题标注层级。

【标注规则】
- 使用【level1】表示最高层级标题(如"第一章"、"1."等)
- 使用【level2】表示次级标题(如"1.1"、"(一)"等)
- 使用【level3】表示更低层级标题
- 非标题行不需要标注

【输出格式要求】
返回JSON对象:
{
  "results": [
    {
      "line_number": 行号(数字),
      "is_title": true或false,
      "level": "level1"、"level2"、"level3"或"无",
      "title": "标题内容"
    }
  ]
}

5.3.2 非结构化文本处理流程

适用场景:没有明显结构化符号的普通文章、叙事文本等

处理步骤
  1. 识别语义边界

    • 描述对象变化:从描述事物A转为描述事物B
    • 话题切换:从讨论主题A转向完全不同的主题B
    • 时间跳跃:时间线明显跳转(如"三年后"、"次日")
  2. 智能生成标题

    • 在识别到的语义边界处插入标题
    • 标题要简洁、准确,符合原文风格
    • 使用 [level1][level2][level3] 标记层级
    • 将已生成的标题作为上下文传递给下一批次
  3. 示例

    text 复制代码
    原文:
    牛肉面是成都的著名小吃,选用上等牛肉...
    鳝鱼面则是另一道美味,选用新鲜鳝鱼...
    -------------------------------------
    处理后:
    [level2] 牛肉面
    牛肉面是成都的著名小吃,选用上等牛肉...
    [level2] 鳝鱼面
    鳝鱼面则是另一道美味,选用新鲜鳝鱼...
提示词设计
text 复制代码
【任务】分析以下文本,在合适的位置插入标题(使用level1/level2/level3标注)。

【插入规则】
- 识别话题转换、描述对象变化等语义边界
- 在边界处插入合适的标题
- 标题要放在相关内容的**前面**
- 标题要简洁、准确,符合原文风格

【输出格式要求】
返回JSON对象:
{
  "insertions": [
    {
      "before_line": 行号(数字),
      "level": "level1"、"level2"或"level3",
      "title": "拟定的标题内容"
    }
  ]
}

5.3.3 分块处理

无论是结构化还是非结构化文本,在完成标题层级标注后,都使用相同的分块逻辑:

  1. 按 level1 标题分块

    • 遍历文本,遇到 [level1] 标题时创建新的分块
    • 每个 chunk 包含该 level1 标题及其下的所有内容
    • 保留 level2、level3 标题作为子结构
  2. 示例

    text 复制代码
    [level1] 第一章
    内容1...
    [level2] 1.1 小节
    内容2...
    
    [level1] 第二章
    内容3...
    
    分块结果:
    Chunk 1:
      [level1] 第一章
      内容1...
      [level2] 1.1 小节
      内容2...
    
    Chunk 2:
      [level1] 第二章
      内容3...

5.4 安装与配置

5.4.1 环境要求

  • Python >= 3.8
  • 依赖库:openai

5.4.2 安装依赖

bash 复制代码
pip install openai

5.4.3 API 配置

使用通义千问(Qwen)API:

python 复制代码
QWEN_API_KEY = "your-api-key"           # 从阿里云获取
QWEN_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
QWEN_MODEL = "qwen-plus"                # 或 "qwen3-8b"、"qwen-turbo" 等

获取 API Key

  1. 访问 阿里云百炼平台
  2. 注册/登录账号
  3. 创建 API Key
  4. 复制 API Key 到代码中

6.完整代码-1

这里会把结构化文档和非结构化文档分开处理,个别情况会出错。如果希望把两种文章用同样的逻辑处理处理,可以参考第7章。

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
方法11 - Auto Title Chunker 独立测试脚本
智能添加标题层级并分块

运行方法:
1. 确保已安装依赖:pip install openai
2. 修改下方的 API 配置(填入你的 QWEN_API_KEY)
3. 准备测试文本文件(或使用默认的 sample_document.txt)
4. 运行:python test_method11.py
"""

import os
from openai import OpenAI
import json

# ==================== 配置区域 ====================

# API 配置(请修改为你的实际 API Key)
QWEN_API_KEY = "sk-xxxx"  # 从阿里云获取
QWEN_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
QWEN_MODEL = "qwen-plus"  # 可选: qwen-plus, qwen3-8b, qwen-turbo 等

# 处理参数
MAX_LINES_PER_BATCH = 30  # 每批处理的行数(可调整:20-50)

# 测试文件路径
DEFAULT_TEST_FILE = "结构化保险.txt"  # 默认测试文件

# ==================== 配置区域结束 ====================


def load_text(file_path):
    """加载文本文件"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            text = f.read()
        print(f"✓ 成功加载文件: {file_path}")
        print(f"  文本总长度: {len(text)} 字符")
        print(f"  文本预览(前100字): {text[:100]}...")
        return text
    except FileNotFoundError:
        print(f"✗ 文件未找到: {file_path}")
        return None
    except Exception as e:
        print(f"✗ 读取文件出错: {e}")
        return None


def print_separator(title=""):
    """打印分隔线"""
    if title:
        print(f"\n{'='*60}")
        print(f"  {title}")
        print(f"{'='*60}")
    else:
        print(f"{'='*60}")


def print_chunks(chunks):
    """打印分块结果"""
    print(f"\n{'='*60}")
    print(f"分块数量: {len(chunks)}")
    print(f"{'='*60}")

    for i, chunk in enumerate(chunks):
        print(f"\n{'='*60}")
        print(f"块 #{i+1} / {len(chunks)}")
        print(f"{'='*60}")
        print(f"字符数: {len(chunk)}")
        print(f"\n完整内容:")
        print(f"{'-'*60}")
        print(chunk)
        print(f"{'-'*60}")


def auto_title_chunker(text, api_key=QWEN_API_KEY, base_url=QWEN_BASE_URL, model=QWEN_MODEL):
    """
    方法11 - Auto Title Chunker
    智能添加标题层级并分块
    """

    print_separator("方法11 - Auto Title Chunker")
    print(f"使用模型: {model}")
    print(f"每批处理行数: {MAX_LINES_PER_BATCH}")

    # 创建客户端
    client = OpenAI(
        api_key=api_key,
        base_url=base_url
    )

    # ========== 步骤1: 判断文本类型 ==========
    print_separator("步骤1: 判断文本类型(结构化 vs 非结构化)")

    # 提取前300个字符用于判断
    preview_text = text[:300]
    print(f"\n文本预览(前300字):")
    print(f"{'-'*60}")
    print(f"{preview_text}")
    print(f"{'-'*60}")

    # 构造第一步的提示词
    step1_prompt = f"""【任务】分析以下文本片段,判断它是否包含结构化内容。

【文本片段】
{preview_text}

【判断标准】
如果文本包含以下任意一种特征,则判定为"结构化":
1. 章节编号:如"第一章"、"1.1"、"第2节"、"Chapter 1"等
2. 标题标记:如"# "、"## "、"一、"、"(一)"等
3. 列表编号:如"1."、"(1)"、"①"、"A."等
4. 明显分隔:如"---"、"==="、"***"等分隔线

【输出要求】
只回答"结构化"或"非结构化",不要输出其他内容。
"""

    print("\n调用模型判断文本类型...")
    step1_response = client.chat.completions.create(
        model=model,
        extra_body={"enable_thinking": False},
        messages=[{"role": "user", "content": step1_prompt}],
    ).choices[0].message.content.strip()

    print(f"模型判断结果: {step1_response}")

    # 判断是否为结构化内容
    is_structured = step1_response == "结构化" or (not step1_response.startswith("非"))

    # ========== 步骤2: 智能添加标题层级 ==========
    print_separator("步骤2: 智能添加标题层级")

    # 将文本按行分割
    lines = text.split('\n')
    print(f"\n原文总行数: {len(lines)}")

    # 用于存储已识别的标题结构(作为上下文)
    title_context = []

    # 用于存储最终结果
    final_lines = []

    if is_structured:
        print("\n✓ 检测到结构化文本,识别现有标题并添加层级标记...")

        # 分批处理
        for batch_start in range(0, len(lines), MAX_LINES_PER_BATCH):
            batch_end = min(batch_start + MAX_LINES_PER_BATCH, len(lines))
            batch_lines = lines[batch_start:batch_end]

            print(f"\n{'='*60}")
            print(f"处理行 {batch_start+1}-{batch_end} / {len(lines)}")
            print(f"{'='*60}")

            # 构造上下文信息(显示已识别的标题)
            context_info = ""
            if title_context:
                context_info = "【已识别的标题结构】\n"
                for title_info in title_context[-10:]:  # 只显示最近10个
                    level = title_info['level']
                    title = title_info['title']
                    preview = title_info['preview']
                    context_info += f"  [{level}] {title}\n"
                    context_info += f"      内容预览: {preview}...\n"
                context_info += "\n"

            # 构造当前批次的内容
            current_batch = ""
            for i, line in enumerate(batch_lines):
                line_num = batch_start + i + 1
                current_batch += f"第{line_num}行: {line}\n"

            # 构造提示词 - 结构化文本
            prompt = f"""【任务】分析以下文本,识别出所有标题行,并为每个标题标注层级。

{context_info}【标注规则】
- 使用【level1】表示最高层级标题(如"第一章"、"1."等)
- 使用【level2】表示次级标题(如"1.1"、"(一)"等)
- 使用【level3】表示更低层级标题
- 如果某行不是标题,不需要在JSON中包含该行

⚠️ 【准确识别要求】
- **不要把正文当成标题**:只有真正的标题行才标注,正文段落即使很短也不算标题
- 判断标准:标题通常简短、概括性强、有编号或标记特征;正文则展开描述、内容详细
- 例如:"第一章 概述"是标题,"本文主要介绍..."是正文,不要标注

⚠️ 【层级关系要求】
- **层级必须逐级递进**:level1 → level2 → level3,不能跳跃
- level1下面包含level2,level2下面包含level3
- 不要出现level1直接跳到level3的情况
- 如果不确定层级,优先使用更高的层级(如level1而非level3)

⚠️ 【切分粒度要求】
- 在准确识别的前提下,尽可能多地识别标题
- 宁可多标明显的标题,不要漏标
- 每个小节、小点都可以作为level3标题

【输出格式要求】⚠️ 必须严格遵守
你**必须**返回一个JSON对象,格式如下:
{{
  "results": [
    {{
      "line_number": 行号(数字),
      "is_title": true,
      "level": "level1"、"level2"或"level3",
      "title": "标题内容"
    }}
  ]
}}

⚠️ 重要提示:
1. 必须包含"results"键
2. results必须是一个数组,只包含识别到的标题行
3. 如果当前批次没有标题,results为空数组:{{"results": []}}
4. 不要直接返回数组,要返回包含results的对象
5. 只返回JSON,不要有任何其他文字说明

【待处理原文】
{current_batch}"""

            # 打印提示词(便于调试)
            print(f"\n【发送给模型的提示词】")
            print(f"{'-'*60}")
            print(prompt[:500] + "..." if len(prompt) > 500 else prompt)  # 只显示前500字
            print(f"{'-'*60}")

            # 调用模型
            response = client.chat.completions.create(
                model=model,
                extra_body={"enable_thinking": False},
                messages=[{"role": "user", "content": prompt}],
                response_format={"type": "json_object"}
            )

            response_text = response.choices[0].message.content
            result = json.loads(response_text)

            # 打印模型返回(便于调试)
            print(f"\n【模型返回结果】")
            print(f"{'-'*60}")
            print(json.dumps(result, ensure_ascii=False, indent=2))
            print(f"{'-'*60}")

            # 处理结果
            if isinstance(result, list):
                results_list = result
            elif isinstance(result, dict):
                results_list = result.get('results', result.get('list', []))
            else:
                results_list = []

            # 统计识别到的标题数量
            title_count = sum(1 for item in results_list if item.get('is_title', False))
            print(f"\n→ 本批次识别到 {title_count} 个标题")

            # 构建行号到结果的映射
            line_results = {}
            for item in results_list:
                line_num = item.get('line_number', batch_start + 1)
                line_results[line_num] = item

            # 处理当前批次的每一行
            for i, line in enumerate(batch_lines):
                line_num = batch_start + i + 1
                original_line = lines[line_num - 1]

                if line_num in line_results:
                    item = line_results[line_num]
                    is_title = item.get('is_title', False)
                    level = item.get('level', '无')
                    title = item.get('title', '')

                    if is_title and level != '无':
                        # 添加标题标记
                        marked_line = f"[{level}] {original_line}"
                        final_lines.append(marked_line)
                        print(f"  ✓ 标记标题: [{level}] {title}")

                        # 添加到上下文(获取后续内容预览)
                        content_preview = ""
                        for j in range(line_num, min(line_num + 5, len(lines))):
                            next_line = lines[j].strip()
                            if next_line and next_line != original_line:
                                content_preview += next_line[:10] + " "
                                if len(content_preview) > 30:
                                    break

                        title_context.append({
                            'level': level,
                            'title': title,
                            'preview': content_preview.strip()
                        })
                    else:
                        final_lines.append(original_line)
                else:
                    final_lines.append(original_line)

    else:
        print("\n✓ 检测到非结构化文本,智能生成并插入标题...")
        print("  → 将识别语义边界,在话题转换处插入新的标题")

        # 分批处理
        for batch_start in range(0, len(lines), MAX_LINES_PER_BATCH):
            batch_end = min(batch_start + MAX_LINES_PER_BATCH, len(lines))
            batch_lines = lines[batch_start:batch_end]

            print(f"\n{'='*60}")
            print(f"处理行 {batch_start+1}-{batch_end} / {len(lines)}")
            print(f"{'='*60}")

            # 构造上下文信息
            context_info = ""
            if title_context:
                context_info = "【已添加的标题】\n"
                for title_info in title_context[-10:]:
                    level = title_info['level']
                    title = title_info['title']
                    context_info += f"  [{level}] {title}\n"
                context_info += "\n"

            # 构造当前批次的内容(带编号)
            current_batch = ""
            for i, line in enumerate(batch_lines):
                line_num = batch_start + i + 1
                current_batch += f"[{line_num:03d}] {line}\n"

            # 构造提示词 - 非结构化文本(完全参照test_chonkie.py)
            prompt = f"""【任务】分析以下文本,在合适的位置插入标题(使用level1/level2/level3标注)。

{context_info}【插入规则】
- 识别话题转换、描述对象变化等语义边界
- 在边界处插入合适的标题
- 标题要放在相关内容的**前面**
- 标题要简洁、准确,符合原文风格
- 使用【level1】标记主要话题
- 使用【level2】标记次级话题
- 使用【level3】标记更细的话题

⚠️ 【准确识别要求】
- **不要为单独一两句话的段落插入标题**:只有真正形成独立话题的内容才需要标题
- 判断标准:是否有明确的描述对象变化、话题转换、时间跳跃等明显的语义边界
- 例如:一段话只是过渡性说明,即使单独成行,也不需要插入标题

⚠️ 【层级关系要求】
- **层级必须逐级递进**:level1 → level2 → level3,不能跳跃
- level1是大主题,level2是level1下的子话题,level3是level2下的细节
- 不要出现level1后直接跟level3,中间缺少level2的情况
- 如果不确定层级,优先使用更高的层级(如level1而非level3)
- 例如:讲美食时,先插入[level1] 成都美食,再插入[level2] 牛肉面,不要直接跳到level3

⚠️ 【切分粒度要求】
- 在准确识别语义边界的前提下,尽可能多地插入标题
- 宁可多插入明显的语义边界,不要漏掉
- 但避免过度切分:如果几句话都是描述同一件事,就不要再细分了

【示例】
如果第4行讲牛肉面,第6行讲鳝鱼面,应该:
- 在第4行之前插入:[level2] 牛肉面
- 在第6行之前插入:[level2] 鳝鱼面

不要这样:
- 第4行:牛肉面很好吃。(不要为这句话单独插入标题)
- 第5行:它选用上等牛肉。(这是同一话题,不要细分)

【输出格式要求】⚠️ 必须严格遵守
你**必须**返回一个JSON对象,格式如下:
{{
  "insertions": [
    {{
      "before_line": 行号(数字),
      "level": "level1"、"level2"或"level3",
      "title": "拟定的标题内容"
    }}
  ]
}}

⚠️ 重要提示:
1. 必须包含"insertions"键
2. insertions必须是一个数组,只包含需要插入的标题
3. 如果当前批次没有语义边界,insertions为空数组:{{"insertions": []}}
4. 不要直接返回数组,要返回包含insertions的对象
5. 只返回JSON,不要有任何其他文字说明

【待处理原文】
{current_batch}"""

            # 打印提示词(便于调试)
            print(f"\n【发送给模型的提示词】")
            print(f"{'-'*60}")
            print(prompt[:500] + "..." if len(prompt) > 500 else prompt)  # 只显示前500字
            print(f"{'-'*60}")

            # 调用模型
            response = client.chat.completions.create(
                model=model,
                extra_body={"enable_thinking": False},
                messages=[{"role": "user", "content": prompt}],
                response_format={"type": "json_object"}
            )

            response_text = response.choices[0].message.content
            result = json.loads(response_text)

            # 打印模型返回(便于调试)
            print(f"\n【模型返回结果】")
            print(f"{'-'*60}")
            print(json.dumps(result, ensure_ascii=False, indent=2))
            print(f"{'-'*60}")

            # 处理结果
            if isinstance(result, list):
                insertions = result
            elif isinstance(result, dict):
                insertions = result.get('insertions', result.get('list', []))
            else:
                insertions = []

            # 记录当前批次在final_lines中的起始位置
            batch_start_pos = len(final_lines)

            # 先添加当前批次的所有行
            for line in batch_lines:
                final_lines.append(line)

            # 根据模型返回插入标题(从后往前插入,避免位置错乱)
            print(f"\n→ 本批次识别到 {len(insertions)} 个语义边界,准备插入标题...")
            for item in sorted(insertions, key=lambda x: x.get('before_line', 0), reverse=True):
                before_line = item.get('before_line', batch_start + 1)
                level = item.get('level', 'level2')
                title = item.get('title', '')

                # 计算在final_lines中的位置
                relative_pos = before_line - batch_start - 1
                if 0 <= relative_pos < len(batch_lines):
                    insert_pos = batch_start_pos + relative_pos
                    if 0 <= insert_pos < len(final_lines):
                        final_lines.insert(insert_pos, f"[{level}] {title}")

                        # 添加到上下文
                        title_context.append({
                            'level': level,
                            'title': title,
                            'preview': ''
                        })

                        print(f"  ✓ 插入标题: [{level}] {title} (在第{before_line}行之前)")

    # 重新组装文本
    result_text = '\n'.join(final_lines)

    # ========== 步骤3: 显示带标题层级的完整文本 ==========
    print_separator("步骤3: 显示带标题层级的完整文本")

    print(f"\n{'='*60}")
    print(f"完整文本(共{len(final_lines)}行):")
    print(f"{'='*60}")
    print(result_text)
    print(f"{'='*60}")

    # ========== 步骤4: 根据标题层级进行分块 ==========
    print_separator("步骤4: 根据标题层级进行分块")

    chunks = []
    current_chunk_lines = []
    current_level1_title = ""

    for line in final_lines:
        current_chunk_lines.append(line)

        # 如果遇到level1标题,保存前一个chunk并开始新的chunk
        if line.startswith('[level1]'):
            if len(current_chunk_lines) > 1:  # 如果chunk不为空
                chunks.append('\n'.join(current_chunk_lines[:-1]))  # 保存之前的chunk
                current_chunk_lines = [line]  # 开始新的chunk
            current_level1_title = line

    # 保存最后一个chunk
    if current_chunk_lines:
        chunks.append('\n'.join(current_chunk_lines))

    print(f"\n✓ 根据标题层级分块完成,共生成 {len(chunks)} 个chunks")

    return chunks


def main():
    """主函数"""
    print_separator("Auto Title Chunker 测试工具")
    print("\n本工具用于测试方法11 - 智能添加标题层级并分块")

    # 检查 API Key
    if QWEN_API_KEY == "your-api-key-here":
        print("\n✗ 错误:请先修改脚本中的 API Key 配置!")
        print("  请将 QWEN_API_KEY 修改为你的实际 API Key")
        return

    # 询问测试文件
    file_path = input(f"\n请输入测试文件路径(直接回车使用默认: {DEFAULT_TEST_FILE}): ").strip()
    if not file_path:
        file_path = DEFAULT_TEST_FILE

    # 加载文本
    text = load_text(file_path)
    if text is None:
        print("\n无法加载文本文件,退出程序")
        return

    # 确认开始测试
    print(f"\n配置信息:")
    print(f"  模型: {QWEN_MODEL}")
    print(f"  批次大小: {MAX_LINES_PER_BATCH} 行/批")
    print(f"  文件: {file_path}")
    print(f"  文本长度: {len(text)} 字符")

    confirm = input("\n是否开始处理?(y/n): ").strip().lower()
    if confirm != 'y':
        print("已取消")
        return

    # 执行分块
    try:
        chunks = auto_title_chunker(text)

        # 显示分块结果
        print_chunks(chunks)

        # 保存结果到文件
        print_separator("保存结果")
        output_file = file_path.rsplit('.', 1)[0] + "_分块结果.txt"
        with open(output_file, 'w', encoding='utf-8') as f:
            for i, chunk in enumerate(chunks):
                f.write(f"{'='*60}\n")
                f.write(f"块 #{i+1} / {len(chunks)}\n")
                f.write(f"{'='*60}\n")
                f.write(chunk)
                f.write("\n\n")

        print(f"✓ 结果已保存到: {output_file}")

    except Exception as e:
        print(f"\n✗ 处理出错: {e}")
        import traceback
        traceback.print_exc()


if __name__ == "__main__":
    main()

7.完整代码-2

这里将两种文章(结构化和非结构化)用了同样的逻辑进行处理,貌似效果更好。这充分说明,大模型设计中,有可能越简单越好。示例效果如下(第六章的方法如果处理以下内容,会出现较多错误):

python 复制代码
[level1] 第三章 城区设施建设与设备升级推进情况
- **污泥无害化处理处置**  
  本季度全市污泥产生量为280吨/日(含水率80%)。市污泥干化焚烧中心(位于安北经开区)运行稳定,采用"深度脱水+热干化+协同焚烧"工艺,污泥无害化处置率保持在100%。目前,正在推进城南区污泥处置点的除臭系统升级,新安装的生物滤池除臭设备已进入调试阶段,预计下月投用,将有效解决周边异味扰民问题。

[level2] 3.2 污水资源化利用与中水回用
**措施2:中水再生利用体系构建**  
为缓解资源性缺水矛盾,本市大力推进"污水资源化"利用,中水已逐步成为城市的"第二水源"。

[level3] 工业园区中水回用工程
- **工业园区中水回用工程**  
  安北经开区作为全市工业用水大户,率先建成了"点对点"工业再生水管网。本季度,安北再生水厂向市第二电厂及安化集团供应冷却循环用水累计达450万吨,中水回用率提升至42%。此举不仅减少了新鲜淡水取用,每年还为工业企业节约水费支出约1200万元。

[level3] 市政杂用与生态补水
- **市政杂用与生态补水**  
  - **生态景观补水**:通过城南河再生水输配管线,本季度向南湖公园及城南河湿地生态补水650万吨,河道生态基流得到有效保障,水体透明度平均提升15厘米。  

[level1] 第四章 第二季度工作前瞻与部署
四  第二季度工作前瞻与部署  

会议最后,市水务局局长张建国结合当前水资源形势及运行数据,对第二季度(即下一阶段,主要针对夏季供水高峰期)的重点工作进行了前瞻性部署,要求各单位立足"防大旱、保供水、优水质"的总体目标,提前谋划,确保全市供水安全万无一失。

[level2] 4.1 应对夏季供水高峰的调度预案
a) 应对夏季供水高峰的调度预案  
根据气象部门预测,今年第二季度后期及第三季度,我市气温将较常年偏高 1--1.5℃,极端高温日数可达 20--25天,预计全市最高日供水量将突破 310万吨,逼近 320万吨 的设计极限。

[level3] 跨区域调水与水源联动
-1 跨区域调水与水源联动  
针对城东区及城南区的潜在缺口,制定"西水东调、南水北补"的应急调度预案。  
具体措施:在用水高峰期(每日10:00--15:00,18:00--22:00),通过"清水河-安北"互联互通管线,将城西区富余的 5万吨/日 供水能力调配至城东区;同时,云湖水库需在6月底前将蓄水

[level3] 高峰期分级限水方案
-2 高峰期分级限水方案  
按照"先生活、后生产,先节水、后调水"的原则,修订《安市供水应急预案》。  
黄色预警(日供水量>295万吨):暂停全市市政绿化、道路冲洗等非生活用水,压减 30% 的景观环境用水。  

具体代码如下:

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
方法12 - Universal Title Chunker 独立测试脚本
统一采用生成标题的方式,为所有文档添加标题层级并分块

运行方法:
1. 确保已安装依赖:pip install openai
2. 修改下方的 API 配置(填入你的 QWEN_API_KEY)
3. 准备测试文本文件(或使用默认的 sample_document.txt)
4. 运行:python test_method12.py
"""

import os
from openai import OpenAI
import json

# ==================== 配置区域 ====================

# API 配置(请修改为你的实际 API Key)
QWEN_API_KEY = "sk-904ae8f1683740f8b067ea272eb0c733"  # 从阿里云获取
QWEN_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
QWEN_MODEL = "qwen-plus"  # 可选: qwen-plus, qwen3-8b, qwen-turbo 等

# 处理参数
MAX_LINES_PER_BATCH = 30  # 每批处理的行数(可调整:20-50)

# 测试文件路径
DEFAULT_TEST_FILE = "结构化保险.txt"  # 默认测试文件

# ==================== 配置区域结束 ====================


def load_text(file_path):
    """加载文本文件"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            text = f.read()
        print(f"✓ 成功加载文件: {file_path}")
        print(f"  文本总长度: {len(text)} 字符")
        print(f"  文本预览(前100字): {text[:100]}...")
        return text
    except FileNotFoundError:
        print(f"✗ 文件未找到: {file_path}")
        return None
    except Exception as e:
        print(f"✗ 读取文件出错: {e}")
        return None


def print_separator(title=""):
    """打印分隔线"""
    if title:
        print(f"\n{'='*60}")
        print(f"  {title}")
        print(f"{'='*60}")
    else:
        print(f"{'='*60}")


def print_chunks(chunks):
    """打印分块结果"""
    print(f"\n{'='*60}")
    print(f"分块数量: {len(chunks)}")
    print(f"{'='*60}")

    for i, chunk in enumerate(chunks):
        print(f"\n{'='*60}")
        print(f"块 #{i+1} / {len(chunks)}")
        print(f"{'='*60}")
        print(f"字符数: {len(chunk)}")
        print(f"\n完整内容:")
        print(f"{'-'*60}")
        print(chunk)
        print(f"{'-'*60}")


def universal_title_chunker(text, api_key=QWEN_API_KEY, base_url=QWEN_BASE_URL, model=QWEN_MODEL):
    """
    方法12 - Universal Title Chunker
    统一采用生成标题的方式,为所有文档添加标题层级并分块
    """

    print_separator("方法12 - Universal Title Chunker")
    print(f"使用模型: {model}")
    print(f"每批处理行数: {MAX_LINES_PER_BATCH}")
    print(f"核心策略: 无论文档类型,统一采用生成标题的方式")

    # 创建客户端
    client = OpenAI(
        api_key=api_key,
        base_url=base_url
    )

    # 将文本按行分割
    lines = text.split('\n')
    print(f"\n原文总行数: {len(lines)}")

    # 用于存储已生成的标题结构(作为上下文)
    title_context = []

    # 用于存储最终结果
    final_lines = []

    print_separator("步骤1: 统一生成标题层级")

    # 分批处理
    for batch_start in range(0, len(lines), MAX_LINES_PER_BATCH):
        batch_end = min(batch_start + MAX_LINES_PER_BATCH, len(lines))
        batch_lines = lines[batch_start:batch_end]

        print(f"\n{'='*60}")
        print(f"处理行 {batch_start+1}-{batch_end} / {len(lines)}")
        print(f"{'='*60}")

        # 构造上下文信息
        context_info = ""
        if title_context:
            context_info = "【已生成的标题】\n"
            for title_info in title_context[-10:]:
                level = title_info['level']
                title = title_info['title']
                context_info += f"  [{level}] {title}\n"
            context_info += "\n"

        # 构造当前批次的内容(带编号)
        current_batch = ""
        for i, line in enumerate(batch_lines):
            line_num = batch_start + i + 1
            current_batch += f"[{line_num:03d}] {line}\n"

        # 构造提示词 - 统一使用生成标题的方式
        prompt = f"""【任务】分析以下文本,**为文本生成并插入标题**(使用level1/level2/level3标注)。

{context_info}【任务说明】
- 你需要为这段文本**生成标题**,无论它原本是否有标题
- 如果原文已有标题,请**重新生成**更规范的标题
- 在语义边界处**插入你生成的标题**

【生成标题规则】
- **识别语义边界**:找到话题转换、描述对象变化、章节结束等位置
- **生成合适标题**:为每个语义边界生成简洁、准确的标题
- **插入位置**:标题要放在相关内容的**前面**
- **标题风格**:简短、概括性强,使用标准的标题格式
- 使用【level1】标记主要话题/章节
- 使用【level2】标记次级话题/小节
- 使用【level3】标记更细的话题/要点

⚠️ 【标题生成要求】
- **不要为单独一两句话的段落生成标题**:只有真正形成独立话题的内容才需要标题
- 判断标准:是否有明确的描述对象变化、话题转换、时间跳跃等明显的语义边界
- 例如:一段话只是过渡性说明,即使单独成行,也不需要生成标题
- 标题要基于内容进行**概括**,不要简单复制原文
- 如果原文已有标题(如"第一章"),请生成**更规范的标题**(如"第一章 总则")

⚠️ 【层级关系要求】
- **层级必须逐级递进**:level1 → level2 → level3,不能跳跃
- level1是大主题/章节,level2是level1下的子话题/小节,level3是level2下的细节
- 不要出现level1后直接跟level3,中间缺少level2的情况
- 如果不确定层级,优先使用更高的层级(如level1而非level3)
- 例如:讲保险合同时,先生成[level1] 保险合同,再生成[level2] 投保人义务

⚠️ 【切分粒度要求】
- 在准确识别语义边界的前提下,尽可能多地生成标题
- 宁可多生成明显的语义边界,不要漏掉
- 但避免过度切分:如果几句话都是描述同一件事,就不要再细分了

【示例】
原文(有标题):
[001] 第一章 概述
[002] 保险法是调整保险关系的法律...
[003] 它规定了投保人和保险人的义务...

你应该(重新生成):
- 在第1行之前生成并插入:[level1] 第一章 保险法概述

原文(无标题):
[001] 成都是一座美食之城。
[002] 牛肉面是成都著名小吃...
[003] 它口感麻辣鲜香。
[004] 鳝鱼面则是另一道美味...

你应该(全新生成):
- 在第1行之前生成并插入:[level1] 成都美食介绍
- 在第2行之前生成并插入:[level2] 牛肉面
- 在第4行之前生成并插入:[level2] 鳝鱼面

不要这样:
- 第2行:牛肉面很好吃。(不要为这句话单独生成标题)
- 第3行:它选用上等牛肉。(这是同一话题,不要细分)

【输出格式要求】⚠️ 必须严格遵守
你**必须**返回一个JSON对象,格式如下:
{{
  "insertions": [
    {{
      "before_line": 行号(数字),
      "level": "level1"、"level2"或"level3",
      "title": "你生成的标题内容"
    }}
  ]
}}

⚠️ 重要提示:
1. 必须包含"insertions"键
2. insertions必须是一个数组,只包含需要插入的标题
3. 如果当前批次没有语义边界,insertions为空数组:{{"insertions": []}}
4. 不要直接返回数组,要返回包含insertions的对象
5. 只返回JSON,不要有任何其他文字说明

【待处理原文】
{current_batch}"""

        # 打印提示词(便于调试)
        print(f"\n【发送给模型的提示词】")
        print(f"{'-'*60}")
        print(prompt[:500] + "..." if len(prompt) > 500 else prompt)  # 只显示前500字
        print(f"{'-'*60}")

        # 调用模型
        response = client.chat.completions.create(
            model=model,
            extra_body={"enable_thinking": False},
            messages=[{"role": "user", "content": prompt}],
            response_format={"type": "json_object"}
        )

        response_text = response.choices[0].message.content
        result = json.loads(response_text)

        # 打印模型返回(便于调试)
        print(f"\n【模型返回结果】")
        print(f"{'-'*60}")
        print(json.dumps(result, ensure_ascii=False, indent=2))
        print(f"{'-'*60}")

        # 处理结果
        if isinstance(result, list):
            insertions = result
        elif isinstance(result, dict):
            insertions = result.get('insertions', result.get('list', []))
        else:
            insertions = []

        # 记录当前批次在final_lines中的起始位置
        batch_start_pos = len(final_lines)

        # 先添加当前批次的所有行
        for line in batch_lines:
            final_lines.append(line)

        # 根据模型返回插入标题(从后往前插入,避免位置错乱)
        print(f"\n→ 本批次识别到 {len(insertions)} 个语义边界,准备生成标题...")
        for item in sorted(insertions, key=lambda x: x.get('before_line', 0), reverse=True):
            before_line = item.get('before_line', batch_start + 1)
            level = item.get('level', 'level2')
            title = item.get('title', '')

            # 计算在final_lines中的位置
            relative_pos = before_line - batch_start - 1
            if 0 <= relative_pos < len(batch_lines):
                insert_pos = batch_start_pos + relative_pos
                if 0 <= insert_pos < len(final_lines):
                    final_lines.insert(insert_pos, f"[{level}] {title}")

                    # 添加到上下文
                    title_context.append({
                        'level': level,
                        'title': title,
                        'preview': ''
                    })

                    print(f"  ✓ 生成标题: [{level}] {title} (在第{before_line}行之前)")

    # 重新组装文本
    result_text = '\n'.join(final_lines)

    # ========== 步骤2: 显示带标题层级的完整文本 ==========
    print_separator("步骤2: 显示带标题层级的完整文本")

    print(f"\n{'='*60}")
    print(f"完整文本(共{len(final_lines)}行):")
    print(f"{'='*60}")
    print(result_text)
    print(f"{'='*60}")

    # ========== 步骤3: 根据标题层级进行分块 ==========
    print_separator("步骤3: 根据标题层级进行分块")

    chunks = []
    current_chunk_lines = []
    current_level1_title = ""

    for line in final_lines:
        current_chunk_lines.append(line)

        # 如果遇到level1标题,保存前一个chunk并开始新的chunk
        if line.startswith('[level1]'):
            if len(current_chunk_lines) > 1:  # 如果chunk不为空
                chunks.append('\n'.join(current_chunk_lines[:-1]))  # 保存之前的chunk
                current_chunk_lines = [line]  # 开始新的chunk
            current_level1_title = line

    # 保存最后一个chunk
    if current_chunk_lines:
        chunks.append('\n'.join(current_chunk_lines))

    print(f"\n✓ 根据标题层级分块完成,共生成 {len(chunks)} 个chunks")

    return chunks


def main():
    """主函数"""
    print_separator("Universal Title Chunker 测试工具")
    print("\n本工具用于测试方法12 - 统一生成标题的分块方法")
    print("核心特点:无论文档类型,统一采用生成标题的方式")

    # 检查 API Key
    if QWEN_API_KEY == "your-api-key-here":
        print("\n✗ 错误:请先修改脚本中的 API Key 配置!")
        print("  请将 QWEN_API_KEY 修改为你的实际 API Key")
        return

    # 询问测试文件
    file_path = input(f"\n请输入测试文件路径(直接回车使用默认: {DEFAULT_TEST_FILE}): ").strip()
    if not file_path:
        file_path = DEFAULT_TEST_FILE

    # 加载文本
    text = load_text(file_path)
    if text is None:
        print("\n无法加载文本文件,退出程序")
        return

    # 确认开始测试
    print(f"\n配置信息:")
    print(f"  模型: {QWEN_MODEL}")
    print(f"  批次大小: {MAX_LINES_PER_BATCH} 行/批")
    print(f"  文件: {file_path}")
    print(f"  文本长度: {len(text)} 字符")
    print(f"  处理策略: 统一生成标题(无论原文是否有标题)")

    confirm = input("\n是否开始处理?(y/n): ").strip().lower()
    if confirm != 'y':
        print("已取消")
        return

    # 执行分块
    try:
        chunks = universal_title_chunker(text)

        # 显示分块结果
        print_chunks(chunks)

        # 保存结果到文件
        print_separator("保存结果")
        output_file = file_path.rsplit('.', 1)[0] + "_方法12分块结果.txt"
        with open(output_file, 'w', encoding='utf-8') as f:
            for i, chunk in enumerate(chunks):
                f.write(f"{'='*60}\n")
                f.write(f"块 #{i+1} / {len(chunks)}\n")
                f.write(f"{'='*60}\n")
                f.write(chunk)
                f.write("\n\n")

        print(f"✓ 结果已保存到: {output_file}")

    except Exception as e:
        print(f"\n✗ 处理出错: {e}")
        import traceback
        traceback.print_exc()


if __name__ == "__main__":
    main()
相关推荐
沛沛老爹4 小时前
Web开发者快速上手AI Agent:基于Advanced-RAG的提示词应用
前端·人工智能·langchain·llm·rag·web转型·advanced-rag
一代明君Kevin学长4 小时前
RAG中的上下文压缩(Contextual Compression)
人工智能·python·深度学习·ai·大模型·检索增强·rag
赋范大模型技术社区4 小时前
Agentic-GraphRAG 架构实践:较 GraphRAG 成本降低90%
agent·rag·graphrag
————A6 小时前
从 RAG 召回失败到故障链推理
人工智能·rag
一语雨在生无可恋敲代码~1 天前
RAG的一点思考
rag
Chukai1232 天前
第3章:基于LlamaIndex+Ollama+ChromaDB搭建本地简单RAG问答系统
开发语言·人工智能·python·rag·rag问答系统
enjoy编程2 天前
Spring-AI RAG 如何提高召回率?
人工智能·rag·recall·重排·召回率·rerank·hyde
Robot侠2 天前
赋予 AI 记忆:在 RTX 3090 上搭建本地 RAG 知识库问答系统
人工智能·langchain·llm·llama·qwen·rag·chromadb
摸鱼仙人~3 天前
企业级 RAG 问答系统开发上线流程分析
后端·python·rag·检索