主题
Few-Shot 与动态 Prompt
要点
- Prompt Template 解决了输入结构问题,但回答风格和格式仍然可能不稳定。Few-Shot 通过给模型看样板回答来解决这个问题。
- LangChain.js 中 chat 场景最常用的是
FewShotChatMessagePromptTemplate。 - Few-Shot 示例要使用
user/assistant角色,与 Agent 主线保持一致。 - 多轮对话中,把
FewShotChatMessagePromptTemplate和MessagesPlaceholder组合使用,注意示例数量和历史消息长度会竞争上下文窗口。 - 动态 Few-Shot 不一定要用向量检索,先从按任务类别选择示例开始,已经足够解决很多实际问题。
1. 背景:为什么 Prompt Template 还不够
上一篇已经把 Prompt Template 接到 Agent 上。一轮请求里的输入结构可以稳定组织出来了。但这时候很快会遇到另一个问题:结构是对的,回答风格却不一定稳定。
比如一个代码审查 Agent,你可能希望它始终做到:
- 先指出问题位置,再给出修改建议。
- 每条建议控制在 2 句话以内。
- 不要只给结论,要说明风险。
这些规则写进 systemPrompt 或 Prompt Template 当然有用。但实际跑起来时,模型有时还是会忽长忽短,或者突然换成一种很生硬的语气。
Few-Shot 要解决的就是这个问题。它不是再多加一条规则,而是给模型看几组「你希望它怎么答」的样板。规则告诉模型该做什么,示例告诉模型做出来应该长什么样。
2. 一个最小可用的 Few-Shot 场景
先把场景压到最小。假设有一个任务分类 Agent,默认设定是:
typescript
import { createAgent } from "langchain";
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
apiKey: process.env.MODEL_API_KEY,
model: process.env.MODEL_NAME ?? "deepseek-chat",
configuration: {
baseURL: process.env.MODEL_BASE_URL ?? "https://api.deepseek.com/v1",
},
});
const agent = createAgent({
model,
tools: [],
systemPrompt:
"你是一名任务分类助手。把用户输入归类为 bug、feature、doc 三类,只输出类别名和一句话理由。",
});如果这一轮输入是:
text
用户登录后看不到历史订单,但刷新页面就正常了。只靠规则,模型大概率能答对方向。但「理由要写多具体」「格式是 JSON 还是纯文本」「类别名要不要大写」,都还是容易飘。所以更稳的做法是补两组示例。
3. Few-Shot 的基本写法
在 LangChain.js 里,chat 场景下最常用的是 FewShotChatMessagePromptTemplate。
typescript
import {
ChatPromptTemplate,
FewShotChatMessagePromptTemplate,
} from "@langchain/core/prompts";
const examples = [
{
input: "提交按钮点击后没有反应,控制台也没有报错。",
output: "bug。用户交互无响应且无错误信息,需要排查事件监听和接口调用。",
},
{
input: "希望在订单列表加一个按时间筛选的入口。",
output: "feature。这是新增功能需求,需要产品确认筛选维度和默认排序。",
},
];
const examplePrompt = ChatPromptTemplate.fromMessages([
["user", "{input}"],
["assistant", "{output}"],
]);
const fewShotPrompt = new FewShotChatMessagePromptTemplate({
examples,
examplePrompt,
inputVariables: [],
});
const prompt = ChatPromptTemplate.fromMessages([
...(await fewShotPrompt.formatMessages({})),
["user", "{input}"],
]);这里分成了三层:
examples:准备示例数据。examplePrompt:规定每条示例怎样变成消息。fewShotPrompt:把所有示例拼成一个可插入的模板块。
这一步里,角色建议统一用 user / assistant,不要再混用 human / ai,这样和前面几篇的 Agent 主线会更一致。
4. 先生成消息,再交给 Agent
Few-Shot 写到这里,还没有真正调用 Agent。先做的事情还是和上一篇一样:先让模板把这一轮消息组织出来。
typescript
const promptValue = await prompt.invoke({
input: "用户登录后看不到历史订单,但刷新页面就正常了。",
});
console.log(promptValue.toChatMessages());打印出来的最终消息大致是这个顺序:
- 第一组示例的
user。 - 第一组示例的
assistant。 - 第二组示例的
user。 - 第二组示例的
assistant。 - 当前这一轮真实输入。
也就是说,Few-Shot 本质上还是在组织消息。只是这一次组织进去的,不只是规则和当前输入,还包括几组样板回答。
整理好之后,再把这些消息交给 Agent:
typescript
const result = await agent.invoke({
messages: promptValue.toChatMessages(),
});
console.log(result.messages.at(-1)?.text);放回主线里看,分工很清楚:
- Prompt Template 负责整理一轮输入。
- Few-Shot 负责补进示例样板。
- Agent 负责真正处理这一轮输入。
5. 为什么 Few-Shot 能让回答更稳
只写规则时,模型需要自己猜「你想要的回答长什么样」。加上 Few-Shot 之后,这部分信息就不再是抽象要求,而是具体示例了。
上面的两组样例实际上给了模型几层信号:
- 输出格式是「类别 + 一句话理由」。
- 类别名使用小写英文。
- 理由要说明判断依据,而不是重复用户输入。
- 整体长度要短,不要散。
所以 Few-Shot 解决的通常不是「知识不够」,而是:
- 风格不稳定。
- 格式不稳定。
- 判断粒度不稳定。
它特别适合放在分类型 Agent、结构化输出型 Agent、代码审查型 Agent 前面。
6. 多轮对话中 Few-Shot 的位置
到了多轮对话,输入里通常不止当前这一轮,还会有历史消息。这时候更常见的写法是把 FewShotChatMessagePromptTemplate 和 MessagesPlaceholder 放在一起:
typescript
import {
ChatPromptTemplate,
FewShotChatMessagePromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
const finalPrompt = ChatPromptTemplate.fromMessages([
fewShotPrompt,
new MessagesPlaceholder("history"),
["user", "{input}"],
]);真正调用时:
typescript
const promptValue = await finalPrompt.invoke({
history: [
{
role: "user",
content: "用户反馈支付完成后没有跳转。",
},
{
role: "assistant",
content: "bug。支付完成后未触发跳转,需要确认回调接口和前端路由监听。",
},
],
input: "用户登录后看不到历史订单,但刷新页面就正常了。",
});
const result = await agent.invoke({
messages: promptValue.toChatMessages(),
});这里没有唯一正确顺序。有的项目会把 history 放在 fewShot 前面,有的会放在后面。判断标准比较稳的是:
- 如果历史消息很多,就减少 few-shot 的数量。
- 如果当前是新会话、上下文很短,就可以保留 1 到 2 组示例。
因为 few-shot 和历史消息本质上都在争上下文长度。
7. 动态选择示例,从按类别开始
很多文章一讲动态 few-shot,就直接跳到向量检索。对新手来说,第一步没必要走这么快。更自然的方式是先按任务类别选示例。
typescript
type Category = "bug" | "feature" | "doc";
type Example = {
category: Category;
input: string;
output: string;
};
const allExamples: Example[] = [
{
category: "bug",
input: "提交按钮点击后没有反应,控制台也没有报错。",
output: "bug。用户交互无响应且无错误信息,需要排查事件监听和接口调用。",
},
{
category: "feature",
input: "希望在订单列表加一个按时间筛选的入口。",
output: "feature。这是新增功能需求,需要产品确认筛选维度和默认排序。",
},
{
category: "doc",
input: "新同事接入项目时不知道环境变量怎么配。",
output: "doc。缺少环境配置说明,需要补充 README 或 onboarding 文档。",
},
];
function selectExamples(category: Category) {
return allExamples.filter((item) => item.category === category).slice(0, 2);
}然后把选出来的示例交给 few-shot:
typescript
const fewShotPrompt = new FewShotChatMessagePromptTemplate({
examples: selectExamples("bug"),
examplePrompt,
inputVariables: [],
});这已经足够算动态 few-shot 了。示例集合不再是固定常量,而是会随着当前任务变化。后续如果示例数量变多,再考虑引入向量检索或相似度排序。
8. 注意事项
第一,不要把 Few-Shot 写成「堆例子比赛」。
示例越多,不一定越稳。无关样例一多,反而会把当前输入淹没。通常 1 到 3 组高质量示例就够了。
第二,不要让 Few-Shot 和 Agent 的长期设定打架。
Agent 已经有 systemPrompt,few-shot 就主要负责补样板,不要再在示例里夹带一套相反的风格要求。如果 systemPrompt 要求输出 JSON,示例里就不要输出自然语言。
放回整章主线里,位置很清楚:
- Prompt Template 负责把输入组织好。
- Few-Shot 负责把回答样板补进去。
- Agent 再根据这份上下文继续运行。
下一篇进入输出这一侧,开始学习 Output Parser。