书接上文。
一、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":"张三"}]