0Pricing
AI Engineering Academy · 课时

针对代码和 HTML 的文档专用策略

使用基于 AST 的函数分割器为 Python 代码应用专用分块,使用能够识别标签的解析器处理 HTML,并依据标题层级处理 Markdown。

针对代码和 HTML 的文档专用策略 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。

通用分块为何无法处理专用文档

基于文本的分块是为散文式文本设计的,但现实中的数据还包括源代码、HTML 页面和Markdown 文档。在固定字符边界处分割代码,可能会从函数体中间截断函数,使文本块无法用于检索。专用文档需要能够理解其内部结构的分块器,而不仅仅是根据长度进行处理。

基于 AST 的 Python 代码分块

Python 文件的抽象语法树(AST)会将每个函数、类和模块表示为结构化节点。遍历 AST 后,您可以将每个函数或方法提取为单独的文本块,同时保留签名、文档字符串和函数体。LangChain 的 PythonCodeTextSplitter 在内部正是采用了这种方法。

import ast
import textwrap

def extract_functions(source_code: str) -> list[dict]:
    tree = ast.parse(source_code)
    chunks = []
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            start = node.lineno - 1
            end = node.end_lineno
            lines = source_code.splitlines()[start:end]
            chunks.append({
                'name': node.name,
                'code': '\n'.join(lines),
                'start_line': node.lineno,
            })
    return chunks

按类边界分块

对于面向对象的代码库,在类级别进行分块通常比在函数级别进行分块更好。类文本块能够保留方法之间的关系,以及这些方法所操作的共享状态。您可以将类的文档字符串和所有方法体作为一个文本块,然后仅针对较长的方法创建更细粒度的文本块。

from langchain_text_splitters import Language, RecursiveCharacterTextSplitter

python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=1000,
    chunk_overlap=100,
)

with open('my_module.py', 'r') as f:
    source = f.read()

chunks = python_splitter.create_documents([source])
print(f'Created {len(chunks)} code chunks')

向文本块添加代码元数据

原始代码文本块的实用程度取决于其元数据。将代码文本块存储到向量数据库时,请包含文件路径、函数名称、编程语言和行范围。这些元数据可以让检索器按语言或文件进行筛选,也能让 LLM 在回答中引用确切的源代码位置。

from langchain_core.documents import Document

def chunk_python_file(filepath: str) -> list[Document]:
    with open(filepath) as f:
        source = f.read()

    functions = extract_functions(source)  # from previous example
    docs = []
    for fn in functions:
        docs.append(Document(
            page_content=fn['code'],
            metadata={
                'source': filepath,
                'function': fn['name'],
                'language': 'python',
                'start_line': fn['start_line'],
            }
        ))
    return docs

HTML:结构优先于字符

HTML 文档具有由标题、节、段落和列表构成的层次结构。按字符数分割 HTML 通常会截断标签,生成格式错误的片段。正确的方法是使用 BeautifulSoup 之类的适当解析器解析 HTML,并提取具有语义意义的元素,例如 <article>、<section> 和 <p> 标签。

from bs4 import BeautifulSoup

def chunk_html_by_section(html: str) -> list[dict]:
    soup = BeautifulSoup(html, 'html.parser')
    chunks = []
    for tag in soup.find_all(['h1', 'h2', 'h3', 'p', 'li']):
        text = tag.get_text(separator=' ', strip=True)
        if len(text) > 40:  # skip trivial fragments
            chunks.append({
                'tag': tag.name,
                'text': text,
            })
    return chunks

按标题层次进行 HTML 分块

一种更完善的 HTML 策略是将内容归入距离最近的标题下方。每个 <h2> 标题之后的段落和列表都属于该节。通过将文本与其父级标题分组,您可以保留主题上下文,而独立的段落原本会失去这些上下文。LangChain 的 HTML 标题文本分割器会自动实现这一点。

from langchain_text_splitters import HTMLHeaderTextSplitter

headers_to_split_on = [
    ('h1', 'Header 1'),
    ('h2', 'Header 2'),
    ('h3', 'Header 3'),
]

splitter = HTMLHeaderTextSplitter(headers_to_split_on=headers_to_split_on)

with open('page.html') as f:
    html = f.read()

sections = splitter.split_text(html)
for sec in sections[:3]:
    print(sec.metadata)
    print(sec.page_content[:200])
    print('---')

Markdown:遵循标题层次结构

Markdown 文档使用 #、## 和 ### 标题进行组织。MarkdownHeaderTextSplitter 会在标题边界处分割,并将标题层次结构存储在元数据中。这意味着每个文本块都知道自己的完整标题路径,因此当用户询问文档的特定节时,可以大幅提高所检索上下文的相关性。

from langchain_text_splitters import MarkdownHeaderTextSplitter

headers_to_split_on = [
    ('#', 'H1'),
    ('##', 'H2'),
    ('###', 'H3'),
]

md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)

with open('README.md') as f:
    markdown = f.read()

