什么是 ‘Few-shot Prompting’ 的动态采样?利用向量索引动态为当前问题匹配最相关的示例

各位同仁,各位对人工智能充满热情的开发者们,大家上午好!

今天,我将带领大家深入探讨一个在大型语言模型(LLM)时代日益重要的技术:Few-shot Prompting 中的动态采样 (Dynamic Sampling in Few-shot Prompting)。具体来说,我们将聚焦于如何利用强大的向量索引 (Vector Indexes) 技术,为每一个特定的问题,智能地匹配并检索出最相关的示例,从而显著提升我们与LLM交互的效率和效果。

在过去几年里,LLM展现出了令人惊叹的语言理解和生成能力。从文本摘要到代码生成,从情感分析到复杂推理,它们的应用场景几乎无所不包。然而,要让这些模型在特定任务上发挥最佳性能,仅仅提供一个简单的指令往往是不够的。这就是 Few-shot Prompting 登场的原因。

第一章:Few-shot Prompting 的基石与挑战

1.1 Few-shot Prompting 概述

Few-shot Prompting,顾名思义,是指在向大型语言模型发出请求时,除了提供任务指令和待处理的查询(Query)之外,还会附带少量("few")相关的输入-输出示例。这些示例作为“情境学习”(In-context Learning)的载体,能够极大地指导LLM理解任务的意图、期望的输出格式以及潜在的约束条件。

其基本结构通常如下:

[任务指令]

[示例1的输入]
[示例1的输出]

[示例2的输入]
[示例2的输出]

...

[示例N的输入]
[示例N的输出]

[待处理查询的输入]

为什么 Few-shot Prompting 如此有效?

  • 情境学习(In-context Learning): LLM在大量数据上预训练后,已经具备了强大的模式识别能力。通过提供少量示例,模型能够快速识别出当前任务的模式,并将其泛化到新的查询上,而无需进行参数微调。
  • 降低歧义性: 明确的示例能够消除指令中可能存在的歧义,确保模型理解任务的精确要求。
  • 引导输出格式: 如果我们希望模型以特定的JSON格式、代码结构或摘要风格输出,示例是最佳的教学工具。
  • 适应特定领域/风格: 对于专业术语、特定行业规范或独特写作风格的任务,示例能够帮助模型快速适应。

1.2 示例选择的困境:从静态到动态的需求

尽管 Few-shot Prompting 威力巨大,但其效果却高度依赖于所选示例的质量和相关性。一个不相关的示例不仅不会帮助模型,反而可能引入噪声,导致模型产生错误或次优的输出。

早期或简单的 Few-shot Prompting 策略通常面临以下问题:

  1. 静态示例集: 开发者为所有查询都使用一组固定的示例。
    • 优点: 简单易实现。
    • 缺点: 缺乏通用性。对于多样化的查询,固定的示例集很难做到对所有情况都相关。例如,一个关于法律文件的示例对技术文档摘要的查询可能毫无帮助。
  2. 随机示例选择: 从一个大型示例库中随机抽取 N 个示例。
    • 优点: 避免了静态示例的局限性,在一定程度上引入了多样性。
    • 缺点: 效率低下。随机选择的示例很可能与当前查询毫不相关,导致LLM“迷失方向”,甚至产生负面影响(Negative Prompting)。

这些方法都未能解决核心问题:如何为当前的具体问题,找到语义上最贴近、信息上最互补的“情境学习”材料?

答案就是:动态采样 (Dynamic Sampling)。我们需要的不是一劳永逸的示例集,也不是盲目的随机选择,而是一个能够根据每个新查询的上下文,智能地从一个更大的示例库中,筛选出最相关、最有助于模型理解和解决当前问题的示例的机制。

这正是我们今天讲座的核心:利用向量索引实现 Few-shot Prompting 的动态采样。

第二章:向量嵌入:文本的数学指纹

要实现动态采样,我们首先需要一种方法来量化文本之间的“相关性”。自然语言文本是离散的符号序列,计算机难以直接处理其语义。而向量嵌入 (Vector Embeddings) 技术正是解决这个问题的关键。

2.1 什么是向量嵌入?

向量嵌入是将文本(单词、短语、句子、段落甚至整个文档)转换成高维实数向量的技术。这些向量捕捉了文本的语义信息,使得语义上相似的文本在向量空间中也彼此靠近,而语义上不相关的文本则距离较远。

例如,在某个嵌入空间中:

  • “苹果公司”和“微软”的向量会比“苹果公司”和“香蕉”的向量更接近。
  • “编程”和“软件开发”的向量会比“编程”和“烹饪”的向量更接近。

2.2 向量嵌入的生成

生成向量嵌入的技术多种多样,但核心思想都是利用深度学习模型(特别是Transformer架构的模型,如BERT、RoBERTa、GPT等)在海量文本数据上进行预训练。这些模型学习如何将文本映射到稠密的向量空间。

