零样本学习、少样本学习,这两个词在自然语言处理相关的话题里出现频率越来越高,而它们的底层机制,就是上下文学习。所谓上下文学习,简单说就是把任务说明、示范样例、待处理内容一起拼成一段输入交给模型,让它在一次推理中完成“理解任务、模仿格式、生成答案”的全过程。你可能早就用过这种能力了——让模型从一份会议纪要里提取待办事项,给它两个已经整理好的表格样例,它就能照着输出,这属于少样本;不给样例,只交代一句“请提取待办事项并以表格形式输出”,这属于零样本。而两者共同指向的能力,恰恰是当前大模型最值钱的部分:通过上下文学习提升推理能力,让一个没有为特定任务训练过的模型,像老员工一样拿着临时需求就能上手干活。
这篇文章适合谁看?如果你正在用大模型做文本抽取、数据清洗、格式转换、逻辑推理,或者你只是想知道为什么“在提示词里塞几个例子”比辛辛苦苦微调还管用,那这篇内容可以帮你省不少试错时间。我会从概念拆起,讲清楚上下文学习为什么能替代微调,再给出模型行为背后的机制,最后用一套可复现的实操流程说完少样本提示的设计、调参与避坑经验。
1. 从“训练一套模型”到“用一次推理”:范式转变
1.1 零样本、少样本到底在说什么
传统做自然语言任务的方式,和现在完全不是一种思路。几年前你想做一个“用户评论情感分类”的功能,流程基本是:采集几千条标注评论,选一个预训练模型,在模型上加一个分类头,跑几个 epoch 的微调,然后部署一个专属于你这个任务的模型。换一个任务,比如从“情感分类”换成“投诉类型识别”,又是一轮新的数据标注和训练。整个模式的核心是“每个任务训练一套模型”。
大模型时代的思路完全不同。因为模型已经在海量文本上做了预训练,它内部其实已经积累了极其丰富的语言模式和任务模式。你现在要做的事情不是再教它一遍怎么分类,而是想办法把“想做哪个任务”这个意图传给它。零样本学习,就是你只给一段任务描述和待处理内容,不给任何输入输出样例,模型直接给出结果。比如:
任务:判断下面这条用户反馈属于“物流问题”“质量投诉”还是“其他”,并给出理由。 反馈:快递到了三天还没派送,联系客服也没人理。 输出:模型会输出:“物流问题。理由:反馈中提到快递未及时派送,且客服未响应。”这是零样本,因为它没有看过任何该任务的样例。
少样本学习则是给模型几个输入输出对作为示范,再让它处理新输入。比如你先给它两条已经分好类的反馈,再让它对第三条进行分类。同样的任务,多了两个示例,模型的输出格式会稳定很多,分类依据也更贴近你期望的粒度。这种“给几个例子就能接手任务”的能力,本质上就是上下文学习——你并没有更新模型参数,只是把示例和任务描述作为上下文放了进去,让模型从条件概率分布中找到了你想要的输出路径。
一个比较贴切的类比:零样本是让一个新同事只听了目标描述就直接干活,少样本是让他看了两份已经完成的工作记录再动手。他没有专门接受过你这套流程的培训,但凭着过去积累的经验,基本能交出合格的东西。
1.2 上下文学习为什么能替代微调
很多人第一次看到少样本效果时都会有一个疑问:模型参数都没动,它凭什么“学会”了任务?
先说成本。微调相当于把一个人送去参加两周封闭培训班,贵、慢、而且每换一个任务就得重新培训。上下文学习相当于开工前给老员工做五分钟情况说明,成本几乎为零,不需要准备训练集,也不需要重新部署模型,只改提示词就行。对于很多临时性、格式多变的长尾任务,比如把用户发来的杂乱截图信息整理成结构化 JSON、从合同文本里抽取关键条款、把一段口语转写成正式公文,上下文学习的性价比远远超过微调。
再说机制。模型预训练阶段见过的文本里,存在大量“任务描述 + 示范 + 回答”的结构。推理时,提示词把这些结构重新激活了,模型不是在获取新知识,而是在它已有的能力矩阵里找合适的行为模式。你可以把上下文学习理解成一种“推理时的模式检索”:示例中的输入输出对定义了一个局部规律,自注意力机制让新的输入与这些示例在表征空间里对齐,从而把输出分布拉向正确的方向。
当然,上下文学习也不是万能的。如果业务逻辑需要持续稳定地存在,比如一个每天被调用几百万次的固定抽取服务,微调仍然是更稳的选择;如果任务依赖大量私有领域知识,比如内部文档规范、独家产品信息,上下文学习的效果会明显受限,这时候更适合接检索增强或者做领域微调。搞清楚这个边界,你就知道什么时候该用提示词,什么时候该老老实实去训练。
2. 上下文学习的工作原理与关键机制
2.1 预训练和自注意力如何撑起上下文学习
要理解上下文学习为什么有效,得回到预训练的两个核心产物:语言知识和注意力机制。
预训练的目标很简单,让模型根据前文预测下一个词。但就是这个简单的目标,让模型被迫学会了海量文本中的结构规律:一段新闻后面可能跟着评论,一个问题后面可能跟着解答,一份说明书后面可能跟着操作步骤。当你在提示词里给出“任务 + 示例 + 待处理内容”时,模型看到的不是一串孤立的句子,而是和它预训练时见过的无数文本结构高度相似的模式。它做的也不是“新学”,而是把当前输入映射到最接近的已学模式上。
自注意力机制在这个过程中扮演了“对表”的角色。在推理过程中,每个 token 都会和其它所有 token 计算关联权重。示例中的输入部分和真实输入越相似,注意力分数就越高,模型就越倾向于让真实输入“复用”示例中标签对应的输出分布。有研究者把上下文学习解释为一种隐式的贝叶斯推断,也有研究认为它等价于在表征空间里做核回归。这些理论细节不必深究,但结论很重要:模型确实是在输入序列内部完成了“按示例调整输出分布”的过程,这与梯度更新毫无关系。
这也是为什么上下文学习对示例质量这么敏感。注意力机制会放大示例中的特征,如果示例本身和目标任务不匹配,或者示例内部存在矛盾,模型很容易把这些噪声也学进输出模式里。后面我会专门讲这个问题。
2.2 示例数量、顺序和格式的敏感性问题
上下文学习并不总是“示例越多越好”。我自己的实测经验是,很多任务在 4 到 8 个示例时收益最大,超过 8 个边际收益会明显递减,个别任务甚至会出现效果下滑。原因在于上下文窗口是有限资源,示例太多会把真正要处理的内容挤到窗口边缘,注意力权重更容易分散到无关细节上。
示例的顺序也有讲究。如果你把同一类标签的示例都堆在一起,模型有可能误以为输出应该按照示例的顺序“连坐”。比如三个“物流问题”示例放在前面,后面跟着一个“质量投诉”示例,模型在处理新输入时可能会更倾向输出“物流问题”,因为它在注意力上更靠前、重复次数更多。更稳的做法是让不同类别的示例交替出现,保持标签分布均衡。
格式敏感性是最容易被忽略的坑。示例的输入输出格式必须和真实输入完全一致,连分隔符、换行位置都要尽量对齐。我曾经调试一个抽取任务,给模型看的示例都是“输入:…… 输出:……”格式,但最后测试时少写了“输出:”三个字,模型直接不输出内容,白白排查了很久。模型对格式的执着程度超出你的想象,它不是在理解你的“意思”,而是在统计你“格式”的模式。所以,设计示例时一定要用真实输入的样子去构造,而不是用一套理想化的模板。
2.3 如何设计高质量示例
既然示例这么关键,怎么选才靠谱?我的建议是四个原则。
第一,语义相似。优先选择和目标输入在语义上接近的样本作为示例。你可以在标注样本集上计算每个候选示例和当前输入的语义相似度,选出 Top K 个放进提示词。这样模型在注意力层更容易对齐示例与新输入的表征,输出自然更稳。
第二,类别覆盖。如果任务有多个分类,示例里每个类别至少出现一次,而且尽量平衡。否则模型会倾向于输出示例中最多的那个类别,这在少样本场景里尤其致命。
第三,内容正确。示例里的答案必须准确,推理过程必须完整。有人觉得少样本示例可以随手写两条,这完全是误区。示例就是模型眼中的“标准答案”,它会像模仿字迹一样模仿示例里的错误。
第四,控制长度。示例不是越长越好,冗长的示例会挤占上下文空间,也会引入与任务无关的噪声。尽量用简洁、信息密度高的示例,把思考过程保留下来,把废话去掉。
3. 上下文学习如何提升推理能力
3.1 少样本思维链:让模型学会“先想再做”
上下文学习价值最大的应用方向之一,就是用少样本方式提升模型的推理能力。这里的关键不是给模型看“输入-答案”的简单对,而是给模型看“输入-推理过程-答案”的完整链条。这种方法通常被叫做少样本思维链,英文是 Chain-of-Thought,业内常简称 CoT。
只给答案的少样本在某些推理任务上帮助不大,原因很容易理解:模型在模仿“跳跃式结论”,却没有模仿“得出结论的路径”。数学题尤其明显。你给模型看两道题直接写了答案,它大概率学不会解题步骤,只是凭感觉猜。可一旦你给它的示例里包含了逐步计算过程,比如先写“设未知数为 x”,再写“根据条件列方程”,最后写“解得答案”,模型就会沿着类似的路径进行多步推理。
一个典型的少样本思维链提示模板长这样:
问题:小明有 3 个苹果,他又买了 5 个,然后吃掉 2 个,还剩几个? 推理:小明原来有 3 个苹果,买来 5 个后一共有 3 + 5 = 8 个。吃掉 2 个后剩下 8 - 2 = 6 个。 答案:6 问题:一列火车每小时行驶 80 公里,行驶 2.5 小时后走了多远? 推理:到“推理:”这里截断,让模型顺着示例的路径继续生成。模型会模仿“先计算后总结”的结构,输出类似“火车速度为每小时 80 公里,行驶 2.5 小时,距离为 80 × 2.5 = 200 公里”这样的内容。
我踩过的一个坑是:推理步骤不是越详细越好。示例里如果写了太多与题目无关的铺垫,模型会把那些冗余表达也学进去,反而增加了输出内容的长度和出错概率。好的 CoT 示例应该只保留必要的推导节点,每一行都在推进答案。
3.2 零样本也能走向推理:指令驱动的逐步思考
少样本思维链效果好,但有时候你手头没有合适的示例,怎么办?其实零样本也能触发类似的推理能力,方式是在提示词末尾追加一句引导模型分步思考的话,比如“请一步一步思考,先写出推理过程,再给出最终答案”。这种操作在业内常被称为零样本思维链。
它为什么有效?因为预训练语料里存在大量“先分析、再下结论”的文本结构。模型不需要示例,只要指令中出现“逐步思考”这类关键词,就会激活它见过无数遍的解题文本模式。这不是什么神秘咒语,而是对预训练分布的一种精准触发。
如果你觉得单次生成的推理结果不稳定,还有一个更稳妥的做法:让模型独立生成多次,每次只生成推理过程,最后对答案做多数投票。我实测下来,这个方法对数学计算、日期推算、逻辑谜题这类有唯一答案的任务提升很明显。比如一个三位数加法任务,零样本直接答可能只有四成正确率,加入分步思考后能到六成左右,再配合多次采样投票甚至能摸到八成。代价是推理次数增多、耗时变长,适合对准确率有要求但不要求极速响应的场景。
3.3 评测维度与效果观察
到底怎么判断上下文学习有没有提升推理能力?光靠自己看几条输出不够,最好用一个固定测试集跑一遍。我自己常用 20 到 30 道题的小测试集,覆盖数学计算、日期计算、逻辑推理三类任务,对比零样本直接回答、少样本直接回答和少样本思维链三种模式。
下表是某次模拟实验的大致结果,仅作参考:
| 任务类型 | 零样本直接回答 | 少样本直接回答 | 少样本思维链 |
|---|---|---|---|
| 三位数加法 | 38% | 45% | 78% |
| 日期推算 | 25% | 36% | 64% |
| 逻辑谜题 | 42% | 51% | 73% |
你会发现,少样本直接把正确答案给模型看,提升幅度其实有限;真正拉开差距的是少样本思维链。这说明模型在推理任务上缺的不是“答案记忆”,而是“推理路径的示范”。少样本中的示例,本质上是给模型指了一条它自己也能走出来的路。
4. 实操:搭一个上下文学习评测流程
前面讲了不少原理,但上下文学习这东西,纸上谈兵没用,得动手跑。下面我按自己常用的流程,完整走一遍如何搭一个少样本评测环境。
4.1 环境与推理服务准备
最简单的做法是找一个支持文本补全接口的推理服务。你可以本地部署一个开源指令模型,也可以在云端开一个模型服务。关键是接口风格统一,方便你用脚本批量测试。
本地部署时,我一般会用一个支持量化加载的推理框架,加载 7B 到 13B 量级的模型。参数太大占用显存多,太小推理效果又不够。7B 模型在 4-bit 量化下只需 6GB 左右显存,足够做实验;13B 模型需要更大的显存,但推理稳定性更好。如果你手头没有本地显卡,也可以直接调用线上文本生成服务,把密钥配好就行。
启动推理服务后,所有请求统一走一个本地 HTTP 接口,这样后续步骤就能完全脚本化,不用每次手点网页。一个通用的调用函数长这样:
import requests def call_llm(prompt, temperature=0.2, max_tokens=512): resp = requests.post( "http://127.0.0.1:8000/v1/completions", json={ "model": "local-model", "prompt": prompt, "temperature": temperature, "max_tokens": max_tokens, }, timeout=120, ) return resp.json()["choices"][0]["text"].strip()我这里用的是 OpenAI 兼容接口风格,实际上很多本地推理框架都认这个格式。你只需要确保接口地址指向自己部署的服务或你采购的服务地址即可。
4.2 构建三种提示模板
上下文学习的核心资产就是提示模板。同一个任务,模板写得好不好,结果差距非常大。我会一次性构造三种模板,方便后续对比:零样本、少样本直接回答、少样本思维链。
零样本模板最简单:
def build_zero_shot_prompt(instruction, test_input): return f"{instruction}\n输入:{test_input}\n输出:"少样本直接回答模板,在零样本基础上插入若干“输入/输出”对:
def build_few_shot_prompt(instruction, examples, test_input): segments = [instruction, "\n"] for i, (x, y) in enumerate(examples, 1): segments.append(f"示例{i}:\n输入:{x}\n输出:{y}\n") segments.append(f"输入:{test_input}\n输出:") return "\n".join(segments)少样本思维链模板,示例里多一项“推理”:
def build_cot_prompt(instruction, examples, test_input): segments = [instruction, "\n请先写出推理过程,再给出最终答案。\n"] for i, (x, y_step, y_ans) in enumerate(examples, 1): segments.append(f"示例{i}:\n问题:{x}\n推理:{y_step}\n答案:{y_ans}\n") segments.append(f"问题:{test_input}\n推理:") return "\n".join(segments)注意几个细节:示例编号“示例1”“示例2”可以让模型更清晰地感知这是范例;输入输出的标签要统一;最后的输出位置要在冒号后截断,把生成起点交给模型。这些不起眼的格式问题,往往正是效果差距的来源。
4.3 示例选择与推理参数调优
示例不是随便从测试集里抽两条就行。我在实践中最常用的一条经验是:用语义检索选示例。先把候选样本库里的每条数据都做向量化,再把测试输入做同样的向量化,最后计算余弦相似度取 Top K 个样本作为示例。这样选出来的示例,和当前输入在语义上最接近,模型更有可能复刻正确的推理方式。
用一个简单的向量化接口就能完成:
def embed_texts(texts): resp = requests.post( "http://127.0.0.1:8000/v1/embeddings", json={"texts": texts}, timeout=120, ) return resp.json()["embeddings"] def pick_examples_by_similarity(test_input, candidate_pool, k=4): test_vec = embed_texts([test_input])[0] similarities = [] for item in candidate_pool: item_vec = embed_texts([item["input"]])[0] sim = cosine_similarity(test_vec, item_vec) similarities.append((sim, item)) similarities.sort(reverse=True) return [item for _, item in similarities[:k]]如果你不想引入额外的向量化服务,也可以退而求其次,从测试集里随机抽几条,保证类别均衡即可。效果略差,但足够做快速验证。
推理参数上,我做实验时一般把温度调到 0.2,既保留一定多样性,又不会让输出过于随机。max_tokens 要给足,尤其用思维链模板时,推理过程可能很长,设小了会在半截被截断。自洽性投票场景下,我常让模型同一道题生成 3 到 5 次,然后把答案频率最高的作为最终输出。
4.4 跑分与人工评测
模板和参数都定了,接下来就是跑测试集。我习惯用一个 20 题的测试集,把三种模板跑一遍,记录每道题的正确率和格式解析成功率。
评测的时候不能只看最终答案对不对,还要看中间推理过程是否合法。拿数学题举例:答案对了但推理步骤是错的,本质上就是蒙对的,在业务场景里不能信任。我自己会在脚本里同时记录“最终答案正确”和“推理过程合法”两个维度,然后人工抽看十几条输出,确认模型不是靠胡编的中间步骤蒙混过关。
一个简单的评测记录表可以参考:
| 题目编号 | 模板类型 | 模型输出 | 最终答案正确 | 推理过程合法 | 格式可解析 |
|---|---|---|---|---|---|
| 001 | few-shot-cot | …… | 是 | 是 | 是 |
| 002 | zero-shot | …… | 否 | 不适用 | 是 |
| 003 | few-shot-cot | …… | 是 | 否 | 否 |
表格里“格式可解析”也很重要,尤其你要把模型输出接到后面业务逻辑里时,格式不稳定会带来大量额外处理成本。
5. 常见问题与排查技巧实录
5.1 示例选了不少,效果反而变差
我遇到过不止一次:示例从 2 个加到 6 个,准确率不升反降。排查下来,问题通常出在示例的“分布”上。不是每个相似的示例都对当前任务有帮助,有时候一个特别难的示例会带偏整个注意力分布。
解决办法分两步。先看示例是不是和目标输入在类型上匹配,如果测试输入是一个短问句,示例却是一段长文章,模型会被示例的长度和结构带跑。再看示例之间的一致性,两个示例如果用的推理方式不同,模型会困惑于该学哪一条路径。实际调整时,我会把示例数量先减到 2 个看效果,再逐步往上加,找到最优数量。记住,少而精的示例永远比多而杂的示例好用。
5.2 输出格式不稳定
少样本提示里明明给了“输入/输出”的固定格式,模型还是会偶尔输出一段解释性的废话,或者多出一个“好的,我来帮你”的前缀。这个问题的根因是你在模板里没有把“必须严格按格式输出”的边界条件说清楚。
我的解决办法有三招。第一,在指令末尾加一句明确的格式约束,比如“只输出标签,不要解释”。第二,在模板中使用特殊分隔符,比如“===输入===”“===输出===”这种结构化标记,模型对界限分明的格式更敏感。第三,把温度调低一点,温度过高会让模型更倾向自由发挥。如果格式还是不稳定,就改为在代码里做后处理,用正则把需要的字段抽出来,别指望模型每次都完美。
5.3 上下文窗口塞不下更多示例
大模型的上下文窗口是有限的,示例一多,再加上待处理的原文,很容易超长。出现这种情况,不是简单地增加“max_tokens”就能解决,得从提示词本身下手。
我常用的方案是:压缩示例、精简指令、截断原文。给每个示例只保留最核心的输入输出,去掉所有修饰性描述;指令控制在两句话以内;待处理文档如果太长,先抽取关键段落再进入提示词。比如你想让模型总结一份长报告,与其把全文塞进去,不如先用自己的规则抽出每章标题和关键句子,再让模型基于这些摘要做总结。这样既跑得动,又不容易被长文本里的噪声干扰。
5.4 示例中的错误被模型放大
少样本场景里,模型对示例中的错误非常敏感。一个很典型的案例:我在一批示例里手滑写错了一个数字,模型在处理新问题时居然照着同样的方式算错了。这也印证了前文说的——示例是模型眼中的标准答案,它会模仿示例的错误,而不是主动纠正。
所以,示例的标注质量必须把关。每一条示例在进入提示词之前,都要单独验证过答案正确、推理合法。如果你维护了一个示例池,最好定期抽查更新,避免之前积累的错误悄悄混进生产请求里。宁可少放一个示例,也不要放一个有问题的示例。
5.5 什么时候该放弃上下文学习,转入微调
上下文学习虽然便宜灵活,但它有天花板。如果任务规则非常固定、对输出格式有严格约束、而且每天调用量大,说明它已经从“临时探索”变成了“稳定业务逻辑”,这时候微调往往更划算。
可以按下面这个表做决策:
| 衡量维度 | 上下文学习适合 | 微调适合 |
|---|---|---|
| 任务变化频率 | 高,经常调整 | 低,长期稳定 |
| 私有知识依赖 | 低 | 高 |
| 延迟要求 | 高,需要快速响应 | 可接受离线训练 |
| 输出格式要求 | 宽松 | 严格,需要稳定 |
| 数据规模 | 小,几十条即可 | 大,上千条起 |
一句话总结:上下文学习适合跑通和试探,微调适合固化产出。你要是发现自己已经把一套提示词用了几百天没换过,那就是时候考虑微调了。
6. 我的几点实操体会与扩展方向
6.1 几个反直觉的经验
第一个反直觉的经验是,示例数量并非越多越好。我在一个文本分类任务上试过 2、4、8、16 四个档位,8 个示例时效果最好,16 个反而因为上下文过长、注意力分散导致准确率下降。很多人想当然认为示例是多多益善,但大模型跟前几轮 token 的注意力权重确实会衰减,示例太多只会把注意力拖垮。
第二个反直觉的经验是,示例的多样性比准确性更关键。准确当然重要,但多样性决定模型能不能泛化。如果我给模型的四个示例全是“快递没送到”这一种投诉类型,它处理“商品质量有问题”时就会不知所措。类别的全覆盖远比堆叠同类示例有效。
第三个经验是,指令和示例必须配合使用。指令负责说明目标和边界,示例负责展示格式和路径。只给指令不给示例,输出容易飘;只给示例不给指令,模型容易把示例里的偶然特征当成规律。两者缺一个,效果都到不了预期。
6.2 后续可以怎么扩展
如果你把基础的少样本流程跑通了,有几个方向值得继续探索。
你可以尝试让模型自己生成示例,从中选出效果最好的几条作为提示词的一部分,这招在缺少标注样本时很实用。也可以做动态示例选择,对每一条测试输入都实时计算最相似的示例,而不是用一套固定示例跑完整个测试集。更进一步,把上下文学习和外部检索结合起来,先用检索召回相关段落,再让模型基于这些段落做推理,两个能力叠加后,效果能比单独使用好不少。
上下文学习最吸引我的地方在于,它没有改变模型本身,却能让模型表现出完全不同的能力层次。设计提示词的每一次调整,都是在探索模型内部已有能力的边界。你花在示例选择、模板结构、参数配比上的功夫,换来的是稳定可靠的大模型应用体验。这套功夫,在任何大模型上都不会白费。