项目工作总结
项目:中华心法问答系统
整理时间:2026-11-03
一、项目概览
中华心法问答系统是一套面向传统文化问答场景的检索系统:Web 端(Jinja2 + 原生 JS)、uni-app 小程序(H5 / 微信小程序)、Flask + MySQL 后端,支持四种语义向量模型、语音输入与播报、知识库回收站,以及面向运维的部署与打包方案。
| 维度 | 现状 |
|---|---|
| 后端 | Flask(app-factory + Blueprint),138+ 路由,MySQL 8/9 |
| 前端 | Jinja2 + 原生 JS,设计系统static/css/app.css(宣纸 / 朱砂) |
| 小程序 | uni-app,8 个页面,H5 与微信小程序双端可构建 |
| 知识库 | 1529 条已入库问答 |
| 用户 | 18 个(user / admin / superadmin) |
| 向量模型 | BERT-base(768)、LAUR 混合权重(768,11 档比例)、BGE-large-zh-v1.5(1024)、Conan-embedding-v1(1792) |
| 模型切换 | 异步 + 进度百分比 + 完成后自动刷新;缓存命中约 3–5 秒 |
| 语音 | Web Speech ASR + Vosk 离线兜底;edge-tts + Windows SAPI 兜底 |
| 部署 | 腾讯云 + 宝塔 + Nginx + Gunicorn;app / bert / bge / conan 四个部署包 |
| 主版本服务 | http://127.0.0.1:5000 |
| 检索实验环境 | F:\mdqa\xinfa-b,http://127.0.0.1:5001,独立库xinfa_db_b |
二、界面统一与小程序
统一全站视觉
- 以原有 mdqa 模板为参考,建立统一设计令牌:
--cinnabar #8C3A1E、--paper #F5F0E8、--card #FFFDF9、--ink #241C18、--line #E2D6C6 - 统一组件:页头
.page-head、卡片.card、按钮.btn、徽标.badge、表格.table-wrap、空状态.empty-state - 所有页面继承
templates/base.html,统一导航、表单、表格与空状态 - 整理目录结构,清理历史备份与临时文件
小程序重写
- 重写 8 个页面:首页、搜索、详情、标签、提交、留言板、个人中心、登录
- H5 与 mp-weixin 两端构建通过,接口与 Web 端对齐
- 文档:
docs/小程序功能对齐PRD.md、docs/小程序后端接口对齐.md - 截图:
docs/screenshots/uniapp/*.png
三、语音输入与播报
能力
- 语音输入:浏览器 Web Speech API 优先,失败时走
/api/asr(Vosk 离线模型vosk-model-small-cn-0.22) - 语音播报:
/api/tts(edge-tts),失败自动降级 Windows SAPI - 支持音色、语速设置,并按页面持久化
- 搜索结果卡片动态生成「朗读」按钮
做过的稳定性修复
| 问题 | 根因 | 处理 |
|---|---|---|
| 朗读没有声音 | stop()把audioEl置空 | 保留元素,只清src |
| 动态按钮点不动 | 事件只绑首屏节点 | 改为 document 捕获阶段事件委托 |
| 前端修改不生效 | Jinja 模板缓存 + 静态缓存 | 重启后端、资源加?v=版本号 |
| 切换音色/点击闪退 | 语音对象状态竞争 | 播放令牌 + 停止旧实例 + 异常兜底 |
| edge-tts 503 / 0 字节缓存 | 服务偶发失败、半成品写入 | 重试、超时、.part原子替换、最小字节校验 |
四、云服务器部署
- 打包脚本
scripts/package_for_deploy.ps1:-IncludeVosk带离线语音模型-IncludeBge带 BGE 模型-IncludeConan带 Conan 模型-SkipModel只打代码包
- 部署配置:
deploy/nginx.conf、deploy/xinfa_qa.service、gunicorn_config.py- gunicorn:
workers=1、gthread、threads=8、timeout=300、bind 127.0.0.1:8000
- gunicorn:
- 指南:
deploy/部署指南-腾讯云宝塔.md(Python 3.11、MySQL、Nginx、COS、systemd、升级 SQL) requirements.txt的 20 个依赖在 Python 3.11 下校验通过;BGE / Conan 依赖拆分为requirements-bge.txt、requirements-conan.txt
当前部署包:
xinfa_qa_app_20261031_1251.zip 85.1 MB (代码 + 四模式向量缓存) xinfa_qa_bert_20261029_1446.zip 364.5 MB xinfa_qa_bge_20261029_1446.zip 721.6 MB xinfa_qa_conan_20261031_1251.zip 732.0 MB五、知识库治理
超级管理员删除
- 搜索结果卡片新增「移入回收」,仅
role == 'superadmin'渲染 - 后端
POST /admin/delete-content/<type>/<id>二次校验角色,非超管返回 403 - 布局:朗读在左、移入回收在右,危险色描边 + 二次确认
软删除与回收站
question / video / article增加is_deleted、deleted_at、deleted_by_id- 通过 SQLAlchemy
do_orm_execute+with_loader_criteria全局过滤,删除后全站不可见 - 删除问答时从内存向量索引移除并刷新缓存
- 新增「已删除数据」页面(仅超管):恢复 / 永久删除;恢复时重新加入索引
批量、筛选与操作日志
- 批量恢复 / 批量永久删除(
/admin/deleted-content/batch,单次 ≤200) - 按类型、删除时间区间筛选
operation_log表记录 delete / restore / purge:操作人、对象、IP、时间- 回收站页展示最近 30 条日志
- 迁移 SQL 写入 README 与部署指南
六、小助手
- 仅首页 / 问答页加载,默认右下角,可拖拽并本地持久化位置
- 尺寸随视口自适应(
clamp(76, min(7vw, 26vh), 240)),2560 分辨率下不再过小 - 三态动画:
- 待机:
assistant-anim.webp(stand.gif,71 帧,5.68 秒循环) - 搜索后:
assistant-search.webp(search.gif,90 帧,7.2 秒循环) - 切换用 160ms 淡入淡出
- 待机:
- 统一画布 420×690、人物高度 618px,切换不跳大小
- 双击播报「今天有什么想问的」;右键复位位置
- 透明背景干净(搜索态约 77% 透明,无白边)
七、模型体系与切换体验
默认模型
- 默认模式改为 BGE(
MODEL_MODE默认bge,.env可覆盖) - 切换成功后 1.6 秒自动刷新页面,并提示「页面即将刷新…」
按模式缓存
- 缓存按模式拆分:
data/embeddings/vectors_{bert,laur,bge}.npz - 指纹包含问题集合、模式、维度、权重、模型路径;数据或配置变化才重建
- 旧版单文件缓存首次启动自动迁移
- 切换改为后台线程 + 轮询
/api/model_switch_progress,进度示例:读取问答数据 241/1529 6%正在生成向量 600/1529(40%)载入向量缓存… 60%✅ 已切换到 XXX 模式,页面即将刷新…
LAUR 混合比例
- 滑块范围
BERT 0.0 : 1.0 LAUR至1.0 : 0.0,共 11 档 - 异步切换 + 同一进度接口,按比例分文件缓存
- 加速技巧:LAUR 向量 =
normalize(BERT 向量 × 权重矩阵),直接由 BERT 缓存推导,3.2–3.8 秒完成(此前约 70 秒)
缓存误判修复
- 问题:指纹含
hybrid_ratio,在 LAUR 下调过比例后切 BGE 会被判缓存失效,重新编码约 12 分钟 - 修复:BERT / BGE 模式对 0.0–1.0 共 11 个比例逐一比对指纹
- 结果:LAUR(0.4) ↔ BGE 来回切换 2.8–3.1 秒
Conan-embedding-v1 集成
- 结构:BERT-large 中文(24 层 / 1024)+ mean pooling + Dense 1024→1792
- 懒加载(1.24GB,切到 CONAN 才加载),独立缓存
vectors_conan.npz(9.7MB) - 与其它模式对齐:按钮、进度、自动刷新、失败回滚
- 首次构建 472 秒;热切换 3.7–5.2 秒
性能对比(1529 条,CPU):
| 模式 | 维度 | 单条编码 | 单次检索 | 文档吞吐 | 热切换 | 缓存 | 内存 |
|---|---|---|---|---|---|---|---|
| BERT | 768 | 46.4 ms | 65 ms | 13.3 条/s | 4.33 s | 4.2 MB | 963 MB |
| LAUR | 768 | 29.8 ms | 38 ms | 13.1 条/s | 3.80 s | 4.2 MB | 977 MB |
| BGE | 1024 | 399.2 ms | 198 ms | 1.9 条/s | 4.47 s | 5.6 MB | 984 MB |
| Conan | 1792 | 132.8 ms | 102 ms | 3.5 条/s | 5.19 s | 9.7 MB | 2168 MB |
C-MTEB(论文 Table 3):Conan72.62,BGE-large-zh-v1.5 官方约 64.5。
详细报告:docs/Conan模型集成与性能对比.md
八、标注与检索优化方案
产出docs/检索与标注优化方案.md:
- 现状诊断:120 条扰动 query 自检索,BERT Recall@1 0.55、BGE 0.95、Conan 0.92;缺少固定评测集
- 标注难的四层解法:
- L0 自动弱标签(LLM 改写问法、答案反推问题、难负例)
- L1 隐式反馈(点击、停留、换词、无结果)
- L2 LLM 预标注 + 人工抽检(成本约为全人工的 1/8)
- L3 主动学习(只标模型最不确定的 100–200 条)
- 检索四阶段路线:
- 混合检索(BM25 + 向量 RRF)、多路召回、置信度阈值、去重
- 查询改写、意图分类、答案分块
- 重排(bge-reranker)
- 微调 / 蒸馏 / RAG 摘要
- A/B/C 方案对比:1 周轻量 / 3 周标准 / 6 周完整
九、检索实验:混合检索与重排
为避免影响线上版本,单独建立了隔离环境:
- 目录
F:\mdqa\xinfa-b(代码副本,模型只读引用主目录) - 数据库
xinfa_db_b(完整复制,1529 条问答、18 用户) - 端口 5001,主版本 5000 不受影响
混合检索
- BM25:jieba 分词,问题权重 3、关键词 2、答案 1;k1=1.5、b=0.75
- 向量 Top100 + BM25 Top50 → RRF(k=60,w_dense=1.0,w_bm25=0.7)
- 近似问题去重(字符 Jaccard ≥0.9)
- 低置信标记:Top1 相似度 < 0.5
/search与/api/search均已接入;开关RETRIEVAL_MODE=hybrid|dense
评测集与脚本
scripts/eval/build_eval_set.py:自动生成 300 条扰动 query(截断 / 丢字 / 口语前缀 / 关键词式),正例已知;100 条 test、200 条 devscripts/eval/run_eval.py:dense / hybrid / hybrid+rerank 对比,指标 Recall@1、Recall@10、MRR、nDCG@10、低置信率、P95 延迟
重排
- 模型:
bge-reranker-base(CrossEncoder,1.06GB,CPU) - 对 RRF 融合后的 Top5 精排;只对问题文本打分,5 条约 0.11 秒
- 开关:
RERANK=1、RERANK_TOP_K=5
低置信兜底
- 后端返回
low_confidence - 前端提示条:「🔍 这条问题没有找到特别匹配的答案?去提问留言板…」
标注后台
/admin/search-eval(管理员 / 超管),导航栏「检索评测」- 展示 query + Top6 候选(问题 / 答案 / 相似度 / 正例标记),三档标注(2 相关 / 1 部分 / 0 不相关),保存后自动跳下一条
- 表
relevance_label(query + question 唯一,记录标注人与时间) - 接口
POST /admin/search-eval/label
评测结果(100 条 test,BGE)
| 指标 | dense | hybrid | hybrid+rerank(Top5) | hybrid+rerank(Top10) |
|---|---|---|---|---|
| Recall@1 | 0.900 | 0.930 | 0.950 | 0.960 |
| Recall@10 | 0.980 | 0.990 | 0.990 | 0.990 |
| MRR | 0.927 | 0.954 | 0.964 | 0.973 |
| nDCG@10 | 0.940 | 0.963 | 0.970 | 0.977 |
| P95 延迟 | 328ms | 502ms | 755–902ms | 1844ms |
结论:Top5 重排以约 +0.2 秒换 Recall@1 +2pt;Top10 再多 +1pt 但延迟翻倍,默认取 Top5。
十、查询改写实验(未采用)
- 实现了规则式改写:去口语前缀、口语词归一(啥 / 咋 / 为啥 / 能否)、去语气词、长查询关键词提取、15 组领域同义词扩展
- 向量检索使用归一化 query;BM25 使用原始 + 归一化 + 关键词 + 同义词四路 RRF
- 前端显示「💡 已按「xxx」理解你的问题」
评测结果:
| 指标 | 不改写 | 改写 |
|---|---|---|
| Recall@1(普通集) | 0.950 | 0.950 |
| Recall@1(口语集) | 0.933 | 0.933 |
| MRR(口语集) | 0.949 | 0.945 |
| P95(口语集) | 951ms | 1050ms |
结论:规则改写对「BGE + hybrid + rerank」没有增益,同义词扩展还略降 MRR、增加约 100ms。已完整回退该功能(删除模块、还原检索链路、移除接口字段与前端提示、清理相关评测数据),环境恢复到「混合检索 + 重排 + 低置信 + 标注后台」状态。
十一、关键指标
检索(100 条 test)
- dense:Recall@1 0.900 / MRR 0.927 / nDCG@10 0.940 / P95 328ms
- hybrid:0.930 / 0.954 / 0.963 / 502ms
- hybrid + rerank Top5:0.950 / 0.964 / 0.970 / 755–902ms
- BERT 基线下 hybrid 提升更明显:Recall@1 0.570 → 0.760(+19pt)
模型切换
| 操作 | 耗时 |
|---|---|
| BERT → LAUR(缓存) | 5.8s |
| LAUR → BGE(缓存) | 2.8–4.2s |
| BGE → Conan(缓存) | 3.7–5.2s |
| LAUR 比例切换(缓存 / 推导) | 3.2–3.8s |
| 首次构建 BERT / LAUR / BGE / Conan | 77s / 148s / 753s / 472s |
向量缓存
| 文件 | 大小 | 说明 |
|---|---|---|
vectors_bert.npz | 4.2MB | 768 维 |
vectors_bge.npz | 5.6MB | 1024 维 |
vectors_laur_*.npz | 各 4.2MB | 11 档比例(0.0–1.0,步长 0.1) |
vectors_conan.npz | 9.7MB | 1792 维 |
vectors_laur_0.00.npz显示 0MB,实际为 20KB 高度可压缩文件,内容完整。
十二、产物清单
核心代码模块
| 路径 | 作用 |
|---|---|
app/modules/qa/__init__.py | 搜索、问答、回收站、删除、历史等主路由 |
app/modules/admin/__init__.py | 模型切换、比例、配置、评测路由 |
app/modules/api/__init__.py | H5 / 小程序 API |
app/qasystem.py | 四模型加载、缓存、异步切换、比例推导 |
app/modules/voice/、app/modules/asr/ | TTS / ASR |
static/js/voice.js | 语音输入与朗读 |
static/js/assistant.js | 小助手三态、拖拽、双击语音 |
templates/deleted_content.html | 已删除数据(回收站) |
models.py | Question / PendingQuestion / OperationLog / EvalQuery 等 |
检索实验新增
| 路径 | 作用 |
|---|---|
app/modules/retrieval/__init__.py | BM25 + RRF + 去重 + 重排 + 低置信 |
templates/search_eval.html | 标注后台 |
scripts/eval/build_eval_set.py | 生成 300 条评测集 |
scripts/eval/run_eval.py | dense / hybrid / rerank 跑分 |
B-方案说明.md | 检索实验全记录 |
文档
README.md(界面、小程序、语音、部署、检索、模型)deploy/部署指南-腾讯云宝塔.mddocs/项目技术栈与团队协作指南.mddocs/小程序功能对齐PRD.md、docs/小程序后端接口对齐.mddocs/Conan模型集成与性能对比.mddocs/检索与标注优化方案.mddocs/项目工作总结.md
十三、当前运行状态
| 服务 | 地址 | 说明 |
|---|---|---|
| 主版本 | http://127.0.0.1:5000 | 稳定版本,检索实验期间未改动 |
| 检索实验 | http://127.0.0.1:5001 | 混合检索 + 重排 + 低置信 + 标注后台 |
| 实验数据库 | xinfa_db_b | 与主库隔离 |
| 模型 | 主目录models/(只读) | BERT / BGE / Conan / reranker |
实验数据:300 条评测 query(100 test / 200 dev)、1 条人工标注、33 条检索日志。
健康检查:
GET / 200 POST /search 200(hybrid) POST /api/search 200(mode=hybrid、reranked=true) GET /admin/search-eval 200(管理员)十四、经验与坑
| 类别 | 问题 | 结论 |
|---|---|---|
| 编码 | PowerShell 写中文变 GBK 乱码 | 统一 UTF-8(写入工具 / Python),脚本尽量 ASCII |
| 文件系统 | exFAT 原子写报EISDIR | 新建文件先建空文件再写入 |
| Flask | 改模板 / 静态后不生效 | Jinja 模板缓存需重启后端;静态加?v= |
| SQLAlchemy | 新增deleted_by_id导致submitted_by外键歧义 | 显式foreign_keys=[submitted_by_id] |
| SQLAlchemy | 列名query覆盖Model.query | 改为query_text |
| 软删除 | 多处查询需要过滤 | do_orm_execute+with_loader_criteria全局过滤 |
| 缓存 | 指纹含hybrid_ratio导致 BGE 误重建 | 非混合模式对比例容错匹配 |
| 缓存 | 中断写入产生 0 字节缓存 | 启动校验大小 / 维度,失败自动重建 |
| 重排 | FlagReranker与 transformers 5 不兼容 | 改用sentence-transformers.CrossEncoder |
| 重排 | 打分返回 ndarray,长度判断失效 | list(scores)归一化 |
| 重排 | 答案全文拼入 pair 太慢 | 只对问题文本打分,Top5 精排 |
| 数据 | 测试期误删 1 条真实问答(1530→1529) | 永久删除需更严格二次确认;定期备份 |
| 进程 | 重启偶发端口占用 | 先按端口杀进程、确认释放后再启动 |
十五、后续建议
- 标注 → 训练重排:先用标注后台标 50–100 条,再用
relevance_label训练或蒸馏小重排模型。 - LLM 查询改写:仅在低置信 / 长查询时触发,避免全量 +100ms 开销。
- 检索方案投产:把
retrieval模块合并回主版本,加开关灰度,重排只对首屏 Top5 生效。 - 长答案分块:对 500 字以上答案切块检索再聚合,提升细粒度问题命中。
- 数据安全:MySQL 每日备份 + 回收站保留策略 + 永久删除二次确认(输入标题 / 密码)。
- 监控:检索 P95、低置信率、模型切换耗时、缓存命中率接入看板。
十六、截图索引
| 主题 | 截图 |
|---|---|
| 首页 / 登录 / 个人中心 | docs/screenshots/{home,login,profile}.png |
| 审核 / 评分 / CSV 导入 | docs/screenshots/{review,rating_stats,csv_import}.png |
| 小程序 8 页 | docs/screenshots/uniapp/*.png |
| 语音 | docs/screenshots/voice/*.png |
| 删除 / 回收站 | docs/screenshots/ui/{superadmin-delete,deleted-page,recycle-page}.png |
| 小助手 | docs/screenshots/ui/{assistant-idle,assistant-search-pose}.png |
| 模型切换 / 比例 | docs/screenshots/ui/{switch-progress,ratio-progress,conan-active}.png |
| 检索实验 | F:\mdqa\xinfa-b\docs\screenshots\b\{search-eval,low-conf,rewrite-hint}.png |