Python LLM 项目实战:落地可上线的高校答疑小助手(两万字完整版,承接工程化避坑指南)

AgenticCoding·十二月创作之星挑战赛 10w+人浏览 543人参与

[博客前置说明]

承接前两篇工程化实战:默认读者已掌握「Python 3.12/MySQL 8.0/PyCharm」环境搭建、工程化目录规范、避坑指南核心规则(逻辑删除、数据校验、连接池等)。本次落地零基础友好的开源 LLM 项目,覆盖 LLM 特有的坑点(幻觉、向量库选择、模型加载等),所有代码可直接复制运行。


一、项目背景:从 “普通系统” 到 “LLM 智能应用”

1.1 真实业务痛点(高校辅导员 / 学生视角大白话)

辅导员每天要回答100 + 重复问题:“什么时候选课?”“宿舍怎么分配?”“助学贷款怎么申请?”学生找不到权威答案:官网信息分散、群聊记录淹没、人工回复慢;纯 LLM 有幻觉风险:随便编一个 “下周选课” 的谎言,学生可能真的信。

1.2 解决方案:RAG 架构的高校答疑小助手

RAG(检索增强生成) 技术:先从权威知识库(学校官网、课件、FAQ、通知)中检索相关内容,再让 LLM 基于这些内容生成答案 ——完全避免幻觉,答案 100% 来自你的知识库。

1.3 技术栈(延续前两篇的 “简单、流行、避坑” 原则)

技术栈用途避坑点关联
Python 3.12后端开发前两篇:版本选择(3.10+)
FastAPIAPI 开发前两篇:异步、自动生成文档
LangChain 0.1.20LLM 开发框架LLM 坑:避免手写复杂 prompt 工程
ChromaDB本地向量库LLM 坑:零基础不用装 Milvus 等复杂向量库
Qwen-1.5-7B-Chat开源 LLMLLM 坑:阿里开源,中文友好,轻量(CPU 可跑)
Pydantic v2数据校验前两篇:拒绝脏数据
SQLAlchemy知识库元数据存储前两篇:防 SQL 注入、逻辑删除
Docker部署上线前两篇:工程化一键部署

二、工程化目录结构(严格对齐前两篇规范)

新增app/llm「LLM 核心逻辑」、app/vector_store「向量库操作」目录,其余结构完全继承前两篇

llm-qa-system/
├── app/  # 主应用目录
│   ├── __init__.py  # 包初始化(空文件)
│   ├── models/  # 数据库模型(前两篇:逻辑删除)
│   │   ├── base.py  # 基础模型(全局逻辑删除)
│   │   └── knowledge.py  # 知识库元数据模型
│   ├── schemas/  # Pydantic校验(前两篇:拒绝脏数据)
│   │   └── knowledge.py  # 知识库校验
│   ├── crud/  # CRUD操作(前两篇:模块化)
│   │   └── knowledge.py  # 知识库CRUD
│   ├── routers/  # API路由
│   │   ├── qa.py  # 答疑接口
│   │   └── knowledge.py  # 知识库管理接口
│   ├── dependencies.py  # 依赖注入(前两篇:数据库连接池)
│   ├── llm/  # LLM核心逻辑(RAG)
│   │   └── rag_engine.py  # RAG引擎
│   ├── vector_store/  # 向量库操作
│   │   └── chroma_store.py  # ChromaDB向量库
│   └── utils/  # 工具函数
│       ├── file_utils.py  # 文件处理
│       └── jwt_utils.py  # 权限控制(继承前两篇)
├── config.py  # 全局配置(前两篇:日志、敏感配置)
├── constants.py  # 常量定义
├── .env  # 敏感配置(LLM模型路径、API密钥)
├── .gitignore  # Git忽略文件
├── requirements.txt  # 依赖列表
├── frontend/  # 前端页面(极简HTML/CSS/JS)
│   ├── qa.html  # 答疑页面
│   ├── knowledge.html  # 知识库管理页面
│   └── styles.css  # 统一样式(继承前两篇)
├── models/  # 本地LLM模型存放目录
│   └── Qwen-1.5-7B-Chat/  # 下载的Qwen模型
├── vector_db/  # ChromaDB向量库存放目录
└── Dockerfile  # Docker部署

