用 Instructor 蒸馏 Chain of Density:将 GPT-4 的迭代摘要能力压缩进单一微调模型
2026/9/14 23:08:44 网站建设 项目流程

用 Instructor 蒸馏 Chain of Density:将 GPT-4 的迭代摘要能力压缩进单一微调模型

【免费下载链接】instructorstructured outputs for llms项目地址: https://gitcode.com/GitHub_Trending/in/instructor

本指南基于 docs/blog/posts/chain-of-density.md 与仓库内examples/chain-of-density示例,完整讲解如何用 Instructor 实现 Chain of Density 迭代摘要,再通过Instructions.distil蒸馏管道把多轮迭代能力固化进一个 GPT-3.5 微调模型,在保持实体密度的同时把延迟降低约 20 倍、推理成本降低数十倍。

Chain of Density(密度链,下文简称 CoD)是一种迭代式摘要技术:先让模型生成一段冗长、非特定的初始摘要,再通过多轮"识别缺失实体 → 重写为等长但更密集的摘要"来不断压入文章中的实体。本文将以 Instructor 为骨架,从零实现这一流程,随后利用 Instructor 的蒸馏工具生成微调数据集、提交 OpenAI 微调任务,最后对比微调模型与 GPT-4 在实体密度、延迟与成本上的表现。读完本文,你将掌握:用 Pydantic 建模迭代摘要状态、用字段校验器强制摘要质量、用@instructions.distil自动录制训练样本,以及用instructor jobs create-from-file一键发起微调。

Part 1:用 Instructor 实现 Chain of Density

用 AI 摘要长文本长期面临"技术不一致、结果不稳定"的痛点。CoD 方法(源自论文From Sparse to Dense: GPT-4 Summarization with Chain of Density Prompting,Adams et al., 2023)给出了一条可复现的路径:模型先产出初始摘要,然后经过多轮重写,每轮都从原文中找出上一轮摘要遗漏的实体并补进去,同时保持摘要长度基本不变。最终得到一份实体密集、信息量大且自包含的摘要,作者团队发现该方法的产出稳定优于人工标注的摘要。

从原始 Prompt 拆分出的迭代流程

原始方法把"找出缺失实体 + 重写"这两步重复 5 次。原文的 Prompt 核心如下:

Article: {{ARTICLE}} You will generate increasingly concise, entity-dense summaries of the above Article. Repeat the following 2 steps 5 times. Step 1. Identify 1-3 informative Entities (";" delimited) from the Article which are missing from the previously generated summary. Step 2. Write a new, denser summary of identical length which covers every entity and detail from the previous summary plus the Missing Entities. A Missing Entity is: - Relevant: to the main story. - Specific: descriptive yet concise (5 words or fewer). - Novel; not in the previous summary. - Faithful: present in the Article. - Anywhere: located anywhere in the Article. Guidelines: - The first summary should be long (4-5 sentences, -80 words) yet highly non-specific... - Make every word count: re-write the previous summary to improve flow and make space for additional entities. - Never drop entities from the previous summary. If space cannot be made, add fewer new entities. Remember, use the exact same number of words for each summary. Answer in JSON. The JSON should be a list (length 5) of dictionaries whose keys are "Missing_Entities" and "Denser_Summary"

Instructor 的价值在于把这段"提示词驱动的流程"改造成"结构化函数调用驱动的流程":每一步都是一次独立的client.create调用,可以单独指定response_model,从而在每一轮都做类型强制与校验。本文实现的具体做法与论文有两处差异(在原文档中已明确说明):使用校验器而非提示词来保证重写摘要的最短长度;只做 3 轮而非 5 轮重写,因此最终实体密度会略低于论文。

数据建模:两个核心 response_model

首先安装依赖(对应 examples/chain-of-density/requirements.txt 的内容):

pip install instructor aiohttp rich

以及 NLTK 分词所需的 punkt 资源:nltk.download('punkt'),spaCy 语言模型en_core_web_smpython -m spacy download en_core_web_sm)。

初始摘要 InitialSummary

第一段摘要刻意要求"冗长、高度非特定、充满 filler",约 80 词。这个需求被直接编码进 Pydantic 模型的 docstring——docstring 不是装饰,它们会被直接用作给 LLM 的指令

class InitialSummary(BaseModel): """ This is an initial summary which should be long ( 4-5 sentences, ~80 words) yet highly non-specific, containing little information beyond the entities marked as missing. Use overly verbose languages and fillers (Eg. This article discusses) to reach ~80 words. """ summary: str = Field( ..., description="This is a summary of the article provided which is overly verbose and uses fillers. It should be roughly 80 words in length", )
重写摘要 RewrittenSummary

