针对代码和 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 docsHTML:结构优先于字符
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 反馈 — 无需本地设置。
此课程中的所有课时
- 朴素分块为何会损害检索效果
- 使用嵌入相似度进行语义分块
- 父子分块与由小到大的检索
- 针对代码和 HTML 的文档专用策略