常用的嵌入模型包括:

  • Sentence-BERT (SBERT) 系列: 专门为句子和短语嵌入优化,能够高效地生成高质量的句子向量。例如 all-MiniLM-L6-v2paraphrase-MiniLM-L6-v2 等。
  • OpenAI Embeddings API: OpenAI 提供了强大的嵌入模型,如 text-embedding-ada-002,通过API调用即可生成高质量的文本嵌入。
  • 其他专有或开源模型: 许多公司和研究机构都发布了各自的嵌入模型,有些针对特定领域进行了优化。

代码示例:使用 Sentence-BERT 生成文本嵌入

我们将使用 sentence-transformers 库,它封装了多种预训练的 SBERT 模型,使用起来非常方便。

首先,确保你已经安装了必要的库:

pip install sentence-transformers numpy

然后,我们可以编写 Python 代码来生成文本嵌入:

from sentence_transformers import SentenceTransformer
import numpy as np

# 1. 加载预训练的 Sentence-BERT 模型
# 'all-MiniLM-L6-v2' 是一个轻量级但性能良好的通用模型
model_name = 'all-MiniLM-L6-v2'
print(f"正在加载 Sentence-BERT 模型: {model_name}...")
model = SentenceTransformer(model_name)
print("模型加载完成。")

# 2. 准备一些文本
texts = [
    "如何使用Python编写一个REST API?",
    "使用Flask框架构建Web服务的最佳实践。",
    "学习JavaScript的入门教程。",
    "关于人工智能的最新发展趋势。",
    "给我一份关于深度学习的报告。",
    "制作一道美味的意大利面。",
    "如何用Python和Django开发一个电子商务网站?"
]

# 3. 生成文本嵌入
print(f"正在生成 {len(texts)} 段文本的嵌入向量...")
embeddings = model.encode(texts)
print(f"嵌入向量生成完成。形状: {embeddings.shape}")
# embeddings.shape 会是 (num_texts, embedding_dimension)
# 例如,(7, 384) 表示有 7 个文本,每个文本被映射到 384 维的向量

# 4. 打印一些示例嵌入(为了演示,只打印前5个维度)
print("n部分文本及其对应的嵌入向量(前5维):")
for i, text in enumerate(texts):
    print(f"文本: '{text}'")
    print(f"嵌入: {embeddings[i][:5]}...")
    print("-" * 30)

# 5. 计算两个文本之间的相似度(例如,余弦相似度)
# 语义相似的文本向量在向量空间中距离更近
# '如何使用Python编写一个REST API?' vs '如何用Python和Django开发一个电子商务网站?'
# '如何使用Python编写一个REST API?' vs '制作一道美味的意大利面。'

from sklearn.metrics.pairwise import cosine_similarity

# 提取相关文本的嵌入
embedding_query_1 = embeddings[0] # "如何使用Python编写一个REST API?"
embedding_query_2 = embeddings[6] # "如何用Python和Django开发一个电子商务网站?"
embedding_irrelevant = embeddings[5] # "制作一道美味的意大利面。"

# 计算相似度
similarity_1_2 = cosine_similarity(embedding_query_1.reshape(1, -1), embedding_query_2.reshape(1, -1))[0][0]
similarity_1_irrelevant = cosine_similarity(embedding_query_1.reshape(1, -1), embedding_irrelevant.reshape(1, -1))[0][0]

print(f"n相似度计算:")
print(f"'{texts[0]}' 和 '{texts[6]}' 的余弦相似度: {similarity_1_2:.4f}")
print(f"'{texts[0]}' 和 '{texts[5]}' 的余弦相似度: {similarity_1_irrelevant:.4f}")

# 预期结果:相似度1_2会显著高于相似度1_irrelevant,证明了嵌入向量捕捉语义的能力。

输出示例 (部分):

正在加载 Sentence-BERT 模型: all-MiniLM-L6-v2...
模型加载完成。
正在生成 7 段文本的嵌入向量...
嵌入向量生成完成。形状: (7, 384)

部分文本及其对应的嵌入向量(前5维):
文本: '如何使用Python编写一个REST API?'
嵌入: [ 0.00762145 -0.01217596  0.03859744 -0.04780517 -0.01633519]...
------------------------------
文本: '使用Flask框架构建Web服务的最佳实践。'
嵌入: [ 0.00754876 -0.02107474  0.05267864 -0.03923912 -0.02434526]...
------------------------------
...

相似度计算:
'如何使用Python编写一个REST API?' 和 '如何用Python和Django开发一个电子商务网站?' 的余弦相似度: 0.7631
'如何使用Python编写一个REST API?' 和 '制作一道美味的意大利面。' 的余弦相似度: 0.0658

