Skip to content

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 设计理念的重要体现。这种变化带来了以下优势:

  1. 一致性 - 与 LangChain 中其他组件保持一致的接口
  2. 可组合性 - 可以无缝集成到 LCEL 管道中
  3. 类型安全 - 利用 TypeScript 提供更强的类型检查
  4. 功能增强 - 支持部分模板、自定义格式器等高级功能
  5. 安全性 - 内置安全转义机制防止注入攻击

通过这种现代化的实现,PromptTemplate 不仅保持了原有的模板功能,还成为了 LangChain 生态系统中一个功能强大且灵活的组件。

在下一章中,我们将探讨 PromptTemplate 的更多高级特性,包括 partial、自定义格式器和安全转义的支持。