[博客前置说明]
承接前两篇工程化实战:默认读者已掌握「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+) |
| FastAPI | API 开发 | 前两篇:异步、自动生成文档 |
| LangChain 0.1.20 | LLM 开发框架 | LLM 坑:避免手写复杂 prompt 工程 |
| ChromaDB | 本地向量库 | LLM 坑:零基础不用装 Milvus 等复杂向量库 |
| Qwen-1.5-7B-Chat | 开源 LLM | LLM 坑:阿里开源,中文友好,轻量(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 流程:
- 学生提问:“什么时候选课?”
- 向量库检索:找到「2024 年选课通知.pdf」中 “10 月 15 日 - 10 月 20 日选课” 的内容
- 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 准备工作:下载模型
- 下载Qwen-1.5-7B-Chat模型:https://modelscope.cn/models/Qwen/Qwen1.5-7B-Chat(选择
pytorch版本) - 下载text2vec-base-chinese嵌入模型:会自动通过 HuggingFace 下载,无需手动
- 将 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 内存
- 解决:用
bitsandbytes4 位量化,代码中已配置load_in_4bit=True
6.2 坑 2:向量检索不准确
- 原因:
chunk_size太大或太小(默认 500 合适) - 解决:调整
.env中的CHUNK_SIZE和CHUNK_OVERLAP
6.3 坑 3:LLM 还是有幻觉
- 原因:prompt 模板没有明确限制 LLM 只能基于知识库回答
- 解决:代码中已在 prompt 模板明确 “只能基于以下给定的知识库内容回答”
6.4 坑 4:文件上传失败
- 原因:未安装
python-magic,无法识别文件类型 - 解决:
pip install python-magic(Windows 需安装libmagic:https://github.com/nscaife/file-windows)
七、实战小练习(巩固前两篇 + LLM 避坑点)
- 给答疑接口加 JWT 权限,只有学生能提问;
- 支持上传 Excel 格式的知识库;
- 给回答加 “相似度评分”,显示每条参考源的相似度;
- 用 Git 将项目提交到 GitHub/Gitee。
八、总结:从 “工程化” 到 “LLM 智能应用” 的跨越
本次项目100% 覆盖前两篇的工程化避坑点,同时解决了 LLM 特有的坑:
- ✅ 工程化目录规范
- ✅ 逻辑删除(禁止物理删除)
- ✅ 数据校验(拒绝脏数据)
- ✅ 连接池(数据库优化)
- ✅ 日志系统(用 logging 代替 print)
- ✅ LLM 幻觉(RAG 架构)
- ✅ 向量库选择(ChromaDB 本地版)
- ✅ 模型加载(4 位量化 CPU 可跑)
- ✅ 工程化部署(Docker 一键部署)
作为零基础学员,你已经从「普通 Python 项目」跨越到「LLM 智能应用」,掌握了企业级 LLM 项目的开发流程和核心避坑点。
717

被折叠的 条评论
为什么被折叠?



