各位同仁,下午好!
今天,我们齐聚一堂,探讨一个在信息爆炸时代日益凸显的挑战——如何高效地从浩瀚的网页信息中提取、组织和检索我们真正所需的内容。传统上,我们习惯于将整个网页视为一个不可分割的整体进行索引,但这在面对日益复杂的现代网页时,效率往往不尽如人意。例如,一篇长篇技术博客,可能涵盖多个子主题,用户若只想了解其中某个特定技术点,完整的网页检索结果往往会淹没在大量无关信息中。
我们今天的主题是:利用 AI 自动拆分复杂网页,生成更适合‘碎片化检索’的语义切片(Chunks)。我们将深入探讨如何借助人工智能的力量,将一个庞大而复杂的网页智能地拆解成一系列具备独立语义、上下文完整、长度适中的“信息碎片”,从而为我们的检索系统、RAG(Retrieval Augmented Generation)应用乃至个性化内容推荐提供更精细、更准确的数据源。这不仅是对现有检索模式的革新,更是迈向更智能、更高效信息管理的关键一步。
1. 碎片化检索的兴起与语义切片的价值
在当前的信息消费习惯下,用户往往倾向于获取高度聚焦、即时可用的信息片段,而非冗长的完整文档。这就是“碎片化检索”的核心需求。当用户输入一个查询时,他们期待得到的不是一个指向整个网页的链接,而是一个直接指向网页内某个特定段落、某个特定观点或某个特定数据点的结果。
为了满足这种需求,“语义切片”应运而生。一个好的语义切片应该具备以下特征:
- 语义完整性 (Semantic Coherence): 切片内部的文本应该围绕一个单一的、明确的主题或观点展开,自成一体,不依赖外部上下文即可理解。
- 上下文关联性 (Contextual Relevance): 尽管切片是独立的,但它应该能够通过元数据(如原始标题、上级标题、URL等)与原始网页及其在网页中的位置保持关联。
- 长度适中 (Appropriate Length): 既不能过短导致信息残缺,也不能过长导致再次出现“信息过载”。理想长度取决于下游应用,通常在100-500个词之间。
- 可检索性 (Retrieval Utility): 切片本身应包含足够的关键词和信息,以便通过向量搜索、关键词匹配等方式被有效检索。
传统网页处理方法的局限性:
| 方法 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 整页索引 | 将整个网页内容作为一个文档进行索引。 | 简单易实现 | 检索粒度粗糙,用户难以快速定位;RAG模型上下文窗口易溢出;返回结果冗余。 |
| 固定长度切片 | 简单粗暴地按字符数或词数将网页切分。 | 实现简单,速度快 | 容易切断语义,导致切片内容不完整或无意义;无法保留上下文;对语义检索效果不佳。 |
| 基于标点符号切片 | 按句号、问号等标点符号切分。 | 较固定长度更合理 | 句子不总是独立的语义单位;长句或复杂句可能仍需进一步拆分;无法处理跨句的段落语义。 |
| 基于HTML标签切片 | 依据<p>, <h1>等HTML标签进行切分。 |
结构化,易实现 | 语义不一定完整(一个段落可能包含多个子主题);HTML结构可能复杂或不规范;大量非内容标签干扰。 |
显然,上述传统方法都未能很好地解决“语义完整性”的问题。而这正是AI能够大展身手的地方。
2. 网页语义切片的技术挑战
在深入AI解决方案之前,我们必须清醒地认识到,自动拆分复杂网页并非易事。它面临着多方面的技术挑战:
- 网页结构的高度异构性: 不同的网站有不同的HTML结构、CSS样式。同一网站的不同页面也可能存在差异。如何从中统一有效地提取“主要内容”是一个难题。
- 动态内容与JavaScript渲染: 许多现代网页的内容是通过JavaScript动态加载的,传统的HTTP请求和HTML解析器无法直接获取这些内容。
- 噪音与干扰信息: 网页上充斥着导航栏、广告、评论区、侧边栏、页脚等与核心内容无关的“噪音”,需要有效识别并剔除。
- 语义边界的模糊性: 人类在阅读时可以轻易判断一个段落何时结束、何时开始下一个主题,但让机器准确判断这些“语义边界”需要复杂的算法和模型。
- 上下文的维护: 即使成功拆分,如何确保每个切片都能保留其在原网页中的上下文信息(例如,切片所属的章节标题、日期、作者等),以便后续检索时提供更丰富的语境。
- 多模态内容处理: 网页除了文本,还有图片、视频、表格等。如何将这些多模态信息有效地融入文本切片,或为其生成相应的文本表示,也是一个挑战。
3. AI 驱动的语义切片核心流程与技术栈
接下来,我们将展开AI驱动的语义切片工作的核心流程,并介绍在每个阶段可以使用的技术和工具。整个流程可以概括为以下几个主要步骤:
- 网页内容获取与预处理 (Web Content Acquisition & Preprocessing)
- 主要内容提取与噪音过滤 (Main Content Extraction & Noise Filtering)
- 结构化初步切分 (Initial Structural Segmentation)
- AI 驱动的语义边界检测与合并/拆分 (AI-driven Semantic Boundary Detection & Merging/Splitting)
- 切片精炼与元数据生成 (Chunk Refinement & Metadata Generation)
我们以 Python 为主要编程语言,结合常见的开源库进行讲解。
3.1 步骤一:网页内容获取与预处理
这是整个流程的起点,我们需要从互联网上获取网页的HTML内容,并对其进行初步的清洗。
技术栈:
requests: 用于获取静态HTML内容。selenium/playwright: 用于处理动态加载内容的网页(JavaScript渲染)。BeautifulSoup4: 用于解析HTML,进行DOM操作。
示例代码:
import requests
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
import time
def get_html_static(url: str) -> str:
"""
获取静态网页的HTML内容。
"""
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status() # 检查HTTP错误
return response.text
except requests.exceptions.RequestException as e:
print(f"获取静态网页失败: {e}")
return ""
def get_html_dynamic(url: str, headless: bool = True) -> str:
"""
使用Selenium获取动态网页的HTML内容。
需要ChromeDriver或对应的浏览器驱动。
"""
chrome_options = Options()
if headless:
chrome_options.add_argument("--headless") # 无头模式
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
# 假设ChromeDriver在PATH中,或者指定路径
# service = Service('/path/to/chromedriver') # 如果chromedriver不在系统PATH中,需要指定路径
driver = webdriver.Chrome(options=chrome_options) # service=service
try:
driver.get(url)
time.sleep(3) # 等待JavaScript加载完成,可根据实际情况调整
return driver.page_source
except Exception as e:
print(f"获取动态网页失败: {e}")
return ""
finally:
driver.quit()
def clean_html_initial(html_content: str) -> str:
"""
对HTML内容进行初步清洗,移除脚本、样式等非文本元素。
"""
soup = BeautifulSoup(html_content, 'html.parser')
# 移除script和style标签
for script_or_style in soup(['script', 'style']):
script_or_style.decompose()
# 移除注释
for comment in soup.find_all(string=lambda text: isinstance(text, Comment)):
comment.extract()
return str(soup)
if __name__ == "__main__":
# 示例URL
static_url = "https://www.example.com/static-article" # 替换为实际的静态文章URL
dynamic_url = "https://www.example.com/dynamic-page" # 替换为实际的动态页面URL
# 获取静态网页
static_html = get_html_static(static_url)
if static_html:
cleaned_static_html = clean_html_initial(static_html)
# print("--- 初步清洗后的静态HTML片段 ---")
# print(cleaned_static_html[:500]) # 打印前500字符
# 获取动态网页 (注意:需要配置好Selenium环境和ChromeDriver)
# dynamic_html = get_html_dynamic(dynamic_url)
# if dynamic_html:
# cleaned_dynamic_html = clean_html_initial(dynamic_html)
# # print("--- 初步清洗后的动态HTML片段 ---")
# # print(cleaned_dynamic_html[:500]) # 打印前500字符
3.2 步骤二:主要内容提取与噪音过滤
这一步是决定切片质量的关键。我们需要从整个HTML中识别出真正承载文章主体内容的区域,并过滤掉所有无关的导航、广告、页脚等。
技术栈:
BeautifulSoup4: 进行DOM遍历和元素选择。- 启发式规则 (Heuristic Rules): 基于HTML标签、CSS类名、ID等特征进行判断。
readability-lxml/goose3: 专门用于提取网页主要内容的库。它们通常结合了多种启发式规则和机器学习模型。
启发式规则举例:
- 标签权重:
div、<article>、<main>、<section>通常包含主要内容。 - 文本密度: 区域内文本字符数与标签数量的比值。
- 链接密度: 导航区通常有大量链接,而正文区相对较少。
- CSS类名/ID: 常见的如
article-content,main-body,post-body等。 - 长度过滤: 剔除过短的文本块(如版权信息、广告语)。
示例代码(结合BeautifulSoup和启发式规则):
from bs4 import BeautifulSoup, Comment
import re
from typing import List, Dict, Optional
def extract_main_content(html_content: str, url: str) -> Optional[BeautifulSoup]:
"""
尝试从HTML中提取主要文章内容区域。
结合了多种启发式规则,并可扩展。
"""
soup = BeautifulSoup(html_content, 'html.parser')
# 移除常见的非内容区域
for tag in soup.find_all(['header', 'footer', 'nav', 'aside', 'form', 'iframe', 'script', 'style']):
tag.decompose()
# 移除评论区 (常见的id或class)
for comment_tag in soup.find_all(lambda tag:
tag.name == 'div' and (
'comment' in (tag.get('id', '') or tag.get('class', [])) or
'comments' in (tag.get('id', '') or tag.get('class', []))
)
):
comment_tag.decompose()
# 尝试查找语义化的主要内容标签
main_content_tags = soup.find(['article', 'main'], recursive=True)
if main_content_tags:
return main_content_tags
# 如果没有语义化标签,尝试通过启发式规则查找
# 寻找包含最多<p>标签的<div>或<section>
best_candidate = None
max_p_tags = 0
# 遍历所有div和section标签
for candidate in soup.find_all(['div', 'section']):
p_tags_count = len(candidate.find_all('p', recursive=False)) # 只计算直接子级的p标签
# 进一步过滤:排除看起来像导航、广告或列表的div
class_id_str = (candidate.get('id', '') + ' ' + ' '.join(candidate.get('class', []))).lower()
if any(keyword in class_id_str for keyword in ['nav', 'menu', 'sidebar', 'ad', 'footer', 'header', 'related-posts', 'comments']):
continue
if p_tags_count > max_p_tags:
max_p_tags = p_tags_count
best_candidate = candidate
# 也可以考虑文本密度,例如:
# text_len = len(candidate.get_text(separator=' ', strip=True))
# if text_len > X and p_tags_count > Y: ...
if best_candidate and max_p_tags > 0: # 至少包含一个段落
return best_candidate
# 终极回退:整个body标签,但此时噪音可能很多
return soup.find('body')
def extract_title(soup: BeautifulSoup) -> str:
"""
从BeautifulSoup对象中提取页面标题。
"""
if soup.title and soup.title.string:
return soup.title.string.strip()
# 尝试从H1标签中提取
h1_tag = soup.find('h1')
if h1_tag:
return h1_tag.get_text(strip=True)
return "无标题"
if __name__ == "__main__":
# 使用之前获取的 cleaned_static_html
# cleaned_static_html = get_html_static("https://www.example.com/article") # 假设我们从这里获取
# cleaned_static_html = clean_html_initial(cleaned_static_html)
# 模拟一个复杂的HTML
sample_html = """
<html>
<head><title>这是一篇关于AI的文章标题</title></head>
<body>
<header>
<nav><a href="#">首页</a><a href="#">关于我们</a></nav>
<h1>AI技术的发展与应用</h1>
</header>
<aside class="sidebar">
<div class="ad">广告内容</div>
<ul><li>相关文章1</li><li>相关文章2</li></ul>
</aside>
<main id="main-content">
<article class="post-body">
<h2>引言</h2>
<p>人工智能(AI)正在以前所未有的速度改变着世界。</p>
<p>从数据分析到自动驾驶,AI的应用无处不在。</p>
<h3>机器学习的基石</h3>
<p>机器学习是AI的核心分支,它让计算机能够从数据中学习。</p>
<p>常见的算法包括监督学习、无监督学习和强化学习。</p>
<img src="ai_graph.png" alt="AI发展图">
<h3>深度学习的崛起</h3>
<p>深度学习作为机器学习的一个子领域,凭借多层神经网络取得了突破。</p>
<p>特别是在图像识别和自然语言处理领域。</p>
<div class="comments">
<h4>评论区</h4>
<p>用户A: 写得真好!</p>
</div>
</article>
</main>
<footer><p>© 2023 版权所有</p></footer>
</body>
</html>
"""
main_content_soup = extract_main_content(sample_html, "https://www.example.com/ai-article")
if main_content_soup:
print("--- 提取到的主要内容 ---")
# print(main_content_soup.prettify()) # 打印美化后的主要内容HTML
# 提取标题
page_title = extract_title(BeautifulSoup(sample_html, 'html.parser')) # 从原始soup提取标题
print(f"原始页面标题: {page_title}")
# 提取主要文本
main_text = main_content_soup.get_text(separator='n', strip=True)
print("n--- 主要文本内容片段 ---")
print(main_text[:500])
else:
print("未能提取到主要内容。")
3.3 步骤三:结构化初步切分
在获得主要内容文本后,我们可以利用HTML的结构信息进行初步的切分。这通常是基于标题标签(<h1>到<h6>)和段落标签(<p>)进行的。这种方法能确保每个切片至少在结构上是相对独立的。
技术栈:
BeautifulSoup4: 遍历DOM树。
切分策略:
- 按标题切分: 将每个H标签(
h1,h2,h3等)视为一个潜在的切片起点,其下的所有内容直到下一个同级或更高级标题为一个切片。 - 按段落组合: 在没有明确标题分隔的情况下,可以将连续的多个段落组合成一个切片,直到遇到一个非段落元素或达到最大切片长度。
示例代码:
from bs4 import BeautifulSoup
from typing import List, Dict
def structural_split(main_content_soup: BeautifulSoup) -> List[Dict[str, str]]:
"""
基于HTML结构对主要内容进行初步切分。
以H标签作为主要分隔符,并将其下的P标签内容聚合。
"""
chunks = []
current_chunk_title = ""
current_chunk_content = []
# 获取所有可能包含内容的标签:h1-h6, p, li, blockquote等
content_tags = main_content_soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'li', 'blockquote', 'pre', 'img'])
for tag in content_tags:
tag_text = tag.get_text(separator=' ', strip=True)
if not tag_text: # 忽略空标签
continue
if tag.name.startswith('h'):
# 遇到新的标题,如果当前切片有内容,则保存
if current_chunk_content:
chunks.append({
"title": current_chunk_title,
"content": "n".join(current_chunk_content).strip(),
"chunk_id": f"chunk_{len(chunks)+1}"
})
current_chunk_content = []
# 更新当前切片标题,并将标题本身作为切片内容的一部分
current_chunk_title = tag_text
current_chunk_content.append(tag_text) # 标题也作为内容的一部分
elif tag.name == 'img': # 处理图片,可以提取其alt或title作为内容
alt_text = tag.get('alt', '')
if alt_text:
current_chunk_content.append(f"[图片描述]: {alt_text}")
else: # 普通内容标签,如p, li, blockquote, pre
current_chunk_content.append(tag_text)
# 添加最后一个切片
if current_chunk_content:
chunks.append({
"title": current_chunk_title if current_chunk_title else "无标题段落",
"content": "n".join(current_chunk_content).strip(),
"chunk_id": f"chunk_{len(chunks)+1}"
})
return chunks
if __name__ == "__main__":
# 沿用之前的 sample_html
sample_html = """
<html>
<head><title>这是一篇关于AI的文章标题</title></head>
<body>
<main id="main-content">
<article class="post-body">
<h2>引言</h2>
<p>人工智能(AI)正在以前所未有的速度改变着世界。</p>
<p>从数据分析到自动驾驶,AI的应用无处不在。</p>
<h3>机器学习的基石</h3>
<p>机器学习是AI的核心分支,它让计算机能够从数据中学习。</p>
<p>常见的算法包括监督学习、无监督学习和强化学习。</p>
<img src="ai_graph.png" alt="AI发展图">
<p>这些算法各有优缺点,适用于不同的场景。</p>
<h3>深度学习的崛起</h3>
<p>深度学习作为机器学习的一个子领域,凭借多层神经网络取得了突破。</p>
<p>特别是在图像识别和自然语言处理领域。</p>
<h4>卷积神经网络</h4>
<p>CNN在图像处理中表现卓越。</p>
<p>而RNN则擅长处理序列数据,如文本。</p>
<h2>未来展望</h2>
<p>AI的未来充满无限可能,但同时也面临挑战。</p>
</article>
</main>
</body>
</html>
"""
main_content_soup = extract_main_content(sample_html, "https://www.example.com/ai-article")
if main_content_soup:
initial_chunks = structural_split(main_content_soup)
print("n--- 结构化初步切片结果 ---")
for i, chunk in enumerate(initial_chunks):
print(f"Chunk {i+1} (Title: '{chunk['title']}'):")
print(f" Content (first 100 chars): {chunk['content'][:100]}...")
print("-" * 20)
else:
print("未能提取主要内容进行结构化切分。")
3.4 步骤四:AI 驱动的语义边界检测与合并/拆分
这是整个流程的核心,也是AI发挥关键作用的环节。我们不再仅仅依赖HTML结构,而是利用语言模型对内容的语义进行深度理解,从而确定更准确的切片边界。
主要方法:
- 基于大语言模型 (LLM) 的语义切分: 直接利用LLM的理解和生成能力来识别语义边界、总结内容。
- 基于文本嵌入 (Embeddings) 和相似度检测: 将文本转换为向量,通过计算向量间的相似度来发现语义上的断裂点。
- 结合规则与AI的混合方法: 先用规则进行初步切分,再用AI进行精炼。
技术栈:
transformers: Hugging Face 的库,用于加载预训练的LLM和Embedding模型。sentence-transformers: 简化句向量模型的使用。scikit-learn/faiss: 用于向量相似度计算和聚类。- LLM API: OpenAI GPT系列、Google Gemini、Anthropic Claude、或自部署的开源模型(如Llama 2, Mistral)。
3.4.1 方法一:LLM 驱动的语义切分
这种方法直接利用LLM对文本的理解能力来完成切分任务。
思路:
- Prompt Engineering: 设计一个清晰的Prompt,指导LLM将长文本拆分为语义连贯的切片,并可能要求提供每个切片的摘要或关键词。
- 迭代与分层处理: 对于超长文本,可能需要先进行初步的结构化切分,然后对每个结构化块再调用LLM进行细粒度切分;或者采用滑动窗口的方式。
Prompt 示例:
你是一个内容切分专家。请将以下文本内容拆分成一系列语义连贯、自成一体的独立切片。
每个切片应围绕一个明确的主题,并包含足够的上下文信息。
请以JSON数组的形式返回结果,每个对象包含 'chunk_title' (切片标题,或从内容中提取的关键短语) 和 'chunk_content' (切片内容)。
请确保切片长度适中,避免过长或过短,理想长度在100-500字之间。
文本内容:
[待切分的文本]
示例代码(概念性,依赖于实际LLM API调用):
import os
import openai # 假设使用OpenAI API
import json
from typing import List, Dict
# 配置OpenAI API Key
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"
# openai.api_key = os.getenv("OPENAI_API_KEY")
def llm_based_semantic_chunking(text: str, max_chunk_chars: int = 1500) -> List[Dict[str, str]]:
"""
使用LLM对文本进行语义切分。
为避免LLM上下文窗口限制,对长文本进行分段处理。
"""
if len(text) < 200: # 对于非常短的文本,直接返回一个切片
return [{"chunk_title": text[:50], "chunk_content": text}]
# 策略:如果文本过长,先进行简单的固定长度切分,然后对每个分段调用LLM
# 更高级的策略可以是先用embeddings识别粗粒度边界
# 模拟LLM调用
# 实际项目中,这里会调用 openai.ChatCompletion.create 或其他LLM API
# 并且需要处理API返回的JSON格式
# 简单模拟,实际LLM会返回更智能的切片
print(f"--- 正在调用LLM进行语义切分 (模拟) ---")
# 假设LLM能够处理的上下文长度有限,这里进行简单分段
segments = []
current_segment = []
current_len = 0
sentences = re.split(r'(?<=[.!?。!?])s+', text) # 按句子切分
for sentence in sentences:
if current_len + len(sentence) + 1 > max_chunk_chars and current_segment:
segments.append(" ".join(current_segment))
current_segment = [sentence]
current_len = len(sentence)
else:
current_segment.append(sentence)
current_len += len(sentence) + 1
if current_segment:
segments.append(" ".join(current_segment))
final_chunks = []
for i, seg in enumerate(segments):
# 实际LLM调用部分
# try:
# response = openai.ChatCompletion.create(
# model="gpt-3.5-turbo", # 或 gpt-4
# messages=[
# {"role": "system", "content": "你是一个内容切分专家。"},
# {"role": "user", "content": f"""请将以下文本内容拆分成一系列语义连贯、自成一体的独立切片。
# 每个切片应围绕一个明确的主题,并包含足够的上下文信息。
# 请以JSON数组的形式返回结果,每个对象包含 'chunk_title' (切片标题,或从内容中提取的关键短语) 和 'chunk_content' (切片内容)。
# 文本内容:n{seg}"""}
# ],
# temperature=0.0
# )
# llm_output_str = response.choices[0].message['content']
# llm_chunks = json.loads(llm_output_str)
# final_chunks.extend(llm_chunks)
# except Exception as e:
# print(f"LLM调用失败或JSON解析错误: {e}. 回退到简单切片。")
# final_chunks.append({"chunk_title": f"内容分段 {i+1}", "chunk_content": seg})
# 模拟LLM返回,将每个分段视为一个切片
final_chunks.append({"chunk_title": f"内容分段 {i+1}", "chunk_content": seg})
return final_chunks
if __name__ == "__main__":
long_text = """
人工智能(AI)正在以前所未有的速度改变着世界。从数据分析到自动驾驶,AI的应用无处不在。
机器学习是AI的核心分支,它让计算机能够从数据中学习。常见的算法包括监督学习、无监督学习和强化学习。
这些算法各有优缺点,适用于不同的场景。例如,监督学习需要大量带标签的数据,而无监督学习则擅长发现数据中的隐藏模式。
深度学习作为机器学习的一个子领域,凭借多层神经网络取得了突破。特别是在图像识别和自然语言处理领域。
卷积神经网络(CNN)在图像处理中表现卓越,而循环神经网络(RNN)则擅长处理序列数据,如文本。
Transformer架构的出现,更是将自然语言处理推向了新的高度。
AI的未来充满无限可能,但也面临着数据隐私、算法偏见和伦理挑战。
如何平衡技术进步与社会责任,是我们需要共同思考的问题。
尽管如此,我们有理由相信,AI将继续为人类社会带来巨大的福祉。
"""
llm_chunks = llm_based_semantic_chunking(long_text, max_chunk_chars=500)
print("n--- LLM语义切片结果 (模拟) ---")
for i, chunk in enumerate(llm_chunks):
print(f"Chunk {i+1} (Title: '{chunk['chunk_title']}'):")
print(f" Content (first 100 chars): {chunk['chunk_content'][:100]}...")
print("-" * 20)
3.4.2 方法二:基于文本嵌入和相似度检测
这种方法更侧重于量化语义距离,通过识别文本流中语义变化的“断层”来确定切片边界。
思路:
- 文本切分为句子或小段落: 将初步处理后的文本切分为更小的单元(如句子或短段落)。
- 生成嵌入向量: 使用Sentence-BERT等预训练模型为每个小单元生成高维向量嵌入。
- 计算相似度: 计算相邻小单元(或滑动窗口内的单元)之间的余弦相似度。
- 识别边界: 相似度显著下降的点被认为是潜在的语义边界。可以设置阈值或使用峰谷检测算法。
- 合并与精炼: 根据相似度分数,合并语义相似的相邻小单元,形成最终的切片。
技术栈:
sentence-transformers: 用于生成高质量的句向量。numpy: 进行数值计算。scikit-learn: 用于余弦相似度计算。
示例代码:
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import re
from typing import List, Dict
# 加载预训练的Sentence-BERT模型
# model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2') # 适用于多语言
model = SentenceTransformer('all-MiniLM-L6-v2') # 英文,效果好,速度快
def embed_and_chunk_by_similarity(text: str, similarity_threshold: float = 0.7, min_chunk_len: int = 50, max_chunk_len: int = 500) -> List[Dict[str, str]]:
"""
基于文本嵌入相似度进行语义切分。
"""
# 1. 将文本切分为句子或短小段落
sentences = re.split(r'(?<=[.!?。!?])s+', text)
sentences = [s.strip() for s in sentences if s.strip()]
if not sentences:
return []
# 2. 生成嵌入向量
print("--- 正在生成句子嵌入向量 ---")
sentence_embeddings = model.encode(sentences, convert_to_tensor=True)
# 3. 计算相邻句子间的余弦相似度
similarities = []
for i in range(len(sentence_embeddings) - 1):
sim = cosine_similarity(
sentence_embeddings[i].reshape(1, -1),
sentence_embeddings[i+1].reshape(1, -1)
)[0][0]
similarities.append(sim)
# 4. 识别边界(相似度低于阈值或显著下降)
# 这是一个简化的边界检测,可以替换为更复杂的算法,如C99, TextTiling等
chunk_boundaries = [0] # 第一个切片从第一个句子开始
for i, sim in enumerate(similarities):
if sim < similarity_threshold:
chunk_boundaries.append(i + 1) # 在相似度低的句子之间插入边界
chunk_boundaries.append(len(sentences)) # 最后一个切片到文本末尾
# 5. 合并与精炼:根据边界构建切片
final_chunks = []
current_start = chunk_boundaries[0]
for i in range(1, len(chunk_boundaries)):
current_end = chunk_boundaries[i]
# 尝试合并短小的切片,或者拆分过长的切片
chunk_sentences = sentences[current_start:current_end]
chunk_content = " ".join(chunk_sentences)
# 简单长度检查和调整
if len(chunk_content) < min_chunk_len and len(final_chunks) > 0:
# 如果当前切片过短,尝试合并到前一个切片
final_chunks[-1]["chunk_content"] += " " + chunk_content
# 重新计算前一个切片的标题(简单处理,实际可能需要更智能)
final_chunks[-1]["chunk_title"] = final_chunks[-1]["chunk_content"][:50] + "..."
elif len(chunk_content) > max_chunk_len:
# 如果切片过长,可能需要进一步拆分(这里简化为不拆分,直接作为一个大块)
# 实际可以递归调用此函数或使用LLM进一步拆分
final_chunks.append({
"chunk_title": chunk_content[:50] + "...",
"chunk_content": chunk_content,
"chunk_id": f"chunk_{len(final_chunks)+1}"
})
else:
final_chunks.append({
"chunk_title": chunk_content[:50] + "...", # 简单提取前50字作为标题
"chunk_content": chunk_content,
"chunk_id": f"chunk_{len(final_chunks)+1}"
})
current_start = current_end
# 进一步处理过短的最后一个切片 (如果存在)
if final_chunks and len(final_chunks[-1]["chunk_content"]) < min_chunk_len and len(final_chunks) > 1:
last_chunk = final_chunks.pop()
final_chunks[-1]["chunk_content"] += " " + last_chunk["chunk_content"]
final_chunks[-1]["chunk_title"] = final_chunks[-1]["chunk_content"][:50] + "..."
return final_chunks
if __name__ == "__main__":
long_text = """
人工智能(AI)正在以前所未有的速度改变着世界。从数据分析到自动驾驶,AI的应用无处不在。
机器学习是AI的核心分支,它让计算机能够从数据中学习。常见的算法包括监督学习、无监督学习和强化学习。
这些算法各有优缺点,适用于不同的场景。例如,监督学习需要大量带标签的数据,而无监督学习则擅长发现数据中的隐藏模式。
深度学习作为机器学习的一个子领域,凭借多层神经网络取得了突破。特别是在图像识别和自然语言处理领域。
卷积神经网络(CNN)在图像处理中表现卓越,而循环神经网络(RNN)则擅长处理序列数据,如文本。
Transformer架构的出现,更是将自然语言处理推向了新的高度。
AI的未来充满无限可能,但也面临着数据隐私、算法偏见和伦理挑战。
如何平衡技术进步与社会责任,是我们需要共同思考的问题。
尽管如此,我们有理由相信,AI将继续为人类社会带来巨大的福祉。
"""
embedding_chunks = embed_and_chunk_by_similarity(long_text, similarity_threshold=0.6)
print("n--- 嵌入相似度语义切片结果 ---")
for i, chunk in enumerate(embedding_chunks):
print(f"Chunk {i+1} (Title: '{chunk['chunk_title']}'):")
print(f" Content (first 100 chars): {chunk['chunk_content'][:100]}...")
print("-" * 20)
3.4.3 混合方法
在实际应用中,通常会采用混合方法以兼顾效率和准确性:
- 初步结构化切分: 利用HTML标签进行粗粒度切分,得到一系列初步的“块”。
- 噪音过滤: 对每个块进行进一步的噪音检测和清理。
- AI精炼: 对每个块(或其组合)应用LLM或嵌入相似度方法,进行细粒度的语义切分或合并。例如,如果一个结构化块过长,可以使用LLM或嵌入相似度进一步拆分;如果多个结构化块过短且语义高度相关,则可以合并。
3.5 步骤五:切片精炼与元数据生成
完成切分后,每个切片仍然需要进一步的精炼,并附加必要的元数据,以便后续检索和利用。
精炼内容:
- 长度调整: 确保切片长度在预设范围内。过短的切片可以尝试合并,过长的切片可能需要进一步拆分。
- 重复内容去重: 避免不同切片包含大量相同内容。
- 上下文补充: 对于某些孤立的切片,可能需要从其父级或兄弟切片中提取少量关键信息作为前缀或后缀,以提供更多语境。
元数据:
为每个切片附加丰富的元数据至关重要。这些元数据将极大地提升检索的准确性和RAG模型的性能。
| 元数据字段 | 描述 | 示例 |
|---|---|---|
chunk_id |
唯一标识符。 | page_123_chunk_001 |
page_url |
原始网页URL。 | https://example.com/article-on-ai |
page_title |
原始网页标题。 | AI技术的发展与应用 |
section_title |
切片所属的最高级(或最近的)章节标题。 | 深度学习的崛起 |
chunk_title |
切片的内部标题,可以是LLM生成的摘要,或从内容中提取的关键短语。 | Transformer架构的突破 |
chunk_content |
切片的主体文本内容。 | Transformer架构的出现,更是将自然语言处理推向了新的高度。 |
start_char_idx |
切片在原始文本中的起始字符索引。 | 1200 |
end_char_idx |
切片在原始文本中的结束字符索引。 | 1500 |
summary |
LLM对切片内容的简短摘要(可选)。 | 本切片介绍了Transformer架构在NLP领域的关键作用。 |
keywords |
从切片中提取的关键词(可选)。 | ['Transformer', 'NLP', '深度学习'] |
publication_date |
原始网页的发布日期。 | 2023-10-26 |
author |
原始网页的作者。 | 张三 |
示例代码(元数据生成):
from typing import List, Dict
import hashlib # 用于生成稳定的chunk_id
import datetime
def generate_chunk_metadata(
chunks: List[Dict[str, str]],
original_url: str,
original_title: str,
original_text: str
) -> List[Dict]:
"""
为切片生成元数据。
"""
final_chunks_with_metadata = []
current_char_idx = 0
for i, chunk in enumerate(chunks):
chunk_content = chunk['content']
# 查找切片内容在原始文本中的位置
start_idx = original_text.find(chunk_content, current_char_idx)
if start_idx == -1: # 如果找不到,可能因为清洗或 LLM 修改了内容,这里简单处理
start_idx = current_char_idx
end_idx = current_char_idx + len(chunk_content)
else:
end_idx = start_idx + len(chunk_content)
current_char_idx = end_idx # 更新起始查找位置
# 生成稳定的 chunk_id
chunk_unique_str = f"{original_url}_{chunk_content}"
chunk_id = hashlib.sha256(chunk_unique_str.encode('utf-8')).hexdigest()[:16] # 取前16位
metadata = {
"chunk_id": chunk_id,
"page_url": original_url,
"page_title": original_title,
"section_title": chunk.get('title', original_title), # 使用结构化切片时的标题
"chunk_title": chunk.get('chunk_title', chunk_content[:50] + "..."), # LLM生成或提取的前50字
"chunk_content": chunk_content,
"start_char_idx": start_idx,
"end_char_idx": end_idx,
"length_chars": len(chunk_content),
"created_at": datetime.datetime.now().isoformat(),
# 更多元数据可以根据需要添加,如作者、发布日期、关键词、摘要等
}
final_chunks_with_metadata.append(metadata)
return final_chunks_with_metadata
if __name__ == "__main__":
# 假设我们已经有了llm_chunks或embedding_chunks
# 这里使用一个简单的示例
sample_chunks = [
{"title": "引言", "content": "人工智能(AI)正在以前所未有的速度改变着世界。从数据分析到自动驾驶,AI的应用无处不在。"},
{"title": "机器学习的基石", "content": "机器学习是AI的核心分支,它让计算机能够从数据中学习。常见的算法包括监督学习、无监督学习和强化学习。"},
{"title": "深度学习的崛起", "content": "深度学习作为机器学习的一个子领域,凭借多层神经网络取得了突破。特别是在图像识别和自然语言处理领域。"}
]
original_full_text = " ".join([c['content'] for c in sample_chunks]) # 假设这是原始的完整文本
final_chunks = generate_chunk_metadata(
chunks=sample_chunks,
original_url="https://www.example.com/ai-overview",
original_title="AI技术发展概览",
original_text=original_full_text
)
print("n--- 带有元数据的最终切片 ---")
for chunk_meta in final_chunks:
print(f"Chunk ID: {chunk_meta['chunk_id']}")
print(f" Page Title: {chunk_meta['page_title']}")
print(f" Section Title: {chunk_meta['section_title']}")
print(f" Chunk Title: {chunk_meta['chunk_title']}")
print(f" Content (first 50 chars): {chunk_meta['chunk_content'][:50]}...")
print(f" Length: {chunk_meta['length_chars']} chars")
print("-" * 30)
4. 评估与迭代
完成语义切片后,评估其质量至关重要。这通常是一个迭代过程,需要结合自动化指标和人工审查。
评估指标:
- Coherence (连贯性): 切片内容是否语义完整、主题单一?
- Completeness (完整性): 切片是否包含了其所表达主题的所有必要信息?
- Relevance (相关性): 切片是否与原始网页的核心内容高度相关,没有引入过多噪音?
- Length Distribution (长度分布): 切片长度是否符合预设的范围?
- Overlap (重叠度): 相邻切片之间是否存在适当的重叠(有助于保持上下文),或是不必要的冗余?
- Downstream Task Performance (下游任务表现): 最直接的评估是看这些切片在实际的检索系统、RAG应用中的表现。例如,检索准确率、RAG生成答案的质量。
迭代优化:
根据评估结果,我们可以调整切分策略、模型参数、Prompt设计,甚至改进数据清洗和主要内容提取的规则。例如:
- 如果发现很多切片过短且语义相关,可以调低相似度阈值或调整合并逻辑。
- 如果LLM切分出现幻觉或切分不准确,可以优化Prompt,增加Few-shot示例。
- 如果特定网站的切分效果不佳,可以为该网站定制特定的HTML解析规则。
5. 高级考量与未来展望
- 多模态切片: 随着多模态AI的发展,将图片、视频、表格等非文本信息与文本切片结合,或为其生成文本描述,将是未来的重要方向。例如,为图片生成详细的caption,或将表格转换为结构化文本。
- 知识图谱集成: 将切片与知识图谱中的实体和关系关联起来,可以进一步增强切片的语义丰富性,实现更复杂的知识检索。
- 动态适应性切片: 根据用户查询的意图和长度,动态调整切片的粒度。例如,宽泛的查询可能需要更长的切片,而精确的查询则需要更短、更聚焦的切片。
- 效率与成本: LLM API的调用成本和延迟是需要考虑的因素。对于大规模的网页处理,需要权衡LLM与基于嵌入方法的结合,或者采用更小、更快的本地模型。
- 实时切片: 对于新闻流或实时更新的网页,如何实现近实时的切片生成和索引,也是一个挑战。
通过今天对AI驱动的网页语义切片的深入探讨,我们看到了一条通往更高效、更智能信息检索的道路。从前端的网页内容获取,到核心的AI语义理解与切分,再到后端的元数据管理和评估,每一步都凝聚着技术与智慧。这条路径不仅提升了信息检索的精准度,也为RAG等前沿AI应用奠定了坚实基础,使我们能够更好地驾驭数字世界的浩瀚信息。这无疑是构建下一代智能信息系统的关键基石。