Few-Shot 示例排序优化:用 Instructor 提升 LLM 结构化输出的示例选择与排序实战

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

在 Few-Shot 提示工程中,示例(exemplar)不仅要"选得对",还要"排得好"。大量研究表明,示例在提示(prompt)中的排列顺序会显著影响 LLM 的输出质量——同一组示例,换个顺序,模型给出的结果就可能不同。本文以 docs/prompting/few_shot/example_ordering.md 为骨架,结合 instructor 仓库中的示例选择、示例生成与模板渲染实现,讲清楚"示例排序为什么重要、有哪些排序/选择方法、如何在 Instructor 中落地为可运行的代码"。读完你将掌握:组合枚举、KATE(K 近邻示例调优)、无监督检索器三种示例优化思路,以及如何在 Instructor 中结合嵌入、Pydantic 响应模型和 Jinja 模板实现一套完整的 Few-Shot 流水线。

示例顺序为什么会影响 LLM 输出

Few-Shot 提示的基本假设是:给定若干输入-输出示例,模型能够学会任务模式并泛化到新输入。但示例在提示中的排列顺序本身也是一种信号。相关研究(如 Fantastically Ordered Prompts and Where to Find Them: Overcoming Few-Shot Prompt Order Sensitivity、Reordering Examples Helps during Priming-based Few-Shot Learning)表明,模型对示例顺序相当敏感,不同排列可能带来明显的性能差异。

因此,原文档给出的核心建议是:考虑对你的示例进行排列组合(permutating),寻找效果更好的顺序。这通常不是一次性的手工活,而是可以与"示例选择"(选哪些)一起做系统化优化的:先选出一批高质量示例,再为它们寻找最优排列。Instructor 仓库中的提示工程技术地图 docs/prompting/index.md 也正是把 Example Ordering(示例排序)、Example Selection(示例选择)和 Example Generation(示例生成)并列为 Few-Shot 三条支线,三者共同构成完整的 Few-Shot 优化闭环。

如何挑选你的示例(Choosing Your Examples)

原文档给出了三种由易到难的示例质量提升方法,它们既是"选择"方法,也直接影响最终排序空间的大小。

组合枚举(Combinatorics)

最容易上手的方法:手工遍历我们拥有的每个示例,尝试所有可能的组合。通过穷举不同示例集合及其排列,找到表现最好的组合。

优点是不需要任何额外基础设施,适合示例数量很少的场景;缺点是组合数随示例数量呈指数增长,示例一多就无法手工穷举,需要下面的自动方法接管。

KATE(k-Nearest Example Tuning)

KATE 是为提升 GPT-3 等模型 Few-Shot 性能而设计的基于语义相似度的示例选择方法,核心流程为:

  1. 对测试集中的每个查询(query),基于语义相似度检索出 K 个最近邻示例;
  2. 在这些 K 个示例中,跨不同查询出现频率最高的示例被选为最佳上下文示例。

KATE 从"选哪些示例"入手:它倾向于那些在多个查询中反复相关的"高复用"示例,天然带有一定的排序意味——被多轮命中的示例往往是先出现、更通用的示例。Instructor 仓库中给出了一个可直接运行的 KNN 示例选择实现,见 docs/prompting/few_shot/exemplar_selection/knn.md,我们在下文第三节会结合代码详细展开。

使用无监督检索器(Unsupervised Retriever)

第三种方法是把"示例与提示的匹配度打分"这件事交给 LLM 去学,训练出一个可以模拟该打分的检索模型:

LLM 打分训练无监督检索器的流程示意图

流程如下:

  1. 用一个大 LLM 针对给定提示(prompt),为每个示例计算一个相关性分数;
  2. 用这些(提示, 示例, 分数)三元组构建一个训练集,训练一个能模仿该打分行为的模型;
  3. 当用户发起新查询时,该模型直接输出最相关的 top k 个示例和最不相关的示例,供我们拼进最终提示。

图中可以看到完整链路:训练数据 D 先经无监督检索器 R_u 产出候选提示集合,再由打分模型(Scoring LM)ĝ 给出 0.9 / 0.5 / 0.1 之类的分数,高分样本作为正例、低分样本作为难负例,通过对比学习训练 utterance encoder 与 prompt encoder,最终得到一个能在推理阶段即时选出高质量示例的检索模型。这套"用 LLM 打分 → 训练模仿模型 → 推理时自动选例"的范式,正是为提示排序/选择这种对每次请求都不同的动态需求而设计的。

在 Instructor 中落地完整的示例选择与排序流水线

原文档聚焦于方法论,下面我们把它与 Instructor 仓库中的实际实现打通,形成一条可运行的 Few-Shot 流水线:生成/收集示例 → 嵌入检索选出最相关示例 → 拼装进提示 → 以结构化模型获得输出。

第一步:用 KNN 选出与查询最相关的示例

KNN 示例选择 的完整代码展示了 Instructor 版的四步流程:嵌入示例 → 嵌入查询 → 找 k 个最近邻示例 → 用这些示例作为上下文请求 LLM:

