Instructor 文档智能分段实战指南:用 Cohere command-a 将长文档切成语义完整的 Section
【免费下载链接】instructorstructured outputs for llms项目地址: https://gitcode.com/GitHub_Trending/in/instructor
本文是一份基于 Instructor 结构化输出能力的文档分段(Document Segmentation)实战指南。文章以Section+StructuredDocument两个 Pydantic 模型为骨架,讲解如何让长上下文 LLM(以 Cohere command-a 为例)把任意长文档切成一个个围绕单一核心概念的分段,并在分段后无损还原原始文本。读完本文,你将掌握一套不依赖正则规则、可复用在论文、教程、代码文档等任意场景的分段流水线。
为什么需要 LLM 驱动的文档分段
很多时候,我们需要把一份长文档拆分成"有意义的"段落,且每个段落都围绕一个核心概念展开——例如把一篇技术教程切分成可以按课时讲授的若干主题块。
传统的基于长度或规则的文本分割器(text-splitter)并不可靠:它们通常依赖空行、标题等表面特征,一旦文档中出现代码片段或数学公式,这些内容既不能简单地在'\n\n'处切开(会破坏语法完整性),也难以为每种文档类型编写大量专门规则。幸运的是,具备足够长上下文窗口的 LLM 天然适合完成这项任务:它能够理解语义边界,判断哪里是一个概念的结束、另一个概念的开始。
这正是 Instructor 的用武之地:我们用 Pydantic 模型声明"分段结果"的 schema,让 LLM 以结构化输出的形式返回分段边界,从而把不可靠的文本切分升级为可校验、可复现的语义切分。
定义分段的数据结构
分段任务的输出模型非常简单,只有两个类:
Section:描述文档中的一段。包含该段主题标题,以及该段在文档中的起止行号。StructuredDocument:封装一份文档全部分段的容器。
from pydantic import BaseModel, Field from typing import List class Section(BaseModel): title: str = Field(description="main topic of this section of the document") start_index: int = Field(description="line number where the section begins") end_index: int = Field(description="line number where the section ends") class StructuredDocument(BaseModel): """obtains meaningful sections, each centered around a single concept/topic""" sections: List[Section] = Field(description="a list of sections of the document")这里有一个非常关键的设计:LLM 只返回分段的起止行号,而不是分段正文。Field(description=...)中给每个字段补充了语义描述,帮助 LLM 准确理解start_index/end_index的含义。这样做的直接好处是:
- 避免 LLM 重新生成原文——模型不参与内容创作,自然不会有改写、遗漏或幻觉文本混入;
- 输出极其轻量——即使输入文档有几千行,结构化输出也只是若干个小整数区间;
- 无损还原——分段正文完全从原始文档按行号切取,保证与原文逐字一致。
文档预处理:给每一行编号
为了让 LLM 能够用行号引用文档位置,我们需要在送入模型前对文档做一次预处理:把每一行前面加上行号标记(如[0]、[1]、[2]),同时维护一张行号 -> 原文的映射表,供后续还原使用。
def doc_with_lines(document): document_lines = document.split("\n") document_with_line_numbers = "" line2text = {} for i, line in enumerate(document_lines): document_with_line_numbers += f"[{i}] {line}\n" line2text[i] = line return document_with_line_numbers, line2text这个函数返回两个东西:
document_with_line_numbers:带行号标记的文档文本,直接作为用户消息发送给 LLM;line2text:字典{行号: 原文},在分段完成后用于把start_index–end_index区间映射回真实文本。
行号从 0 开始连续编号,LLM 在 system prompt 中会被告知方括号里的数字即行号。
使用 Instructor + Cohere 提取分段
接下来是核心环节:创建 Instructor 客户端,让 LLM 从带行号的文档中提取StructuredDocument。
import instructor # Apply the patch to the cohere client # enables response_model keyword client = instructor.from_provider("cohere/command-r-plus") system_prompt = f"""\ You are a world class educator working on organizing your lecture notes. Read the document below and extract a StructuredDocument object from it where each section of the document is centered around a single concept/topic that can be taught in one lesson. Each line of the document is marked with its line number in square brackets (e.g. [1], [2], [3], etc). Use the line numbers to indicate section start and end. """ def get_structured_document(document_with_line_numbers) -> StructuredDocument: return client.create( model="command-a-03-2025", response_model=StructuredDocument, messages=[ { "role": "system", "content": system_prompt, }, { "role": "user", "content": document_with_line_numbers, }, ], ) # type: ignore这段代码有四个要点:
客户端初始化:
instructor.from_provider(...)是 Instructor 提供的统一工厂方法,传入形如"cohere/command-a-03-2025"的provider/model字符串即可自动完成 provider 识别、客户端创建与补丁(patch)注入,从而让client.create支持response_model关键字。从当前仓库源码看,该方法实现在 instructor/v2/auto_client.py,要求模型字符串必须是"provider/model-name"格式,并支持async_client、cache、mode等扩展参数。System prompt 设计:这里把 LLM 塑造成"整理讲义的世界级教育家",明确要求"每个 section 围绕一个可在一节课内讲完的单一概念",并强调方括号内的数字是行号,必须用行号标注起止。Prompt 的角色设定与输出约束直接决定了分段质量。
模型选择:
command-a-03-2025是 Cohere 的 256k 长上下文模型,足以容纳一篇完整教程;command-r-plus则为相对轻量的备选。两者的选择取决于你的文档长度与预算。返回类型标注:
-> StructuredDocument让 IDE 与类型检查器能感知返回值结构,response_model=StructuredDocument则让 Instructor 在运行时完成校验与反序列化。
根据起止行号还原分段正文
拿到StructuredDocument后,借助预处理阶段的line2text映射,把每个分段的起止行号区间还原成真实文本:
def get_sections_text(structured_doc, line2text): segments = [] for s in structured_doc.sections: contents = [] for line_id in range(s.start_index, s.end_index): contents.append(line2text.get(line_id, '')) segments.append( { "title": s.title, "content": "\n".join(contents), "start": s.start_index, "end": s.end_index, } ) return segments注意两点实现细节:
- 循环使用
range(s.start_index, s.end_index),即左闭右开区间,第end_index行本身不包含在本段内,分段之间不会重叠; - 使用
line2text.get(line_id, '')兜底,即便 LLM 偶尔给出越界行号也不会抛异常,只会得到空行,增强了健壮性。
每个返回的 segment 是一个包含title、content、start、end的字典,既便于阅读,也方便后续接知识图谱、向量化索引等下游任务。
完整示例:切分一篇 Transformer 教程
下面把上述类与函数串起来,演示如何分段 Sebastian Raschka 的《Self-Attention from Scratch》教程。我们使用trafilatura包抓取并抽取网页正文:
from trafilatura import fetch_url, extract import instructor from pydantic import BaseModel, Field from typing import List def doc_with_lines(document): document_lines = document.split("\n") document_with_line_numbers = "" line2text = {} for i, line in enumerate(document_lines): document_with_line_numbers += f"[{i}] {line}\n" line2text[i] = line return document_with_line_numbers, line2text client = instructor.from_provider("cohere/command-r-plus") system_prompt = f"""\ You are a world class educator working on organizing your lecture notes. Read the document below and extract a StructuredDocument object from it where each section of the document is centered around a single concept/topic that can be taught in one lesson. Each line of the document is marked with its line number in square brackets (e.g. [1], [2], [3], etc). Use the line numbers to indicate section start and end. """ class Section(BaseModel): title: str = Field(description="main topic of this section of the document") start_index: int = Field(description="line number where the section begins") end_index: int = Field(description="line number where the section ends") class StructuredDocument(BaseModel): """obtains meaningful sections, each centered around a single concept/topic""" sections: List[Section] = Field(description="a list of sections of the document") def get_structured_document(document_with_line_numbers) -> StructuredDocument: return client.create( model="command-a-03-2025", response_model=StructuredDocument, messages=[ { "role": "system", "content": system_prompt, }, { "role": "user", "content": document_with_line_numbers, }, ], ) # type: ignore def get_sections_text(structured_doc, line2text): segments = [] for s in structured_doc.sections: contents = [] for line_id in range(s.start_index, s.end_index): contents.append(line2text.get(line_id, '')) segments.append( { "title": s.title, "content": "\n".join(contents), "start": s.start_index, "end": s.end_index, } ) return segments url = 'https://sebastianraschka.com/blog/2023/self-attention-from-scratch.html' downloaded = fetch_url(url) document = extract(downloaded) document_with_line_numbers, line2text = doc_with_lines(document) structured_doc = get_structured_document(document_with_line_numbers) segments = get_sections_text(structured_doc, line2text)运行之后,segments就是一个按语义切分好的分段列表。例如第 6 个分段的标题与内容如下:
print(segments[5]['title']) """ Introduction to Multi-Head Attention """ print(segments[5]['content']) """ Multi-Head Attention In the very first figure, at the top of this article, we saw that transformers use a module called multi-head attention. How does that relate to the self-attention mechanism (scaled-dot product attention) we walked through above? In the scaled dot-product attention, the input sequence was transformed using three matrices representing the query, key, and value. These three matrices can be considered as a single attention head in the context of multi-head attention. The figure below summarizes this single attention head we covered previously: As its name implies, multi-head attention involves multiple such heads, each consisting of query, key, and value matrices. This concept is similar to the use of multiple kernels in convolutional neural networks. To illustrate this in code, suppose we have 3 attention heads, so we now extend the \(d' \times d\) dimensional weight matrices so \(3 \times d' \times d\): In: h = 3 multihead_W_query = torch.nn.Parameter(torch.rand(h, d_q, d)) multihead_W_key = torch.nn.Parameter(torch.rand(h, d_k, d)) multihead_W_value = torch.nn.Parameter(torch.rand(h, d_v, d)) Consequently, each query element is now \(3 \times d_q\) dimensional, where \(d_q=24\) (here, let's keep the focus on the 3rd element corresponding to index position 2): In: multihead_query_2 = multihead_W_query.matmul(x_2) print(multihead_query_2.shape) Out: torch.Size([3, 24]) """可以看到:含数学公式与 PyTorch 代码块的整块内容被完整保留在一个分段里,标题也准确概括为 "Introduction to Multi-Head Attention"——这正是基于语义的分段相对朴素文本切分的核心价值。同样的方法可以迁移到任意需要把复杂长文档拆分成语义块的其他领域。
源码级的实现原理:from_provider 与 from_cohere
为了让你在排查问题或扩展功能时心里有底,这里结合当前仓库源码说明这条流水线背后的实现机制。
统一入口instructor.from_provider:其实现在 instructor/v2/auto_client.py 中,逻辑是:按"/"拆分模型字符串得到 provider 名与模型名,再从 provider 注册表(instructor/v2/core/provider_specs.py,Cohere 的别名注册为"cohere")查找对应的from_cohere工厂函数,最终返回一个 Instructor 实例。它支持async_client=True返回异步客户端、cache注入缓存适配器、mode覆盖 provider 默认模式。
Cohere 适配层:instructor.providers.cohere.client是一个兼容门面,真实实现在 instructor/v2/providers/cohere/client.py 的from_cohere。从源码可以看到它同时兼容 Cohere 的 V1(cohere.Client/AsyncClient)与 V2(cohere.ClientV2/AsyncClientV2)SDK,并做两件事:
- 模式归一化:将 Cohere 特有的工具调用模式归一化为通用的
Mode.TOOLS等模式,并校验该模式已在 Cohere 的注册表中注册(未注册则抛出ModeError); - 补丁注入:通过
patch_v2包装client.chat/client.chat_stream,使create方法获得response_model结构化输出能力,同时根据客户端版本自动切换消息格式——V2 使用 OpenAI 兼容的messages格式,V1 使用message+chat_history格式。
仓库内的可运行参考实现见 examples/cohere/cohere.py,它演示了from_cohere的完整用法(含temperature=0以提升确定性);更详细的 Cohere 集成说明(安装pip install "instructor[cohere]"、导出CO_API_KEY等)见 docs/integrations/cohere.md。
使用要点与注意事项
- 行号格式要与 prompt 约定一致:预处理阶段用什么格式编号(
[0]起始、每行一个编号),prompt 中就要如实说明,两者不一致会显著降低分段准确率。 - 区间语义要统一:
start_index包含、end_index排他(左闭右开)的约定应在Field(description)中写清楚,并保证get_sections_text的range逻辑与之一致。 - 长文档与大模型:分段质量依赖模型的上下文长度与指令遵循能力。若使用其他模型或更长文档,需评估上下文窗口是否足够;本文使用的 command-a 系列模型具备 256k 长上下文,是这类任务的合适选择。
- 确定性优先:分段属于"定位"类任务而非"生成"类任务,在预算允许时建议将
temperature设为 0(参考 examples/cohere/cohere.py 的做法),以获得稳定、可复现的边界结果。 - 越界兜底:
line2text.get(line_id, '')保证了极端情况下流水线不会崩溃,但在生产环境建议额外校验start_index < end_index并过滤空分段。
延伸阅读
本文的分段结果可以直接作为下游任务的数据源,仓库中提供了相关配套指南:
- 知识图谱构建 —— 从文档构建知识图谱
- 实体解析 —— 识别并对齐实体
- 列表抽取 —— 抽取多个对象
- 嵌套结构 —— 复杂层级模型建模
- from_provider 客户端配置 —— 更全面的客户端初始化方式
【免费下载链接】instructorstructured outputs for llms项目地址: https://gitcode.com/GitHub_Trending/in/instructor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考