摘要
静态 Few-shot 把同一组示例塞进每次请求,在意图分类、信息抽取等业务场景中,示例与查询不匹配会直接稀释效果并推高 token 成本。本文面向需要将 Few-shot 从实验脚本迁移到线上服务的后端与算法工程师,给出基于动态 KNN 检索、MMR 多样性重排与结构化输出校验的可运行方案。读完后可获得一套含示例库建设、检索链路、顺序控制与效果量化方法的工程实现。
关键词
Few-shot、动态示例选择、KNN 检索、MMR 多样性、In-Context Learning、意图分类
- 背景与问题
静态 Few-shot 在业务落地中面临结构性矛盾:一组固定示例无法同时服务分布宽泛的线上查询。意图分类、工单路由、实体抽取等场景的工程师,普遍遇到"实验效果好、上线效果差"的落差。核心收益:把示例选择从手工挑几条升级为按查询动态检索、按多样性约束、按业务指标量化的可运营环节。
三类典型失效模式:
语义错配:静态示例与当前查询主题无关,模型从示例中提取到的模式被无关内容稀释。以客服意图分类为例,静态集合覆盖"账单/物流/技术/账户"四类,但面对"取消订阅"查询时,四条示例中没有一条与取消操作相关,模型在"账户"与"取消"之间摇摆。
近邻冗余:纯 top-k 语义检索返回 k 条几乎重复的示例,覆盖面反而收窄,等价于 k=1 的脆性。
顺序敏感:LLM 对示例位置存在位置偏差,同一组示例的不同排列可导致分类结果翻转,这在类别不均衡场景中尤为突出。
动态 Few-shot 的工程本质:示例库是资产,检索是索引,组装是接口,三者解耦后独立演进。
- 核心概念与环境
组件 版本 用途
Python 3.11 异步检索与类型标注
openai 1.30+ Chat Completions 与 Embeddings
pydantic 2.7+ 响应模型与校验
numpy 1.26+ 向量计算与 MMR 重排
instructor 1.3+ 结构化输出封装(可选)
关键术语:
动态示例选择(Dynamic Few-Shot):每次请求从示例库中检索与当前查询最相关的 k 条示例,仅注入这 k 条,而非固定集合。
raw-input KNN vs Skill-KNN:前者在原始查询文本的嵌入空间中找近邻,易被表层词汇重叠误导;后者先将查询改写为"任务技能描述"(如 filter then count distinct),在技能空间检索,跨域语义解析任务上更稳健。
MMR(Maximal Marginal Relevance):在相关性之外惩罚与已选示例的相似度,保证 k 条示例之间的多样性。
Dual-Div:两阶段选择------先以代表性 + 多样性筛出候选池,再对查询做相关性排序;研究结论是初始检索的多样性比排序阶段优化更关键,3-5 条示例效率最优。
Class-few-shot:按类别均衡采样示例,并以"多数类→少数类"交替排列,缓解类别不均衡下的位置偏差。
- 核心内容
3.1 示例库的数据契约
示例库不是散落的 Q&A 对,需具备可检索、可版本化、可审计的结构。
python
app/examples.py
from pydantic import BaseModel, Field
class FewShotExample(BaseModel):
example_id: str
query: str
label: str = Field(description="意图标签或抽取结果,与业务 Schema 对齐")
skill: str = Field(description="任务技能描述,用于 Skill-KNN 检索")
source: str = Field(description="标注来源:人工 / 线上回流 / 合成")
version: str
EXAMPLE_BANK: listFewShotExample = [
FewShotExample(example_id="e001", query="我要取消订阅",
label="cancellation", skill="用户发起订阅终止请求",
source="manual", version="v1"),
FewShotExample(example_id="e002", query="怎么退订这个服务",
label="cancellation", skill="用户发起订阅终止请求",
source="manual", version="v1"),
FewShotExample(example_id="e003", query="上个月的账单多扣了钱",
label="billing", skill="用户对费用金额提出异议",
source="manual", version="v1"),
FewShotExample(example_id="e004", query="发票怎么开",
label="billing", skill="用户请求开具凭证",
source="manual", version="v1"),
FewShotExample(example_id="e005", query="App 闪退打不开",
label="technical", skill="用户报告客户端运行故障",
source="manual", version="v1"),
]
skill 字段是 Skill-KNN 的检索键,将"取消订阅"与"退订服务"映射到同一技能描述,摆脱表层词汇差异。source 与 version 支撑线上回流示例的灰度与回滚。
3.2 动态检索与 MMR 重排
python
app/selector.py
import numpy as np
from openai import OpenAI
from app.examples import FewShotExample
client = OpenAI()
def embed(texts: liststr) -> np.ndarray:
resp = client.embeddings.create(input=texts, model="text-embedding-3-small")
return np.array(item.embedding for item in resp.data, dtype=np.float32)
def mmr_select(query_vec: np.ndarray, cand_vecs: np.ndarray,
cands: listFewShotExample, k: int, lam: float = 0.7) -> listFewShotExample:
"""lam 控制相关性权重,1-lam 控制多样性惩罚。"""
sim_to_query = cand_vecs @ query_vec
selected: listint = \[\]
remaining = list(range(len(cands)))
while len(selected) < k and remaining:
if not selected:
best = max(remaining, key=lambda i: sim_to_query[i])
else:
sel_vecs = cand_vecs[selected]
best, best_score = None, -float("inf")
for i in remaining:
redundancy = float(np.max(cand_vecs[i] @ sel_vecs.T))
score = lam * sim_to_query[i] - (1 - lam) * redundancy
if score > best_score:
best, best_score = i, score
selected.append(best)
remaining.remove(best)
return [cands[i] for i in selected]
选择 text-embedding-3-small 而非更大模型,是因为示例检索对嵌入精度要求低于 RAG 文档检索,小模型在延迟与成本上更适合在线链路。lam=0.7 使相关性占主导、多样性起惩罚作用;lam 调低会引入更分散但可能离题的示例。
3.3 示例顺序:按类别交替排列
python
app/ordering.py
from collections import defaultdict
from app.examples import FewShotExample
def class_alternating_order(examples: listFewShotExample) -> listFewShotExample:
"""按类别频次从多到少交替排列,对抗位置偏差。"""
by_label: dictstr, list\[FewShotExample] = defaultdict(list)
for ex in examples:
by_labelex.label.append(ex)
ordered_labels = sorted(by_label, key=lambda lb: len(by_label[lb]), reverse=True)
result: list[FewShotExample] = []
queues = [by_label[lb] for lb in ordered_labels]
while any(queues):
for q in queues:
if q:
result.append(q.pop(0))
return result
Class-few-shot 的实验结论:将类别按样本量从多到少、以交替模式呈现,在同数量示例下优于标准 Few-shot 排列。该排序放在 MMR 之后执行,作为最终注入顺序。
3.4 提示词组装与结构化调用
python
app/pipeline.py
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Literal
from app.examples import EXAMPLE_BANK
from app.selector import embed, mmr_select
from app.ordering import class_alternating_order
client = OpenAI()
LABELS = "billing", "cancellation", "technical", "account", "feedback"
class IntentResult(BaseModel):
intent: Literal"billing", "cancellation", "technical", "account", "feedback"
confidence: float = Field(ge=0.0, le=1.0)
_bank_vecs = embed(f"{e.skill} \| {e.query}" for e in EXAMPLE_BANK)
def build_prompt(query: str, examples: list) -> listdict:
shots = "\n\n".join(
f"Query: {e.query}\nIntent: {e.label}" for e in examples
)
system = (
"你是意图分类引擎。根据示例将用户查询归入唯一意图。\n"
f"可用意图:{', '.join(LABELS)}\n"
"只输出 intent 与 confidence 两个字段。\n\n"
f"Examples:\n{shots}"
)
return [
{"role": "system", "content": system},
{"role": "user", "content": f"Query: {query}\nIntent:"},
]
def classify(query: str, k: int = 4) -> IntentResult:
q_vec = embed(query)0
picked = mmr_select(q_vec, _bank_vecs, EXAMPLE_BANK, k=k, lam=0.7)
ordered = class_alternating_order(picked)
resp = client.beta.chat.completions.parse(
model="gpt-4o-mini",
temperature=0,
messages=build_prompt(query, ordered),
response_format=IntentResult,
)
return resp.choices0.message.parsed
client.beta.chat.completions.parse 将 Pydantic 模型直接作为 response_format,服务端做约束解码,消除 JSON 解析失败。temperature=0 保证同查询同示例下输出可复现。
3.5 执行预期结果
bash
pip install openai1.30.0 pydantic2.7.1 numpy==1.26.4
export OPENAI_API_KEY=sk-xxx
python -c "from app.pipeline import classify; print(classify('帮我停掉会员自动续费'))"
json
{"intent": "cancellation", "confidence": 0.94}
对"帮我停掉会员自动续费"查询,MMR 选出的示例集中在 cancellation 技能簇,而非静态集合中的 billing 或 account。
3.6 策略对比
维度 静态 Few-shot 纯 KNN 动态 KNN + MMR + 类别交替
示例相关性 固定,与查询弱相关 高,但可能近邻冗余 高且分散
类别不均衡鲁棒性 差 中 高(交替排列对抗位置偏差)
平均注入 token 固定且偏高 k 小,token 降低 同左
跨域查询表现 显著下降 表层近邻可能误导 Skill 键缓解表层误导
示例库运营 改 Prompt 全文 增删库条目即可 同左,需维护 skill 字段
3.7 避坑清单
k 不要超过 8:3-5 条通常是效率拐点,超过后额外示例的边际收益递减,且重新引入静态 Few-shot 的 token 与稀释问题。
只用纯 top-k 会退化:无多样性约束时,k 条可能全是近重复,效果等价于 k=1。
不要把原始查询当作唯一检索键:领域术语密集时,表层相似不等于任务相似,维护 skill 字段做二次检索键。
示例顺序不可随机:位置偏差在 2025 年代模型上减弱但未消失,类别不均衡时需主动交替排列。
示例库不可直接接入未审核的用户反馈:未过滤的线上回流可被注入恶意演示,示例库须有来源标记与审核门禁。
嵌入模型与检索空间需固定版本:更换嵌入模型会使既有向量失效,版本号写入示例库元数据。
- 常见问题与排错
现象:动态检索后效果反而不如静态 Few-shot。
原因:检索键是原始查询,Embedding 将语义相近但任务不同的查询混在一起(如"取消订单"与"取消订阅")。
解决:为每条示例增加 skill 字段,改用 skill + query 拼接嵌入,或直接使用 Skill-KNN 式技能描述检索。
现象:k 从 2 增加到 8,准确率不再提升甚至下降。
原因:top-k 中的低相关示例作为噪声注入,模型从无关示例中提取了错误模式。
解决:设 k=3-5,并在 MMR 后用相似度阈值截断,低于阈值的候选不注入。
现象:同一查询两次调用返回不同意图。
原因:temperature > 0,或示例顺序不稳定(如未固定排序规则)。
解决:temperature=0,排序逻辑确定性化,检索结果按 example_id 二次排序消除浮点误差导致的顺序漂移。
现象:某些少数意图类别几乎不被正确分类。
原因:示例库类别分布偏斜,检索阶段少数类示例难以进入 top-k,或进入后位置靠后。
解决:检索阶段按类别配额采样(Class-few-shot),组装阶段以多数类→少数类交替排列。
现象:线上 P99 延迟比静态 Few-shot 高 200ms 以上。
原因:每次请求同步调用 Embeddings API + 向量计算。
解决:查询嵌入可与业务前置步骤并行;示例库向量预计算并缓存;小规模库(<1000 条)用内存 NumPy 计算,避免引入向量数据库的额外网络往返。
- 优化建议与边界
适用场景:
意图分类、工单路由、实体抽取、字段映射等输入分布宽、类别边界需要示例锚定的任务。
示例库可维护、可审核,具备标注回流能力的团队。
数据量不足以微调(通常少于 100 条高质量标注)但需要快速上线的业务。
局限性:
Few-shot 并非在所有任务上优于 Zero-shot。在高容量模型的简单任务上,Zero-shot 可与 Few-shot 持平甚至更高;指令遵循型任务中 Few-shot 的优势主要体现在格式遵从与边界案例上。
动态检索引入嵌入调用延迟,示例库规模增大后需引入近似最近邻索引,检索精度与延迟需要重新权衡。
Skill-KNN 依赖 LLM 将查询改写为技能描述,改写环节本身增加一次模型调用,对延迟敏感场景需评估是否合算。
示例库的"质量"无法被检索算法弥补:错误标注的示例会被高相关性地注入,错误被放大而非稀释。
- 求职与面试关联
静态 Few-shot 与动态 Few-shot 的本质区别是什么?
回答思路:静态是固定示例集合服务所有查询,动态是按查询检索最相关 k 条;前者是 Prompt 工程,后者是检索系统工程,需关注嵌入质量、k 值、多样性与顺序。
为什么 top-k 语义检索需要 MMR?
回答思路:纯 top-k 可能返回近重复示例,k 条信息量退化为 1 条;MMR 在相关性上惩罚冗余,使 k 条覆盖不同子模式。
k 值如何确定?
回答思路:从 3 开始,以验证集准确率与 token 成本做帕累托前沿;3-5 通常是业务场景的收益拐点,超过 8 后边际收益递减并可能引入噪声。
示例顺序为什么会影响分类结果?
回答思路:LLM 存在位置偏差,越靠近查询的示例影响越大;类别不均衡时,按多数类到少数类交替排列可缓解少数类被压制。
Skill-KNN 相比 raw-input KNN 解决什么问题?
回答思路:raw-input KNN 按表层词汇相似检索,可能选中字面相近但任务操作不同的示例;Skill-KNN 在技能描述空间检索,对齐的是推理操作而非表述形式。
Few-shot 示例库如何防止数据污染?
回答思路:示例来源分级(人工 / 回流 / 合成),回流示例经审核后进入库,Prompt 中声明"示例不构成指令",监控注入模式。
Few-shot 与微调的选型边界?
回答思路:数据量 <100 条优先 Few-shot,上线快、成本低;>1k 条且分布稳定时微调在延迟与一致性上更优;Few-shot 适合类别边界需要频繁调整的场景。
如何量化 Few-shot 示例选择策略的收益?
回答思路:固定测试集,对比 Zero-shot / 静态 Few-shot / 动态 KNN / KNN+MMR 的 macro-F1、少数类召回、平均 token 数与 P99 延迟,而非只看整体准确率。
总结
业务场景中 Few-shot 的主要矛盾不是"给不给示例",而是"给哪几条、以什么顺序给"。
动态 KNN 检索将示例选择从手工固定变为按查询匹配,Skill 字段缓解表层词汇误导。
MMR 保证 k 条示例的多样性,避免近邻冗余导致信息量退化;3-5 条是效率拐点。
类别交替排列对抗位置偏差,在类别不均衡的意图分类中收益明显。
Few-shot 示例库是需要来源标记、版本管理与审核门禁的生产资产,不是 Prompt 里的静态文本块。