三、核心功能实现(每一步关联避坑点)

3.1 配置文件层:敏感信息 + LLM 参数(延续前两篇避坑点)

3.1.1 .env 敏感配置文件(禁止硬编码!)
# .env(不要提交到Git)
# 数据库配置(继承前两篇)
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=你的MySQL密码
DB_NAME=llm_qa_system

# LLM配置(新增LLM特有的避坑点:模型路径/参数)
LLM_MODEL_PATH=./models/Qwen-1.5-7B-Chat  # 本地模型路径
LLM_TEMPERATURE=0.1  # 0-1,值越小答案越稳定(避坑:防止幻觉)
CHUNK_SIZE=500  # 文本拆分长度(避坑:RAG检索精准度)
CHUNK_OVERLAP=50  # 文本重叠长度(避坑:上下文连贯性)
VECTOR_DB_PATH=./vector_db  # 向量库路径
JWT_SECRET_KEY=qa-2024-abcdefghijklmnopqrstuvwxyz
3.1.2 config.py 全局配置(日志 + 向量库 + LLM)
# config.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from dotenv import load_dotenv
import os
import logging
from logging.handlers import RotatingFileHandler

# 加载.env配置(前两篇:敏感信息管理)
load_dotenv()

# 1. 数据库配置(继承前两篇:连接池)
DATABASE_URL = f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
engine = create_engine(
    DATABASE_URL,
    pool_size=10,
    max_overflow=20,
    pool_pre_ping=True,
    pool_recycle=3600
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

# 2. 日志配置(前两篇:用logging代替print)
def setup_logging():
    log_dir = "./logs"
    os.makedirs(log_dir, exist_ok=True)
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(pathname)s:%(lineno)d - %(message)s",
        handlers=[
            logging.StreamHandler(),
            RotatingFileHandler(os.path.join(log_dir, "app.log"), maxBytes=10*1024*1024, backupCount=10)
        ]
    )
    logging.info("日志系统初始化成功(前两篇避坑点)")

# 3. LLM配置(新增LLM避坑点:参数统一管理)
LLM_CONFIG = {
    "model_path": os.getenv("LLM_MODEL_PATH"),
    "temperature": float(os.getenv("LLM_TEMPERATURE", 0.1)),
    "chunk_size": int(os.getenv("CHUNK_SIZE", 500)),
    "chunk_overlap": int(os.getenv("CHUNK_OVERLAP", 50))
}

# 4. 向量库配置(新增LLM避坑点:本地存储)
VECTOR_DB_CONFIG = {
    "persist_directory": os.getenv("VECTOR_DB_PATH", "./vector_db"),
    "collection_name": "university_qa"
}

# 5. 其他配置
UPLOAD_DIR = "./uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")

# 初始化日志
setup_logging()
3.1.3 app/dependencies.py 依赖注入(继承前两篇)
# app/dependencies.py
from config import SessionLocal
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

# 1. 数据库依赖(前两篇:连接池归还)
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# 2. JWT权限依赖
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_current_user(token: str = Depends(oauth2_scheme)):
    from app.utils.jwt_utils import verify_token
    payload = verify_token(token)
    if payload is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="无效Token",
            headers={"WWW-Authenticate": "Bearer"}
        )
    return payload

3.2 数据库模型层:知识库元数据(逻辑删除,前两篇核心避坑点)

3.2.1 app/models/base.py 基础模型(继承前两篇:全局逻辑删除)
# app/models/base.py
from sqlalchemy import Column, Integer, Boolean, DateTime
from datetime import datetime
from config import Base

class BaseModel(Base):
    __abstract__ = True
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    is_deleted = Column(Boolean, default=False, comment="是否删除:0=未删除,1=已删除(前两篇避坑点:禁止物理删除)")
    created_at = Column(DateTime, default=datetime.now, comment="创建时间")
    updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
    deleted_at = Column(DateTime, nullable=True, comment="删除时间")

    # 全局查询过滤器(前两篇:自动排除已删除数据)
    @classmethod
    def __declare_last__(cls):
        if hasattr(cls, 'is_deleted'):
            original_query = cls.query
            def new_query():
                return original_query().filter_by(is_deleted=False)
            cls.query = property(new_query)
