第 13 课: ExampleSelector 与高级 Few-Shot

0 阅读6分钟

课程目标

掌握 LangChain.js 的示例选择器体系:BaseExampleSelector 抽象接口、LengthBasedExampleSelector 按长度选择、SemanticSimilarityExampleSelector 语义相似度选择、ConditionalPromptSelector 条件选择,以及与 FewShotPromptTemplate 的集成方式。


13.1 为什么需要 ExampleSelector?

在第 12 课中我们学了 FewShotPromptTemplate,可以给 LLM 提供静态示例。但实际场景中有两个问题:

  1. 上下文窗口有限:如果有 100 个示例,全部塞进去会超出 token 限制
  2. 相关性差异大:并非所有示例对当前问题都有帮助

ExampleSelector 就是解决这个问题的:根据当前输入,从示例池中动态选出最合适的几个。


13.2 BaseExampleSelector — 抽象接口

源码位置: libs/langchain-core/src/example_selectors/base.ts

export abstract class BaseExampleSelector extends Serializable {
  // 向示例池中添加新示例
  abstract addExample(example: Example): Promise<void | string>;

  // 根据输入变量选择最相关的示例
  abstract selectExamples(input_variables: Example): Promise<Example[]>;
}

接口非常简洁,只有两个方法:

方法作用调用时机
addExample()往示例池中添加一条示例初始化或运行时动态添加
selectExamples()根据输入选择最匹配的示例每次 Prompt 格式化时调用

Example 类型就是 Record<string, string>,一个简单的键值对。


13.3 LengthBasedExampleSelector — 按长度选择

源码位置: libs/langchain-core/src/example_selectors/length_based.ts

最简单的选择策略:按顺序选取示例,直到总长度达到上限。

13.3.1 核心逻辑

// 源码简化(length_based.ts 第 119-137 行)
async selectExamples(inputVariables: Example): Promise<Example[]> {
  const inputs = Object.values(inputVariables).join(" ");
  let remainingLength = this.maxLength - this.getTextLength(inputs);
  let i = 0;
  const examples: Example[] = [];

  while (remainingLength > 0 && i < this.examples.length) {
    const newLength = remainingLength - this.exampleTextLengths[i];
    if (newLength < 0) {
      break;  // 当前示例放不下了,停止
    }
    examples.push(this.examples[i]);
    remainingLength = newLength;
    i += 1;
  }
  return examples;
}

长度计算函数 默认按空格和换行分词:

function getLengthBased(text: string): number {
  return text.split(/\n| /).length;
}

13.3.2 使用示例

import { LengthBasedExampleSelector } from "@langchain/core/example_selectors";
import { PromptTemplate, FewShotPromptTemplate } from "@langchain/core/prompts";

const examplePrompt = new PromptTemplate({
  inputVariables: ["input", "output"],
  template: "Input: {input}\nOutput: {output}",
});

// 从示例列表创建选择器
const selector = await LengthBasedExampleSelector.fromExamples(
  [
    { input: "happy", output: "sad" },
    { input: "tall", output: "short" },
    { input: "energetic", output: "lethargic" },
    { input: "sunny", output: "gloomy" },
    { input: "windy", output: "calm" },
  ],
  { examplePrompt, maxLength: 25 }
);

// 短输入 → 选出更多示例
const examples1 = await selector.selectExamples({ adjective: "big" });
// 可能选出 3-4 个

// 长输入 → 选出更少示例(因为输入本身已占用较多预算)
const examples2 = await selector.selectExamples({
  adjective: "big and huge and massive and large and gigantic",
});
// 可能只选出 1-2 个

13.3.3 与 FewShotPromptTemplate 集成

const dynamicPrompt = new FewShotPromptTemplate({
  exampleSelector: selector,  // 传入选择器
  examplePrompt,
  prefix: "Give the antonym of every input",
  suffix: "Input: {adjective}\nOutput:",
  inputVariables: ["adjective"],
});

// 每次 format 时自动调用 selector.selectExamples()
const result = await dynamicPrompt.format({ adjective: "big" });

13.4 SemanticSimilarityExampleSelector — 语义相似度选择

源码位置: libs/langchain-core/src/example_selectors/semantic_similarity.ts

这是最强大的选择器:利用向量嵌入(Embedding),根据语义相似度从示例池中选出最相关的示例。

