AI 助手模块工作流程技术文档
一、系统架构总览
复制代码
┌──────────────────────────────────────────────────────────┐
│ 前端 (Vue 3) │
│ index.vue --- ai.js (API 客户端 + SSE 流式接收) │
└────────────────────────┬─────────────────────────────────┘
│ HTTP / SSE
▼
┌──────────────────────────────────────────────────────────┐
│ API 层 (FastAPI) │
│ ai_controller.py --- 路由分发 + 意图识别 + 模式编排 │
│ IntentRouter --- 关键词 + RAG 混合意图识别 │
└────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 服务层 │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ ai_service │ │rag_service │ │education_tools│ │
│ │ (LLM通信) │ │(RAG检索) │ │ (8个教育工具) │ │
│ └──────┬───────┘ └──────┬───────┘ └───────────────┘ │
│ ┌──────┴───────┐ ┌──────┴───────┐ ┌───────────────┐ │
│ │chat_history │ │memory_store │ │permission_ │ │
│ │_service │ │_service │ │text2sql │ │
│ │ (会话管理) │ │ (用户记忆) │ │ (SQL安全) │ │
│ └──────────────┘ └──────────────┘ └───────────────┘ │
└────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 数据层 │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ MySQL │ │ Milvus │ │ BM25 Model │ │
│ │ (会话/消息) │ │ (向量库) │ │ (稀疏索引) │ │
│ └──────────────┘ └──────┬───────┘ └───────────────┘ │
│ ┌──────────────┐ ┌──────┴───────┐ │
│ │ qa_pairs.json│ │ 四大名著.txt │ (原始语料) │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ AI 大模型 (阿里云 Qwen 系列) │
│ qwen-max / qwen-plus --- 对话、意图识别、Text2SQL │
│ text-embedding-v4 --- 文本向量化 (1024维) │
└──────────────────────────────────────────────────────────┘
二、离线数据处理流水线(知识库构建)
2.1 整体流程
复制代码
《三国演义》.txt (原始文本)
│
▼
[txt_parser.py]
parse_txt_file() --- 读取 TXT 文件(自动检测编码 utf-8/gbk)
clean_text() --- 清除 HTML 标签,统一格式
extract_chapters() --- 按 "第X回" 正则拆分章节
│
▼
章节列表 [{index, title, content}, ...] (120章, 约60万字)
│
├──────────────────┬──────────────────┐
▼ ▼ ▼
[text_splitter] [generate_qa_pairs] [BM25Encoder]
文本分块 AI生成问答对 训练词频模型
│ │ │
▼ ▼ ▼
chunks (~2500块) qa_pairs.json bm25_model.json
每块500字重叠50字 (~600条) 稀疏向量权重
│ │
└────────┬─────────┘
▼
[vdb_init.py / import_qa_to_milvus.py]
将数据导入 Milvus 向量库
│
▼
┌────────────────────────────────────────────────────┐
│ Milvus 向量库 (8个集合) │
│ │
│ sgyy_chunks (1768行) │ sgyy_qa_pairs (1744行)│
│ hlm_chunks (5050行) │ hlm_qa_pairs (3008行)│
│ xyj_chunks (1928行) │ xyj_qa_pairs (1514行)│
│ shuihu_chunks (2305行) │ shuihu_qa_pairs(1723行)│
└────────────────────────────────────────────────────┘
2.2 各技术切片详解
2.2.1 文档解析 --- txt_parser.py
| 函数 |
职责 |
技术细节 |
parse_txt_file() |
读取 TXT 文件 |
自动尝试 utf-8/gbk/gb2312/utf-16 编码 |
clean_text() |
清洗文本 |
正则替换 HTML 标签为换行,压缩多余空行,去除中文间空格 |
extract_chapters() |
按章节拆分 |
正则 ^(第[一二三四五六七八九十百零\d]+回\s*.+?)$ 匹配章节标题,返回 {index, title, content} 列表 |
2.2.2 文本分块 --- text_splitter.py
| 组件 |
技术 |
细节 |
| 主方案 |
LangChain RecursiveCharacterTextSplitter |
分隔符优先级:\n\n → \n → 。」 → !」 → ?」 → ; → , → → "" |
| 备选方案 |
_simple_split() |
无 LangChain 时按句子边界分块,保留重叠 |
| 参数 |
chunk_size=500 |
每块约 500 字 |
| 参数 |
chunk_overlap=50 |
重叠 50 字,保证上下文连贯 |
| 元数据 |
metadata |
每块携带 chapter(章节名)、chapter_index(序号)、chunk_index(块索引)、char_count(字数) |
2.2.3 问答对生成 --- generate_qa_pairs.py
| 步骤 |
说明 |
| 模型 |
阿里云 Qwen 系列 (qwen3.6-plus / qwen-max) |
| 输入 |
每章原文前 2000 字 |
| 输出 |
每个章节 5 个问答对 |
| Prompt |
要求从人物、事件、原因、结果多角度出题,输出严格 JSON 格式 |
| 后处理 |
清理 Qwen 的 <think> 思考标签,清理 markdown 代码块标记 |
| 限流 |
每 5 章暂停 3 秒,避免 API 限流 |
| 累计 |
120 章 × 5 ≈ 600 条问答对 |
2.2.4 BM25 稀疏向量 --- bm25_encoder.py
| 组件 |
说明 |
| 分词 |
优先 jieba 分词,备选单字符切分 |
| 停用词 |
过滤中英文标点、空白符 |
| 训练 |
fit(documents) --- 遍历所有文档统计词频和文档频率 |
| 词汇表 |
按词频降序分配 token_id |
| IDF 公式 |
log((N - df + 0.5) / (df + 0.5) + 1) |
| BM25 评分 |
tf_norm = (tf * (k1+1)) / (tf + k1*(1-b + b*doc_len/avg_doc_len)),参数 k1=1.5, b=0.75 |
| 查询编码 |
encode_query() --- 使用 IDF × TF 权重 |
| 空向量保护 |
添加占位符 {0: 0.001} 避免 Milvus 报错 |
2.2.5 稠密向量 --- Embedding API
| 参数 |
值 |
| API |
阿里云 DashScope https://dashscope.aliyuncs.com/compatible-mode/v1 |
| 模型 |
text-embedding-v4 (环境变量 EMBEDDING_MODEL) |
| 维度 |
1024 维 (环境变量 EMBEDDING_DIMENSIONS) |
| SDK |
openai 库兼容模式调用 |
| 批量限制 |
每批最多 10 条,get_embeddings_batch() 自动分批 |
2.2.6 Milvus 向量库 --- vdb_init.py
8 个集合命名规则:
| 小说 ID |
章节切片集合 |
问答对集合 |
标签 |
sgyy |
sgyy_chunks |
sgyy_qa_pairs |
三国演义 |
hlm |
hlm_chunks |
hlm_qa_pairs |
红楼梦 |
xyj |
xyj_chunks |
xyj_qa_pairs |
西游记 |
shz |
shuihu_chunks |
shuihu_qa_pairs |
水浒传 |
sgyy_chunks 集合 Schema:
| 字段名 |
类型 |
说明 |
chunk_id |
INT64 (主键, 自增) |
切片 ID |
text |
VARCHAR(2000) |
切片文本内容 |
chapter |
VARCHAR(100) |
所属章节名称 |
chapter_index |
INT32 |
章节序号 |
char_count |
INT32 |
切片字数 |
dense_vector |
FLOAT_VECTOR(1024) |
稠密向量 |
sparse_vector |
SPARSE_FLOAT_VECTOR |
BM25 稀疏向量 |
sgyy_qa_pairs 集合 Schema:
| 字段名 |
类型 |
说明 |
qa_id |
INT64 (主键, 自增) |
问答对 ID |
question |
VARCHAR(500) |
问题 |
answer |
VARCHAR(2000) |
答案 |
reasoning |
VARCHAR(2000) |
推理过程 |
chapter |
VARCHAR(100) |
所属章节 |
scene |
VARCHAR(200) |
场景描述 |
dense_vector |
FLOAT_VECTOR(1024) |
问题的稠密向量 |
sparse_vector |
SPARSE_FLOAT_VECTOR |
问题的 BM25 稀疏向量 |
索引配置:
| 索引 |
字段 |
类型 |
参数 |
| 稠密索引 |
dense_vector |
IVF_FLAT |
metric_type=COSINE, nlist=128 |
| 稀疏索引 |
sparse_vector |
SPARSE_INVERTED_INDEX |
metric_type=IP |
数据导入 :insert_chunks_data() 和 insert_qa_pairs_data() 每 100 条一批插入。
三、在线推理工作流
3.1 完整用户请求处理流程
复制代码
用户输入: "曹操在官渡之战用了什么计策?"
│
▼
┌─── STEP 1: API 入口 ───────────────────────────────────┐
│ POST /api/ai/chat → ai_chat() (非流式) │
│ POST /api/ai/chat/stream → ai_chat_stream() (流式) │
│ │
│ ├─ 参数提取: user_message, mode, session_id │
│ ├─ RBAC 鉴权: 需 AI_CHAT 权限 (admin/teacher) │
│ └─ 自动创建会话: 若 session_id 为 None │
└─────────────────────────────────────────────────────────┘
│
▼
┌─── STEP 2: 记忆加载 ───────────────────────────────────┐
│ memory_store_service.build_memory_prompt(user_id) │
│ │
│ ├─ 从 UserPreference 表加载 4 个命名空间: │
│ │ preferences---对话风格偏好 │
│ │ profile---兴趣话题 │
│ │ interests---用户画像信息 │
│ │ facts---提取的事实信息 │
│ ├─ 构建记忆提示词, 例如: │
│ │ "【用户长期记忆】 │
│ │ 用户偏好的对话风格:喜欢风趣幽默的回答 │
│ │ 用户感兴趣的话题:编程、文学、历史 │
│ │ 用户画像:25岁男程序员 │
│ │ 关于用户:用户名叫张三,喜欢读红楼梦" │
│ └─ 超时兜底: 5秒超时返回空字符串 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─── STEP 3: 意图识别 ───────────────────────────────────┐
│ IntentRouter.recognize_async(user_message) │
│ (若 mode 已指定则跳过, 直接路由) │
│ │
│ 匹配顺序 (优先级降序): │
│ 1. CRITICAL_KEYWORDS → "critical" │
│ 自杀/自残/不想活/活不下去了... │
│ 2. rag_service.detect_novel() → "knowledge" │
│ 四大名著关键词匹配 │
│ 3. IMAGE_KEYWORDS → "image" │
│ 画/图片/生成图/作图/画画... │
│ 4. TOOL_KEYWORDS → "tools" │
│ 评语/违纪/校规/面试/分班... │
│ 5. QUERY_KEYWORDS → "text2sql" │
│ 查询/统计/排名/成绩单/就业率... │
│ 6. LIN_DAIYU_KEYWORDS → "lin_daiyu" │
│ 颦儿/诗词/黛玉...(强制林黛玉模式) │
│ 7. (无匹配) → "general" │
│ 默认模式 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─── STEP 4: 模式分发 ───────────────────────────────────┐
│ 根据 intent 分发到 6 个处理函数 │
│ │
│ critical ──→ _handle_critical() (危机干预) │
│ knowledge ──→ _handle_knowledge_mode() (名著的RAG) │
│ image ──→ _handle_image_mode() (文生图) │
│ text2sql ──→ _handle_text2sql_mode() (SQL查询) │
│ tools ──→ _handle_tools_mode() (教育工具) │
│ lin_daiyu/ │
│ general ──→ _handle_chat_mode() (林黛玉对话) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─── STEP 5: 消息持久化 ──────────────────────────────────┐
│ _save_chat_messages() │
│ │
│ ├─ chat_history_service.save_messages() │
│ │ → 写入 ChatMessage 表 + 更新 ChatSession 统计 │
│ ├─ chat_history_service.trigger_summary_if_needed() │
│ │ → 消息数 ≥ 20 时自动生成会话摘要, 写入 │
│ │ SessionSummary 表 │
│ └─ memory_store_service.extract_facts_from_messages() │
│ → AI 从对话中提取用户事实, 存入 │
│ UserPreference 表 (facts 命名空间) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─── STEP 6: 响应返回 ───────────────────────────────────┐
│ 非流式: BaseResponse.success(data=AIChatResponse) │
│ 流式: StreamingResponse (SSE 格式) │
│ 格式: {"code":200, "data":{"reply":"...", │
│ "msg_type":"knowledge", "suggestions":[...]}} │
└─────────────────────────────────────────────────────────┘
3.2 知识库检索流程(5阶段RAG管道)
复制代码
用户: "曹操在官渡之战用了什么计策?"
│
▼
detect_novel(question)
│
├─ 关键词匹配: 曹操、官渡之战 → 匹配 "sgyy"
│
▼
retrieve_novel_context(question, novel_id="sgyy")
│
▼
┌─── 阶段1: 查询预处理 ──────────────────────────────────┐
│ query_clean() --- 去特殊符号,过滤 <3字短查询 │
│ query_rewrite_llm() --- 口语化→检索式(当前为空实现) │
│ query_expand() --- 同义词扩展(曹操→曹孟德/曹阿瞒等) │
│ 输入: "曹操在官渡之战用了什么计策" │
│ 输出: "曹操在官渡之战用了什么计策 曹孟德 曹阿瞒 曹公" │
└─────────────────────────────────────────────────────────┘
│
▼
┌─── 阶段2: 多路召回 (hybrid_search) ─────────────────────┐
│ │
│ 诊断: 检查集合存在性 + get_collection_stats().row_count │
│ ├─ 集合不存在 → 返回空 (明确日志告警) │
│ ├─ 行数为 0 → 返回空 (明确日志告警) │
│ └─ 有数据 → 继续检索 │
│ │
│ ┌─ 稠密向量: _get_embedding() → DashScope text-embedding│
│ │ -v4 → 1024维 │
│ └─ 稀疏向量: _get_sparse_vector() → BM25 jieba 分词 │
│ │
│ 并行执行 (ThreadPoolExecutor + 各自 try/except): │
│ │
│ 2a. search_chunks() --- 原文切片 (output_fields: text, │
│ chapter, chapter_index, char_count) │
│ ┌─ AnnSearchRequest(稠密, COSINE, nprobe=10) │
│ └─ AnnSearchRequest(稀疏, IP) │
│ → RRFRanker(k=100) 融合 → top_k=8 │
│ │
│ 2b. search_qa_pairs() --- 问答对 (output_fields: question,│
│ answer, reasoning, chapter, scene) │
│ ┌─ AnnSearchRequest(稠密, COSINE, nprobe=10) │
│ └─ AnnSearchRequest(稀疏, IP) │
│ → RRFRanker(k=100) 融合 → top_k=5 │
│ │
│ 超时保护: RAG_TIMEOUT_SEC = 15s │
│ RRF阈值过滤: RRF_MIN_SCORE = 0.001 (RRF为排名倒数值) │
│ │
│ 召回qa=5, chunks=8, 耗时~4s │
└─────────────────────────────────────────────────────────┘
│
▼
┌─── 阶段3: 融合去重 (fusion_dedup) ───────────────────────┐
│ chunks 来源 + QA pairs 来源 → 统一列表 │
│ 去重: 按 text/question 前80字做 hash │
│ 压缩: 最多保留 15 条 │
│ 输出: qa=5, chunks=8 → 融合后=13 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─── 阶段4: Rerank 重排精筛 ──────────────────────────────┐
│ │
│ 模型: BAAI/bge-reranker-v2-m3 (本地缓存, 2.27GB) │
│ 加载: HuggingFace Transformers, local_files_only=True │
│ 降级: 模型加载失败 → 关键词过滤 (_rerank_fallback) │
│ │
│ 推理方式: 同步推理 + 事后超时检查 │
│ 超时保护: 推理耗时 > 8s → 降级为关键词过滤 │
│ 首次优化: 模型刚加载(已耗5s) → 跳过推理, 直接关键词过滤 │
│ │
│ BGE 推理: │
│ ├─ 输入: (query, candidate) 13对 │
│ ├─ Tokenizer: max_length=512 │
│ ├─ 模型: CrossEncoder → logits │
│ ├─ 归一化: torch.sigmoid(logits) → [0,1] 得分 │
│ └─ 过滤: threshold ≥ 0.30 → Top5 │
│ │
│ 保底: 如果Rerank过滤所有候选, 保留融合阶段最高分1条 │
│ │
│ 输出: qa=2, chunks=1 (命中) 或 qa=1, chunks=1 (保底) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─── 阶段5: 上下文构建与输出 ─────────────────────────────┐
│ │
│ direct模式 (QA对得分 ≥ 0.35): │
│ ├─ direct_answer = QA对答案 + 推理 + 相关问题 │
│ ├─ trim_context(direct_answer, MAX_CONTEXT_CHARS=2500) │
│ └─ 进入LLM林黛玉润色 (不走原始流式) │
│ │
│ augment模式 (QA对得分 < 0.35): │
│ ├─ context = 【相关问答】 + 【原文片段】拼接 │
│ ├─ trim_context(context, MAX_CONTEXT_CHARS=2500) │
│ └─ 注入 system_prompt 作为参考资料 │
│ │
│ 未命中 (None): │
│ └─ LLM提示: "颦儿在知识库中未查到相关内容,以下凭记忆回答"│
│ + "💡 此回答来自AI自身知识,非知识库检索结果" │
│ │
│ 流式端点额外输出 SSE 事件: │
│ data: {"type":"references", "data":{"qa_pairs":[...], │
│ "chunks":[...]}} (在[DONE]之前) │
└─────────────────────────────────────────────────────────┘
3.3 流式推理流程
复制代码
用户请求流式对话 (POST /api/ai/chat/stream)
│
▼
记忆加载 (同非流式)
│
▼
四大名著知识库检测 (detect_novel)
│
├── 命中名著 → retrieve_novel_context (5阶段管道, 超时30s)
│ │
│ ├── direct模式 → 注入system prompt, 走LLM林黛玉润色
│ ├── augment模式 → 注入上下文到system prompt
│ └── 未命中 → 提示"未查到相关内容,以下凭记忆回答"
│
├── 敏感问题检测
│ └── 调用 _handle_critical() 获得完整回复后分片输出
│
├── Text2SQL 检测
│ └── 成功时分段流式输出: 文本 → SQL 代码 → 结果表格
│
└── 通用流式
└── 构建 system prompt (含记忆+风格+RAG上下文)
→ ai_service.chat_stream()
→ SSE 逐 chunk 返回
流式额外输出 (在 [DONE] 之前):
└── data: {"type":"references", "data":{"qa_pairs":[...], "chunks":[...]}}
3.4 Text2SQL 处理流程
复制代码
用户: "查一下计算机一班有多少学生"
│
▼
_try_text2sql(question, db, user_id, current_user)
│
▼
┌─── Text2SQL 执行链 ──────────────────────────────────┐
│ 1. 构建 System Prompt │
│ ├─ 数据库表结构 (show create table) │
│ ├─ 外键关系描述 │
│ └─ 权限上下文 (用户角色、可见范围) │
│ │
│ 2. ai_service.chat() → LLM 生成 SQL │
│ ├─ prompt 要求: 只输出 SQL, 不要注释 │
│ ├─ 仅允许 SELECT, 禁止 INSERT/UPDATE/DELETE │
│ └─ SQL 提取: 从 ''```sql...'''' 块中提取 │
│ │
│ 3. validate_and_sanitize_sql() --- 安全校验 │
│ ├─ 检测写操作关键字 │
│ ├─ 移除注释和分号 │
│ └─ 验证为纯 SELECT │
│ │
│ 4. PermissionText2SQL.apply_row_level_security() │
│ ├─ 教师: WHERE class_id IN (所带班级) │
│ ├─ 学生: WHERE student_id = 自己 │
│ └─ 管理员: 无过滤 │
│ │
│ 5. execute_sql() --- 执行 SQL │
│ ├─ SET TRANSACTION READ ONLY (保证只读) │
│ └─ 超时: 10 秒 │
│ │
│ 6. clean_results() --- 清理结果 │
│ ├─ 移除自增 id 字段 │
│ ├─ Decimal → float (JSON 序列化) │
│ └─ 字段按优先级排序 │
│ │
│ 7. ai_service.chat() → AI 润色为自然语言 │
│ └─ 返回 {reply, sql, results, suggestions} │
└─────────────────────────────────────────────────────────┘
3.5 教育工具处理流程
复制代码
用户点击 "📝 期末评语" 或输入 "生成期末评语:张三"
│
▼
IntentRouter.detect_tool(user_message)
│
├── 未检测到工具名
│ └── 展示工具列表让用户选择 (AI生成)
│
└── 检测到工具名
│
├── 权限检查: teacher 工具需 role=teacher
│
├── 纯触发检测 (is_pure_trigger)
│ ├── 条件: 无前缀剥离 + 去掉关键词后剩余<3字
│ └── 返回 TOOL_ENTRY_MESSAGES 静态话术 (零AI调用)
│ 例: "(铺开宣纸,执笔蘸墨)公子要写期末评语?..."
│
└── 有实质内容
└── 调用 TOOL_MAP[tool_name]["fn"](content)
│
├── generate_comment() (期末评语)
├── generate_discipline_talk() (违纪话术)
├── polish_notice() (通知润色)
├── query_school_rules() (校规问答)
├── diagnose_scores() (成绩诊断)
├── generate_activity() (班会策划)
├── generate_interview() (模拟面试)
└── smart_grouping() (智能分班)
│
└── 所有工具通过 _call_ai() 调用 AI 服务
└── ai_service.chat([system, user])
│
▼
结果以林黛玉引语包装后返回
3.6 会话管理与长对话策略
复制代码
load_history(session_id) --- 三级加载策略
│
├── 消息数 ≤ 20 (BUFFER_THRESHOLD)
│ └── 全量加载所有消息
│
├── 20 < 消息数 ≤ 50 (WINDOW_THRESHOLD)
│ ├── 注入最新的 SessionSummary 作为上下文
│ └── + 最近 20 轮 (WINDOW_KEEP) 消息
│
└── 消息数 > 50
├── 注入最近 N 层摘要(多层摘要压缩)
└── + 最近 10 轮 (SUMMARY_KEEP) 消息
自动摘要触发:
trigger_summary_if_needed() --- 每次保存消息时检测
- 条件:
message_count % 10 == 0 且 message_count >= 20
- 调用
ai_service.chat() 用 AI 生成摘要
- 摘要写入
SessionSummary 表
四、关键技术切片说明
4.1 林黛玉角色提示词分层策略
| 层次 |
内容 |
用途 |
PERSONA_BASE |
角色基础设定:林黛玉身份、语言风格、行为约束 |
所有模式的基础人设 |
STYLE_FULL_DAIYU |
全程林黛玉风格:动作描写+半文半白 |
chat/image 模式全程保持 |
STYLE_OPENING_DAIYU |
首段林黛玉引语 + 后续标准白话 |
knowledge/text2sql/tools 模式用于开场白 |
INTENT_OPENINGS |
各模式的固定开场白模板 |
注入到 system prompt 指引回答格式 |
CRITICAL_RESPONSE_TEMPLATE |
危机干预预设话术 + 心理热线 |
检测到敏感词时直接使用 |
4.2 混合检索(Hybrid Search)技术
| 技术 |
检索方式 |
距离度量 |
参数 |
用途 |
| 稠密检索 (Dense) |
Embedding + IVF_FLAT |
COSINE |
nprobe=10 |
语义相似度匹配 |
| 稀疏检索 (Sparse) |
BM25 + SPARSE_INVERTED_INDEX |
IP (内积) |
--- |
关键词精确匹配 |
| 融合排序 (Fusion) |
RRF (Reciprocal Rank Fusion) |
k=100 |
--- |
合并两种检索结果 |
| RRF 滤除 |
RRF_MIN_SCORE = 0.001 |
--- |
score < 0.001 丢弃 |
过滤完全无关结果 |
RRF 分数说明:RRF 分数 = 1 / (k + rank),k=100 时 top1 ≈ 0.0099,远小于余弦相似度。因此 RRF 滤除阈值必须极低(0.001),不可用类似余弦相似度 0.3 这类阈值。
4.3 BGE Reranker 重排策略
| 配置项 |
值 |
说明 |
| 模型 |
BAAI/bge-reranker-v2-m3 |
中文最优CrossEncoder,2.27GB |
| 加载方式 |
local_files_only=True |
仅读取本地缓存,不触发联网下载 |
| 降级策略 |
加载失败 → _rerank_fallback |
关键词重叠度计算(无需模型) |
| 得分归一化 |
torch.sigmoid(logits) |
将任意范围 logits 映射到 [0, 1] |
| 过滤阈值 |
≥ 0.30 |
低于此值的候选片段淘汰 |
| Top-K 保留 |
5 |
Rerank 后最多保留 5 条 |
| 超时保护 |
推理 > 8s → 降级关键词过滤 |
防止 Reranker 拖垮整个管道 |
| 首次优化 |
模型刚加载 → 跳过首次推理 |
模型加载已耗 5s,跳过推理直接降级 |
| 保底机制 |
Rerank 全过滤 → 保留融合最高分 1 条 |
确保知识库至少有条目可用 |
4.4 多层 Prompt 注入机制
复制代码
最终 system prompt 的构建顺序(以 knowledge + augment 模式为例):
┌─ 1. PERSONA_BASE "你是林黛玉,金陵十二钗之首..."
├─ 2. memory_prompt "【用户长期记忆】..."
├─ 3. STYLE_OPENING_DAIYU "第一段:以1-2句林黛玉引语开头..."
├─ 4. 模式引导 "你是{sgyy}的知识专家,请引用原文回答..."
│ 或 direct模式: "以下是知识库答案,请用林黛玉口吻重新组织..."
│ 或 未命中: "未查到相关内容,以下凭记忆回答..."
├─ 5. RAG上下文 "【参考资料】\n{context}\n..."
│ 或 direct答案: "【知识库答案】\n{direct_answer}\n..."
├─ 6. INTENT_OPENING "请以以下引语开头回复:..."
└─ 7. (用户实际消息) "曹操在官渡之战用了什么计策?"
4.6 数据库表结构汇总
| 表名 |
引擎 |
主键 |
关键索引 |
Milvus 数据量 |
用途 |
chat_sessions |
InnoDB |
id (INT) |
user_id, update_time |
--- |
会话元数据 |
chat_messages |
InnoDB |
id (BIGINT) |
session_id, (session_id, create_time) |
--- |
消息内容 |
session_summaries |
InnoDB |
id (INT) |
session_id |
--- |
会话摘要 |
user_preferences |
InnoDB |
id (INT) |
user_id |
--- |
用户偏好/记忆 |
sgyy_chunks |
Milvus |
chunk_id |
IVF_FLAT + SPARSE_INVERTED |
1768行 |
三国演义原文切片 |
sgyy_qa_pairs |
Milvus |
qa_id |
IVF_FLAT + SPARSE_INVERTED |
1744行 |
三国演义问答对 |
hlm_chunks |
Milvus |
chunk_id |
IVF_FLAT + SPARSE_INVERTED |
5050行 |
红楼梦原文切片 |
hlm_qa_pairs |
Milvus |
qa_id |
IVF_FLAT + SPARSE_INVERTED |
3008行 |
红楼梦问答对 |
xyj_chunks |
Milvus |
chunk_id |
IVF_FLAT + SPARSE_INVERTED |
1928行 |
西游记原文切片 |
xyj_qa_pairs |
Milvus |
qa_id |
IVF_FLAT + SPARSE_INVERTED |
1514行 |
西游记问答对 |
shuihu_chunks |
Milvus |
chunk_id |
IVF_FLAT + SPARSE_INVERTED |
2305行 |
水浒传原文切片 |
shuihu_qa_pairs |
Milvus |
qa_id |
IVF_FLAT + SPARSE_INVERTED |
1723行 |
水浒传问答对 |
4.7 外部服务依赖
| 服务 |
用途 |
连接方式 |
配置项 |
| 阿里云 DashScope |
LLM 对话、文生图、Embedding |
OpenAI 兼容 SDK |
AI_API_KEY, AI_BASE_URL, AI_MODEL |
| Milvus |
向量数据库 |
pymilvus MilvusClient |
MILVUS_HOST:PORT, MILVUS_DB_NAME |
| MySQL |
业务数据存储 |
SQLAlchemy |
标准数据库连接串 |
五、部署与初始化
5.1 知识库初始化命令
bash
复制代码
# 1. 解析文档 + 创建 Milvus 表 + 切片 + 插入数据
python rag_demo/vdb_init.py init_all
# 或分步执行:
python rag_demo/vdb_init.py create_tables # 创建 Milvus 集合和索引
python rag_demo/vdb_init.py insert_chunks # 插入切片数据
python rag_demo/import_qa_to_milvus.py # 插入问答对数据
5.2 问答对生成
bash
复制代码
# 为所有 120 章生成问答对 (需先有《三国演义》.txt 文件)
python rag_demo/generate_qa_pairs.py
# 仅处理前 10 章 (测试用)
python rag_demo/generate_qa_pairs.py 10
5.3 环境变量配置
复制代码
# .env 文件核心配置
AI_API_KEY=sk-xxxxx # DashScope API Key
AI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
AI_MODEL=qwen-max
MILVUS_HOST=127.0.0.1
MILVUS_PORT=19530
MILVUS_DB_NAME=ai80
EMBEDDING_MODEL=text-embedding-v4
EMBEDDING_DIMENSIONS=1024
5.4 检索超时配置
| 配置项 |
旧值 |
新值 |
说明 |
| 检索整体超时 |
15s |
30s |
覆盖 Embedding API + BM25 + Milvus + Reranker 全链路 |
| 名著检测超时 |
5s |
5s |
纯关键词匹配,毫秒级,无需更改 |
| Rerank 推理超时 |
无 |
8s |
BGE 模型 CPU 推理 13 对候选可能超时,超时后降级关键词过滤 |
5.5 RAG 管道常见故障排查指南
| 现象 |
日志关键词 |
可能原因 |
解决方案 |
| 无召回结果 |
阶段2: 无召回结果 |
RRF_MIN_SCORE 过滤了所有结果 |
检查 RRF_MIN_SCORE 是否过高(应 ≤ 0.001) |
| 无召回结果 |
集合存在但为空 |
Milvus 集合有结构无数据 |
运行 vdb_init.py insert_{chunks,qa} 导入数据 |
| 检索超时 |
流式名著知识库检索 超时 |
Reranker 推理过慢阻塞管道 |
检查 Rerank 超时保护是否正常工作(≤ 8s 降级) |
| 没有命中但 LLM 回答正确 |
无引用标识 |
结果被 Rerank 过滤 + 无保底 |
检查 rerank() 中的保底逻辑是否生效 |
| 所有小说返回"未命中" |
Milvus集合状态 无输出 |
Milvus 连接失败 |
检查 MILVUS_HOST:PORT 是否可达 |
| 流式有回答但无引用数据 |
mode=chat |
外层 timeout 超时后返回 None |
延长检索超时或加速 Reranker |