3.2.2 app/models/knowledge.py 知识库元数据模型
# app/models/knowledge.py
from sqlalchemy import Column, String, Integer, Text
from app.models.base import BaseModel

class Knowledge(BaseModel):
    __tablename__ = "knowledge"  # 知识库元数据表
    file_name = Column(String(100), nullable=False, comment="上传的文件名")
    file_path = Column(String(200), nullable=False, comment="文件存储路径")
    file_type = Column(String(20), nullable=False, comment="文件类型(PDF/TXT/Word)")
    content_length = Column(Integer, default=0, comment="文件内容长度(字符数)")
    description = Column(Text, nullable=True, comment="文件描述(如:2024年选课通知)")
    vector_status = Column(Integer, default=0, comment="向量入库状态:0=未入库,1=已入库")

3.3 数据校验层:知识库文件 + 答疑请求(前两篇:拒绝脏数据)

3.3.1 app/schemas/knowledge.py 知识库校验
# app/schemas/knowledge.py
from pydantic import BaseModel, Field, HttpUrl
from typing import Optional, List
from fastapi import UploadFile

class KnowledgeCreate(BaseModel):
    description: Optional[str] = Field(None, max_length=500, comment="文件描述")

class KnowledgeResponse(BaseModel):
    id: int
    file_name: str
    file_type: str
    content_length: int
    description: Optional[str]
    vector_status: int
    created_at: str
    class Config: from_attributes = True

class QARequest(BaseModel):
    question: str = Field(..., min_length=2, max_length=500, message="问题长度2-500字")
    top_k: Optional[int] = Field(5, ge=1, le=10, message="检索相关文档数量1-10")

3.4 工具函数层:文件处理 + JWT(继承前两篇)

3.4.1 app/utils/file_utils.py 文件处理(LLM 知识库上传)
# app/utils/file_utils.py
import os
from config import UPLOAD_DIR
import magic  # 自动识别文件类型(避坑:防止伪装文件)

# 保存上传的知识库文件
def save_upload_file(file: UploadFile) -> str:
    # 自动识别文件类型(避坑:不是只看扩展名)
    file_type = magic.from_buffer(file.file.read(1024), mime=True)
    file.file.seek(0)  # 重置文件指针

    # 限制文件类型(仅支持PDF/TXT/Word)
    allowed_types = ["application/pdf", "text/plain", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]
    if file_type not in allowed_types:
        raise ValueError("仅支持PDF/TXT/Word文件")

    # 生成唯一文件名
    file_ext = os.path.splitext(file.filename)[1]
    unique_filename = f"{file.filename.replace(file_ext, '')}_{int(time.time())}{file_ext}"
    file_path = os.path.join(UPLOAD_DIR, unique_filename)

    # 保存文件
    with open(file_path, "wb") as f:
        f.write(file.file.read())
    return file_path, file_type.split("/")[-1]  # 返回路径+类型

# 读取文件内容(支持PDF/TXT/Word)
def read_file_content(file_path: str) -> str:
    from langchain.document_loaders import TextLoader, PyPDFLoader, Docx2txtLoader
    if file_path.endswith(".pdf"):
        loader = PyPDFLoader(file_path)
    elif file_path.endswith(".txt"):
        loader = TextLoader(file_path, encoding="utf-8")
    elif file_path.endswith(".docx") or file_path.endswith(".doc"):
        loader = Docx2txtLoader(file_path)
    else:
        raise ValueError("不支持的文件类型")
    docs = loader.load()
    return "\n".join([doc.page_content for doc in docs])
3.4.2 app/utils/jwt_utils.py JWT 权限(继承前两篇)
# app/utils/jwt_utils.py
import jwt
from datetime import datetime, timedelta
from config import JWT_SECRET_KEY