从输出中我们可以清晰地看到,关于Python编程的两个查询之间的相似度(0.7631)远高于一个编程查询与一个烹饪查询之间的相似度(0.0658)。这正是我们利用向量嵌入来衡量文本相关性的基础。

第三章:向量索引:高效检索的利器

当我们拥有了文本的向量表示后,下一步就是如何高效地找到与给定查询向量最相似的那些示例向量。对于一个只有几十个或几百个示例的小型库,我们可以直接进行暴力(Brute-force)搜索,即计算查询向量与所有示例向量的相似度,然后排序取前 K 个。然而,在实际应用中,我们的示例库可能包含数千、数万甚至数百万个示例。此时,暴力搜索的计算成本将变得不可接受。

这就是向量索引 (Vector Index) 发挥作用的地方。向量索引是一种专门设计用于加速高维向量相似度搜索的数据结构和算法。

3.1 暴力搜索的局限性

假设我们有 $N$ 个示例,每个示例对应一个 $D$ 维向量。对于一个查询向量,我们需要计算它与这 $N$ 个向量的相似度。如果使用余弦相似度,每次计算需要 $O(D)$ 的复杂度。那么总复杂度就是 $O(N cdot D)$。

  • 当 $N = 10^6$ (一百万个示例)
  • $D = 384$ (Sentence-BERT 的维度)
  • 每次查询需要 $10^6 times 384 approx 3.8 times 10^8$ 次浮点运算。
  • 这在实时应用中是无法接受的。

3.2 近似最近邻 (Approximate Nearest Neighbors, ANN) 算法

为了解决暴力搜索的效率问题,研究者们开发了各种近似最近邻 (ANN) 搜索算法。ANN 算法的核心思想是在可接受的精度损失下,显著提高搜索速度。它们不保证找到绝对的 K 个最近邻,但通常能找到非常接近真实最近邻的 K 个向量。

常见的 ANN 算法和库包括:

  • FAISS (Facebook AI Similarity Search): Facebook AI 开发的库,提供了多种高效的 ANN 算法实现,包括 IVFFlat、HNSW 等。广泛应用于工业界。
  • Annoy (Approximate Nearest Neighbors Oh Yeah): Spotify 开发的库,基于随机投影树。
  • HNSW (Hierarchical Navigable Small World): 一种基于图的 ANN 算法,提供了非常好的速度与召回率平衡。
  • ScaNN (Scalable Nearest Neighbors): Google 开发的库,针对高维向量进行了优化。

这些算法通过不同的策略来减少搜索空间,例如:

  • 量化 (Quantization): 将高维向量压缩成更小的表示,减少存储和计算成本。
  • 树结构 (Tree-based): 构建树形结构来划分向量空间,加速搜索。
  • 图结构 (Graph-based): 构建一个图,其中节点是向量,边表示相似性,通过图遍历来查找最近邻。

3.3 FAISS 快速入门与使用

在本讲座中,我们将重点介绍 FAISS,因为它功能强大,易于使用,并且在生产环境中表现出色。

FAISS 的基本使用流程:

  1. 准备数据: 将所有示例的嵌入向量组织成一个 NumPy 数组。
  2. 选择索引类型: 根据数据规模、精度要求和性能目标选择合适的 FAISS 索引。
  3. 构建索引: 将示例向量添加到索引中。
  4. 搜索: 使用查询向量在索引中查找最近邻。

FAISS 索引类型概览 (简化版):

索引类型 描述 优点 缺点 适用场景
IndexFlatL2 / IP 暴力搜索(精确最近邻),分别使用 L2 距离和内积(余弦相似度)。 100% 准确率,实现简单。 速度慢,不适合大数据集。 小规模数据集(<10k 向量)
IndexIVFFlat 倒排文件索引 (Inverted File Index),将向量空间划分为多个聚类。 速度快,内存效率高,可扩展。 精度略有下降(近似),需要训练阶段。 中大型数据集(10k – 10M 向量)
IndexHNSWFlat 分层可导航小世界图 (Hierarchical Navigable Small World)。 搜索速度极快,召回率高。 索引构建时间较长,内存占用相对较高。 对速度和精度要求高的大型数据集(>1M 向量)
IndexPQ / IndexSQ 乘积量化 (Product Quantization) / 标量量化 (Scalar Quantization)。 大幅度压缩向量,节省内存。 精度损失较大。 极大规模数据集,内存受限场景

对于大多数 Few-shot Prompting 的动态采样场景,IndexIVFFlatIndexHNSWFlat 是非常好的选择。我们先从 IndexFlatL2 开始,因为它最直观,然后过渡到 IndexIVFFlat

代码示例:使用 FAISS 构建索引并搜索

我们将扩展之前的代码,使用 FAISS 来管理和搜索我们的示例嵌入。

