我们在开发智能问答系统时,常常面临前后端集成与大语言模型(LLM)交互的挑战。如何让非结构化文档数据高效接入 LLM?怎样设计一个支持用户上传、查询、管理的全栈应用?本文将以 LlamaIndex 为核心,结合 Flask 后端与 React 前端,手把手教你搭建一个具备文档检索与智能回答能力的全栈 Web 应用。

一、技术选型与整体架构

核心技术栈

  • 后端:Flask(轻量级 Web 框架) + LlamaIndex(LLM 索引构建) + BaseManager(并发控制)
  • 前端:React + TypeScript(组件化开发) + Axios(API 请求)
  • 交互流程
    1. 用户通过前端上传文档或输入查询
    2. 后端使用 LlamaIndex 构建 / 查询索引,并返回结构化结果
    3. 前端渲染回答及相关文档来源

二、Flask 后端开发:从基础 API 到索引管理

1. 快速搭建 Flask 基础服务

python

# flask_demo.py
from flask import Flask, request, jsonify
app = Flask(__name__)

@app.route("/")
def home():
    return "LlamaIndex全栈应用后端"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5601, debug=True)

访问http://localhost:5601即可看到 “LlamaIndex 全栈应用后端” 字样,基础服务搭建完成。

2. 集成 LlamaIndex 实现查询功能

(1)索引初始化与持久化

python

# 引入LlamaIndex组件
from llama_index.core import (
    SimpleDirectoryReader, VectorStoreIndex, StorageContext, load_index_from_storage
)
import os

# 全局索引对象
index = None
index_dir = "./.index"
os.environ["OPENAI_API_KEY"] = "your-key-here"  # 仅测试用,生产环境需配置管理

def initialize_index():
    global index
    storage_context = StorageContext.from_defaults()
    if os.path.exists(index_dir):
        # 加载已有索引
        index = load_index_from_storage(storage_context)
    else:
        # 首次构建索引
        documents = SimpleDirectoryReader("./documents").load_data()
        index = VectorStoreIndex.from_documents(
            documents, storage_context=storage_context
        )
        storage_context.persist(index_dir)  # 持久化索引到磁盘

# 在Flask启动时初始化索引
if __name__ == "__main__":
    initialize_index()
    app.run(...)
(2)定义查询接口

python

@app.route("/query", methods=["GET"])
def query_index():
    query_text = request.args.get("text")
    if not query_text:
        return jsonify({"error": "请提供查询文本"}), 400
    query_engine = index.as_query_engine()
    response = query_engine.query(query_text)
    return jsonify({
        "answer": str(response),
        "sources": [node.get_content() for node in response.source_nodes]
    }), 200

测试 URL:http://localhost:5601/query?text=what did the author do growing up

3. 高级功能:支持用户文档上传与并发控制

(1)多线程安全的索引服务器

python

# index_server.py(独立进程)
from multiprocessing import Lock
from multiprocessing.managers import BaseManager
from llama_index.core import Document

index = None
lock = Lock()

def initialize_index():
    global index
    with lock:
        # 索引初始化逻辑同上...

def query_index(query_text):
    with lock:
        return index.as_query_engine().query(query_text)

def insert_into_index(filepath, doc_id=None):
    with lock:
        document = SimpleDirectoryReader([filepath]).load_data()[0]
        if doc_id:
            document.doc_id = doc_id
        index.insert(document)
        index.storage_context.persist()  # 插入后重新持久化

# 启动索引服务器
if __name__ == "__main__":
    BaseManager.register("query_index", query_index)
    BaseManager.register("insert_into_index", insert_into_index)
    manager = BaseManager(("", 5602), b"password")  # 端口5602,密码验证
    server = manager.get_server()
    print("索引服务器启动,端口5602")
    server.serve_forever()
(2)Flask 对接索引服务器

python

# flask_demo.py(修改后)
from multiprocessing.managers import BaseManager

# 连接索引服务器
manager = BaseManager(("", 5602), b"password")
manager.register("query_index")
manager.register("insert_into_index")
manager.connect()