def create_token(user_id: int, username: str) -> str:
    expire = datetime.now() + timedelta(hours=24)
    payload = {"user_id": user_id, "username": username, "exp": expire}
    return jwt.encode(payload, JWT_SECRET_KEY, algorithm="HS256")

def verify_token(token: str) -> dict:
    try:
        return jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
    except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
        return None

3.5 向量库层:ChromaDB 本地存储(LLM 避坑点:零基础友好)

向量库是 RAG 的核心 —— 将文字转为向量(电脑能理解的数字),快速找到和问题相似的知识库内容。避坑:用 ChromaDB 本地版,不用装 Docker/Milvus,零基础 5 分钟搞定。

3.5.1 app/vector_store/chroma_store.py 向量库操作
# app/vector_store/chroma_store.py
from langchain_community.vectorstores import Chroma
from langchain_huggingface import HuggingFaceEmbeddings
from config import VECTOR_DB_CONFIG, LLM_CONFIG
import os
import logging

# 初始化嵌入模型(把文字转向量的模型)
# 避坑:用开源的text2vec-base-chinese,中文效果好,轻量
embeddings = HuggingFaceEmbeddings(
    model_name="shibing624/text2vec-base-chinese",
    model_kwargs={"device": "cpu"}  # 用CPU跑,零基础不需要GPU
)

# 初始化Chroma向量库
def init_chroma_store():
    # 如果向量库目录不存在,创建空向量库
    if not os.path.exists(VECTOR_DB_CONFIG["persist_directory"]):
        chroma_store = Chroma(
            collection_name=VECTOR_DB_CONFIG["collection_name"],
            embedding_function=embeddings,
            persist_directory=VECTOR_DB_CONFIG["persist_directory"]
        )
        chroma_store.persist()
        logging.info("ChromaDB向量库初始化成功(本地存储,LLM避坑点:不用复杂部署)")
    else:
        # 加载已有向量库
        chroma_store = Chroma(
            collection_name=VECTOR_DB_CONFIG["collection_name"],
            embedding_function=embeddings,
            persist_directory=VECTOR_DB_CONFIG["persist_directory"]
        )
        logging.info("ChromaDB向量库加载成功")
    return chroma_store

# 将知识库文本存入向量库
def add_text_to_vector_store(text: str, metadata: dict = None):
    chroma_store = init_chroma_store()
    # 拆分文本(避坑:chunk_size/chunk_overlap影响检索精准度)
    from langchain.text_splitter import RecursiveCharacterTextSplitter
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=LLM_CONFIG["chunk_size"],
        chunk_overlap=LLM_CONFIG["chunk_overlap"]
    )
    docs = text_splitter.create_documents([text], metadatas=[metadata] if metadata else None)
    # 存入向量库
    chroma_store.add_documents(docs)
    chroma_store.persist()
    logging.info(f"向量库新增{len(docs)}条文档")
    return len(docs)

# 检索相似内容
def retrieve_similar_content(question: str, top_k: int = 5):
    chroma_store = init_chroma_store()
    # 检索top_k条相似内容
    results = chroma_store.similarity_search(query=question, k=top_k)
    logging.info(f"检索到{len(results)}条相似内容")
    return [{"content": doc.page_content, "metadata": doc.metadata} for doc in results]

3.6 LLM 层:RAG 引擎(核心避坑点:防止 LLM 幻觉)

RAG 流程:

  1. 学生提问:“什么时候选课?”
  2. 向量库检索:找到「2024 年选课通知.pdf」中 “10 月 15 日 - 10 月 20 日选课” 的内容
  3. LLM 生成答案:基于检索到的内容,生成 “2024 年选课时间为 10 月 15 日 - 10 月 20 日”避坑:拒绝纯 LLM 回答,必须基于检索到的知识库内容。
3.6.1 app/llm/rag_engine.py RAG 引擎
# app/llm/rag_engine.py
from langchain_community.llms import HuggingFacePipeline
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, pipeline
from config import LLM_CONFIG
from app.vector_store.chroma_store import retrieve_similar_content
import logging

# 配置模型加载参数(避坑:CPU可跑,量化压缩)
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,  # 4位量化,模型大小从28G压缩到7G,CPU可跑
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.float16
)