import faiss
from sentence_transformers import SentenceTransformer
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

# 1. 加载预训练的 Sentence-BERT 模型
model_name = 'all-MiniLM-L6-v2'
model = SentenceTransformer(model_name)
embedding_dimension = model.get_sentence_embedding_dimension()

# 2. 准备一个更大、更真实的示例语料库
example_texts = [
    "如何用Python和Flask构建一个RESTful API?",
    "使用Django框架开发一个功能完善的电商网站。",
    "学习ReactJS前端框架的基本概念。",
    "Vue.js组件化开发的最佳实践。",
    "关于机器学习模型在医疗领域的应用。",
    "深度学习在自然语言处理中的最新进展。",
    "如何优化SQL查询性能?",
    "PostgreSQL数据库的索引类型及其选择。",
    "Kubernetes集群的部署与管理。",
    "Docker容器化技术的核心概念。",
    "编写高效Go语言并发程序。",
    "Rust语言的内存安全特性探讨。",
    "如何使用AWS Lambda构建无服务器应用?",
    "Azure云平台上的微服务架构设计。",
    "Python数据分析库Pandas的进阶用法。",
    "Numpy在科学计算中的应用示例。",
    "理解HTTP协议的工作原理。",
    "Websocket与传统HTTP请求的区别。",
    "Git版本控制的高级操作技巧。",
    "CI/CD流程在软件开发中的重要性。",
    "如何调试Java应用程序?",
    "JVM内存模型深度解析。",
    "使用TensorFlow构建一个图像识别模型。",
    "PyTorch深度学习框架的入门教程。",
    "区块链技术的工作原理。",
    "智能合约在以太坊上的实现。",
    "云计算服务的种类和优势。",
    "边缘计算与物联网的结合。",
    "如何进行有效的软件测试?",
    "敏捷开发方法论的核心原则。"
]

# 3. 生成示例文本的嵌入向量
print(f"正在生成 {len(example_texts)} 个示例的嵌入向量...")
example_embeddings = model.encode(example_texts)
print(f"嵌入向量生成完成。形状: {example_embeddings.shape}")

# 4. 构建 FAISS 索引

# 我们先使用 IndexFlatL2 (暴力搜索,但精确) 来演示
# L2 距离 (欧氏距离) 和余弦相似度是负相关的,L2 距离越小,余弦相似度越大
# 为了直接使用余弦相似度,通常我们将向量归一化到单位长度,然后使用内积 (IndexFlatIP) 或 L2 距离。
# 在单位向量情况下,内积就是余弦相似度。
# 让我们先对向量进行归一化,这样 L2 距离和余弦相似度就能更好地对应起来
faiss.normalize_L2(example_embeddings)

# 创建一个 IndexFlatL2 索引,维度为 embedding_dimension
index_flat = faiss.IndexFlatL2(embedding_dimension)
print(f"IndexFlatL2 索引是否训练: {index_flat.is_trained}")

# 向索引中添加向量
index_flat.add(example_embeddings)
print(f"IndexFlatL2 索引中的向量数量: {index_flat.ntotal}")

# 5. 准备一个查询
query_text = "我需要一些关于深度学习在图像处理方面应用的示例。"
print(f"n查询文本: '{query_text}'")

# 6. 生成查询文本的嵌入向量
query_embedding = model.encode([query_text])[0]
faiss.normalize_L2(query_embedding.reshape(1, -1)) # 同样进行归一化

# 7. 在索引中搜索 K 个最近邻
k = 3 # 我们希望找到 3 个最相关的示例
print(f"正在搜索 {k} 个最相似的示例...")
distances, indices = index_flat.search(query_embedding.reshape(1, -1), k)

# distances 是 L2 距离,越小越相似
# indices 是原始 example_texts 数组中的索引

print("n搜索结果:")
for i in range(k):
    idx = indices[0][i]
    dist = distances[0][i]
    # 计算余弦相似度 (对于L2归一化向量,余弦相似度 = 1 - 0.5 * L2_distance^2)
    # 或者直接用sklearn计算,更直观
    cos_sim = cosine_similarity(query_embedding.reshape(1, -1), example_embeddings[idx].reshape(1, -1))[0][0]

    print(f"  排名 {i+1}:")
    print(f"    原始文本: '{example_texts[idx]}'")
    print(f"    L2 距离: {dist:.4f}")
    print(f"    余弦相似度: {cos_sim:.4f}")
    print("-" * 20)

# --- 现在切换到 IndexIVFFlat 来演示 ANN 的使用 ---
# IndexIVFFlat 需要一个训练阶段来创建聚类中心
nlist = 100 # 聚类中心的数量,通常是 sqrt(N) 或 4*sqrt(N)
quantizer = faiss.IndexFlatL2(embedding_dimension) # 基础量化器,这里用L2
index_ivf = faiss.IndexIVFFlat(quantizer, embedding_dimension, nlist, faiss.METRIC_L2)

