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都会做两件事:
- 加标题:为原文添加层级化的标题标记
- 切分块:按照最高层级标题(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、第2节、Chapter 1等 - 标题标记:
#、##、一、、(一)等 - 列表编号:
1.、(1)、①、A.等 - 明显分隔:
---、===、***等
- 章节编号:
-
提取标题并标注层级
- 将文本按行分割,每批处理 30 行(可配置)
- 构造提示词,要求模型识别标题行并标注层级
- 为每个标题添加层级标记,如
[level1]、[level2]、[level3] - 将已识别的标题作为上下文传递给下一批次
-
示例
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 非结构化文本处理流程
适用场景:没有明显结构化符号的普通文章、叙事文本等
处理步骤
-
识别语义边界
- 描述对象变化:从描述事物A转为描述事物B
- 话题切换:从讨论主题A转向完全不同的主题B
- 时间跳跃:时间线明显跳转(如"三年后"、"次日")
-
智能生成标题
- 在识别到的语义边界处插入标题
- 标题要简洁、准确,符合原文风格
- 使用
[level1]、[level2]、[level3]标记层级 - 将已生成的标题作为上下文传递给下一批次
-
示例
text原文: 牛肉面是成都的著名小吃,选用上等牛肉... 鳝鱼面则是另一道美味,选用新鲜鳝鱼... ------------------------------------- 处理后: [level2] 牛肉面 牛肉面是成都的著名小吃,选用上等牛肉... [level2] 鳝鱼面 鳝鱼面则是另一道美味,选用新鲜鳝鱼...
提示词设计
text
【任务】分析以下文本,在合适的位置插入标题(使用level1/level2/level3标注)。
【插入规则】
- 识别话题转换、描述对象变化等语义边界
- 在边界处插入合适的标题
- 标题要放在相关内容的**前面**
- 标题要简洁、准确,符合原文风格
【输出格式要求】
返回JSON对象:
{
"insertions": [
{
"before_line": 行号(数字),
"level": "level1"、"level2"或"level3",
"title": "拟定的标题内容"
}
]
}
5.3.3 分块处理
无论是结构化还是非结构化文本,在完成标题层级标注后,都使用相同的分块逻辑:
-
按 level1 标题分块
- 遍历文本,遇到
[level1]标题时创建新的分块 - 每个 chunk 包含该 level1 标题及其下的所有内容
- 保留 level2、level3 标题作为子结构
- 遍历文本,遇到
-
示例
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:
- 访问 阿里云百炼平台
- 注册/登录账号
- 创建 API Key
- 复制 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()