# 加载本地LLM模型
def load_local_llm():
    try:
        tokenizer = AutoTokenizer.from_pretrained(LLM_CONFIG["model_path"])
        model = AutoModelForCausalLM.from_pretrained(
            LLM_CONFIG["model_path"],
            quantization_config=bnb_config,
            device_map="auto",  # 自动分配GPU/CPU
            trust_remote_code=True  # 开源模型需要
        )
        # 创建pipeline
        llm_pipeline = pipeline(
            "text-generation",
            model=model,
            tokenizer=tokenizer,
            max_new_tokens=512,  # 最大生成512字
            temperature=LLM_CONFIG["temperature"],  # 0.1=稳定,0=绝对准确
            top_p=0.9,
            repetition_penalty=1.1
        )
        # 封装为LangChain的LLM
        llm = HuggingFacePipeline(pipeline=llm_pipeline)
        logging.info("本地LLM模型加载成功(LLM避坑点:用4位量化CPU可跑)")
        return llm
    except Exception as e:
        logging.error(f"LLM模型加载失败:{e}")
        raise

# RAG核心逻辑:基于知识库回答问题
def rag_answer(question: str, top_k: int = 5) -> dict:
    # 1. 检索相似内容
    retrieved_docs = retrieve_similar_content(question, top_k)
    if not retrieved_docs:
        return {"answer": "抱歉,我没有找到相关信息,请联系管理员添加知识库", "source": []}
    
    # 2. 构建prompt(避坑:明确告诉LLM只能基于给定内容回答)
    prompt_template = """
    请你作为高校的智能答疑助手,**只能基于以下给定的知识库内容**回答学生的问题,不要添加任何额外信息或猜测:
    
    知识库内容:
    {retrieved_content}
    
    学生的问题:
    {question}
    
    回答要求:
    1. 简洁明了,用中文回答;
    2. 只回答给定知识库中的内容,不要编造;
    3. 若没有相关内容,回答“抱歉,我没有找到相关信息”;
    4. 若有多个内容,整合后回答。
    """
    retrieved_content = "\n\n".join([doc["content"] for doc in retrieved_docs])
    prompt = prompt_template.format(retrieved_content=retrieved_content, question=question)
    
    # 3. 调用LLM生成答案
    llm = load_local_llm()
    response = llm(prompt)
    
    # 4. 处理响应(去掉多余的prompt)
    answer = response.strip()
    if "抱歉,我没有找到相关信息" not in answer:
        # 只保留相关的内容,去掉LLM的额外输出
        answer = answer.split("学生的问题:")[-1].split("回答要求:")[-1].strip()
    
    return {
        "answer": answer,
        "source": [doc["content"] for doc in retrieved_docs]  # 返回参考源(避坑:可追溯)
    }

3.7 CRUD 层:知识库管理(继承前两篇:逻辑删除)

# app/crud/knowledge.py
from sqlalchemy.orm import Session
from app.models.knowledge import Knowledge
from app.schemas.knowledge import KnowledgeCreate
from app.utils.file_utils import save_upload_file, read_file_content
from app.vector_store.chroma_store import add_text_to_vector_store
from datetime import datetime
import os

# 上传知识库文件
def create_knowledge(db: Session, file, description: str = None) -> Knowledge:
    # 保存文件
    file_path, file_type = save_upload_file(file)
    # 读取文件内容
    content = read_file_content(file_path)
    # 存入数据库
    knowledge = Knowledge(
        file_name=file.filename,
        file_path=file_path,
        file_type=file_type,
        content_length=len(content),
        description=description,
        vector_status=0
    )
    db.add(knowledge)
    db.commit()
    db.refresh(knowledge)
    # 存入向量库
    try:
        add_text_to_vector_store(content, {"knowledge_id": knowledge.id})
        knowledge.vector_status = 1
        db.commit()
        db.refresh(knowledge)
    except Exception as e:
        print(f"向量入库失败:{e}")
        knowledge.vector_status = 2  # 入库失败
        db.commit()
        db.refresh(knowledge)
    return knowledge

