做机器学习问答系统这件事,我这两年陆续被问过很多次。项目标题听上去像一门课的作业题,但真正跑起来会发现,它把数据清洗、文本检索、语义向量、生成模型、工程部署整条链路全都串在了一起。这篇文章就以真实项目为主线,把从零搭一个机器学习问答系统的完整思路拆开讲。已经做知识库问答、想把一堆文档变成可对话形态的同学,应该能直接拿去参考。
先把核心框架说透:一个典型的机器学习问答系统,本质上做两件事——先“找”到跟问题相关的资料,再“读”懂这些资料并组织成一句人话答案。前者是检索问题,后者是生成问题。我见过很多把重心全放在模型上、最后在检索上翻车的项目,因为上下文找不准,模型再强也只是在编瞎话。所以整篇内容的组织方式,会按数据、检索、生成、评估、部署这条主线往下走,每一步都会给可落地的方案和踩坑记录。
1. 项目设计与思路拆解
1.1 先搞明白:问答系统到底在解决什么问题
很多人把问答系统想得太玄幻,觉得只要接一个大模型就能回答任何问题。实际上在真实业务里,问答系统的核心问题只有一个:如何从存量信息中快速、准确地对用户的问题给出答案。
这里的难点有两层。第一层是“存量信息”本身是脏的、异构的、分散的。我接手过的项目里,有把产品手册、客服聊天记录、故障工单、代码注释全放在一个库里当语料用的,格式五花八门,质量参差不齐。第二层是“答案”的形态不统一,有的问题只需要一个名词解释,有的问题需要步骤说明,有的问题本身就是错的、得先校验。
所以动手之前,我会先做两件事:把可回答的问题类型列成清单,把语料来源盘点清楚。问题类型决定了检索策略和提示词模板的写法,语料来源决定了清洗方案。这两项没想清楚就急着搭模型,后期一定反复返工。
1.2 两种主流架构:检索式问答与生成式问答
问答系统大致分两类。检索式问答先把候选答案切成片段存进索引,用户提问的时候直接找出最匹配的片段返回,它的优点是答案可控、有据可查,缺点是无法应对从来没见过的问法,也无法归纳整合多个片段的信息。
生成式问答则反着来,模型根据问题直接“编”出答案,优点是表达自然、能处理开放式问题,缺点也很明显:没有知识约束时容易一本正经地胡说,还很难追溯答案来源。
实际项目中我更推荐的做法是两者混合:用生成模型做最终回答,但回答之前必须先经过检索环节,把相关知识片段作为依据喂给模型。这种“先检索、后生成”的组合,就是这几年在工程里被验证过的检索增强生成(RAG)模式。它的核心价值在于,模型不再凭记忆回答,而是基于你喂给它的真实资料做约束生成,既保留生成的灵活性,又保证回答有出处。
1.3 为什么选择“检索增强生成”作为主方案
我在好几个项目里做过方案选型对比,最终都倒向检索增强生成,原因很实际。
第一,知识更新成本低。如果模型把知识都训练进参数里,每次业务调整都要重新训练,成本高周期长。检索增强把知识外置到知识库,更新资料只需重新索引文档,几十分钟就能生效。
第二,可解释性可控。生成模型输出的答案,可以回溯到具体检索出的文档片段,出问题时能定位是检索没找对,还是生成没用好。这点在企业内部场景至关重要,总不能客户问了个政策问题,回复的出处是模型自己编的。
第三,幻觉概率能压到工程可接受范围。虽然不能完全消除,但当你把上下文限制在检索出来的两三段资料里,再给提示词加约束,模型“自由发挥”的空间就被压缩了非常多。我在测试集上做过对比,同样的模型,不加检索直接用,事实类问题的正确率只有六成多,接上检索增强后能到九成左右。
2. 核心准备:数据清洗、知识库构建与向量化
2.1 语料质量决定了问答系统的天花板
模型再强也只是把语料里的信息重新表达出来,语料本身有问题,后面所有环节都会跟着错。我在数据清洗阶段踩过不少坑,现在固定按照下面这个清单处理。
首先是格式统一。常见的来源包括办公文档、网页、数据库导出、聊天记录,需要全部转成纯文本或统一的结构化格式,去掉页眉页脚、导航菜单、广告痕迹。这里有个容易忽略的点:很多文档里都有表格,直接转纯文本会把表格结构弄丢,问答时往往需要保留表格的行列关系,所以我建议结构化数据单独存,问答时按需查询,不跟长文本混在一起。
其次是去重和去噪。同样的内容出现在多个文档里时,不处理会导致检索结果被重复内容淹没。我在项目里会用内容哈希或两两相似度做去重,相似度阈值一般设在0.9左右,小超过就合并。对于明显无意义的段落,比如只有标题没有正文的空白块,直接用规则过滤。
最后是敏感信息筛查。企业内部语料必然涉及敏感数据,必须在上线前做一遍脱敏处理,把手机号、身份证、内部账号等用正则和人工抽检结合的方式清理干净,这既是合规要求,也是后续系统能稳定使用的底线。
2.2 文本切片:怎么切才能让召回更准
知识库里的文档不可能整篇丢给检索器,因为太长的话,向量表示会模糊,检索引擎也容易跑偏。必须把文档切成更小、语义更完整的片段。
切片规则没有标准答案,我试过固定字数、段落、章节几种方式,最终用的是“章节优先、段落兜底、按需重叠”的组合策略。具体做法是:先按文档的层级结构,尽量把标题和内容作为一个单元;如果一个章节太长,再按自然段落拆分;每个切片控制在几百字以内,相邻切片之间保留几十字重叠,防止语义被拦腰截断。
切片的粒度直接影响后续效果。切大了,一个片段的向量可能被多个无关主题稀释;切小了,又可能把完整逻辑拆碎。我在实际项目中总结了一个经验:切完以后人工抽看一批切片,如果单单看某一片段都能理解它在讲什么,这个粒度基本可用;如果片段看起来前言不搭后语,就重新调策略。这个人工检查的成本不高,却非常有效。
2.3 向量化与索引:把文档变成可以搜索的数字
切片做好之后,要把文本转换成向量,也就是用嵌入模型把一段文字映射到高维空间里的一个点,语义相近的文本在空间里距离更近。这一步是整个问答系统体现“机器学习”味道最浓的地方。
选择嵌入模型时我一般看三个指标:语义准确度、向量维度、和硬件的兼容性。维度太高虽然表达能力强,但存储和检索开销也大;维度太低则容易损失语义信息。实际项目中常用的范围在几百维到上千维之间。如果预算有限,也可以先用公开的中文预训练模型做一遍效果验证,再决定是否上更重的方案。
向量化完成后,需要把向量存进专门的检索库,同时建立索引。索引的核心作用是让“找最近邻”变快,否则几百万条向量暴力遍历一遍,延迟没法看。我现在的流程是:把切片文本、切片原始路径、向量、元数据一起入库,建一个高召回率索引。建索引之前,强烈的建议是数据先做归一化处理,不然不同的量级会影响距离计算的稳定性。
2.4 混合检索:向量检索与关键词检索互补
只用向量检索在实际业务里头是不够的。向量擅长捕捉语义相似的表达,比如“怎么退货”和“退款流程是什么”能匹配上,但遇到专有名词、型号、编号这类精确信息,向量往往不如传统的关键词匹配。
这很好理解。用户报一个“型号B-3200”,嵌入模型可能把它理解成某种语义相关的描述,但精确命中完全要靠字符层面的匹配。所以我在项目里普遍采用混合检索:一边用关键词算法(类似传统全文检索引擎的思路)按字面命中打分,一边用向量检索按语义相似度打分,最后把两路结果融合排序。
融合的方式不复杂,最简单的是加权和,关键词分数和向量分数分别归一化后相加。更精细的做法是让关键词命中的结果得到额外加分,同时引入时间衰减、点击反馈等业务信号。混合检索显著提升了实际效果,我在测试里,加了这个环节后,包含型号和编号类问题的正确率直接提升了一截。
3. 模型选择与问答链路实现
3.1 生成模型的选型思路
生成模型是整个问答链路的最后一个大脑。选型的时候,我优先考虑三点:生成质量、上下文长度限制、部署成本。
生成质量直接决定答案是否通顺、是否正确。对于大多数垂直领域项目,直接用公开的中文预训练对话模型做微调或直接零样本测试,效果通常就能满足内部使用。多轮对话能力也是个加分项,但项目初期建议先聚焦单轮问答,把准确率跑稳再扩展多轮。
上下文长度限制是个硬指标。检索增强方案需要把检索到的片段拼进上下文,片段一多,占用空间就大。我记得有一个项目里,每轮请求要拼三千多字的资料,模型上下文窗口小了,系统会自动丢弃后半部分,结果答案总缺尾巴。后来换成上下文更大的模型,问题才解决。
如果项目要上线服务大量用户,部署成本要提前估算。模型的参数量级直接影响显存和推理速度,我一般先拿数据量最小但效果满足要求的模型起步,等业务量涨了再平滑升级,而不是一上来就追求最大的模型。
3.2 上下文拼接与提示词模板
模型不会自动理解你的业务规则,必须通过提示词把任务约束清楚。我的提示词模板通常包含四个部分:角色设定、任务说明、检索到的参考资料、用户问题。
角色设定让模型进入“客服/助手”状态,任务说明明确“只能基于资料回答,不要编造”,参考资料按“第1段”“第2段”编号,最后一并放上用户问题。为了让模型区分哪段资料对应哪个出处,我还会在参考片段前加上来源标记。这样当模型回答时,即使它没有显式把来源写出来,也能通过标记做出更准确的引用。
这里有一个重要细节:提示词里的参考资料如果顺序混乱,模型会自动认为排在前面的资料更权威。所以我在拼接之前,通常会把检索结果按分数从高到低排序,并明确告诉模型“按相关度选择最贴切的段落”。别小看这步,同样的模型,加了这个约束之后,回答的准确率能再提升几个点。
3.3 排序与重排:让最相关的证据排到最前面
从混合检索引擎拿回来的候选片段通常有几十个,但真正喂给生成模型的只有几个,所以中间需要加一层重排。重排的目的是在更小的候选集上做更精细的相关度打分。
重排的做法有两类。简单做法是用一个交叉编码器模型,把问题分别和每个候选片段拼接起来,一起输入模型,输出一个相关度分数,然后按分数重新排序。这一类的缺点是速度慢,但因为只对几十个候选做,耗时完全可接受。另一类做法是用更重的生成模型直接问“这段文字能否回答这个问题”,得到答案后再排序,这种方法更慢,但效果更好。
我在项目里的标准流程是:混合检索先召回大概三五十个候选,重排后取前三到五名喂给生成模型。这个流程看似多了一个环节,实际上稳住了最终答案的质量。如果没有重排,向量检索直接取TopN,经常会有跑题片段混进去,模型再聪明也容易被带偏。
3.4 带源码的完整问答流程
到这里,整条链路已经清晰了,写一段流程代码串起来会更容易理解。下面这段是示意代码,用伪代码的形式展示,实际项目里按你选用的组件替换成对应调用就行。
def answer_question(question): # 1. 混合检索:关键词召回 + 向量召回 keyword_hits = keyword_search(question, top_k=20) vector_hits = vector_search(question, top_k=20) # 2. 合并候选集并按分数重排 candidates = merge_and_dedupe(keyword_hits, vector_hits) reranked = rerank(question, candidates, top_k=5) # 3. 拼接上下文和提示词 context = format_context(reranked) prompt = build_prompt(question, context) # 4. 调用生成模型得到答案 answer = generate(prompt) return answer, reranked # 返回答案和引用来源这段流程写出来很短,但每一个函数背后都有很多细节。尤其注意最后返回了引用来源,这对线上排查问题非常关键,也是我做问答系统以来一直坚持的习惯。用户看不到或不在乎这个返回值,但运维和迭代的时候,它能帮你把问题定位到非常具体的位置。
4. 评估与上线:衡量问答系统质量的方法
4.1 离线评估:用指标先跑一遍
问答系统必须评测才能迭代。我在项目里建了一套离线评估集,包含几百条真实问题和它们的标准答案,标注方式根据问题类型区分:事实类问题标标准答案,步骤类问题标关键步骤列表,无答案问题标“不可回答”。
离线评估指标里,最常用的是答案正确率、检索召回率、答案完整率。召回率看的是正确答案是否出现在检索结果中,正确率看的是最终生成的答案是否可用。我一般会跑两组实验:一组直接看最终端到端的正确率,一组单独看检索阶段的召回率。这样能快速定位问题是出在“没找到”还是“找到了但没答好”。
还要专门准备一批“无答案问题”放进测试集,看看系统会不会强行作答。一个做得好的问答系统,应该能对没有依据的问题说“资料中没有相关内容”。如果系统遇到什么问题都硬答,说明提示词约束不够,或者阈值设定过松,这就需要在生成前加一道判断:最高分的参考片段如果分数都低于阈值,直接提示用户换个问法。
4.2 端到端质量:人工评估与在线观察
离线指标不能完全代表真实体验。我在项目里会做一个简单的盲测:把问题和答案随机排列,让人工按“完全正确、部分正确、错误、无法判断”四个等级打分。这个流程虽然耗时,但每做一次迭代都能发现不少意外问题,比如答案有歧义、引用源混乱、措辞不礼貌,这些都是指标看不出来的。
上线之后,还要记录线上反馈。我在系统里会记录用户是否对答案点赞点踩,同时不定期抽看用户真实提问,看它们和评估集分布是否一致。真实用户提问题的口语化程度往往超出预期,比如不写完整句子、带错别字、夹杂方言。这些反馈积累下来,会成为下一轮评估集的重要来源,让测试集越来越贴近真实。
线上观察中最值得盯的是一个指标:无答案比例。如果无答案比例过高,说明知识库覆盖不足或检索能力太弱;如果过低,可能系统在不懂装懂。健康的系统应该有自己的边界,而不是对所有问题都给出一个看起来像答案的东西。
4.3 上线部署的工程细节
问答系统的部署,最需要关心的是接口延迟和并发稳定性。检索部分通常还好,瓶颈主要在生成模型上。我在项目里常用的优化手段包括:把模型推理过程做成独立服务,和业务主流程解耦;对生成结果做缓存,相同或相似问题直接命中缓存;批量推理合并请求,减少重复计算。
缓存是性价比极高的优化项。内部知识库问答的常见问题集中度高,很多用户会问类似的问题,命中缓存后响应时间能降到原来的十分之一。相似问题匹配可以用向量相似度做,超过阈值直接复用历史答案,但要注意带业务参数的问题不能盲目缓存,否则一个用户问“我的订单怎么退”,另一个用户问同样问题,缓存返回的是前一个人的订单信息,这就出大事了。
模型服务还需要做好超时处理。生成模型在极端情况下可能卡住很久,所以我一直在接口层面加了超时时间,超时就返回“暂时无法回答”,而不是让用户一直等待。同时加上降级方案,生成服务不可用时至少还能返回检索出的相关资料片段,保住基本可用性。
5. 常见问题与排查技巧实录
5.1 检索不到正确答案
这是最容易遇到也最让人头疼的问题。排查时我会先看检索召回里有没有正确答案:把候选片段打出来,人工确认正确资料到底有没有被找回来。
如果正确资料根本没进入候选,问题多半出在知识库构建环节,比如切片切错了、文档没入库、索引没更新。如果正确资料进了候选但排名不在前面,那就升级重排模型的权重,或调整混合检索的融合分数。还有一种场景:用户问法跟资料里的写法差异很大,这时需要在切片和检索之前加一层问法改写,把口语化问题改写成更规范的问法再检索,效果往往立竿见影。
5.2 生成结果答非所问
模型答非所问,先别急着换模型。我会先检查喂给它的上下文是否正确,上下文不对时,模型自然答非所问。上下文正确时,再检查提示词,看任务说明够不够清晰。有时候只是加一句“请直接根据参考内容作答,不要复述问题”,输出质量就会明显改善。
如果上下文正确、提示词也没问题,还是答得不好,就要考虑是不是片段里信息不足。比如用户问某功能支持哪些平台,而资料里这段功能描述和平台支持情况分散在两处,单看一段就答不全。这时候需要把检索返回的K值调大,或改用“多段摘要”模式,让模型从多个片段里提取信息再整合。更深一层,也可以在切片阶段尽量保证逻辑完整性,减少信息被切散的概率。
5.3 回答引用了错误上下文
有时系统最后生成的答案文本本身是对的,但引用来源很离谱。这类问题通常藏在重排阶段:重排返回的候选里有正确也有无关片段,模型却选了一段不相关的“搭配”来佐证。解决思路是在提示词里强调“只依据编号片段作答”,同时让系统在输出时明确标记基于哪几个编号。一旦出现引用错误,线上日志就能快速回溯是哪一步排序出了问题。
还有一种情况,通用常识和业务资料打架。比如用户问一个系统常识问题,模型没在检索资料里找到依据,却依靠训练时学到的常识硬答,这时引用来源必然对不上。应对办法是在生成前增加判断环节,如果最高分参考片段低于预设阈值,直接放弃生成,返回“暂无相关资料”。这个机制虽然牺牲了一部分覆盖率,但换来的是可信度。
5.4 性能与成本问题
问答系统的成本,大头基本都在生成模型的调用上。项目初期,我会先限制每个用户每天或每分钟的请求次数,防止少数恶意或异常请求把预算打爆。同时把生成长度限制在需要的范围内,答案写得越长,耗时越长、成本越高,但用户真正需要的往往只是前两百字。
为了进一步控制成本,我会把高频问题的答案缓存起来。常见问题如果已经回答过,完全没必要每次重新调用生成模型。还有一种思路是在检索阶段就把问题分发到不同层级的模型:简单名词解释用轻量模型回答,复杂综合问题才走重量模型。分层处理不仅省成本,整体响应速度也更好。
如果系统响应明显变慢,最有可能卡在重排或生成的串行调用上。我习惯把检索、重排、生成拆成独立的步骤打点计时,看每一步的耗时占比,然后针对瓶颈做优化。实测下来,检索和重排绝大多数场景能在几百毫秒内完成,大头还是在生成上,所以上一步说的缓存和分层,往往也是提速效果最明显的方案。
做个通过真实反馈不断修正的问答系统,比搭一个花哨的Demo有价值得多。刚开始别追求一步到位,先用最小可行版本跑通全链路,再把精力集中在检索准确率和答案可信度上。我自己踩过最大的坑,就是一开始把时间花在换更大更好的模型上,结果发现检索不行,换什么模型效果都上不去。数据、切片、检索、重排,这四个环节打磨到位,模型能力才能对得起调用它的成本。