import instructor
from pydantic import BaseModel
from openai import OpenAI
import math
from textwrap import dedent


class Example(BaseModel):
    question: str
    answer: str


class Response(BaseModel):
    answer: str


oai = OpenAI()
client = instructor.from_provider("openai/gpt-4o")


def distance(a: list[float], b: list[float]):
    return 1 - sum(ai * bi for ai, bi in zip(a, b)) / (
        math.sqrt(sum(ai**2 for ai in a)) * math.sqrt(sum(bi**2 for bi in b))
    )


def embed_queries(queries: list[str]) -> list[tuple[list[float], str]]:
    return [
        (embedding_item.embedding, query)
        for embedding_item, query in zip(
            oai.embeddings.create(input=queries, model="text-embedding-3-large").data,
            queries,
        )
    ]


def knn(
    embedded_examples: list[tuple[list[float], str]],
    query_embedding: list[float],
    k: int,
):
    distances = [
        (distance(embedding, query_embedding), example)
        for embedding, example in embedded_examples
    ]
    distances.sort(key=lambda x: x[0])
    return distances[:k]


def generate_response(examples: list[str], query: str):
    formatted_examples = "\n".join(examples)
    return client.create(
        model="gpt-4o",
        response_model=Response,
        messages=[
            {
                "role": "user",
                "content": dedent(
                    f"""
                    Respond to the following query with the most accurate
                    and concise answer possible.
                    <examples>
                    {formatted_examples}
                    </examples>
                    <query>
                    {query}
                    </query>
                """
                ),
            }
        ],
    )


def generate_question_and_answer_pair(
    questions: list[str], question_and_answers: list[dict[str, str]]
) -> list[str]:
    question_to_answer = {}

    for question in question_and_answers:
        question_to_answer[question["question"]] = question["answer"]

    return [
        dedent(
            f"""
        <example>
        <question>{question}</question>
        <answer>{question_to_answer[question]}</answer>
        </example>
        """
        )
        for question in questions
    ]


if __name__ == "__main__":
    examples = [
        {"question": "What is the capital of France?", "answer": "Paris"},
        {"question": "Who wrote Romeo and Juliet", "answer": "Shakespeare"},
        {"question": "What is the capital of Germany?", "answer": "Berlin"},
    ]

    query = "What is the capital of Italy?"

    # Step 1 : Embed the Examples
    embeddings = embed_queries([example["question"] for example in examples] + [query])

    embedded_examples = embeddings[:-1]
    embedded_query = embeddings[-1]

    # Step 3: Find the k closest examples to the query
    k_closest_examples = knn(embedded_examples, embedded_query[0], 2)

    for example in k_closest_examples:
        print(example)
        #> (0.4013468481736857, 'What is the capital of France?')
        #> (0.4471368596136872, 'What is the capital of Germany?')

    # Step 4: Use these examples as in-context examples
    formatted_examples = generate_question_and_answer_pair(
        [example[1] for example in k_closest_examples], examples
    )
    response = generate_response(formatted_examples, query)
    print(response.answer)
    #> Rome

关键点拆解:

  • 余弦距离:distance 用 1 - 余弦相似度 定义距离,值越小越相似,knn 中按该距离升序取前 k 个——排序的依据就是语义距离;
  • 结构化约束:Example、Response 都是 Pydantic 模型,response_model=Response 保证 LLM 的输出被解析为带 answer 字段的对象;
  • 示例顺序即最终提示顺序:generate_question_and_answer_pair 按 k_closest_examples 的顺序生成 <example> 块,因此 KNN 排序结果直接决定了提示中示例的先后位置。若想进一步做顺序消融(即第一节的组合枚举思想),只需把这段示例列表 shuffle 后再传入 generate_response 对比效果即可。

注意:示例中使用的模型名(gpt-4o、text-embedding-3-large)与提供商(openai)请按你的账号可用模型替换,代码结构本身与模型无关。

第二步:没有现成示例时,用 LLM 生成示例

当领域内缺少现成标注数据时,可以采用 SG-ICL(Self-Generated In-Context Learning,自生成上下文学习),见 docs/prompting/few_shot/example_generation/sg_icl.md。思路是让 LLM 自己按类别生成示例,再作为 Few-Shot 上下文使用:

import instructor
from pydantic import BaseModel
from typing import Literal
n = 4  # num examples to generate per class


class GeneratedReview(BaseModel):
    review: str
    sentiment: Literal["positive", "negative"]


class SentimentPrediction(BaseModel):
    sentiment: Literal["positive", "negative"]


client = instructor.from_provider("openai/gpt-5-nano")


def generate_sample(input_review, sentiment):
    return client.create(
        model="gpt-4o",
        response_model=GeneratedReview,
        messages=[
            {
                "role": "user",
                "content": f"""
                           Generate a '{sentiment}' review similar to: {input_review}
                           Generated review:
                           """,
            }
        ],
    )


