实战:利用 AI 自动拆分复杂网页,生成更适合‘碎片化检索’的语义切片(Chunks)

各位同仁,下午好!

今天,我们齐聚一堂,探讨一个在信息爆炸时代日益凸显的挑战——如何高效地从浩瀚的网页信息中提取、组织和检索我们真正所需的内容。传统上,我们习惯于将整个网页视为一个不可分割的整体进行索引,但这在面对日益复杂的现代网页时,效率往往不尽如人意。例如,一篇长篇技术博客,可能涵盖多个子主题,用户若只想了解其中某个特定技术点,完整的网页检索结果往往会淹没在大量无关信息中。

我们今天的主题是:利用 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解决方案之前,我们必须清醒地认识到,自动拆分复杂网页并非易事。它面临着多方面的技术挑战:

  1. 网页结构的高度异构性: 不同的网站有不同的HTML结构、CSS样式。同一网站的不同页面也可能存在差异。如何从中统一有效地提取“主要内容”是一个难题。
  2. 动态内容与JavaScript渲染: 许多现代网页的内容是通过JavaScript动态加载的,传统的HTTP请求和HTML解析器无法直接获取这些内容。
  3. 噪音与干扰信息: 网页上充斥着导航栏、广告、评论区、侧边栏、页脚等与核心内容无关的“噪音”,需要有效识别并剔除。
  4. 语义边界的模糊性: 人类在阅读时可以轻易判断一个段落何时结束、何时开始下一个主题,但让机器准确判断这些“语义边界”需要复杂的算法和模型。
  5. 上下文的维护: 即使成功拆分,如何确保每个切片都能保留其在原网页中的上下文信息(例如,切片所属的章节标题、日期、作者等),以便后续检索时提供更丰富的语境。
  6. 多模态内容处理: 网页除了文本,还有图片、视频、表格等。如何将这些多模态信息有效地融入文本切片,或为其生成相应的文本表示,也是一个挑战。

3. AI 驱动的语义切片核心流程与技术栈

接下来,我们将展开AI驱动的语义切片工作的核心流程,并介绍在每个阶段可以使用的技术和工具。整个流程可以概括为以下几个主要步骤:

  1. 网页内容获取与预处理 (Web Content Acquisition & Preprocessing)
  2. 主要内容提取与噪音过滤 (Main Content Extraction & Noise Filtering)
  3. 结构化初步切分 (Initial Structural Segmentation)
  4. AI 驱动的语义边界检测与合并/拆分 (AI-driven Semantic Boundary Detection & Merging/Splitting)
  5. 切片精炼与元数据生成 (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>&copy; 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结构,而是利用语言模型对内容的语义进行深度理解,从而确定更准确的切片边界。

主要方法:

  1. 基于大语言模型 (LLM) 的语义切分: 直接利用LLM的理解和生成能力来识别语义边界、总结内容。
  2. 基于文本嵌入 (Embeddings) 和相似度检测: 将文本转换为向量,通过计算向量间的相似度来发现语义上的断裂点。
  3. 结合规则与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 方法二:基于文本嵌入和相似度检测

这种方法更侧重于量化语义距离,通过识别文本流中语义变化的“断层”来确定切片边界。

思路:

  1. 文本切分为句子或小段落: 将初步处理后的文本切分为更小的单元(如句子或短段落)。
  2. 生成嵌入向量: 使用Sentence-BERT等预训练模型为每个小单元生成高维向量嵌入。
  3. 计算相似度: 计算相邻小单元(或滑动窗口内的单元)之间的余弦相似度。
  4. 识别边界: 相似度显著下降的点被认为是潜在的语义边界。可以设置阈值或使用峰谷检测算法。
  5. 合并与精炼: 根据相似度分数,合并语义相似的相邻小单元,形成最终的切片。

技术栈:

  • 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 混合方法

在实际应用中,通常会采用混合方法以兼顾效率和准确性:

  1. 初步结构化切分: 利用HTML标签进行粗粒度切分,得到一系列初步的“块”。
  2. 噪音过滤: 对每个块进行进一步的噪音检测和清理。
  3. 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应用奠定了坚实基础,使我们能够更好地驾驭数字世界的浩瀚信息。这无疑是构建下一代智能信息系统的关键基石。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注