13.4.1 工作原理

  1. 初始化时,将所有示例文本转为向量,存入 VectorStore
  2. 选择时,将输入转为向量,在 VectorStore 中做相似度搜索
  3. 返回最相似的 k 个示例

13.4.2 核心实现

// 源码简化(semantic_similarity.ts 第 127-151 行)
async selectExamples<T>(inputVariables: Record<string, T>): Promise<Example[]> {
  const inputKeys = this.inputKeys ?? Object.keys(inputVariables);
  // 将输入变量拼接为查询字符串
  const query = sortedValues(
    inputKeys.reduce((acc, key) => ({ ...acc, [key]: inputVariables[key] }), {})
  ).join(" ");

  // 通过 vectorStoreRetriever 检索最相似的文档
  const exampleDocs = await this.vectorStoreRetriever.invoke(query);

  // 从文档 metadata 中提取原始示例
  const examples = exampleDocs.map((doc) => doc.metadata);

  // 如果指定了 exampleKeys,则只返回指定字段
  if (this.exampleKeys) {
    return examples.map((example) =>
      this.exampleKeys.reduce((acc, key) => ({ ...acc, [key]: example[key] }), {})
    );
  }
  return examples;
}

13.4.3 addExample 方法

运行时可以动态添加新示例:

// 源码(semantic_similarity.ts 第 103-118 行)
async addExample(example: Example): Promise<void> {
  const inputKeys = this.inputKeys ?? Object.keys(example);
  const stringExample = sortedValues(
    inputKeys.reduce((acc, key) => ({ ...acc, [key]: example[key] }), {})
  ).join(" ");

  // 将示例作为 Document 添加到 VectorStore
  await this.vectorStoreRetriever.addDocuments([
    new Document({
      pageContent: stringExample,  // 用于向量化的文本
      metadata: example,            // 原始示例数据存在 metadata 中
    }),
  ]);
}

13.4.4 使用示例

import { SemanticSimilarityExampleSelector } from "@langchain/core/example_selectors";
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";

const examples = [
  { input: "happy", output: "sad" },
  { input: "tall", output: "short" },
  { input: "energetic", output: "lethargic" },
  { input: "sunny", output: "gloomy" },
  { input: "windy", output: "calm" },
];

const selector = await SemanticSimilarityExampleSelector.fromExamples(
  examples,
  new OpenAIEmbeddings(),
  MemoryVectorStore,
  { k: 2 }  // 每次选 2 个最相关的
);

// "joyful" 语义上最接近 "happy"
const selected = await selector.selectExamples({ input: "joyful" });
// [{ input: "happy", output: "sad" }, { input: "sunny", output: "gloomy" }]

13.4.5 fromExamples 工厂方法

fromExamples 是一个方便的静态工厂方法(第 167-201 行),它:

  1. 将示例文本转为字符串数组
  2. 调用 VectorStore 的 fromTexts() 批量创建向量存储
  3. 将原始示例存储为 metadata
  4. 返回配置好的 Selector 实例

13.4.6 构造函数的两种形式

// 形式 1:传入 vectorStore + k + filter
new SemanticSimilarityExampleSelector({
  vectorStore: myVectorStore,
  k: 3,
  filter: { category: "math" },  // 可选的过滤条件
  exampleKeys: ["input", "output"],
  inputKeys: ["input"],
});

// 形式 2:传入已有的 vectorStoreRetriever
new SemanticSimilarityExampleSelector({
  vectorStoreRetriever: myRetriever,
  exampleKeys: ["input", "output"],
});

13.5 ConditionalPromptSelector — 条件选择器

源码位置: libs/langchain-core/src/example_selectors/conditional.ts

注意:ConditionalPromptSelector 不是 ExampleSelector,而是 PromptSelector——它根据模型类型选择不同的 Prompt 模板。

import { ConditionalPromptSelector, isChatModel } from "@langchain/core/example_selectors";

const selector = new ConditionalPromptSelector(
  defaultPrompt,      // 默认模板
  [
    [isChatModel, chatPrompt],  // 如果是 ChatModel,使用 chatPrompt
    // 可以添加更多条件
  ]
);

// 根据模型类型自动选择合适的模板
const prompt = selector.getPrompt(myModel);

核心逻辑很简单(conditional.ts 第 76-83 行):