# 查询所有知识库
def get_knowledge_list(db: Session) -> list[Knowledge]:
    return Knowledge.query.all()

# 根据ID查询知识库
def get_knowledge_by_id(db: Session, knowledge_id: int) -> Knowledge:
    return Knowledge.query.filter_by(id=knowledge_id).first()

# 逻辑删除知识库(前两篇避坑点:禁止物理删除)
def delete_knowledge(db: Session, knowledge_id: int) -> bool:
    knowledge = get_knowledge_by_id(db, knowledge_id)
    if not knowledge:
        return False
    # 逻辑删除
    knowledge.is_deleted = True
    knowledge.deleted_at = datetime.now()
    db.commit()
    # TODO:后续可以加向量库删除逻辑
    return True

3.8 API 路由层:FastAPI 自动生成文档(继承前两篇)

3.8.1 app/routers/qa.py 答疑接口
# app/routers/qa.py
from fastapi import APIRouter, Depends, HTTPException
from app.schemas.knowledge import QARequest
from app.llm.rag_engine import rag_answer

router = APIRouter(prefix="/qa", tags=["智能答疑"])

# 智能答疑接口(无需登录)
@router.post("/")
def qa_api(request: QARequest):
    try:
        result = rag_answer(request.question, request.top_k)
        return {"code": 200, "data": result}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"答疑失败:{str(e)}")
3.8.2 app/routers/knowledge.py 知识库管理接口
# app/routers/knowledge.py
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from sqlalchemy.orm import Session
from app.crud.knowledge import *
from app.schemas.knowledge import *
from app.dependencies import get_db, get_current_user

router = APIRouter(prefix="/knowledge", tags=["知识库管理"])

# 上传知识库文件
@router.post("/upload", response_model=KnowledgeResponse)
def upload_knowledge_api(
    file: UploadFile = File(...),
    description: str = None,
    db: Session = Depends(get_db)
):
    try:
        return create_knowledge(db, file, description)
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"上传失败:{str(e)}")

# 查询所有知识库
@router.get("/list", response_model=list[KnowledgeResponse])
def get_knowledge_list_api(db: Session = Depends(get_db)):
    return get_knowledge_list(db)

# 删除知识库(逻辑删除,前两篇避坑点)
@router.delete("/{knowledge_id}")
def delete_knowledge_api(knowledge_id: int, db: Session = Depends(get_db)):
    if delete_knowledge(db, knowledge_id):
        return {"message": "删除成功(逻辑删除,保留历史数据)"}
    raise HTTPException(status_code=404, detail="知识库不存在")

3.9 前端页面层:极简 HTML/CSS/JS(继承前两篇)

3.9.1 frontend/qa.html 智能答疑页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>高校智能答疑小助手</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <h1>高校智能答疑小助手</h1>
        <div class="form-group">
            <label for="question">你的问题:</label>
            <input type="text" id="question" placeholder="比如:什么时候选课?" required>
        </div>
        <div class="form-group">
            <label for="top_k">检索相关文档数量:</label>
            <input type="number" id="top_k" value="5" min="1" max="10">
        </div>
        <button id="submit-question">提问</button>
        <div id="answer" class="result"></div>
        <div id="source" class="result"></div>

        <script>
            document.getElementById('submit-question').addEventListener('click', function() {
                const question = document.getElementById('question').value;
                const top_k = document.getElementById('top_k').value;
                if (!question) return;

                fetch('http://localhost:8000/qa/', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({question, top_k})
                }).then(res => res.json()).then(data => {
                    if (data.code === 200) {
                        document.getElementById('answer').innerHTML = `<h3>回答:</h3><p class="success">${data.data.answer}</p>`;
                        document.getElementById('source').innerHTML = `<h3>参考知识库:</h3><pre class="success">${data.data.source.join('\n\n----------------\n\n')}</pre>`;
                    } else {
                        document.getElementById('answer').innerHTML = `<p class="error">${data.detail}</p>`;
                    }
                }).catch(err => {
                    document.getElementById('answer').innerHTML = `<p class="error">提问失败:${err.message}</p>`;
                });
            });
        </script>
    </div>