print(f"n正在训练 IndexIVFFlat 索引 (nlist={nlist})...")
index_ivf.train(example_embeddings) # 使用示例向量训练索引
print(f"IndexIVFFlat 索引是否训练: {index_ivf.is_trained}")

index_ivf.add(example_embeddings)
print(f"IndexIVFFlat 索引中的向量数量: {index_ivf.ntotal}")

# 设置 nprobe 参数,它决定了搜索时检查多少个聚类。
# nprobe 越大,精度越高,但速度越慢。
index_ivf.nprobe = 10 # 检查 10 个最近的聚类

print(f"n使用 IndexIVFFlat 搜索 {k} 个最相似的示例 (nprobe={index_ivf.nprobe})...")
distances_ivf, indices_ivf = index_ivf.search(query_embedding.reshape(1, -1), k)

print("nIndexIVFFlat 搜索结果:")
for i in range(k):
    idx = indices_ivf[0][i]
    dist = distances_ivf[0][i]
    cos_sim = cosine_similarity(query_embedding.reshape(1, -1), example_embeddings[idx].reshape(1, -1))[0][0]

    print(f"  排名 {i+1}:")
    print(f"    原始文本: '{example_texts[idx]}'")
    print(f"    L2 距离: {dist:.4f}")
    print(f"    余弦相似度: {cos_sim:.4f}")
    print("-" * 20)

输出示例 (部分):

...
查询文本: '我需要一些关于深度学习在图像处理方面应用的示例。'
正在搜索 3 个最相似的示例...

搜索结果:
  排名 1:
    原始文本: '使用TensorFlow构建一个图像识别模型。'
    L2 距离: 0.4418
    余弦相似度: 0.9023
--------------------
  排名 2:
    原始文本: 'PyTorch深度学习框架的入门教程。'
    L2 距离: 0.6974
    余弦相似度: 0.7570
--------------------
  排名 3:
    原始文本: '深度学习在自然语言处理中的最新进展。'
    L2 距离: 0.8142
    余弦相似度: 0.6677
--------------------

正在训练 IndexIVFFlat 索引 (nlist=100)...
IndexIVFFlat 索引是否训练: True
IndexIVFFlat 索引中的向量数量: 30

使用 IndexIVFFlat 搜索 3 个最相似的示例 (nprobe=10)...

IndexIVFFlat 搜索结果:
  排名 1:
    原始文本: '使用TensorFlow构建一个图像识别模型。'
    L2 距离: 0.4418
    余弦相似度: 0.9023
--------------------
  排名 2:
    原始文本: 'PyTorch深度学习框架的入门教程。'
    L2 距离: 0.6974
    余弦相似度: 0.7570
--------------------
  排名 3:
    原始文本: '深度学习在自然语言处理中的最新进展。'
    L2 距离: 0.8142
    余弦相似度: 0.6677
--------------------

可以看到,无论是 IndexFlatL2 还是 IndexIVFFlat,都能准确地找到与查询“深度学习在图像处理”相关的示例,如“TensorFlow构建图像识别模型”和“PyTorch深度学习框架”。这证明了向量索引在语义搜索中的有效性。

第四章:Few-shot Prompting 中的动态采样:端到端工作流

现在,我们已经掌握了向量嵌入和向量索引这两个核心技术,是时候将它们整合起来,构建 Few-shot Prompting 的动态采样系统了。

4.1 动态采样的完整工作流

整个过程可以概括为以下步骤:

  1. 构建示例语料库 (Example Corpus): 收集大量高质量的、覆盖任务多样性的输入-输出示例对。这些示例是LLM学习的“知识库”。
  2. 生成示例嵌入 (Embed Examples): 使用预训练的嵌入模型,将语料库中每个示例的输入部分(或整个示例)转换为高维向量。
  3. 构建向量索引 (Build Vector Index): 使用 FAISS 等库,基于生成的示例嵌入构建一个高效的向量索引。这个索引将用于快速查找相似示例。
  4. 接收用户查询 (Receive User Query): 当用户提交一个新的问题或任务时。
  5. 生成查询嵌入 (Embed Query): 使用与示例嵌入相同的模型,将用户查询转换为高维向量。
  6. 检索最相关示例 (Retrieve Relevant Examples): 利用查询嵌入,在预先构建的向量索引中搜索 K 个(例如3到5个)与查询最相似的示例向量。
  7. 构造动态 Prompt (Construct Dynamic Prompt): 将任务指令、检索到的 K 个示例(及其对应的输出)以及用户查询,按照 Few-shot Prompting 的格式组装成一个完整的 Prompt 字符串。
  8. 调用 LLM (Call LLM): 将构造好的 Prompt 发送给大型语言模型,获取最终的响应。