getPrompt(llm: BaseLanguageModelInterface): BasePromptTemplate {
  for (const [condition, prompt] of this.conditionals) {
    if (condition(llm)) {
      return prompt;
    }
  }
  return this.defaultPrompt;
}

框架还提供了两个类型守卫函数:

  • isLLM(llm) — 判断是否为 BaseLLM
  • isChatModel(llm) — 判断是否为 BaseChatModel

13.6 自定义 ExampleSelector

继承 BaseExampleSelector 只需实现两个方法:

import { BaseExampleSelector } from "@langchain/core/example_selectors";
import type { Example } from "@langchain/core/prompts";

class RecentExampleSelector extends BaseExampleSelector {
  lc_namespace = ["custom"];
  private examples: Array<Example & { timestamp: number }> = [];
  private k: number;

  constructor(k: number = 3) {
    super({});
    this.k = k;
  }

  async addExample(example: Example): Promise<void> {
    this.examples.push({ ...example, timestamp: Date.now() });
  }

  async selectExamples(_inputVariables: Example): Promise<Example[]> {
    // 按时间倒序,取最近的 k 个
    return [...this.examples]
      .sort((a, b) => b.timestamp - a.timestamp)
      .slice(0, this.k)
      .map(({ timestamp, ...rest }) => rest);
  }
}

13.7 完整实战:语义 Few-Shot 链

import { ChatPromptTemplate } from "@langchain/core/prompts";
import { FewShotChatMessagePromptTemplate } from "@langchain/core/prompts";
import { SemanticSimilarityExampleSelector } from "@langchain/core/example_selectors";
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

// 1. 准备示例
const examples = [
  { input: "How to sort an array?", output: "Use Array.sort() with a comparison function." },
  { input: "How to read a file?", output: "Use fs.readFileSync() or fs.promises.readFile()." },
  { input: "How to make HTTP request?", output: "Use fetch() or axios library." },
  { input: "How to connect to database?", output: "Use an ORM like Prisma or direct driver." },
  { input: "How to handle errors?", output: "Use try-catch blocks and proper error types." },
];

// 2. 创建语义选择器
const selector = await SemanticSimilarityExampleSelector.fromExamples(
  examples,
  new OpenAIEmbeddings(),
  MemoryVectorStore,
  { k: 2 }
);

// 3. 构建 Few-Shot 模板
const examplePrompt = ChatPromptTemplate.fromMessages([
  ["human", "{input}"],
  ["ai", "{output}"],
]);

const fewShotPrompt = new FewShotChatMessagePromptTemplate({
  exampleSelector: selector,
  examplePrompt,
  inputVariables: ["input"],
});

// 4. 组合完整链
const fullPrompt = ChatPromptTemplate.fromMessages([
  ["system", "You are a coding assistant. Follow the style of these examples:"],
  fewShotPrompt,
  ["human", "{input}"],
]);

const chain = fullPrompt
  .pipe(new ChatOpenAI({ model: "gpt-4o-mini" }))
  .pipe(new StringOutputParser());

// 5. 运行
const answer = await chain.invoke({ input: "How to parse JSON?" });

13.8 源码精读路线

优先级文件关注点
P0example_selectors/base.tsBaseExampleSelector 抽象接口,只有 23 行但定义了全部契约
P0example_selectors/length_based.tsselectExamples() 的贪心选择逻辑,fromExamples() 工厂方法
P1example_selectors/semantic_similarity.ts向量检索选择逻辑,addExample() 的文档存储设计
P1example_selectors/conditional.tsConditionalPromptSelector,条件匹配逻辑
P2prompts/few_shot.tsgetExamples() 如何对接 ExampleSelector

本课收获总结

级别你应该掌握的
🟢 基础理解 Few-shot learning 的原理:给 LLM 看几个例子再让它干活
🔵 中阶掌握 LengthBasedExampleSelector:按 token 预算选择示例
🟡 高阶理解 SemanticSimilarityExampleSelector:用向量相似度选最相关的示例
🟠 资深分析 ConditionalPromptSelector:按条件分支选择不同 Prompt 策略
🔴 架构设计动态示例管理系统:示例库的维护、评估与自动更新;自定义 Selector 的扩展点

下一课预告

第 14 课讲 Tools 工具系统——StructuredTooltool() 函数、DynamicTool,以及 Zod schema 如何转化为 LLM 可理解的工具描述。