每一轮重写需要同时建模三个信息:新的摘要文本、本轮遗漏的实体(missing)、被错误丢弃的上一轮实体(absent)。missingabsent会在下一轮作为"反馈信号"注入消息,形成闭环:

class RewrittenSummary(BaseModel): """ This is a new, denser summary of identical length which covers every entity and detail from the previous summary plus the Missing Entities. Guidelines - Make every word count : Rewrite the previous summary to improve flow and make space for additional entities - Never drop entities from the previous summary. If space cannot be made, add fewer new entities. - The new summary should be highly dense and concise yet self-contained, eg., easily understood without the Article. - Make space with fusion, compression, and removal of uninformative phrases like "the article discusses" - Missing entities can appear anywhere in the new summary An Entity is a real-world object that's assigned a name - for example, a person, country a product or a book title. """ summary: str = Field( ..., description="This is a new, denser summary of identical length which covers every entity and detail from the previous summary plus the Missing Entities. It should have the same length ( ~ 80 words ) as the previous summary and should be easily understood without the Article", ) absent: List[str] = Field( ..., default_factory=list, description="this is a list of Entities found absent from the new summary that were present in the previous summary", ) missing: List[str] = Field( default_factory=list, description="This is a list of 1-3 informative Entities from the Article that are missing from the new summary which should be included in the next generated summary.", )
docstring 为什么会生效:response_model 到 function call 的转换

Instructor 会把传入的response_model解析成一次 OpenAI 函数调用,因此最终输出与 Pydantic 模型强绑定。以用于微调的GeneratedSummary为例:

class GeneratedSummary(BaseModel): """ This represents a highly concise summary that includes as many entities as possible from the original source article. An Entity is a real-world object that's assigned a name - for example, a person, country a product or a book title. Guidelines - Make every word count - The new summary should be highly dense and concise yet self-contained, eg., easily understood without the Article. - Make space with fusion, compression, and removal of uninformative phrases like "the article discusses" """ summary: str = Field( ..., description="This represents the final summary generated that captures the meaning of the original article which is as concise as possible. ", )

它会被展开成下面的函数调用结构:

{ "functions": [ { "name": "GeneratedSummary", "description": "This represents a highly concise summary that includes as many entities as possible from the original source article.\n\nAn Entity is a real-world object that's assigned a name - for example, a person, country a product or a book title.\n\nGuidelines\n- Make every word count\n- The new summary should be highly dense and concise yet self-contained, eg., easily understood without the Article.\n- Make space with fusion, compression, and removal of uninformative phrases like \"the article discusses\"", "parameters": { "type": "object", "properties": { "summary": { "description": "This represents the final summary generated that captures the meaning of the original article which is as concise as possible. ", "title": "Summary", "type": "string" } }, "required": ["summary"] } } ] }

这一转换在源码层面由 instructor/processing/function_calls.py 中的response_schema完成(见 instructor/distil.py 中schema_model.openai_schema的调用)。所以 docstring 与Field(description=...)写得越精细,LLM 越能产出符合预期的结构;而因为底层就是 Pydantic,你还可以对返回结果做任意校验与解析——"It's all python all the way down"。

用 Pydantic 校验器强制摘要质量

理想情况下,我们希望:missing长度在 1~3 之间、absent为空列表、重写摘要保持最低实体密度。这些都可以用原生 Pydanticfield_validator声明在类内部,Instructor 会自动在每次生成后执行校验,不通过就触发重试:

import nltk import spacy nlp = spacy.load("en_core_web_sm") @field_validator("summary") @classmethod def min_length(_cls, v: str): tokens = nltk.word_tokenize(v) # 与论文一致,用 NLTK 分词器统计 token 数 num_tokens = len(tokens) if num_tokens < 60: raise ValueError( "The current summary is too short. Please make sure that you generate a new summary that is around 80 words long." ) return v @field_validator("missing") @classmethod def has_missing_entities(_cls, missing_entities: List[str]): if len(missing_entities) == 0: raise ValueError( "You must identify 1-3 informative Entities from the Article which are missing from the previously generated summary to be used in a new summary" ) return missing_entities @field_validator("absent") @classmethod def has_no_absent_entities(_cls, absent_entities: List[str]): absent_entity_string = ",".join(absent_entities) if len(absent_entities) > 0: print(f"Detected absent entities of {absent_entity_string}") raise ValueError( f"Do not omit the following Entities {absent_entity_string} from the new summary" ) return absent_entities @field_validator("summary") @classmethod def min_entity_density(_cls, v: str): tokens = nltk.word_tokenize(v) num_tokens = len(tokens) # 用 spaCy 提取实体,计算实体密度 doc = nlp(v) num_entities = len(doc.ents) density = num_entities / num_tokens if density < 0.08: # 0.08 是任意选择的经验阈值 raise ValueError( f"The summary of {v} has too few entities. Please regenerate a new summary with more new entities added to it. Remember that new entities can be added at any point of the summary." ) return v