</body>
</html>

四、项目运行与测试(默认环境已搭建)

4.1 准备工作:下载模型

  1. 下载Qwen-1.5-7B-Chat模型:https://modelscope.cn/models/Qwen/Qwen1.5-7B-Chat(选择pytorch版本)
  2. 下载text2vec-base-chinese嵌入模型:会自动通过 HuggingFace 下载,无需手动
  3. 将 Qwen 模型解压到./models/Qwen-1.5-7B-Chat目录

4.2 安装依赖

pip install fastapi sqlalchemy pydantic[email] python-dotenv uvicorn pymysql pandas openpyxl python-jose langchain langchain-community langchain-huggingface chromadb transformers torch bitsandbytes python-magic

4.3 生成数据库表

修改main.py

# main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import qa, knowledge
from config import Base, engine, setup_logging
import uvicorn

# 创建所有数据库表(第一次运行必须执行)
Base.metadata.create_all(bind=engine)

# 创建FastAPI应用
app = FastAPI(title="高校智能答疑小助手", version="1.0")
# 解决跨域问题
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"]
)

# 注册路由
app.include_router(qa.router)
app.include_router(knowledge.router)

# 运行
if __name__ == "__main__":
    uvicorn.run(app="main:app", host="0.0.0.0", port=8000, reload=True)

4.4 运行项目

执行python main.py,访问:

  • API 文档:http://localhost:8000/docs
  • 答疑页面:直接打开frontend/qa.html
  • 知识库管理:直接打开frontend/knowledge.html

五、项目上线部署(Docker 一键部署)

# Dockerfile
FROM python:3.12-slim
WORKDIR /app

# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制模型和代码
COPY models/ ./models/
COPY vector_db/ ./vector_db/
COPY app/ ./app/
COPY config.py ./
COPY constants.py ./
COPY .env ./
COPY main.py ./

# 暴露端口
EXPOSE 8000

# 运行
CMD ["python", "main.py"]

执行部署命令:

# 构建镜像
docker build -t llm-qa-system .
# 运行容器
docker run -d -p 8000:8000 llm-qa-system

六、LLM 特有踩坑复盘(100% 会遇到)

6.1 坑 1:LLM 模型加载失败(内存不足)

  • 原因:Qwen-1.5-7B-Chat 未量化,占用 28G 内存
  • 解决:用bitsandbytes 4 位量化,代码中已配置load_in_4bit=True

6.2 坑 2:向量检索不准确

  • 原因chunk_size太大或太小(默认 500 合适)
  • 解决:调整.env中的CHUNK_SIZECHUNK_OVERLAP

6.3 坑 3:LLM 还是有幻觉

  • 原因:prompt 模板没有明确限制 LLM 只能基于知识库回答
  • 解决:代码中已在 prompt 模板明确 “只能基于以下给定的知识库内容回答”

6.4 坑 4:文件上传失败


七、实战小练习(巩固前两篇 + LLM 避坑点)

  1. 给答疑接口加 JWT 权限,只有学生能提问;
  2. 支持上传 Excel 格式的知识库;
  3. 给回答加 “相似度评分”,显示每条参考源的相似度;
  4. 用 Git 将项目提交到 GitHub/Gitee。

八、总结:从 “工程化” 到 “LLM 智能应用” 的跨越

本次项目100% 覆盖前两篇的工程化避坑点,同时解决了 LLM 特有的坑:

  • ✅ 工程化目录规范
  • ✅ 逻辑删除(禁止物理删除)
  • ✅ 数据校验(拒绝脏数据)
  • ✅ 连接池(数据库优化)
  • ✅ 日志系统(用 logging 代替 print)
  • ✅ LLM 幻觉(RAG 架构)
  • ✅ 向量库选择(ChromaDB 本地版)
  • ✅ 模型加载(4 位量化 CPU 可跑)
  • ✅ 工程化部署(Docker 一键部署)

作为零基础学员,你已经从「普通 Python 项目」跨越到「LLM 智能应用」,掌握了企业级 LLM 项目的开发流程和核心避坑点。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值