ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深度学习驱动的地铁智能问答:BERT微调与Faiss向量检索实战

深度学习驱动的地铁智能问答:BERT微调与Faiss向量检索实战 简介这是一份基于深度学习的地铁智能问答系统Python源码包聚焦智能交通场景面向计算机、人工智能、数据科学等相关专业在校生与从业者可用于毕业设计、课程设计、期末大作业或创新项目起步。资源共17个文件以15个Python脚本为核心另有项目说明txt和介绍md文档压缩包大小7KB源码采用Django框架组织包含subway_robot主工程、backend业务模块、数据库迁移文件以及ChineseNER命名实体识别相关代码目录划分清晰可快速理解从交互接口到业务处理的完整链路。目前已有126人学习使用且经过本地运行验证功能稳定可直接复现。既能直接体验完整问答流程也便于按需替换语料或扩展地铁知识库对希望快速上手智能问答开发的学习者具有较高参考价值。1. 智能交通里的地铁问答为什么深度学习的路子能落地乘客问「末班车是几点」另外一个人问「最后一趟车到几点」还有老人直接说「最晚那班车啥时候走」。关键词检索系统在「末班车」「最后」「最晚」面前经常翻车这类问题每天在地铁客服里被问上百次。基于深度学习的地铁智能问答系统要解决的正是这种同义多变的问法用深度模型把用户问题映射到标准问再从知识库里取回答案。这套方案用 Python 实现主线是 BERT 微调加向量检索搭配 Faiss 做候选召回适合智能交通场景里的客服机器人、智能语音助手也适合想入门深度学习和 NLP 问答的人照着源码复现并投入。2. 问答系统的整体架构与选型为什么检索式撑不住地铁口语2.1 封闭域问答的需求拆解地铁问法到底有多「散」地铁客服是典型的封闭域问答问题范围有限答案基本固定。我把地铁站被问得最多的问题归成六类运营时间、票价、换乘、车站设施、失物招领、突发运营调整。每一类下面用户的表达方式差异非常大。比如「运营时间」有人问「末班车几点」有人问「最后一趟车到几点」还有人问「晚上最晚一班是几点的」。如果系统只做关键词匹配「最后一趟」「最晚一班」和「末班车」之间没有公共关键词传统倒排索引会直接算成低相关。意图类别标准问示例口语变体示例关键词命中难点运营时间末班车几点发最后一趟车几点末班 / 最后一趟无共词票价到浦东机场多少钱去机场坐地铁要几块钱问句里没有「票价」换乘到虹桥火车站怎么换乘去虹桥站倒几号线换乘 / 倒 / 转 说法不一失物招领失物招领处电话东西落车上了怎么办没有「失物」字样更麻烦的是地铁问答经常夹带站名。「从人民广场到虹桥火车站怎么换乘」和「人民广场站可以换几号线」都包含「人民广场」和「换乘」但意图完全不同前者要路径规划后者要换乘线路查询。这类区分不能靠关键词命中要靠对整句语义的理解。这也是标题里强调「基于深度学习」的原因深度学习模型学的是句子里词与词之间的关系而不是简单有没有某几个词。我在整理真实客服日志时还发现同一个问题会被写成「虹桥火车站怎么走」「去虹桥火车站坐几号线」「到虹桥站怎么倒车」站名也经常用简称。如果做纯关键词系统必须手动维护一套庞大的同义词库还要处理简称、错别字、语气词。基于深度学习的方案把这些都丢给语义模型代价是训练数据和算力收益是维护成本大幅下降。2.2 检索式、生成式与混合式为什么最终选「分类 向量召回」地铁问答系统可选的技术路线有三类。第一类是传统检索式用 BM25 或编辑距离召回 FAQ。优点是快、可解释、好维护缺点是前面说的词汇鸿沟同义改写能力差。第二类是端到端生成式用大模型或 Seq2Seq 直接生成答案优点是灵活缺点是地铁信息不允许编造模型一旦记混站点会给乘客指错路风险不可接受。第三类是混合式用深度模型做意图分类和语义召回用知识库存放可控答案再用置信度决定是否交给人工。方案准确率天花板答案可控性开发成本维护成本传统检索式中等受限于关键词高低高同义词库要一直补生成式高但不稳定低高高要持续收集语料防幻觉分类 向量召回高高中中主要维护FAQ数据从标题「基于深度学习的地铁智能问答系统」来看绝大多数可运行的 Python 源码实现的是第三类。这一类方案训练成本可控单卡就能跑答案可控知识库不撒谎效果能解释相似度分数可以追溯。相比生成式它不需要动辄几十 G 的模型和昂贵的推理资源很适合地铁站内的客服机器人或语音助手后台。我一般还会在混合式后面加一层规则兜底。比如「今天末班车提前」这种临时运营调整很难靠静态知识库里的问答覆盖。做法是在意图识别之前先用正则匹配「提前」「临时」「停运」这类触发词命中后直接返回公告不让模型抢答。深度学习负责语义泛化规则负责硬约束两者不冲突。2.3 系统数据流从query到answer的五个环节我习惯把系统拆成五个环节预处理、意图识别、候选召回、兜底判断、答案返回。预处理主要处理全角半角、大小写、多余空格以及把「地铁站」这类明显语气词清理掉意图识别用 BERT 微调的多分类模型把问题分到运营时间、票价、换乘、失物等桶里候选召回在对应桶里用 Faiss 做向量相似度检索取 Top 5兜底判断看最高相似度分数有没有过阈值没过就不硬答答案返回直接取知识库里的标准答案不经过模型生成。这五个环节的顺序有讲究。先做意图识别再召回比全量检索要稳。因为「票价」和「换乘」两个问题在向量空间里可能离得很近如果直接在整个 FAQ 上做相似度用户问「到机场多少钱」系统可能召回「到机场坐几号线」因为句子里的共同词是「机场」。先分桶再在桶内做向量检索可以从源头减少这类跨意图误召回。每条用户问题还应该带一个会话ID和时间戳哪怕当前版本不做多轮对话。原因很简单日志回流要做线上问题要复盘。没有会话ID你很难知道同一个用户连续问了两遍是不是因为第一遍答错了。源码里如果看到 API 直接打印结果而不写日志我会先补上raw_query、intent、score、answer四个字段再上线。3. 数据清洗与知识库构建把地铁FAQ变成BERT能学的样本3.1 语料来源与标注格式如何从官网文本整理出JSON和TSV不管源码包里训练脚本写得多完整最花时间的永远是数据。地铁 FAQ 语料常见来源有三个地铁官网「乘客服务」页面、站内公告的 PDF 与 Excel、客服对话历史。前两种是标准问和答案但口语变体少第三种是真实用户问题语法乱、有错别字、有语气词但价值最高。我建议以官网 FAQ 为主客服日志做补充两边合并。标准的数据格式我会用 JSON 保存完整知识库字段包含standard_question、answer、intent、similar_questions。同时生成一个训练意图分类用的 TSV每行是一个自然语言问题和对应的意图编号。意图编号先按业务定好一张表不要随手编。intent_idintent 名称标准问示例0运营时间地铁首末班车时间1票价地铁票价怎么计算2换乘到虹桥火车站怎么换乘3车站设施站内有洗手间吗4失物招领失物招领处在哪里5运营调整今天末班车提前吗注意意图类别数量不要拍脑袋。我见过有人把「票价」拆成「单程票价」「交通卡票价」「二维码票价」三个意图结果训练数据严重不平衡小类几乎学不动。地铁是封闭域维持 5 到 10 个粗粒度意图最合适细粒度差异交给相似问匹配去解决。下面给出一个从 Excel 清洗到生成 JSON 的脚本import json import pandas as pd df pd.read_excel(raw_qa.xlsx, engineopenpyxl) df df.dropna(subset[问题, 答案]) # 去掉只有一两个字的无效问句 df df[df[问题].str.strip().str.len() 4] # 合并相同答案的问题作为相似问 merged {} for _, row in df.iterrows(): key row[答案].strip() if key not in merged: merged[key] { answer: key, intent: row[所属类别], similar_questions: [], } merged[key][similar_questions].append(row[问题].strip()) records [] for m in merged.values(): standard_question m[similar_questions][0] records.append({ standard_question: standard_question, answer: m[answer], intent: m[intent], similar_questions: m[similar_questions][1:], }) with open(data/faq.json, w, encodingutf-8) as f: json.dump(records, f, ensure_asciiFalse, indent2)逻辑说明先按答案文本合并因为同一个答案对应的不同问法可以互为相似问然后取第一条问法作为标准问其余放进similar_questions。这个脚本只做了最基础的清洗真实场景还要打掉答案里的换行、统一站名写法例如「上海站」和「上海火车站」要归一化否则向量检索会把它们当成两个独立实体。参数说明str.len() 4是最小问句长度。地铁场景里「几点」「票价」这类极短问题确实存在但训练分类器时太短的句子信息量不足。我一般会保留在原始表里单独做规则处理而不会直接丢。如果你手里的数据量够大可以把阈值改成 2后面靠意图分类的置信度去过滤。3.2 口语变体扩充用同义词替换和客服日志做数据增强假设官网整理出的标准问只有几百条直接拿去训练 BERT 意图分类很容易过拟合。我一般会做两层数据增强。第一层是规则同义词替换针对地铁领域高频词写一个替换表第二层是把客服日志里真实的 query 按意图抽出来直接加入训练集。这里给出一个可配置的替换生成器import re SYNONYMS { 末班车: [最后一趟车, 最晚一班车, 最后一班车], 换乘: [换线, 倒车, 转乘, 转线], 怎么走: [怎么去, 怎么到, 路线是], } def expand_with_synonyms(text, max_candidates20): results [text] for key, values in SYNONYMS.items(): new_results [] for item in results: for value in values: new_results.append(item.replace(key, value)) results.extend(new_results) results list(dict.fromkeys(results)) # 去重 if len(results) max_candidates: break return results逻辑说明每一轮替换生成的新句子会再进入下一轮所以如果一句话同时包含「末班车」和「换乘」就能组合出更多变体。max_candidates是上限防止组合爆炸。地铁问答的意图模型不需要海量生成每个标准问最多 20 个变体就够了再多会出现「末班车末班车」这类无意义拼接。如果你手头有真实客服日志数据增强的正确姿势是先把日志按归属意图分类再和同义词生成结果合并。真实问法里有语气词「呗」「哈」「呢」比如「末班车几点呢」这些是同义词表生成不出来的。我一般会再写一个简单的口语模板套用把「末班车几点呢」这种句子人为复制几份本质上是在教模型把语气词当作无关信息。数据增强之后要做一次去重和长度过滤。同一个标准问生成出 20 条变体其中 3 条可能在清洗后完全一样。dict.fromkeys只对单轮生成有效多轮之后还是会有重复所以建议在合并完所有来源的样本后用drop_duplicates(subset[text])再清一次。重复样本会造成验证集虚高线上真实效果反而差这个现象后面踩坑章节还会提到。3.3 训练集校验与意图映射用python脚本排查脏数据数据扩充之后还要做一次校验。最常见的脏数据是两条几乎一样的问句被标成不同意图这样的样本会把 BERT 的 loss 直接带偏。我一般会写一个简单的冲突检查import pandas as pd df pd.read_csv(data/train_intent.tsv, sep\t, encodingutf-8) # 检查同一文本是否对应多个标签 dups df[df.duplicated(subset[text], keepFalse)] conflict dups[dups[label] ! dups.groupby(text)[label].transform(first)] print(冲突样本数:, len(conflict)) print(conflict.head())这段代码会把同一个问题对应多个意图的行打出来。遇到这种情况我的处理原则是如果一条问法真的会落到两个意图比如「票价和末班车时间」那就把这条样本拆成两个训练样本分别保留上下文或者把它归到更细的子类里。不要硬保留一个错误标签否则模型会在两类之间反复震荡。TSV 里的label必须是从 0 开始的连续整数。BERT 分类模型的输出层num_labels按df[label].nunique()自动算如果发现类别编号是从 1 开始模型的输出维度会多一个训练时 loss 计算会报错或者把 0 类永远学成空。这种问题在源码排错时很常见看起来像模型代码错了实际上是数据文件里多了一列行号被读进来了。到这一步data/faq.json给向量检索用data/train_intent.tsv给意图分类微调用。两个文件可以共用同一个标准问集合但训练 TSV 里会加入大量口语变体FAQ JSON 里保持少量标准问就可以。这样的结构在后面增量更新时也方便。4. 模型训练与部署从PyTorch到Flask API的最小可跑实现4.1 项目结构源码zip里该有的目录长什么样一个规范的「地铁智能问答」Python 源码包文件结构一般是这样subway_qa/ ├── data/ │ ├── faq.json │ └── train_intent.tsv ├── models/ │ ├── intent_model/ │ └── faq.index ├── src/ │ ├── train_intent.py │ ├── build_index.py │ └── api_server.py └── requirements.txtdata/faq.json是知识库负责提供答案data/train_intent.tsv是意图分类训练集models/intent_model/是微调后的 BERT 分类模型models/faq.index是 Faiss 向量索引src/里三个脚本分别完成训练、索引构建和 API 服务。拿到源码后先按这个结构把目录补全再看requirements.txt安装依赖。依赖一般包含 PyTorch、transformers、datasets、faiss-cpu、flask。这里不写死版本号是因为版本匹配本身很容易踩坑后面第 5 章会专门讲。安装命令通常是这样pip install torch transformers datasets faiss-cpu flask pandas scikit-learn如果你是 python 入门阶段建议先用python -m venv venv建一个虚拟环境再装避免把系统 Python 环境搞乱。深度学习训练时经常要装新包虚拟环境是最基本的后悔药。装好后先用一行命令验证python -c import transformers, torch; print(transformers.__version__, torch.__version__)能打印出版本就说明环境通了。如果报ModuleNotFoundError不要继续往下跑训练脚本先解决依赖否则后面每一步都会失败。4.2 训练意图分类模型精简版train_intent.py意图分类模型我选bert-base-chinese因为地铁问答的句子短、领域词集中中文 BERT 预训练模型已经能覆盖大部分语义而且模型不大单卡就能微调。训练脚本核心代码如下import pandas as pd from sklearn.model_selection import train_test_split from datasets import Dataset from transformers import ( AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, ) df pd.read_csv(data/train_intent.tsv, sep\t, encodingutf-8) df[label] df[label].astype(int) train_texts, eval_texts, train_labels, eval_labels train_test_split( df[text], df[label], test_size0.15, random_state42, stratifydf[label] ) model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) def make_dataset(texts, labels): enc tokenizer( texts.tolist(), truncationTrue, paddingTrue, max_length64 ) enc[labels] labels.tolist() return Dataset.from_dict(enc) train_dataset make_dataset(train_texts, train_labels) eval_dataset make_dataset(eval_texts, eval_labels) model AutoModelForSequenceClassification.from_pretrained( model_name, num_labelsdf[label].nunique() ) training_args TrainingArguments( output_dirmodels/intent_model, num_train_epochs5, per_device_train_batch_size32, per_device_eval_batch_size64, learning_rate2e-5, weight_decay0.01, evaluation_strategyepoch, save_strategyepoch, logging_steps20, report_toNone, seed42, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_dataseteval_dataset, ) trainer.train()逻辑说明train_test_split用stratifydf[label]是为了按意图类别分层划分避免「票价」这类大类全部掉进训练集或验证集。tokenizer统一做截断和 paddingmax_length64对地铁问题足够因为最长的问句一般不超过 30 个中文字。num_labels由df[label].nunique()自动计算不用在代码里写死。参数说明per_device_train_batch_size32在有 12G 显存时能跑如果只有 8G 或更小改成 8同时给TrainingArguments加gradient_accumulation_steps4实际效果接近 32 的 batch size。learning_rate2e-5是 BERT 微调的常见起步值不要一上来就用1e-3。evaluation_strategyepoch每个 epoch 结束后跑一次验证5 个 epoch 大概就能看到验证集 loss 不再降。训练结束后要把模型和分词器保存下来供向量检索和 API 加载使用model.save_pretrained(models/intent_model) tokenizer.save_pretrained(models/intent_model)预测时用pipeline或者AutoModelForSequenceClassification.from_pretrained加载。注意一点保存目录里如果没有config.json大概率是save_pretrained没执行成功后面加载会直接报目录不存在。新手上路很容易漏掉这一步。4.3 构建向量索引与应用接口从embedding到Faiss检索意图分类只是把问题分桶真正要答到哪条 FAQ靠的是语义向量检索。这里用 BERT 的last_hidden_state[:, 0, :]即 CLS 向量做句向量。对地铁这种一二十个字的短文本CLS 向量作为 baseline 足够后续要更准可以换成双塔模型。索引构建脚本import json import numpy as np import torch import faiss from transformers import AutoTokenizer, AutoModel model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModel.from_pretrained(model_name) model.eval() faq json.load(open(data/faq.json, encodingutf-8)) questions [item[standard_question] for item in faq] def embed(texts): inputs tokenizer( texts, paddingTrue, truncationTrue, max_length64, return_tensorspt ) with torch.no_grad(): outputs model(**inputs) vec outputs.last_hidden_state[:, 0, :].numpy().astype(float32) norm np.linalg.norm(vec, axis1, keepdimsTrue) return vec / norm vectors embed(questions) index faiss.IndexFlatIP(vectors.shape[1]) index.add(vectors) faiss.write_index(index, models/faq.index) print(向量维度:, vectors.shape[1], FAQ数量:, len(questions))逻辑说明IndexFlatIP是内积索引因为向量已经做 L2 归一化内积等价于余弦相似度分数范围在 -1 到 1。astype(float32)是 Faiss 的硬性要求它不支持 float64。每个向量对应faq.json里同一下标的标准问这个顺序关系在增量更新时要特别注意。API 服务用 Flask 封装对外暴露一个/qa接口from flask import Flask, request, jsonify import json import faiss app Flask(__name__) faq json.load(open(data/faq.json, encodingutf-8)) index faiss.read_index(models/faq.index) def embed(texts): # 与 build_index.py 中的 embed 实现保持一致 ... app.route(/qa, methods[POST]) def qa(): query request.get_json().get(query, ).strip() if not query: return jsonify({error: query不能为空}), 400 vec embed([query]) scores, idxs index.search(vec, k5) best_score float(scores[0][0]) best_idx int(idxs[0][0]) if best_score 0.72: return jsonify({ query: query, answer: 这个问题我还没学会建议转人工客服。, score: best_score, }) return jsonify({ query: query, answer: faq[best_idx][answer], score: best_score, }) if __name__ __main__: app.run(host0.0.0.0, port5000)参数说明k5表示召回 5 条候选但最终只取第一条如果你后续要做重排可以先把 Top 5 输出。0.72是兜底阈值它不是拍脑袋定的我会用一批真实 query 跑一遍把每条 query 的 top1 分数画出来选一个能滤掉 80% 错误答问的分数。阈值设太高会让很多问题转人工设太低会硬答错。启动后可以用下面这条命令做冒烟测试curl -X POST http://localhost:5000/qa \ -H Content-Type: application/json \ -d {query:最后一趟地铁几点}如果返回的score在 0.85 以上说明索引和 API 链路基本通。如果返回转人工多半是faq.json里没有覆盖这个问题或者向量索引没有包含新知识先去排查数据而不是调阈值。5. 地铁智能问答系统的常见坑与排查从中文编码到显存溢出5.1 现象训练时中文全部变成乱码loss曲线直接崩掉我在跑第一个版本时就吃过这个亏。.tsv文件是从 Excel 导出的默认编码是 GBK而 Python 脚本里用utf-8读取读出来的中文变成「鍦扮牬」之类的乱码。BERT 分词器拿到乱码后词表里找不到对应 token模型学不到任何语义信息loss 怎么降都降不到合理范围。原因很简单文件编码与读取编码不一致。解决方法是统一用 UTF-8并在读取时指定编码涉及open的代码全部加上encodingutf-8如果文件带 BOM则用utf-8-sig。一个排查命令with open(data/train_intent.tsv, rb) as f: raw f.read(10) print(raw)正常 UTF-8 中文应该是一串连续字节不是转义成\x的乱码。看到开头有b\xef\xbb\xbf就是 UTF-8 BOM读取时用utf-8-sig即可。这个问题在 Linux 服务器上特别常见因为 Windows 下创建的文本文件经常是 GBK用pandas.read_csv时不指定编码就全乱了。5.2 现象验证集准确率有93%真实乘客问法却答非所问这是新手最容易困惑的。验证集是官网 FAQ 扩出来的干净样本真实用户的问题往往带着语气词、错别字和口语省略例如「人工客服怎么找」「虹桥能退卡不」。模型在花式口语上表现不好本质是训练分布和线上分布不一致。解决分三步。第一从客服日志里抽真实 query人工标注后加入训练集哪怕只有几百条效果也远好过盲目调参。第二增加规则增强把「能不能」「为啥」「咋」「呢」这类口语词套到标准问上。第三把评估指标从整体准确率换成每类意图的召回率重点看「失物招领」这种低频高价值类别别被「票价」类别的高占比带偏。整体指标好看不等于线上好用这是我反复踩出来的经验。5.3 现象batch_size设成328G显存直接OOMBERT 中文模型参数量是 1 亿多训练时加上 Adam 优化器状态和梯度显存占用远大于模型文件本身。per_device_train_batch_size32在常见 8G 显卡上大概率会报CUDA out of memory。解决方法是把 batch size 降到 8然后用gradient_accumulation_steps4累积梯度让实际更新步接近 32 的 batch。在TrainingArguments里两个参数配合使用training_args TrainingArguments( per_device_train_batch_size8, gradient_accumulation_steps4, per_device_eval_batch_size16, fp16True, )参数说明gradient_accumulation_steps4表示 4 个小 batch 后做一次优化器更新不影响评估逻辑。fp16True在支持半精度加速的显卡上能把显存占用再压一大截但注意如果训练 loss 出现 NaN优先关掉它。没有 GPU 的时候可以把训练规模压到最低试跑一次代码确认数据没问题再去租卡比一次直接上大 batch 省钱。5.4 现象FAQ.json里新加了一条知识Faiss检索结果却全部错位这个坑非常隐蔽。向量索引是按照faq.json中questions列表的下标顺序构建的如果你在faq.json中间插入一条新数据没有重新运行build_index.pyAPI 返回的faq[best_idx]就会对应到旧索引的位置答案串位。我之前在「末班车时间」和「首班车时间」之间插了一条数据结果用户问末班车系统答了首班车还认为置信度很高。解决方法是给每条 FAQ 记录一个稳定的idAPI 检索时通过id_map映射到当前记录而不是依赖列表下标。更省事的办法是每次改完faq.json后无条件重建整个索引。如果知识库规模超过几万条再考虑只增量添加新向量但删除和修改还是全量重建最稳。Faiss 的index.add只会追加不会自动感知你删了哪条知识。5.5 现象问题里同时带起终点「到虹桥火车站怎么走」把起点忽略了这是单轮问答模型的常见边界意图分类能识别「换乘」但分不清「从哪到哪」。用户说「从静安寺到虹桥火车站要多久」如果你只按标准问「虹桥火车站怎么走」匹配就会丢掉起点静安寺返回一个不完整答案。解决思路是做轻量槽位提取不需要引入复杂 NER。先用正则把「从X到Y」或「从X出发到Y」的 X 和 Y 抽出来再查站名表确认然后直接用规则拼接换乘方案。深度模型负责意图和语义站名槽位用规则负责这个组合在地铁场景里最不容易出错。如果只靠 Faiss 相似度模型很容易被「虹桥火车站」这个显眼站名带偏因为训练集里大量问法都是围绕终点站展开的。6. 进阶技巧置信度阈值、增量更新与召回评估6.1 冷启动阶段的置信度阈值与兜底话术阈值不要拍脑袋。冷启动时我会准备一批真实用户问题自己先跑一遍把每条 query 的 Top1 相似度记录下来按分数从高到低排列。阈值定在「错答率可以接受」的位置一般在 0.65 到 0.75 之间。如果答错比拒答后果严重阈值就往高调。兜底话术也要写清楚让乘客知道这个系统不是死机了而是真的需要人工介入。6.2 增量更新新FAQ入库的「后悔药」地铁线路经常调整FAQ 随时要变。新知识入库不需要重训意图分类模型只要更新faq.json并重建 Faiss 索引。重建脚本一跑老模型照常服务几乎没有成本。但意图分类模型每两周到一个月用合并后的新语料重训一次避免新意图被旧模型错分。如果你不想全量重建索引可以把新问题单独 embed 后index.add前提是别删除旧数据。6.3 日志回流让系统越用越准我一般每周五跑一次未命中日志把「相似度低于阈值」的 query 按频次排序人工标注出 Top 30合并进训练集。这个动作比调任何参数都有效。拿到的源码如果只跑通一个 demo真正能上线维持的是这个回流闭环。我的习惯是先在日志里加一个raw_query字段别等到要复盘时才发现日志里只有最终答案。系统上线只是开始不是结束希望帮到你。本文还有配套的精品资源点击获取
返回列表