@app.route("/uploadFile", methods=["POST"])
def upload_file():
    file = request.files["file"]
    filename = secure_filename(file.filename)
    filepath = os.path.join("documents", filename)
    file.save(filepath)
    
    try:
        manager.insert_into_index(filepath, doc_id=filename)
        return jsonify({"message": "文件上传并索引成功"}), 200
    except Exception as e:
        os.remove(filepath)
        return jsonify({"error": str(e)}), 500

三、React 前端开发:构建用户交互界面

1. 封装 API 请求

typescript

// src/apis/index.ts
export const fetchDocuments = async () => {
    const res = await fetch("http://localhost:5601/getDocuments", { mode: "cors" });
    return res.json() as Promise<{ id: string; text: string }[]>;
};

export const queryIndex = async (query: string) => {
    const url = new URL("http://localhost:5601/query");
    url.searchParams.append("text", query);
    const res = await fetch(url, { mode: "cors" });
    return res.json() as Promise<{ answer: string; sources: string[] }>;
};

export const uploadFile = async (file: File) => {
    const formData = new FormData();
    formData.append("file", file);
    formData.append("filename_as_doc_id", "true");
    return fetch("http://localhost:5601/uploadFile", {
        method: "POST",
        body: formData,
        mode: "cors"
    });
};

2. 核心组件:查询界面

typescript

// src/components/QueryForm.tsx
import { useState } from 'react';
import { queryIndex } from '../apis';

const QueryForm = () => {
    const [query, setQuery] = useState<string>('');
    const [answer, setAnswer] = useState<string>('');
    const [sources, setSources] = useState<string[]>();

    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        const result = await queryIndex(query);
        setAnswer(result.answer);
        setSources(result.sources);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                value={query}
                onChange={(e) => setQuery(e.target.value)}
                placeholder="输入你的问题..."
                className="w-full p-2"
            />
            <button type="submit" className="bg-blue-500 text-white p-2 ml-2">
                搜索
            </button>
            {answer && (
                <div className="mt-4">
                    <h3>回答:</h3>
                    <p>{answer}</p>
                    <h4 className="mt-2">参考来源:</h4>
                    <ul>
                        {sources?.map((source, idx) => (
                            <li key={idx}>{source.substr(0, 100)}...</li>
                        ))}
                    </ul>
                </div>
            )}
        </form>
    );
};

四、关键技术点解析

1. 并发控制与线程安全

  • 问题:多用户同时上传文档可能导致索引损坏
  • 方案
    • 使用multiprocessing.Lock确保同一时间只有一个线程操作索引
    • 通过独立的index_server.py进程管理索引,避免 Flask 多线程冲突

2. CORS 跨域处理

python

# Flask端安装flask-cors并配置
from flask_cors import CORS
CORS(app, resources={r"/*": {"origins": "*"}})

typescript

// 前端请求添加mode: "cors"
fetch(url, { mode: "cors" })

3. 文档管理增强

  • 需求:跟踪已上传文档列表
  • 实现
    1. 后端新增/getDocuments接口,遍历索引节点元数据
    2. 前端渲染文档标题与摘要

python

@app.route("/getDocuments", methods=["GET"])
def get_documents():
    documents = []
    for node in index.docstore.docs.values():
        documents.append({
            "id": node.doc_id,
            "text": node.text[:200]  # 取前200字作为摘要
        })
    return jsonify(documents)

五、部署与扩展建议

1. 本地部署步骤

  1. 启动索引服务器:python index_server.py
  2. 启动 Flask 后端:python flask_demo.py
  3. 启动前端:cd react_frontend && npm start

2. 生产环境优化

  • 索引存储:将StorageContext切换为云存储(如 S3)
  • 向量数据库:使用 Pinecone/Chroma 替代内存存储
  • 用户认证:添加 JWT token 验证接口访问
  • 负载均衡:使用 Gunicorn 部署 Flask,配合 Nginx 处理并发请求

结语

通过 LlamaIndex 与全栈技术的结合,我们实现了从文档上传、索引构建到智能问答的完整流程。这种架构不仅适用于企业知识库、智能客服等场景,还可通过扩展索引类型(如 TreeIndex)、优化检索策略(如 HybridRetriever)进一步提升性能。

如果本文对你有帮助,别忘了点赞收藏,关注我,一起探索更高效的开发方式~

Logo

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。

更多推荐