AI 学习手记一个普通学习者的 AI 知识笔记
首页/实践记录/在自己的服务器上搭一套文档问答:全流程与踩坑

实践记录

在自己的服务器上搭一套文档问答:全流程与踩坑

切块、向量库、召回、重排、生成、引用——把六个环节逐个拆开,记录每一处真正影响效果的细节。

需求很实在:几十页中文技术资料,希望提问时能拿到有出处的回答,而不是模型凭印象编。全文塞进长上下文我也试过,参数名编得很流畅。所以走了检索这条路。

一、六个环节,重要性不平均

环节我的默认做法出问题时的表现
解析按标题层级保留结构表格拍平后语义丢失,检索命中但答案错
切块按段落聚合到 400~600 token,重叠 12%命中半句话,指代对象在前一块
向量化本地 bge 中文模型与查询分布不匹配,召回离谱
召回向量 top-20 + BM25 top-20 合并去重纯向量漏掉精确实体、型号、编号
重排cross-encoder 取 top-3相关但不关键的内容挤掉正确答案
生成温度 0 + 强制引用 + 允许拒答输出与材料无关的内容

影响最大的是切块和召回,不是模型。我从 7B 换到更大的模型,效果提升远小于把"纯向量"改成"向量 + 关键词混合"。

二、切块:把上下文写进块里

按字数硬切是最常见的错误起点。我现在的做法:

  • 优先在段落边界切,凑到目标长度为止;
  • 每块前面拼 "《文档标题》> 《章节标题》\n" 再编码。

第二条是我收益最明显的改动。很多块的单独语义是残缺的——"该参数需设置为 true"里的"该参数",脱离章节就没有指向。加上标题之后,短块的召回质量明显改善。

表格是硬骨头。拍平成纯文本基本等于毁掉。我的处理是把每行转成一句自然语言("字段 X,类型 Y,默认值 Z,说明 W"),检索单位从"块"变成"行"。这方案不优雅但有效。

三、混合召回:向量不是万金油

纯向量有一个我一直没预料到的弱点:它对必须字面命中的东西不可靠。搜具体型号、错误码、函数名,它能给你召回一堆"意思相近"的同族内容,就是不给那一条。

所以加一路关键词检索。Postgres 上是 tsvector + zhparser 中文分词,代码里:

SELECT id, content, ts_rank(search, query) AS rank
  FROM chunks, plainto_tsquery('zhcfg', $1) query
 WHERE search @@ query
 ORDER BY rank DESC LIMIT 20;

两路结果用 RRF(Reciprocal Rank Fusion)合并,不用去纠结怎么把余弦相似度和 BM25 分数放到一个尺度上:

// RRF:只看名次不看分数,对两路量纲差异天然免疫
const K = 60;
const score = new Map();
for (const list of [vectorHits, keywordHits]) {
  list.forEach((hit, i) => {
    score.set(hit.id, (score.get(hit.id) || 0) + 1 / (K + i + 1));
  });
}
const merged = [...score.entries()].sort((a, b) => b[1] - a[1]);

这个改动把我评测集里的 recall@5 拉高了一档,是整件事里性价比最高的一步。

四、重排:慢,但值得

双塔式的向量召回里,查询和文档各自编码、彼此看不见,快但粗。cross-encoder 把"查询 + 文档"拼在一起过模型,能看见交互,准但慢。所以典型搭配是召回几十条、重排留几条。

代价说清楚:每个候选都要过一次模型前向。20 个候选就是 20 次,这一步在我的机器上通常比生成还慢。想压低延迟,先减候选数,别急着换小模型。

五、生成:拒答比编造值钱

提示词里三条硬要求:

  • 每条结论必须给依据(块 ID + 原文片段);
  • 材料没有就说"材料未覆盖";
  • 不复述材料之外的行业惯例。

第二条最容易被忽略,但它决定了这套东西能不能用。一个不会拒答的问答系统,会把"没查到"和"不存在"混成一类,用户分不清,这种错误比明显答错更贵。

伪引用是必然要抽查的。我最初的实现里引用能对上号但内容对不上,因为我把整段拼接后一起给模型,它自由发挥了。修法不是提示词里骂它,是程序化校验:引用片段必须真是那个块的子串。这条校验上线后,"看起来有据但其实编造"的输出在我这边的发生频次明显下降。

六、别跳过的评测

先攒 30~50 个真实问题,标好"正确应该召回哪几块",之后每次改动只跑 recall@k 和 MRR。有了这两个数,争论才有落脚点:

  • 召回率不够 → 查切块和混合检索,别换生成模型。
  • 召回命中但答错 → 查上下文顺序和提示词,考虑重排。
  • 答对但引用为空或不对 → 查校验逻辑,这是代码问题。

七、这套东西的适用边界

RAG 不适合解决"模型缺乏常识"或"任务需要复杂推理",它只适合"知识在我手里、量大、要新、要出处"的场景。如果知识少且稳定,写进提示词就行;如果需要模型学新的表达风格或输出格式,那是另一件事。

这个判断我花了不少时间才建立起来——因为"文档问答"听起来万能,很容易什么都往里塞。

内容标识:本文正文与脚本为本人编写;文中效果对比来自我自己的 40 题评测集,仅说明方向,不是通用结论。

本文出处:http://honeysss.cn/post/rag-on-2g-vps.html 转载请注明出处。