def predict_sentiment(input_review, in_context_samples):
    return client.create(
        model="gpt-4o",
        response_model=SentimentPrediction,
        messages=[
            {
                "role": "user",
                "content": "".join(
                    [
                        f"Review: {sample.review}\nSentiment: {sample.sentiment}\n\n"
                        for sample in in_context_samples
                    ]
                )
                + f"Review: {input_review}\nSentiment:",
            }
        ],
    ).sentiment

GeneratedReview 用 Literal["positive", "negative"] 强制每个生成的示例都带类别标签,predict_sentiment 再把生成的示例按"示例在前、待预测输入在后"的顺序拼进提示。生成的示例集就是上一节 KNN 检索的候选池来源——两条流水线可以无缝衔接。

第三步:基于一致性筛选示例(COSP)

除了"语义相似"和"自生成",还可以用 COSP(Consistency Based Self Adaptive Prompting,一致性自适应提示) 从一致性角度筛例,见 docs/prompting/few_shot/cosp.md。其核心是两阶段:

  1. 示例生成:对每个候选示例用模型重复生成 3~5 次响应,收集内容与置信度;
  2. 示例选择:按响应熵(entropy)与重复度(repetitiveness)评估每个示例,选得分最优的 k 个进入提示。

文档中给出 COSPSelector 的实现思路:对每个候选计算 entropy_score - repetitiveness 作为综合得分(越低越好),排序后取前 k。它和 KATE、无监督检索器一样,都在回答同一个问题:"哪些示例值得放进提示、以什么顺序放"——只不过打分依据从"语义距离"换成了"模型自洽性"。

第四步:用 Jinja 模板控制示例的位置与排序逻辑

示例拼进提示的位置同样可以用 Instructor 的 Jinja 模板机制精确控制。docs/concepts/templating.md 说明:client.create(..., context={...}) 中的 context 字典会同时传入模板渲染引擎和Pydantic 校验器,提示里可以用 {% for %}、{% if %} 等 Jinja 语法动态渲染列表与条件。

以仓库中的 examples/reranker/run.py(RAG 结果重排)为例,它演示了与"无监督检索器"同构的"给候选打分并排序"实现:系统提示要求模型对每个 chunk 输出 0-10 的相关性分数,用户消息用 Jinja 模板把查询与候选 chunks 渲染出来:

<query>{{ query }}</query>

<chunks_to_rank>
{% for chunk in chunks %}
<chunk chunk_id="{{ chunk.id }}">
    {{ chunk.text }}
</chunk>
{% endfor %}
</chunks_to_rank>

而 RerankedResults 模型通过字段校验器自动按 relevancy 降序排列结果:

class RerankedResults(BaseModel):
    labels: list[Label] = Field(description="List of labeled and ranked chunks")

    @field_validator("labels")
    @classmethod
    def model_validate(cls, v: list[Label]) -> list[Label]:
        return sorted(v, key=lambda x: x.relevancy, reverse=True)

把这种"模板渲染候选 + 模型打分 + 校验器排序"的模式套用到 Few-Shot 场景,即可实现:在 context 中传入排好序的示例列表,提示模板按该顺序渲染 <examples> 块;同时模板引擎基于沙箱环境(jinja2.sandbox.SandboxedEnvironment)运行,禁止任意 Python 代码,因此传入模板的数据需提前清洗,避免注入风险(详见 docs/concepts/templating.md 的 Security 一节)。

三条方法如何配合使用

方法回答的问题打分/排序依据适合场景
组合枚举哪种示例组合和顺序最好无(人工试错)示例少、追求最优上界
KATE / KNN选哪些示例最相关语义相似度有嵌入模型、候选示例较多
无监督检索器示例与提示匹配度如何LLM 打分 + 对比学习训练高频线上查询、可离线建训练集
COSP哪些示例模型能稳定答好响应熵与重复度想减少示例集的噪音

实际工程中,常见组合是:SG-ICL 生成候选 → KNN/KATE 检索最相关子集 → 对子集做顺序消融(组合枚举或按分数降序)→ 模板渲染进提示。三条支线的完整索引见 docs/prompting/index.md 的 Few-Shot 章节,各方法的完整代码分别位于 docs/prompting/few_shot/exemplar_selection/knn.md、docs/prompting/few_shot/example_generation/sg_icl.md 和 docs/prompting/few_shot/cosp.md。

参考资源

仓库内可直接继续深入的材料:

原文档引用的相关研究(均为示例顺序/选择方向的公开工作,可按标题检索):

  1. Fantastically Ordered Prompts and Where to Find Them: Overcoming Few-Shot Prompt Order Sensitivity(arXiv:2104.08786)
  2. Reordering Examples Helps during Priming-based Few-Shot Learning(arXiv:2106.01751)
  3. What Makes Good In-Context Examples for GPT-3?(arXiv:2101.06804)
  4. Learning To Retrieve Prompts for In-Context Learning(NAACL 2022)
  5. The Prompt Report: A Systematic Survey of Prompting Techniques(arXiv:2406.06608)

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

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