Skip to content

Few-Shot 与动态 Prompt

要点

  • Prompt Template 解决了输入结构问题,但回答风格和格式仍然可能不稳定。Few-Shot 通过给模型看样板回答来解决这个问题。
  • LangChain.js 中 chat 场景最常用的是 FewShotChatMessagePromptTemplate
  • Few-Shot 示例要使用 user / assistant 角色,与 Agent 主线保持一致。
  • 多轮对话中,把 FewShotChatMessagePromptTemplateMessagesPlaceholder 组合使用,注意示例数量和历史消息长度会竞争上下文窗口。
  • 动态 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}"],
]);

这里分成了三层:

  1. examples:准备示例数据。
  2. examplePrompt:规定每条示例怎样变成消息。
  3. fewShotPrompt:把所有示例拼成一个可插入的模板块。

这一步里,角色建议统一用 user / assistant,不要再混用 human / ai,这样和前面几篇的 Agent 主线会更一致。

4. 先生成消息,再交给 Agent

Few-Shot 写到这里,还没有真正调用 Agent。先做的事情还是和上一篇一样:先让模板把这一轮消息组织出来。

typescript
const promptValue = await prompt.invoke({
  input: "用户登录后看不到历史订单,但刷新页面就正常了。",
});

console.log(promptValue.toChatMessages());

打印出来的最终消息大致是这个顺序:

  1. 第一组示例的 user
  2. 第一组示例的 assistant
  3. 第二组示例的 user
  4. 第二组示例的 assistant
  5. 当前这一轮真实输入。

也就是说,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 的位置

到了多轮对话,输入里通常不止当前这一轮,还会有历史消息。这时候更常见的写法是把 FewShotChatMessagePromptTemplateMessagesPlaceholder 放在一起:

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。

基于 MIT 协议开源