课程目标
掌握 LangChain.js 的示例选择器体系:BaseExampleSelector 抽象接口、LengthBasedExampleSelector 按长度选择、SemanticSimilarityExampleSelector 语义相似度选择、ConditionalPromptSelector 条件选择,以及与 FewShotPromptTemplate 的集成方式。
13.1 为什么需要 ExampleSelector?
在第 12 课中我们学了 FewShotPromptTemplate,可以给 LLM 提供静态示例。但实际场景中有两个问题:
- 上下文窗口有限:如果有 100 个示例,全部塞进去会超出 token 限制
- 相关性差异大:并非所有示例对当前问题都有帮助
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 工作原理
- 初始化时,将所有示例文本转为向量,存入 VectorStore
- 选择时,将输入转为向量,在 VectorStore 中做相似度搜索
- 返回最相似的 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 行),它:
- 将示例文本转为字符串数组
- 调用 VectorStore 的
fromTexts()批量创建向量存储 - 将原始示例存储为 metadata
- 返回配置好的 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)— 判断是否为BaseLLMisChatModel(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 源码精读路线
| 优先级 | 文件 | 关注点 |
|---|---|---|
| P0 | example_selectors/base.ts | BaseExampleSelector 抽象接口,只有 23 行但定义了全部契约 |
| P0 | example_selectors/length_based.ts | selectExamples() 的贪心选择逻辑,fromExamples() 工厂方法 |
| P1 | example_selectors/semantic_similarity.ts | 向量检索选择逻辑,addExample() 的文档存储设计 |
| P1 | example_selectors/conditional.ts | ConditionalPromptSelector,条件匹配逻辑 |
| P2 | prompts/few_shot.ts | getExamples() 如何对接 ExampleSelector |
本课收获总结
| 级别 | 你应该掌握的 |
|---|---|
| 🟢 基础 | 理解 Few-shot learning 的原理:给 LLM 看几个例子再让它干活 |
| 🔵 中阶 | 掌握 LengthBasedExampleSelector:按 token 预算选择示例 |
| 🟡 高阶 | 理解 SemanticSimilarityExampleSelector:用向量相似度选最相关的示例 |
| 🟠 资深 | 分析 ConditionalPromptSelector:按条件分支选择不同 Prompt 策略 |
| 🔴 架构 | 设计动态示例管理系统:示例库的维护、评估与自动更新;自定义 Selector 的扩展点 |
下一课预告
第 14 课讲 Tools 工具系统——StructuredTool、tool() 函数、DynamicTool,以及 Zod schema 如何转化为 LLM 可理解的工具描述。