四个校验器的作用分别是:

  • min_length:与论文一致,用 NLTK 分词器统计 token 数,目标至少 60 个 token,避免重写后信息丢失;
  • has_missing_entities:每轮必须识别出至少 1 个缺失实体,否则无法推进迭代;
  • has_no_absent_entities:禁止从上一轮摘要中丢弃任何实体(检测到即提示并报错,触发重试);
  • min_entity_density:用 spaCy 计算实体密度(实体数 / token 数),低于 0.08 时强制重新生成——这样"密度只升不降"。

关于校验器与 Instructor 的配合,可参考仓库中的专题文章 Good LLM Validation is just Good Validation。

把流程串起来:summarize_article

下面实现完整的 CoD 摘要函数(对应示例 examples/chain-of-density/chain_of_density.py):

import instructor client = instructor.from_provider("openai/gpt-5-nano") # 示例代码中使用 instructor.from_openai(OpenAI()) def summarize_article(article: str, summary_steps: int = 3): summary_chain = [] # 第一步:生成初始摘要(冗长、非特定、约 80 词) summary: InitialSummary = client.create( model="gpt-5.4-mini", response_model=InitialSummary, messages=[ { "role": "system", "content": "Write a summary about the article that is long (4-5 sentences) yet highly non-specific. Use overly, verbose language and fillers(eg.,'this article discusses') to reach ~80 words", }, {"role": "user", "content": f"Here is the Article: {article}"}, { "role": "user", "content": "The generated summary should be about 80 words.", }, ], max_retries=2, ) prev_summary = None summary_chain.append(summary.summary) # 后续每一轮:识别缺失实体 -> 重写更密集的等长摘要 for _ in range(summary_steps): missing_entity_message = ( [] if prev_summary is None else [ { "role": "user", "content": f"Please include these Missing Entities: {','.join(prev_summary.missing)}", }, ] ) new_summary: RewrittenSummary = client.create( model="gpt-5.4-mini", messages=[ { "role": "system", "content": """ You are going to generate an increasingly concise,entity-dense summary of the following article. Perform the following two tasks - Identify 1-3 informative entities from the following article which is missing from the previous summary - Write a new denser summary of identical length which covers every entity and detail from the previous summary plus the Missing Entities Guidelines - Make every word count: re-write the previous summary to improve flow and make space for additional entities - Make space with fusion, compression, and removal of uninformative phrases like "the article discusses". - The summaries should become highly dense and concise yet self-contained, e.g., easily understood without the Article. - Missing entities can appear anywhere in the new summary - Never drop entities from the previous summary. If space cannot be made, add fewer new entities. """, }, {"role": "user", "content": f"Here is the Article: {article}"}, { "role": "user", "content": f"Here is the previous summary: {summary_chain[-1]}", }, *missing_entity_message, ], max_retries=3, # 若你把密度阈值调高到 0.08 以上,可相应增大该值 max_tokens=1000, response_model=RewrittenSummary, ) summary_chain.append(new_summary.summary) prev_summary = new_summary return summary_chain

几个关键点:

  1. 对 OpenAI 客户端应用from_provider(旧版本为from_openaipatch)后,即可获得 Instructor 的全部能力:输出自动类型强转 + 非法输出自动重试
  2. 初始摘要的系统提示明确要求"冗长 + 充满 filler + 约 80 词",为后续压入实体预留空间;
  3. 重写轮次对原论文 prompt 做了小幅改编,且会触发前面定义的所有field_validator
  4. max_retries=3与密度阈值 0.08 是配套的:如果你把阈值调大,应同步调大重试次数,否则可能频繁重试耗尽配额。

实际运行中(以示例仓库使用gpt-4-0613为例),同样长度的文本,首轮与末轮的差异非常直观——实体数量成倍增长,措辞也从"灌水"变得自然、信息密集:

第一轮(初始摘要)

This article discusses the highly-anticipated boxing match between Manny Pacquiao and Floyd Mayweather. The article revolves around Manny Pacquiao's statements about his upcoming fight and his preparations for the same. A portion of the article provides details about the financial stipulations of the match and its significance in the sporting arena. Quotes from Pacquiao illustrating his determination and his battle strategy are highlighted. The tone of the article is largely centered around creating a build-up to the upcoming mega event.