docs = md_splitter.split_text(markdown)
for doc in docs[:2]:
    print('Metadata:', doc.metadata)
    print('Content:', doc.page_content[:300])
    print()

标题分割后的二次分割

按标题分割后,单个节仍可能过长,超出嵌入模型的令牌限制。推荐采用两步分割模式:首先按标题层次结构分割,以保留语义上下文;然后对任何超过文本块大小限制的节应用基于字符的分割器。这样既能确保文本块不会过大,又能保留标题元数据。

from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter

header_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[('#', 'H1'), ('##', 'H2')]
)
char_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
)

with open('docs.md') as f:
    md = f.read()

header_chunks = header_splitter.split_text(md)
final_chunks = char_splitter.split_documents(header_chunks)
print(f'{len(final_chunks)} final chunks produced')

考虑表格的 PDF 分块

使用 PyMuPDF 或 pdfplumber 等工具提取 PDF 时,通常会丢失表格结构,生成杂乱的文本行。为处理这一问题,请使用能够感知布局的 PDF 解析器,检测表格边界框,并在分块前将其转换为 Markdown 或 CSV 格式。应将每个表格作为一个独立文本块,并使用结构化元数据标识其为表格,而不是散文文本。

import pdfplumber

def extract_pdf_chunks(pdf_path: str) -> list[dict]:
    chunks = []
    with pdfplumber.open(pdf_path) as pdf:
        for page_num, page in enumerate(pdf.pages):
            # Extract tables separately
            for table in page.extract_tables():
                rows = ['|'.join(str(c) for c in row) for row in table]
                chunks.append({
                    'type': 'table',
                    'content': '\n'.join(rows),
                    'page': page_num + 1,
                })
            # Extract prose text
            text = page.extract_text()
            if text:
                chunks.append({'type': 'text', 'content': text, 'page': page_num + 1})
    return chunks

混合语料库的语言检测

企业知识库通常混合使用不同的文件类型:Python 脚本、HTML 格式的 API 文档、Markdown 格式的架构说明,以及 CSV 格式的数据导出文件。稳健的分块流程应根据扩展名或 MIME 类型检测文件类型,并将每个文档路由到相应的专用分块器。这样可以避免将代码分割逻辑应用于散文文本,或反过来错误应用。

from pathlib import Path

def route_document(filepath: str) -> list[dict]:
    ext = Path(filepath).suffix.lower()
    if ext == '.py':
        return chunk_python_file(filepath)
    elif ext in ('.html', '.htm'):
        with open(filepath) as f:
            return chunk_html_by_section(f.read())
    elif ext == '.md':
        # use MarkdownHeaderTextSplitter
        return chunk_markdown(filepath)
    elif ext == '.pdf':
        return extract_pdf_chunks(filepath)
    else:
        # fallback: plain text recursive splitter
        return chunk_plain_text(filepath)

通过周围行保留上下文

按函数对代码进行分块时,加入几行周围上下文通常很有价值,例如文件顶部的导入语句,或包含某个方法的类定义。这些上下文有助于 LLM 理解可用的库,以及该函数在更大类结构中的作用,从而提高生成答案的质量。

def chunk_with_imports(source_code: str, fn_node, lines: list[str]) -> str:
    # Gather top-of-file imports (first block before first non-import)
    import_lines = []
    for line in lines:
        stripped = line.strip()
        if stripped.startswith('import ') or stripped.startswith('from '):
            import_lines.append(line)
        elif stripped and not stripped.startswith('#'):
            break

    fn_body = '\n'.join(lines[fn_node.lineno - 1:fn_node.end_lineno])
    return '\n'.join(import_lines) + '\n\n' + fn_body

快速检查

测试您对本课特定文档分块策略的理解。

课程回顾

本课中您学到了:基于 AST 的分块能够保留 Python 函数和类的边界;HTML 标题文本分割器和 MarkdownHeaderTextSplitter 会遵循标题层次结构,将上下文与所属节保留在一起;两步处理方式(先按标题分割,再按字符分割)能够在不丢失结构化元数据的情况下处理过大的节。接下来,我们将探索结合密集检索和稀疏检索的混合搜索。

常见问题解答

「针对代码和 HTML 的文档专用策略」课时是免费的吗?

是的 — 「针对代码和 HTML 的文档专用策略」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。

「针对代码和 HTML 的文档专用策略」这节课中我会学到什么?

使用基于 AST 的函数分割器为 Python 代码应用专用分块,使用能够识别标签的解析器处理 HTML,并依据标题层级处理 Markdown。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 AI Engineering Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「针对代码和 HTML 的文档专用策略」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 AI Engineering Academy 课中编写并运行代码吗?

能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 朴素分块为何会损害检索效果
  2. 使用嵌入相似度进行语义分块
  3. 父子分块与由小到大的检索
  4. 针对代码和 HTML 的文档专用策略
← 返回 AI Engineering Academy