nestjs+langchain:Chains

7 阅读3分钟

书接上文

一、Chains

Chain的基本概念

Chain:链,用于将多个组件(提示模板、LLM模型、记忆、工具等)连接起来,形成可复用的 工作流 ,完成复杂的任务。

Chain 的核心思想是通过组合不同的模块化单元,实现比单一组件更强大的功能。比如:

  • 将 LLM 与 Prompt Template (提示模板)结合
  • 将 LLM 与 输出解析器 结合
  • 将 LLM 与 外部数据 结合,例如用于问答
  • 将 LLM 与 长期记忆 结合,例如用于聊天历史记录
  • 通过将 第一个LLM 的输出作为 第二个LLM 的输入,...,将多个LLM按顺序结合在一起

LCEL及其基本构成

使用 LCEL (LangChain 表达式语言) ,可以构造出结构最简单的 Chain。LCEL 是一种声明式编程方法,能够轻松将多个 LangChain 组件(提示模板、大模型、输出解析器等)组合为完整的 AI 工作流。在 NestJS/TypeScript 开发中,通过 LangChain 内置的 .pipe() 方法将组件串联成可执行流程,大幅简化大模型应用的开发。

基于LCEL构建的Chains的类型

createSqlQueryChain

createSqlQueryChain 是 LangChain 提供的开箱即用的 SQL 生成链

  • 输入:自然语言问题 + 数据库表结构 (Schema)
  • 输出:合法可执行的 SQL 语句
安装依赖
npm install @langchain/classic typeorm
npm install mysql2

.env

DB_PORT=3306
DB_HOST=localhost
DB_NAME=langchain_test
DB_USER=root
DB_PASS=Iamsilly@10086
DB_ENTITY_NAME=mong0
DB_SYNCHRONIZE=false
DB_LOGGING=true

agents.service.ts

import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ChatOpenAI } from '@langchain/openai';
// 注意:确保导入路径正确,不同版本可能略有差异
import { SqlDatabase } from '@langchain/classic/sql_db';
import { createSqlQueryChain } from '@langchain/classic/chains/sql_db';
import { DataSource } from 'typeorm';

@Injectable()
export class AgentsService implements OnModuleInit {
  private llm: ChatOpenAI;
  private db: SqlDatabase;
  private dataSource: DataSource;

  constructor(private readonly configService: ConfigService) {
    // 1. 从环境变量获取配置,避免硬编码
    // 建议在 .env 文件中定义 VITE_OPENAI_API_KEY 或 OPENAI_API_KEY
    const apiKey = process.env.OPENAI_API_KEY; // 生产环境务必使用环境变量
    const baseUrl = process.env.OPENAI_BASE_URL;

    // 2. 初始化 ChatOpenAI
    this.llm = new ChatOpenAI({
      modelName: 'ernie-3.5-8k',
      openAIApiKey: apiKey,
      configuration: {
        baseURL: baseUrl,
      },
      temperature: 0, // 可选:设置温度
    });
  }

  // 模块初始化时创建连接
  async onModuleInit() {
    // 3. 创建 TypeORM 数据源
    this.dataSource = new DataSource({
      type: 'mysql',
      host: this.configService.get('DB_HOST'),
      port: this.configService.get('DB_PORT'),
      username: this.configService.get('DB_USER'),
      password: this.configService.get('DB_PASS'),
      database: this.configService.get('DB_NAME'),
      entities: [], // 无需实体,纯SQL查询
      synchronize: false,
    });

    // 初始化连接
    await this.dataSource.initialize();

    // 3. 使用静态方法 fromDataSourceParams 创建实例
    this.db = await SqlDatabase.fromDataSourceParams({
      appDataSource: this.dataSource,
      includesTables: ['user'], // 仅允许查询user表,安全
    });
  }

  // 自然语言转SQL + 执行
  async query(question: string = '北京的用户有哪些') {
    // 先 await 获取 chain 实例
    const chain = await createSqlQueryChain({
      llm: this.llm,
      db: this.db,
      dialect: 'mysql',
    });

    // 生成SQL
    const response = await chain.invoke({ question });

    console.log('Raw response from chain:', response);

    // 提取纯 SQL 语句
    let sql: string;

    // 更稳健的提取方法:查找第一个双引号和最后一个双引号之间的内容
    if (typeof response === 'string') {
      const firstQuoteIndex = response.indexOf('"');
      const lastQuoteIndex = response.lastIndexOf('"');

      if (
        firstQuoteIndex !== -1 &&
        lastQuoteIndex !== -1 &&
        lastQuoteIndex > firstQuoteIndex
      ) {
        // 提取引号内的内容
        sql = response.substring(firstQuoteIndex + 1, lastQuoteIndex);
      } else {
        // 如果没有引号,尝试移除前缀并清理
        sql = response.replace(/^SQLQuery:\s*/i, '').trim();
      }
    } else {
      // 处理非字符串情况(虽然根据日志不太可能)
      sql = String(response)
        .replace(/^SQLQuery:\s*"?/i, '')
        .replace(/"?$/, '')
        .trim();
    }

    // 确保移除可能的末尾分号后的多余字符(虽然 substring 方法应该已经处理了)
    sql = sql.trim();

    // 如果 SQL 以分号结尾,保留它通常没问题,但有些驱动可能不喜欢。
    // MySQL 通常接受带分号的 SQL。如果报错,可以尝试 sql = sql.replace(/;$/, '');

    console.log('Cleaned SQL:', sql);

    // 执行SQL
    // 注意:run 方法返回的结果格式可能因版本而异,建议根据实际返回调整解析逻辑
    const result = await this.db.run(sql);

    console.log('result:', result);
  }
}

输出结果:

Raw response from chain: SQLQuery: "SELECT `id`, `name` FROM `user` WHERE `city` = '北京' LIMIT 5;"
SQLResult:
Cleaned SQL: SELECT `id`, `name` FROM `user` WHERE `city` = '北京' LIMIT 5;
result: [{"id":1,"name":"张三"}]