最后一轮(实体密集摘要)

Manny Pacquiao, the Filipino boxer, anticipates the forthcoming May 2 showdown at the MGM Grand as the fight of his life, against the undefeated American Floyd Mayweather, in a $300m bout. Despite being seen as the underdog in this high-stakes Las Vegas match, Pacquiao is confident, promising a warrior's spirit and assuring the fans who have been awaiting this encounter for a decade, that it will indeed be the biggest sporting spectacle in history worthy of their anticipation

Part 2:把迭代方法蒸馏进单一模型

CoD 每篇摘要要发起多次串行 API 调用,延迟与成本都很高。更聪明的做法是:让 GPT-4 跑完整 CoD 流程生成"金标摘要",再用这些数据微调一个小模型,让它在单次调用里直接产出同等质量的摘要。

生成训练集:@instructions.distil

为了防止数据污染,作者从griffin/chain-of-density数据集中随机抽取了 120 篇文章,拆成train.csvtest.csv(作者将生成数据上传至 Hugging Face 供复现)。接下来用 Instructor 的Instructions模块把每次调用自动录制成.jsonl训练文件,完整脚本见 examples/chain-of-density/finetune.py:

from typing import List from chain_of_density import summarize_article # 复用上面定义的函数 import csv import logging import instructor from pydantic import BaseModel client = instructor.from_provider("openai/gpt-5-nano") # 示例代码中使用 instructor.from_openai(OpenAI()) logging.basicConfig(level=logging.INFO) # 必须配置 INFO 级别日志,否则不会输出训练数据 instructions = instructor.Instructions( name="Chain Of Density", finetune_format="messages", # log handler 用于把数据保存到文件,也可以换成数据库等任意存储 log_handlers=[logging.FileHandler("generated.jsonl")], openai_client=client, ) class GeneratedSummary(BaseModel): """ This represents a highly concise summary that includes as many entities as possible from the original source article. An Entity is a real-world object that's assigned a name - for example, a person, country a product or a book title. Guidelines - Make every word count - The new summary should be highly dense and concise yet self-contained, eg., easily understood without the Article. - Make space with fusion, compression, and removal of uninformative phrases like "the article discusses" """ summary: str = Field( ..., description="This represents the final summary generated that captures the meaning of the original article which is as concise as possible. ", ) @instructions.distil # 自动捕获函数的输入与输出 def distil_summarization(text: str) -> GeneratedSummary: summary_chain: List[str] = summarize_article(text) return GeneratedSummary(summary=summary_chain[-1]) # 取链条最后一轮的摘要作为金标 with open("train.csv") as file: reader = csv.reader(file) next(reader) # Skip the header for article, _summary in reader: # Run Distillisation to generate the values distil_summarization(article)

脚本要点:

  • logging.basicConfig(level=logging.INFO)必须配置:蒸馏数据是通过日志处理器落盘的,不开启 INFO 日志就不会生成generated.jsonl
  • Instructionslog_handlers参数决定数据写到哪,这里用logging.FileHandler("generated.jsonl")
  • @instructions.distil装饰器要求函数返回类型注解必须是 Pydantic BaseModel 且实际返回 Pydantic 对象——从源码 instructor/distil.py 可以看到,is_return_type_base_model_or_instance会强制断言这一点;
  • 数据录制走的是messages格式(FinetuneFormat.MESSAGES):源码 instructor/distil.py 会把函数的系统/用户消息、函数签名、以及response_model的 JSON 输出拼装成一次带function_call的完整对话记录,这正是 OpenAI 微调所要求的格式。

建议先在数据集的小子集上跑一遍确认配置正确。正式运行前记得设置OPENAI_API_KEY环境变量,并按需用 tenacity 增加限流重试。

创建微调任务

脚本跑完后,本地会生成generated.jsonl。接下来只需要一条命令即可发起微调:

instructor jobs create-from-file generated.jsonl

CLI 提供四个子命令(create-from-file/create-from-id/list/cancel),完整说明见 docs/cli/finetune.md。create-from-file会把"上传文件 + 发起训练"一步完成,常用参数包括:

参数说明默认值
--model用于微调的基础模型gpt-5.4-mini
--n-epochs训练轮数由调度器决定
--batch-size批大小未指定
--learning-rate-multiplier学习率倍率未指定
--validation-file验证集文件路径None
--model-suffix模型标识后缀None
--poll轮询间隔(秒)2

例如带验证集与超参数的一次训练:

instructor jobs create-from-file generated.jsonl \ --validation_file validation.jsonl \ --n_epochs 3 \ --batch_size 16 \ --learning_rate_multiplier 0.5