这个工作流的核心优势在于,对于每一个新的用户查询,系统都会“智能地”挑选出最能帮助LLM理解当前任务并给出高质量回答的示例。

4.2 端到端代码示例:文本分类任务

让我们通过一个具体的文本分类任务来演示这个端到端的工作流。假设我们的任务是判断一段文本的情感(正面、负面、中立)。

import faiss
from sentence_transformers import SentenceTransformer
import numpy as np
import os

# 模拟一个 LLM API 调用,实际中会替换为 OpenAI, Anthropic, Google 等的 SDK
def mock_llm_call(prompt: str) -> str:
    print("n--- 模拟 LLM 调用 ---")
    print("发送到 LLM 的 Prompt:")
    print("=" * 50)
    print(prompt)
    print("=" * 50)
    # 实际场景中,这里会调用 LLM API,并返回其响应
    # 例如:
    # from openai import OpenAI
    # client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
    # response = client.chat.completions.create(
    #     model="gpt-4o-mini",
    #     messages=[{"role": "user", "content": prompt}],
    #     temperature=0.7
    # )
    # return response.choices[0].message.content
    return "LLM 成功处理了动态 Prompt,并根据示例给出了响应。" # 模拟响应

# 1. 加载预训练的 Sentence-BERT 模型
model_name = 'all-MiniLM-L6-v2'
print(f"正在加载 Sentence-BERT 模型: {model_name}...")
embedding_model = SentenceTransformer(model_name)
embedding_dimension = embedding_model.get_sentence_embedding_dimension()
print("模型加载完成。")

# 2. 构建示例语料库
# 每个示例包含 'input' (文本) 和 'output' (情感标签)
# 真实场景中,这些示例会存储在数据库或文件中
example_data = [
    {"input": "这电影真是太棒了,我看了两遍!", "output": "情感: 正面"},
    {"input": "服务很差,食物也很冷,一次糟糕的体验。", "output": "情感: 负面"},
    {"input": "这家餐厅的环境不错,菜品中规中矩。", "output": "情感: 中立"},
    {"input": "新产品发布会非常成功,市场反响热烈。", "output": "情感: 正面"},
    {"input": "等待时间太长了,效率低下,令人失望。", "output": "情感: 负面"},
    {"input": "报告详细且全面,数据分析准确。", "output": "情感: 正面"},
    {"input": "我对这次的购买体验感到非常满意。", "output": "情感: 正面"},
    {"input": "这手机电池续航太短了,很不方便。", "output": "情感: 负面"},
    {"input": "这本书的故事情节平淡,没有太多亮点。", "output": "情感: 负面"},
    {"input": "会议内容丰富,但时间安排有点紧凑。", "output": "情感: 中立"},
    {"input": "尽管有些小瑕疵,但总体来说还算可以。", "output": "情感: 中立"},
    {"input": "这个软件的界面设计很简洁,功能强大。", "output": "情感: 正面"},
    {"input": "客服响应速度慢,问题没能得到解决。", "output": "情感: 负面"},
    {"input": "今天天气晴朗,适合外出游玩。", "output": "情感: 中立"}, # 中立示例,但可能被误判
    {"input": "这个方案听起来很有前景,值得尝试。", "output": "情感: 正面"},
    {"input": "我不确定这个决定是否正确。", "output": "情感: 中立"},
]

# 提取示例输入文本用于嵌入
example_inputs = [item['input'] for item in example_data]

# 3. 生成示例嵌入
print(f"正在生成 {len(example_inputs)} 个示例的嵌入向量...")
example_embeddings = embedding_model.encode(example_inputs)
faiss.normalize_L2(example_embeddings) # 归一化
print(f"嵌入向量生成完成。形状: {example_embeddings.shape}")

