构建基于Azure OpenAI的ChatGPT风格应用(含RAG功能)
项目概述
这个应用将具备以下核心功能:
- 使用Azure OpenAI API进行对话生成
- 允许用户上传文件作为知识库
- 将上传的文件处理成向量存储(RAG系统)
- 在用户提问时结合RAG知识库生成回答
- 提供流畅的聊天界面体验
技术栈选择
前端
- React/Next.js: 构建用户界面
- Tailwind CSS: 快速样式设计
- Axios: API请求处理
- React Dropzone: 文件上传组件
后端
- Node.js + Express: 服务器框架
- Azure OpenAI SDK: 与Azure OpenAI服务交互
- LangChain.js: RAG实现框架
- Multer: 文件上传处理
数据存储
- Vector数据库: Pinecone, Qdrant或Chroma
- MongoDB/PostgreSQL: 存储用户数据和对话历史
系统架构
- 前端应用: 展示聊天界面,处理用户输入和文件上传
- API服务: 处理聊天请求和文件处理
- 向量数据库: 存储文档嵌入向量
- Azure OpenAI服务: 提供LLM能力
详细开发步骤
1. 项目设置
1.1 创建项目目录结构
chatgpt-azure-rag/
├── client/ # 前端React应用
├── server/ # 后端Node.js服务
├── .env # 环境变量
└── README.md # 项目文档
1.2 初始化前端项目
# 创建React应用
npx create-react-app client
# 或使用Next.js (推荐)
npx create-next-app client
cd client
# 安装依赖
npm install axios react-dropzone react-markdown
npm install tailwindcss postcss autoprefixer
npx tailwindcss init -p
1.3 初始化后端项目
mkdir server
cd server
npm init -y
npm install express cors dotenv multer azure-openai langchain @langchain/openai
npm install @pinecone-database/pinecone
npm install nodemon --save-dev
2. Azure OpenAI设置
2.1 创建Azure OpenAI资源
- 登录Azure门户
- 创建新的OpenAI资源
- 选择合适的区域和价格套餐
- 部署完成后获取API密钥和终端点URL
2.2 部署模型
- 在Azure OpenAI Studio中导航到"部署"
- 部署gpt-35-turbo或gpt-4模型
- 为嵌入功能部署text-embedding-ada-002模型
- 记录模型部署名称
2.3 配置环境变量
创建一个.env
文件在根目录:
# Azure OpenAI
AZURE_OPENAI_API_KEY=your_api_key_here
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
AZURE_OPENAI_API_VERSION=2023-05-15
AZURE_OPENAI_DEPLOYMENT_NAME=your-gpt-deployment
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME=your-embedding-deployment
# Pinecone
PINECONE_API_KEY=your_pinecone_api_key
PINECONE_ENVIRONMENT=your_environment
PINECONE_INDEX_NAME=your_index_name
# Server
PORT=5000
3. 后端开发
3.1 创建Express服务器
在server/index.js
中:
const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
// 加载环境变量
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
// 中间件
app.use(cors());
app.use(express.json());
// 文件上传配置
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
cb(null, uploadDir);
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname);
}
});
const upload = multer({ storage: storage });
// 路由
app.get('/', (req, res) => {
res.send('Azure OpenAI ChatGPT API正在运行');
});
// 启动服务器
app.listen(PORT, () => {
console.log(`服务器运行在端口 ${PORT}`);
});
3.2 实现文件上传与处理
创建server/utils/documentProcessor.js
:
const { RecursiveCharacterTextSplitter } = require('langchain/text_splitter');
const { OpenAIEmbeddings } = require('@langchain/openai');
const { PineconeStore } = require('@langchain/pinecone');
const { Pinecone } = require('@pinecone-database/pinecone');
const { Document } = require('langchain/document');
const fs = require('fs');
const path = require('path');
const { PDFLoader } = require("langchain/document_loaders/fs/pdf");
const { DocxLoader } = require("langchain/document_loaders/fs/docx");
const { TextLoader } = require("langchain/document_loaders/fs/text");
// 初始化Pinecone客户端
const pinecone = new Pinecone({
apiKey: process.env.PINECONE_API_KEY,
environment: process.env.PINECONE_ENVIRONMENT,
});
// 创建OpenAI嵌入对象
const embeddings = new OpenAIEmbeddings({
azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY,
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION,
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_ENDPOINT.split('.')[0].split('//')[1],
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME,
});
// 处理文件函数
async function processFile(filePath) {
try {
const fileExtension = path.extname(filePath).toLowerCase();
let documents = [];
// 根据文件类型选择加载器
if (fileExtension === '.pdf') {
const loader = new PDFLoader(filePath);
documents = await loader.load();
} else if (fileExtension === '.docx') {
const loader = new DocxLoader(filePath);
documents = await loader.load();
} else if (fileExtension === '.txt') {
const loader = new TextLoader(filePath);
documents = await loader.load();
} else {
throw new Error('不支持的文件类型');
}
// 文本分割
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
});
const splitDocs = await textSplitter.splitDocuments(documents);
// 为文档添加元数据
const fileName = path.basename(filePath);
const enhancedDocs = splitDocs.map((doc, i) => {
return new Document({
pageContent: doc.pageContent,
metadata: {
...doc.metadata,
fileName,
chunkId: i,
fileType: fileExtension.substring(1),
},
});
});
// 生成唯一的命名空间ID
const namespaceId = `doc_${Date.now()}`;
// 获取Pinecone索引
const index = pinecone.Index(process.env.PINECONE_INDEX_NAME);
// 创建向量存储
await PineconeStore.fromDocuments(enhancedDocs, embeddings, {
pineconeIndex: index,
namespace: namespaceId,
});
return {
success: true,
namespaceId,
documentInfo: {
fileName,
chunkCount: enhancedDocs.length,
fileType: fileExtension.substring(1),
}
};
} catch (error) {
console.error('处理文件时出错:', error);
return {
success: false,
error: error.message
};
}
}
module.exports = { processFile };
3.3 实现聊天和RAG功能
创建server/utils/chatProcessor.js
:
const { AzureOpenAI } = require('@azure/openai');
const { PineconeStore } = require('@langchain/pinecone');
const { OpenAIEmbeddings } = require('@langchain/openai');
const { Pinecone } = require('@pinecone-database/pinecone');
// 初始化Pinecone客户端
const pinecone = new Pinecone({
apiKey: process.env.PINECONE_API_KEY,
environment: process.env.PINECONE_ENVIRONMENT,
});
// 创建OpenAI嵌入对象
const embeddings = new OpenAIEmbeddings({
azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY,
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION,
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_ENDPOINT.split('.')[0].split('//')[1],
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME,
});
// 创建Azure OpenAI客户端
const client = new AzureOpenAI({
apiKey: process.env.AZURE_OPENAI_API_KEY,
endpoint: process.env.AZURE_OPENAI_ENDPOINT,
apiVersion: process.env.AZURE_OPENAI_API_VERSION,
});
async function processChat(messages, namespaceId = null) {
try {
let systemMessage = "你是一个有帮助的AI助手。";
const userMessage = messages[messages.length - 1].content;
// 如果有命名空间ID,使用RAG
if (namespaceId) {
// 获取Pinecone索引
const index = pinecone.Index(process.env.PINECONE_INDEX_NAME);
// 创建向量存储
const vectorStore = await PineconeStore.fromExistingIndex(embeddings, {
pineconeIndex: index,
namespace: namespaceId,
});
// 执行相似性搜索
const results = await vectorStore.similaritySearch(userMessage, 3);
// 合并搜索结果作为上下文
const context = results.map(item => item.pageContent).join('\n\n');
// 使用检索到的信息更新系统消息
systemMessage = `你是一个有帮助的AI助手。请使用以下信息回答用户的问题:
${context}
如果以上信息不足以回答用户的问题,请坦诚地说明你不知道,不要编造信息。`;
}
// 准备完整的消息数组
const completeMessages = [
{ role: 'system', content: systemMessage },
...messages.slice(0, -1), // 之前的消息历史
{ role: 'user', content: userMessage } // 最新的用户消息
];
// 调用Azure OpenAI API
const response = await client.chat.completions.create({
messages: completeMessages,
model: process.env.AZURE_OPENAI_DEPLOYMENT_NAME,
temperature: 0.7,
max_tokens: 800
});
return {
success: true,
content: response.choices[0].message.content,
};
} catch (error) {
console.error('处理聊天时出错:', error);
return {
success: false,
error: error.message
};
}
}
module.exports = { processChat };
3.4 添加API路由
在server/index.js
中添加路由:
// 引入处理函数
const { processFile } = require('./utils/documentProcessor');
const { processChat } = require('./utils/chatProcessor');
// 文件上传路由
app.post('/api/upload', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ success: false, message: '没有文件上传' });
}
const result = await processFile(req.file.path);
// 处理完成后删除临时文件
fs.unlinkSync(req.file.path);
if (result.success) {
res.json({
success: true,
namespaceId: result.namespaceId,
documentInfo: result.documentInfo
});
} else {
res.status(500).json({ success: false, message: result.error });
}
} catch (error) {
console.error('上传处理错误:', error);
res.status(500).json({ success: false, message: '文件处理失败' });
}
});
// 聊天路由
app.post('/api/chat', async (req, res) => {
try {
const { messages, namespaceId } = req.body;
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return res.status(400).json({ success: false, message: '消息格式不正确' });
}
const result = await processChat(messages, namespaceId);
if (result.success) {
res.json({ success: true, content: result.content });
} else {
res.status(500).json({ success: false, message: result.error });
}
} catch (error) {
console.error('聊天处理错误:', error);
res.status(500).json({ success: false, message: '处理聊天请求失败' });
}
});
4. 前端开发
4.1 创建聊天界面组件
创建client/src/components/ChatInterface.js
:
import React, { useState, useRef, useEffect } from 'react';
import axios from 'axios';
import FileUpload from './FileUpload';
const ChatInterface = () => {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [namespaceId, setNamespaceId] = useState(null);
const [documentInfo, setDocumentInfo] = useState(null);
const messagesEndRef = useRef(null);
// 滚动到最新消息
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// 处理聊天提交
const handleSubmit = async (e) => {
e.preventDefault();
if (!input.trim()) return;
// 添加用户消息
const userMessage = { role: 'user', content: input };
setMessages([...messages, userMessage]);
setInput('');
setIsLoading(true);
try {
// 发送API请求
const response = await axios.post('http://localhost:5000/api/chat', {
messages: [...messages, userMessage],
namespaceId
});
// 添加AI回复
if (response.data.success) {
setMessages(prevMessages => [
...prevMessages,
{ role: 'assistant', content: response.data.content }
]);
} else {
console.error('聊天请求失败:', response.data.message);
setMessages(prevMessages => [
...prevMessages,
{ role: 'assistant', content: '抱歉,处理您的请求时出错了。' }
]);
}
} catch (error) {
console.error('聊天请求错误:', error);
setMessages(prevMessages => [
...prevMessages,
{ role: 'assistant', content: '抱歉,无法连接到服务器。' }
]);
} finally {
setIsLoading(false);
}
};
// 处理文件上传成功
const handleFileProcessed = (data) => {
setNamespaceId(data.namespaceId);
setDocumentInfo(data.documentInfo);
// 添加系统消息
setMessages(prevMessages => [
...prevMessages,
{
role: 'assistant',
content: `文件 "${data.documentInfo.fileName}" 已成功上传和处理!我现在可以回答关于这个文档的问题了。`
}
]);
};
return (
<div className="flex flex-col h-screen max-w-4xl mx-auto p-4">
<h1 className="text-2xl font-bold text-center mb-4">AI文档助手</h1>
{/* 文件上传区域 */}
{!namespaceId && (
<div className="mb-4">
<FileUpload onFileProcessed={handleFileProcessed} />
</div>
)}
{/* 显示当前文档信息 */}
{documentInfo && (
<div className="bg-blue-50 p-3 rounded-lg mb-4 text-sm">
<p><span className="font-semibold">当前文档:</span> {documentInfo.fileName}</p>
<p><span className="font-semibold">类型:</span> {documentInfo.fileType}</p>
<p><span className="font-semibold">内容块数:</span> {documentInfo.chunkCount}</p>
</div>
)}
{/* 聊天消息区域 */}
<div className="flex-1 overflow-auto border border-gray-300 rounded-lg p-4 mb-4">
{messages.map((message, index) => (
<div
key={index}
className={`mb-4 p-3 rounded-lg ${
message.role === 'user'
? 'bg-blue-100 ml-auto max-w-[80%]'
: 'bg-gray-100 mr-auto max-w-[80%]'
}`}
>
{message.content}
</div>
))}
{isLoading && (
<div className="bg-gray-100 p-3 rounded-lg mr-auto max-w-[80%] mb-4">
<div className="flex space-x-2">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100"></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200"></div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* 消息输入区域 */}
<form onSubmit={handleSubmit} className="flex">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="输入您的问题..."
className="flex-1 p-2 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isLoading}
/>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded-r-lg hover:bg-blue-600 focus:outline-none disabled:bg-blue-300"
disabled={isLoading || !input.trim()}
>
发送
</button>
</form>
</div>
);
};
export default ChatInterface;
4.2 创建文件上传组件
创建client/src/components/FileUpload.js
:
import React, { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import axios from 'axios';
const FileUpload = ({ onFileProcessed }) => {
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadError, setUploadError] = useState('');
const onDrop = useCallback(async (acceptedFiles) => {
const file = acceptedFiles[0];
if (!file) return;
// 检查文件类型
const allowedTypes = ['.pdf', '.docx', '.txt'];
const fileExt = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
if (!allowedTypes.includes(fileExt)) {
setUploadError('不支持的文件类型。请上传PDF、DOCX或TXT文件。');
return;
}
setIsUploading(true);
setUploadError('');
setUploadProgress(0);
// 创建FormData对象
const formData = new FormData();
formData.append('file', file);
try {
// 发送上传请求
const response = await axios.post('http://localhost:5000/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
setUploadProgress(percentCompleted);
}
});
if (response.data.success) {
onFileProcessed(response.data);
} else {
setUploadError(response.data.message || '文件上传失败');
}
} catch (error) {
console.error('上传错误:', error);
setUploadError(error.response?.data?.message || '服务器错误,请稍后重试');
} finally {
setIsUploading(false);
}
}, [onFileProcessed]);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
multiple: false,
disabled: isUploading
});
return (
<div className="w-full">
<div
{...getRootProps()}
className={`border-2 border-dashed p-6 rounded-lg text-center cursor-pointer transition-colors ${
isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-blue-400'
} ${isUploading ? 'opacity-50 pointer-events-none' : ''}`}
>
<input {...getInputProps()} />
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-12 w-12 mx-auto text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
{isUploading ? (
<div className="mt-4">
<p className="text-sm font-medium text-gray-700">正在处理文件...</p>
<div className="w-full bg-gray-200 rounded-full h-2.5 mt-2">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${uploadProgress}%` }}
></div>
</div>
</div>
) : (
<p className="mt-2 text-sm text-gray-600">
{isDragActive ? '拖放文件到此处' : '点击或拖放文件到此处上传'}
</p>
)}
<p className="mt-1 text-xs text-gray-500">
支持的文件类型: PDF, DOCX, TXT
</p>
</div>
{uploadError && (
<p className="mt-2 text-sm text-red-600">{uploadError}</p>
)}
</div>
);
};
export default FileUpload;
4.3 更新App.js
更新client/src/App.js
:
import React from 'react';
import ChatInterface from './components/ChatInterface';
import './App.css';
function App() {
return (
<div className="App bg-gray-50 min-h-screen">
<ChatInterface />
</div>
);
}
export default App;
4.4 添加Tailwind CSS配置
更新client/tailwind.config.js
:
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
更新client/src/index.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
5. 启动与测试应用
5.1 运行后端服务
cd server
npm run dev # 假设你在package.json中设置了"dev": "nodemon index.js"
5.2 运行前端应用
cd client
npm start
5.3 测试功能
- 打开浏览器访问 http://localhost:3000
- 上传PDF、DOCX或TXT文件
- 等待文件处理完成
- 开始向AI提问关于文档的问题
6. 生产环境部署
6.1 后端部署到Azure App Service
- 在Azure门户中创建新的App Service
- 配置应用设置(环境变量)
- 使用Azure CLI、GitHub Actions或Azure DevOps进行部署
6.2 前端部署到Azure Static Web Apps或Vercel
- 构建React应用:
npm run build
- 部署到选择的平台
- 配置API URL指向后端服务
7. 高级功能扩展
7.1 多文档管理
- 实现文档列表和切换功能
- 添加文档标签和分类功能
7.2 对话历史保存
- 使用数据库存储对话历史
- 实现历史对话恢复功能
7.3 流式响应
- 实现服务器发送事件(SSE)或WebSocket
- 展示打字机效果的回复
7.4 用户认证系统
- 添加登录/注册功能
- 实现用户专属知识库
故障排除
常见问题与解决方案
-
向量数据库连接失败
- 检查API密钥和环境配置
- 确认网络连接和防火墙设置
-
文件处理超时
- 增加服务器超时限制
- 对大文件实现分批处理
-
Azure API限额问题
- 实现请求节流和队列
- 考虑升级API套餐
-
内存占用过大
- 优化文本分块大小
- 增加服务器资源或使用无服务器架构
总结
通过以上步骤,你可以构建一个具备以下功能的ChatGPT风格应用:
- 使用Azure OpenAI API进行对