训练期间可用instructor jobs list实时监控任务状态(每 5 秒自动刷新),用instructor files list查看已上传文件。任务完成后,只需把@instructions.distil改为显式指定微调模型并以dispatch模式运行,就能用新模型直接出结果:

@instructions.distil(model='gpt-5.4-mini:finetuned-123', mode="dispatch") # 替换成你的模型 id def distil_summarization(text: str) -> GeneratedSummary: summary_chain: List[str] = summarize_article(text) return GeneratedSummary(summary=summary_chain[-1])

OpenAI 的微调模型 id 形如ft:gpt-5.4-mini:personal::<id>,可在其仪表盘 Fine-tuning 页签下找到。源码 instructor/distil.py 显示distil支持两种模式:mode="distil"(默认,执行函数并录制数据)与mode="dispatch"(不再执行原函数,而是直接把输入打包成消息发给微调模型,走response_model结构化返回)——这正是从"蒸馏数据"切换到"生产推理"的开关。

结果与基准对比

作者用 20 篇未参与微调的文章,从三个维度对比了几种方案:实体密度(实体数/token,越高越好)、延迟(生成最后一个 token 的秒数)、成本(拆分为训练成本与推理成本)。

  • 3.5 Finetuned (n):在 n 个样本上微调的 GPT-3.5 模型,每个模型训练 4~5 个 epoch(epoch 数由 OpenAI 调度器自动决定);
  • GPT-4 (COD):用上述方法对 GPT-4 应用 3 轮 CoD 重写;
  • GPT-3.5 (Vanilla):单次调用生成 80~90 token 的实体密集摘要,作为基线。
ModelMean Latency (s)Mean Entity Density
3.5 Finetuned (20)2.10.15
3.5 Finetuned (50)2.10.14
3.5 Finetuned (76)2.10.14
GPT-3.5 (Vanilla)16.80.12
GPT-4 (COD)49.50.15

成本方面(基于 OpenAI Usage Dashboard 对 20 篇摘要的统计):

ModelTraining Cost ($)Inference Cost ($)Tokens UsedTotal Cost ($)
GPT-3.5 (Vanilla)-0.2051,1620.2
3.5 Finetuned (20)0.70.2056,5730.8
3.5 Finetuned (50)1.40.1749,0571.3
3.5 Finetuned (76)1.80.1751,5832.5
GPT-4 (COD)-12.9409,06212.9

根据作者测算:GPT-4 每篇摘要的推理成本约 0.65 美元,而微调模型仅约 0.0091 美元,便宜约 72 倍;延迟从 GPT-4 (COD) 的 49.5 秒降到 2.1 秒(约 20x 加速),综合训练与推理,总成本节省约 50 倍(这正是原文档 description 中 20x latency reduction、50x cost savings 的来源)。

一个值得注意的现象:样本最少的微调模型(20 例)实体密度反而最高。作者给出的推测是:要么默认 5 个 epoch 训练不足,要么样本更多后模型开始模仿其他行为(如更抽象的写作风格),反而拉低了实体密度。这里补充一个原文档提及但更严格的进阶策略:构建微调数据集时,只保留密度 ≥ 0.15 的摘要、取整条链中密度最高的一轮作为金标、强制每次重写密度 ≥ 0.12、不达标最多重试 3 次——这种策略成本约为本教程的 2.5 倍以上(作者实测生成 75 例共花费 63.46 美元,约 0.85 美元/例),但对性能提升明显,适合对质量要求更高的场景。

结论

将 CoD 这种迭代方法蒸馏进单一微调模型,可以获得约 20~40 倍的加速,同时整体质量不降反升——这正是"用蒸馏把昂贵能力固化进专用小模型"带来的效率红利。从数据建模、字段校验、蒸馏录制到微调任务下发,Instructor 在整个链条上都提供了结构化、可复现的工具支持:

  • 用 Pydantic 模型 + docstring 直接塑造 LLM 输出结构;
  • field_validator在每一轮强制摘要长度与实体密度;
  • Instructions.distil一键把函数调用录制为messages格式的微调数据;
  • instructor jobsCLI 一行发起训练、监控进度。

完整的可运行示例(数据建模、CoD 流程、蒸馏脚本、依赖清单)都在仓库 examples/chain-of-density 目录下,相关 CLI 细节可查阅 docs/cli/finetune.md,Instructionsdistil的底层实现见 instructor/distil.py。

【免费下载链接】instructorstructured outputs for llms项目地址: https://gitcode.com/GitHub_Trending/in/instructor

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询