# 4. 构建向量索引 (使用 IndexIVFFlat,更适合大规模数据)
nlist = min(len(example_data) // 5, 100) # 聚类中心数量,至少为1,不超过数据量的1/5,上限100
if nlist < 1: nlist = 1
quantizer = faiss.IndexFlatL2(embedding_dimension)
vector_index = faiss.IndexIVFFlat(quantizer, embedding_dimension, nlist, faiss.METRIC_L2)

print(f"n正在训练 FAISS IndexIVFFlat 索引 (nlist={nlist})...")
vector_index.train(example_embeddings)
vector_index.add(example_embeddings)
vector_index.nprobe = min(nlist, 5) # 搜索时检查的聚类数量,不超过 nlist 或 5
print(f"FAISS 索引构建完成。包含 {vector_index.ntotal} 个向量。")

# 5. 定义任务指令
instruction = (
    "你是一个情感分析专家。请根据提供的文本判断其情感是 '正面', '负面', 还是 '中立'。n"
    "请严格按照 '情感: [标签]' 的格式输出。"
)

# 6. 动态采样与 Prompt 构建函数
def get_dynamic_prompt(query: str, num_examples: int = 3) -> str:
    # 生成查询嵌入
    query_embedding = embedding_model.encode([query])[0]
    faiss.normalize_L2(query_embedding.reshape(1, -1))

    # 在索引中搜索最相关的示例
    _, indices = vector_index.search(query_embedding.reshape(1, -1), num_examples)

    # 提取完整的示例对
    selected_examples = [example_data[idx] for idx in indices[0]]

    # 构造 Prompt
    prompt_parts = [instruction]
    for ex in selected_examples:
        prompt_parts.append(f"n输入: {ex['input']}n{ex['output']}")

    prompt_parts.append(f"n输入: {query}n") # 加上待处理的查询

    return "".join(prompt_parts)

# 7. 演示动态采样和 LLM 调用
test_queries = [
    "这家酒店的设施非常陈旧,Wi-Fi信号也不好。", # 负面
    "新发布的手机功能强大,设计时尚,我很喜欢。", # 正面
    "这个报告只是对现有信息的总结,没有新的见解。", # 中立
    "昨晚的演唱会气氛热烈,歌手表现出色!", # 正面
    "快递送货慢,包裹还破损了,非常不满意。", # 负面
]

for i, query in enumerate(test_queries):
    print(f"n--- 处理查询 {i+1}: '{query}' ---")
    dynamic_prompt = get_dynamic_prompt(query, num_examples=3)

    llm_response = mock_llm_call(dynamic_prompt)
    print(f"LLM 模拟响应: {llm_response}")

代码输出示例 (部分):

正在加载 Sentence-BERT 模型: all-MiniLM-L6-v2...
模型加载完成。
正在生成 16 个示例的嵌入向量...
嵌入向量生成完成。形状: (16, 384)

正在训练 FAISS IndexIVFFlat 索引 (nlist=3)...
FAISS 索引构建完成。包含 16 个向量。

--- 处理查询 1: '这家酒店的设施非常陈旧,Wi-Fi信号也不好。' ---

--- 模拟 LLM 调用 ---
发送到 LLM 的 Prompt:
==================================================
你是一个情感分析专家。请根据提供的文本判断其情感是 '正面', '负面', 还是 '中立'。
请严格按照 '情感: [标签]' 的格式输出。

输入: 服务很差,食物也很冷,一次糟糕的体验。
情感: 负面
输入: 这手机电池续航太短了,很不方便。
情感: 负面
输入: 快递送货慢,包裹还破损了,非常不满意。
情感: 负面

输入: 这家酒店的设施非常陈旧,Wi-Fi信号也不好。
==================================================
LLM 模拟响应: LLM 成功处理了动态 Prompt,并根据示例给出了响应。

--- 处理查询 2: '新发布的手机功能强大,设计时尚,我很喜欢。' ---

--- 模拟 LLM 调用 ---
发送到 LLM 的 Prompt:
==================================================
你是一个情感分析专家。请根据提供的文本判断其情感是 '正面', '负面', 还是 '中立'。
请严格按照 '情感: [标签]' 的格式输出。

输入: 新产品发布会非常成功,市场反响热烈。
情感: 正面
输入: 这个软件的界面设计很简洁,功能强大。
情感: 正面
输入: 这电影真是太棒了,我看了两遍!
情感: 正面

输入: 新发布的手机功能强大,设计时尚,我很喜欢。
==================================================
LLM 模拟响应: LLM 成功处理了动态 Prompt,并根据示例给出了响应。
...

从这些模拟输出中,我们可以清晰地看到,对于每一个新的查询,get_dynamic_prompt 函数都根据其语义内容,从示例库中检索出了3个最相关的示例。例如,对于负面评价的酒店查询,它检索到了其他负面评价的示例。这种根据上下文动态调整 Prompt 内容的能力,正是 Few-shot Prompting 动态采样的核心价值。

第五章:高级考量与最佳实践

动态采样并非一劳永逸,其效果和效率还会受到多种因素的影响。

5.1 嵌入模型的选择

  • 通用性与领域专用性: all-MiniLM-L6-v2 这样的模型在通用领域表现良好。但如果你的任务涉及高度专业的领域(如医学、法律、金融),考虑使用在该领域数据上训练过的模型,或者对现有模型进行微调。
  • 维度与性能: 嵌入维度越高,通常能捕获更多信息,但也意味着更大的存储和计算开销。权衡好维度与实际需求。
  • 模型更新: 嵌入模型也在不断发展,定期评估和更新你的嵌入模型可能带来性能提升。

5.2 示例数量 (K值) 的选择

num_examples (K) 的选择至关重要:

  • 太少: 可能无法提供足够的上下文,LLM难以学习模式。
  • 太多:
    • 上下文窗口限制: LLM有最大输入Token限制,过多的示例可能导致 Prompt 过长。
    • 成本增加: LLM通常按Token计费,长Prompt意味着更高的成本。
    • 性能下降: 过长的Prompt可能稀释有效信息,甚至导致模型注意力分散,效果不升反降("lost in the middle" 效应)。

最佳 K 值通常需要通过实验来确定,一般在 3 到 8 之间。

5.3 示例多样性与相关性

纯粹的相关性有时可能不够。例如,如果所有最相关的示例都非常相似,它们可能无法覆盖任务的完整范围。可以考虑引入最大边际相关性 (Maximal Marginal Relevance, MMR) 策略:

  • MMR: 优先选择与查询相关性高,同时与已选示例的相似性低的示例。这有助于确保选出的示例既相关又具有多样性。

实现 MMR 的思路:

  1. 首先,找到与查询最相似的 TOP N 个示例。
  2. 从这 N 个中,选择第一个最相似的示例。
  3. 然后,在剩余的 N-1 个示例中,选择一个与查询高度相似,但与已选示例(例如第一个)差异最大的示例。
  4. 重复此过程,直到选出 K 个示例。

5.4 向量索引的优化与维护

  • 索引类型选择: 根据示例库的规模和查询延迟要求,选择合适的 FAISS 索引类型(如前所述)。
  • 索引参数调优: 对于 IndexIVFFlatnlist (聚类中心数) 和 nprobe (搜索时检查的聚类数) 是关键参数,需要根据实际数据进行调优。
  • 索引更新: 当示例库发生变化时(添加、删除、修改示例),需要更新或重建索引。对于大规模数据,这可能需要增量更新或定期全量重建的策略。
  • 持久化: 索引需要持久化到磁盘,以便在应用重启后快速加载。FAISS 提供了 faiss.write_index()faiss.read_index() 函数。
# 示例:保存和加载 FAISS 索引
# 保存索引
faiss.write_index(vector_index, "sentiment_examples.faiss")

# 重新加载索引
loaded_index = faiss.read_index("sentiment_examples.faiss")
print(f"加载的索引包含 {loaded_index.ntotal} 个向量。")

5.5 性能与可伸缩性

  • 嵌入生成速度: 批量生成嵌入比逐个生成更高效。
  • 向量数据库: 对于超大规模的示例库(数千万甚至上亿),可以考虑使用专门的向量数据库(如 Milvus, Pinecone, Weaviate, Qdrant)。它们提供了分布式存储、高可用性、实时索引更新和更复杂的查询功能。
  • 缓存: 对于重复的查询,可以缓存其生成的 Prompt 或 LLM 响应。

5.6 成本考量

  • 嵌入成本: 每次生成嵌入向量都会产生计算资源或 API 调用成本。
  • LLM 调用成本: LLM 通常按 Token 计费,动态采样的目标之一就是通过选择最相关的短示例,在保证效果的前提下,尽量减少 Prompt 的总 Token 数,从而降低成本。

第六章:实际应用场景

动态采样在 Few-shot Prompting 中具有广泛的应用前景:

  • 智能客服与问答系统: 根据用户问题,从海量 FAQ、历史对话记录或知识库中检索最相关的问答对,作为LLM的上下文,生成更准确、个性化的回复。
  • 代码助手与自动化编程: 当开发者提出一个编程问题时,匹配相似的代码片段、文档示例或错误解决方案,帮助LLM生成正确的代码或调试建议。
  • 数据提取与信息抽取: 提供不同格式的文本和对应的抽取结果作为示例,LLM可以更准确地从新文本中提取实体、关系或结构化数据。
  • 文本摘要与改写: 匹配相似主题或风格的原文与摘要/改写示例,指导LLM生成高质量的摘要或进行风格转换。
  • 法律与医疗助手: 在特定法律条款或医疗案例中,匹配相似的案例或法规条文,帮助LLM进行推理和提供建议。
  • 多语言翻译与本地化: 提供特定领域或术语的源语言与目标语言的翻译示例,提高LLM在专业翻译任务上的准确性。

结语

我们今天深入探讨了 Few-shot Prompting 中动态采样的强大潜力,以及如何通过向量嵌入和向量索引这两个核心技术来将其变为现实。通过为LLM智能地匹配最相关的上下文示例,我们不仅能够显著提升其在特定任务上的性能,还能优化资源利用,为构建更加智能、高效的AI应用奠定坚实基础。这项技术是连接大规模预训练模型与特定业务场景的桥梁,也是未来AI应用开发中不可或缺的一环。希望今天的讲座能为大家带来启发,期待大家在各自的项目中实践并创新!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注