PromptTemplate:从字符串模板到 Runnable<string, string>
在 LangChain 的演进过程中,PromptTemplate 经历了重要的设计变化。在 V3 版本中,PromptTemplate 不再仅仅是一个字符串模板工具,而是成为了一个完整的实现。这种变化不仅提升了其功能性和一致性,还使其能够更好地融入 LangChain 的整体架构。本章将深入探讨 PromptTemplate 的现代化实现。
从字符串模板到 Runnable 的演进
在早期版本的 LangChain 中,PromptTemplate 主要是一个字符串模板引擎,负责将变量插入到预定义的模板中:
typescript
// 早期版本的使用方式
const template = new PromptTemplate({
template: "将 {input_language} 翻译为 {output_language}: {text}",
inputVariables: ["input_language", "output_language", "text"]
});
const formatted = template.format({
input_language: "英语",
output_language: "法语",
text: "Hello, world!"
});
// 结果: "将 英语 翻译为 法语: Hello, world!"而在 LangChain V3 中,PromptTemplate 被重新设计为一个完整的 Runnable 实现:
typescript
// V3 版本的使用方式
const template = new PromptTemplate({
template: "将 {input_language} 翻译为 {output_language}: {text}",
inputVariables: ["input_language", "output_language", "text"]
});
// 可以使用 invoke 方法
const formatted = await template.invoke({
input_language: "英语",
output_language: "法语",
text: "Hello, world!"
});
// 也可以使用 format 方法(作为 invoke 的别名)
const formatted2 = await template.format({
input_language: "英语",
output_language: "法语",
text: "Hello, world!"
});Runnable 接口的完整实现
现代的 PromptTemplate 实现了完整的 Runnable 接口:
typescript
class PromptTemplate extends Runnable<any, string> {
private template: string;
private inputVariables: string[];
constructor(config: { template: string; inputVariables: string[] }) {
super();
this.template = config.template;
this.inputVariables = config.inputVariables;
}
// 实现 invoke 方法
async invoke(input: Record<string, any>): Promise<string> {
return this.format(input);
}
// 实现 stream 方法
async *stream(input: Record<string, any>): AsyncGenerator<string> {
const result = this.format(input);
yield result;
}
// 实现 batch 方法
async batch(inputs: Record<string, any>[]): Promise<string[]> {
return inputs.map(input => this.format(input));
}
// 核心的格式化方法
format(input: Record<string, any>): string {
let result = this.template;
for (const variable of this.inputVariables) {
if (!(variable in input)) {
throw new Error(`缺少必需的变量: ${variable}`);
}
result = result.replace(
new RegExp(`{${variable}}`, 'g'),
String(input[variable])
);
}
return result;
}
// 静态工厂方法
static fromTemplate(template: string): PromptTemplate {
// 自动提取变量
const variableRegex = /{([^}]+)}/g;
const variables = [];
let match;
while ((match = variableRegex.exec(template)) !== null) {
if (!variables.includes(match[1])) {
variables.push(match[1]);
}
}
return new PromptTemplate({ template, inputVariables: variables });
}
}与 LCEL 的无缝集成
作为 Runnable,PromptTemplate 可以无缝集成到 LCEL 管道中:
typescript
// 创建组件
const template = PromptTemplate.fromTemplate(
"将 {sourceLanguage} 翻译为 {targetLanguage}: {text}"
);
const llm = new ChatOpenAI({ modelName: "gpt-3.5-turbo" });
const parser = new StringOutputParser();
// 使用 LCEL 构建链
const translationChain = template | llm | parser;
// 使用链
const result = await translationChain.invoke({
sourceLanguage: "英语",
targetLanguage: "中文",
text: "Hello, world!"
});类型安全的增强
现代 PromptTemplate 利用 TypeScript 的泛型系统提供更强的类型安全:
typescript
interface TranslationInput {
sourceLanguage: string;
targetLanguage: string;
text: string;
}
// 使用泛型的 PromptTemplate
class TypedPromptTemplate<Input extends Record<string, any>>
extends Runnable<Input, string> {
private template: string;
private inputVariables: (keyof Input)[];
constructor(config: { template: string; inputVariables: (keyof Input)[] }) {
super();
this.template = config.template;
this.inputVariables = config.inputVariables;
}
async invoke(input: Input): Promise<string> {
// TypeScript 会在编译时检查所有必需的字段是否存在
return this.format(input);
}
format(input: Input): string {
let result = this.template;
for (const variable of this.inputVariables) {
if (!(variable in input)) {
throw new Error(`缺少必需的变量: ${String(variable)}`);
}
result = result.replace(
new RegExp(`{${String(variable)}}`, 'g'),
String(input[variable])
);
}
return result;
}
}
// 使用类型安全的模板
const typedTemplate = new TypedPromptTemplate<TranslationInput>({
template: "将 {sourceLanguage} 翻译为 {targetLanguage}: {text}",
inputVariables: ["sourceLanguage", "targetLanguage", "text"]
});
// TypeScript 会检查输入是否符合 TranslationInput 接口
const result = await typedTemplate.invoke({
sourceLanguage: "英语",
targetLanguage: "中文",
text: "Hello, world!"
// 如果缺少任何字段,TypeScript 会在编译时报错
});高级功能支持
现代化的 PromptTemplate 还支持许多高级功能:
部分模板 (Partial Templates)
typescript
class PromptTemplate extends Runnable<any, string> {
// 部分应用变量
partial(values: Record<string, any>): PromptTemplate {
let newTemplate = this.template;
const newInputVariables = [...this.inputVariables];
for (const [key, value] of Object.entries(values)) {
newTemplate = newTemplate.replace(
new RegExp(`{${key}}`, 'g'),
String(value)
);
const index = newInputVariables.indexOf(key);
if (index > -1) {
newInputVariables.splice(index, 1);
}
}
return new PromptTemplate({
template: newTemplate,
inputVariables: newInputVariables
});
}
}
// 使用示例
const baseTemplate = PromptTemplate.fromTemplate(
"将 {sourceLanguage} 翻译为 {targetLanguage}: {text}"
);
const englishToChineseTemplate = baseTemplate.partial({
sourceLanguage: "英语",
targetLanguage: "中文"
});
// 现在只需要提供 text 变量
const result = await englishToChineseTemplate.invoke({
text: "Hello, world!"
});自定义格式器
typescript
class PromptTemplate extends Runnable<any, string> {
private template: string;
private inputVariables: string[];
private formatter: (template: string, input: Record<string, any>) => string;
constructor(config: {
template: string;
inputVariables: string[];
formatter?: (template: string, input: Record<string, any>) => string;
}) {
super();
this.template = config.template;
this.inputVariables = config.inputVariables;
this.formatter = config.formatter || this.defaultFormatter;
}
private defaultFormatter(template: string, input: Record<string, any>): string {
let result = template;
for (const variable of this.inputVariables) {
result = result.replace(
new RegExp(`{${variable}}`, 'g'),
String(input[variable])
);
}
return result;
}
async invoke(input: Record<string, any>): Promise<string> {
return this.formatter(this.template, input);
}
}
// 使用自定义格式器
const customTemplate = new PromptTemplate({
template: "问题: {question}\n答案: {answer}",
inputVariables: ["question", "answer"],
formatter: (template, input) => {
// 自定义格式化逻辑
return template
.replace("{question}", input.question.toUpperCase())
.replace("{answer}", input.answer.toLowerCase());
}
});安全转义
typescript
class PromptTemplate extends Runnable<any, string> {
private escapeInput(input: string): string {
// 转义特殊字符以防止注入攻击
return input
.replace(/\\/g, '\\\\')
.replace(/\{/g, '\\{')
.replace(/\}/g, '\\}');
}
format(input: Record<string, any>): string {
let result = this.template;
for (const variable of this.inputVariables) {
if (!(variable in input)) {
throw new Error(`缺少必需的变量: ${variable}`);
}
const escapedValue = this.escapeInput(String(input[variable]));
result = result.replace(
new RegExp(`{${variable}}`, 'g'),
escapedValue
);
}
return result;
}
}实际应用示例
让我们看一个完整的实际应用示例:
typescript
// 创建一个复杂的提示模板系统
class AdvancedPromptTemplate extends Runnable<Record<string, any>, string> {
private templates: Record<string, string>;
private defaultTemplate: string;
private inputVariables: string[];
constructor(config: {
templates: Record<string, string>;
defaultTemplate: string;
inputVariables: string[];
}) {
super();
this.templates = config.templates;
this.defaultTemplate = config.defaultTemplate;
this.inputVariables = config.inputVariables;
}
async invoke(input: Record<string, any>): Promise<string> {
// 根据输入选择模板
const templateKey = input.template || 'default';
const template = this.templates[templateKey] || this.defaultTemplate;
// 格式化模板
return this.formatTemplate(template, input);
}
private formatTemplate(template: string, input: Record<string, any>): string {
let result = template;
for (const variable of this.inputVariables) {
if (variable in input) {
result = result.replace(
new RegExp(`{${variable}}`, 'g'),
String(input[variable])
);
}
}
return result;
}
}
// 定义多种模板
const multiTemplate = new AdvancedPromptTemplate({
templates: {
translation: "将 {sourceLanguage} 翻译为 {targetLanguage}: {text}",
summarization: "请总结以下文本: {text}",
question: "回答以下问题: {text}"
},
defaultTemplate: "{text}",
inputVariables: ["sourceLanguage", "targetLanguage", "text"]
});
// 使用示例
const translationResult = await multiTemplate.invoke({
template: "translation",
sourceLanguage: "英语",
targetLanguage: "中文",
text: "Hello, world!"
});
const summaryResult = await multiTemplate.invoke({
template: "summarization",
text: "这是一个需要总结的长文本..."
});总结
PromptTemplate 从简单的字符串模板演进为完整的 实现,是 LangChain V3 设计理念的重要体现。这种变化带来了以下优势:
- 一致性 - 与 LangChain 中其他组件保持一致的接口
- 可组合性 - 可以无缝集成到 LCEL 管道中
- 类型安全 - 利用 TypeScript 提供更强的类型检查
- 功能增强 - 支持部分模板、自定义格式器等高级功能
- 安全性 - 内置安全转义机制防止注入攻击
通过这种现代化的实现,PromptTemplate 不仅保持了原有的模板功能,还成为了 LangChain 生态系统中一个功能强大且灵活的组件。
在下一章中,我们将探讨 PromptTemplate 的更多高级特性,包括 partial、自定义格式器和安全转义的支持。