☰
本地RAG问答不准?多轮指代消解与云端向量优化实战
2026/10/4 5:04:28 网站建设 项目流程

1. 为什么本地 RAG 问答总是“答不准”

1.1 从一次翻车现场说起

去年年底我帮一个做工业设备售后的小团队搭了套本地知识库问答,硬件就是一台 32G 内存的迷你主机,模型跑的是 7B 量级的量化版本,向量库用的本地文件方案。第一版 demo 演示的时候效果惊艳,问“XX 型号的保养周期是多久”,答案张口就来。结果上线第三天,客服主管直接甩过来一段对话记录:用户问“它的滤芯多久换一次”,系统答的是另一台设备的参数。再问“那这个呢”,系统彻底懵了,开始胡言乱语。

问题出在哪?不是模型不行,也不是向量库不行,而是检索环节拿到的上下文本身就是错的。用户说的“它”和“这个”,在系统眼里就是两个毫无意义的代词,拿这种 query 去向量库里搜,搜出来的东西自然驴唇不对马嘴。这就是本地 RAG 最容易被忽视、也最致命的瓶颈——多轮对话里的指代消解。

很多人搭 RAG 的路径是这样的:找一堆文档,切一切,灌进向量库,接个大模型,完事。单轮问答确实能跑通,但只要用户开始追问、开始用代词、开始省略主语,整个系统就崩了。而真实场景里,用户几乎不会用完整的主谓宾跟你说话。所以“把本地 RAG 问答做准”这件事,核心不在模型多大,而在检索前的 query 处理和切分策略这两个容易被跳过的环节。

这篇内容适合谁看?如果你已经搭过一个能跑但不够准的本地 RAG,或者正准备动手搭一套,尤其是面向客服、售后、内部文档查询这类多轮追问场景的,那接下来的东西应该能帮你少走不少弯路。我会围绕三个抓手展开:多轮指代消解怎么做、云端语义向量怎么选怎么用、TXT 章节切分怎么切才不丢信息。

1.2 三个抓手的关系,先理清楚

在动手之前,得先明白这三件事不是并列的,而是有先后依赖的。

多轮指代消解解决的是“用户到底在问什么”。它发生在检索之前,把“它的滤芯多久换”还原成“XX 型号设备的滤芯更换周期”。这一步做不好,后面全白搭。

云端语义向量解决的是“怎么把还原后的 query 和文档匹配上”。本地小模型做 embedding 在中文长文本上经常力不从心,云端 API 的语义向量模型在中文语义理解上普遍更稳,尤其是处理同义替换、行业术语的时候。

TXT 章节切分解决的是“文档怎么进库”。切得太碎,上下文断裂,检索到的片段答非所问;切得太粗,一个片段里混了好几个主题,向量被平均掉,匹配精度下降。TXT 这种没有结构标记的纯文本,切分尤其考验策略。

三者串起来就是一条链路:原始多轮对话 → 指代消解还原 query → 云端向量化 → 与章节切分后的文档块匹配 → 喂给大模型生成答案。任何一个环节掉链子,最终答案都会歪。下面我按这条链路的顺序,把每个环节的实操细节掰开讲。

2. 多轮指代消解:让“它”和“这个”说人话

2.1 指代消解到底在解决什么问题

先明确概念。指代消解(Coreference Resolution)在 NLP 里是个老课题,但在 RAG 场景下,我们要的不是学术意义上的完整消解,而是够用就行的 query 重写(Query Rewriting)。目标是:把当前这一轮用户输入里省略的、指代的信息,从对话历史里补回来,形成一个自包含的、可以独立检索的 query。

举个实际例子。对话历史是这样的:

  • 用户:XX-200 型号的保养周期是多久?
  • 系统:XX-200 建议每 500 小时保养一次。
  • 用户:那它的滤芯呢?

第三轮用户输入“那它的滤芯呢”,如果直接拿去检索,向量库里全是“滤芯”相关的片段,但不知道是哪个型号的滤芯。指代消解要做的,就是把“它”还原成“XX-200”,把 query 重写成“XX-200 型号的滤芯更换周期是多久”。这样检索出来的片段才精准。

这里有个关键判断:不是所有代词都需要消解。像“这个多少钱”里的“这个”,如果上文刚提过具体产品,那必须消解;但如果用户是在一个全新话题里说“这个方案不错”,那可能只是口语习惯,强行消解反而引入噪声。所以指代消解的第一步是判断当前 query 是否依赖上下文。

2.2 判断是否需要重写的三个信号

我总结下来,出现以下三种情况时,query 大概率需要重写:

信号一:出现代词或指示词。包括“它、他、这个、那个、该、此、其”等。但要注意,有些代词是泛指,比如“这个东西怎么用”如果上文没有明确对象,消解不了,这时候应该触发澄清反问,而不是硬猜。

信号二:句子成分残缺。比如“那滤芯呢”“多久换一次”“还有别的吗”,这类 query 单独看语义不完整,必须结合上文补全。

信号三:话题延续但主语省略。中文里特别常见,用户说“保养周期呢”,其实是在延续上一个型号的话题,但主语没了。

判断逻辑可以用一个轻量规则引擎先过滤,比如检测到代词或句子长度低于阈值就标记为“待重写”。但规则引擎容易误判,所以更稳的做法是用一个小模型做分类,判断当前 query 是否自包含。这个分类任务很轻,7B 以下的模型甚至规则加关键词就能做到八九不离十。

2.3 重写策略:从简单拼接到模型改写

确定需要重写之后,具体怎么重写?我试过三种方案,效果和成本各不一样。

方案一:历史拼接。最简单粗暴,把最近 N 轮对话直接拼在当前 query 前面,一起丢给检索。缺点是噪声大,历史里的无关信息会干扰向量匹配,而且拼接后的文本变长,向量语义被稀释。实测下来,N 取 2 到 3 轮还行,再多就明显掉点。

方案二:规则模板替换。针对高频代词做映射,比如把“它”替换成上一轮的主语实体。这个方案在特定场景下很准,但维护成本高,遇到复杂指代就歇菜。

方案三:小模型改写。用一个本地小模型,把对话历史和当前 query 一起喂进去,让它输出一个自包含的 query。这是目前我觉得最平衡的方案。提示词可以这样设计:

你是一个查询重写助手。根据以下对话历史,将用户的最新问题重写为一个不依赖上下文、可以独立理解的完整问题。只输出重写后的问题,不要解释。 对话历史: {history} 用户最新问题:{query} 重写后的问题:

实测下来,7B 模型在这个任务上表现已经够用,重写准确率能到 85% 以上。关键是提示词里要强调“只输出重写后的问题”,否则模型容易画蛇添足加一堆解释。

2.4 重写失败的兜底:澄清反问

再好的重写也有翻车的时候。如果对话历史里根本没有可消解的实体,比如用户上来就说“它的参数呢”,这时候硬猜就是瞎编。正确的做法是触发澄清反问:让系统回复“您指的是哪款设备呢?”,引导用户补充信息。

这个兜底机制很重要,因为 RAG 最怕的就是“一本正经地胡说八道”。宁可多问一句,也不要给一个错误答案。判断是否触发澄清,可以看重写后的 query 里是否还残留无法解析的代词,或者重写置信度低于阈值。

注意:澄清反问会打断对话流畅度,所以只在确实无法消解时使用。如果每轮都反问,用户体验会很差。我的做法是设置一个计数器,连续两轮无法消解才触发反问。

3. 云端语义向量:本地 embedding 不够用怎么办

3.1 本地 embedding 的真实短板

很多人搭本地 RAG 时,embedding 也用本地模型,图的是全离线、零成本。但实际用下来,本地小 embedding 模型在中文场景有几个明显短板。

第一是长文本语义压缩能力弱。一个 500 字的文档块,本地小模型编码出来的向量,往往只能抓住前几句的重点,后面的信息被稀释掉了。云端大模型在这方面明显更稳。

第二是行业术语和同义替换处理差。比如“滤芯”和“滤网”在很多设备手册里是同义词,但本地模型可能认为它们向量距离很远。云端模型因为训练语料更丰富,对这种语义关联的捕捉更准。

第三是多语言混排场景。有些技术文档中英文夹杂,本地模型容易在语言切换处丢失语义。

当然,云端 embedding 不是没有代价。数据要出本地这件事,对某些团队是硬伤。所以选不选云端,得先看你的数据敏感度。如果文档本身不涉密,云端方案在准确率上的提升是实打实的。

3.2 云端语义向量的选型考量

选云端 embedding 服务,我一般看四个维度:

维度说明我的建议
中文语义能力在中文长文本上的检索准确率优先选中文语料训练充分的模型
向量维度维度越高表达力越强,但存储和计算成本也高1024 到 1536 维是甜点区
批量接口是否支持一次请求编码多条文本必须支持,否则建库慢到崩溃
成本按 token 计费还是按请求计费建库阶段用量大,要算清楚

维度这块补充一句:不是越高越好。我试过 3072 维的模型,检索准确率相比 1536 维提升不到 2%,但存储翻倍、检索变慢。除非你的文档语义极其细腻,否则 1536 维足够。

3.3 向量化实操:批量编码与缓存

建库阶段的向量化是个体力活。假设你有 5000 个文档块,每个块平均 300 字,那就是 150 万字。如果一条条调 API,光网络往返就够你等的。所以必须用批量接口,一次塞几十条进去。

代码层面大概是这样:

import requests def batch_embed(texts, batch_size=32): all_vectors = [] for i in range(0, len(texts), batch_size): batch = texts[i:i+batch_size] resp = requests.post( EMBEDDING_API_URL, json={"input": batch, "model": "your-model"}, headers={"Authorization": f"Bearer {API_KEY}"} ) vectors = resp.json()["data"] all_vectors.extend([v["embedding"] for v in vectors]) return all_vectors

这里有个坑:批量大小不是越大越好。有些服务对单次请求的 token 总量有限制,批次太大直接报错。我的经验是 batch_size 取 16 到 32 之间,既不太慢也不容易超限。

另一个关键点是缓存。文档更新频率通常很低,但你可能反复重建索引。所以向量算完要存下来,用文档块的哈希值做 key,下次重建时先查缓存,命中就不重复调 API。这一招能省下大量成本和时间。

3.4 向量库的本地存储与检索

向量算完了得有地方存。本地 RAG 常用的方案有 FAISS、Chroma、Qdrant 本地模式等。我一般用 FAISS,因为它轻、快、纯本地,不依赖额外服务。

存的时候要注意:向量和原文块要一一对应。我习惯用一个 JSON 文件存元数据,字段包括块 ID、原文、来源文件、章节路径,向量单独存成 FAISS 索引。检索时先拿 FAISS 返回的 ID 去 JSON 里捞原文。

检索参数里,top_k 不要设太大。很多人怕漏,top_k 设成 10,结果一堆无关片段挤进上下文,反而干扰大模型。我的经验是 top_k 取 3 到 5,配合一个相似度阈值,低于阈值的直接丢掉。宁可少给,不要给错。

4. TXT 章节切分:纯文本怎么切才不丢信息

4.1 为什么 TXT 切分比 PDF 还难

PDF 好歹有版面信息,标题字号大、加粗,能靠这些线索识别章节。TXT 是纯文本,所有字都一样大,章节边界全靠内容本身判断。这就导致两个极端:切太碎,一个完整段落被拆成好几块,检索到的片段缺头少尾;切太粗,一个块里混了好几个主题,向量被平均,匹配精度暴跌。

我见过最离谱的切分是按固定字数硬切,每 500 字一刀。结果一个设备参数表被从中间切开,前半截在块 A,后半截在块 B,用户问参数,检索到块 A,答案缺了一半。这种切法在 TXT 场景下必须避免。

4.2 基于章节标记的切分策略

TXT 虽然没格式,但通常有隐式的章节标记。常见的有:

  • 数字编号:第一章、1.1、一、
  • 关键词:概述、注意事项、技术参数、维护保养
  • 分隔线:====、----、****
  • 空行密度:章节之间通常有空行

我的切分策略是两级切分。第一级按章节标记切,把文档切成大块;第二级在大块内部按段落切,保证每块不超过设定长度。

具体实现上,先用正则匹配章节标题行:

import re CHAPTER_PATTERN = re.compile( r'^(第[一二三四五六七八九十]+[章节]|[0-9]+\.[0-9]*\s|' r'[一二三四五六七八九十]+、|【.*?】)' ) def split_by_chapter(text): lines = text.split('\n') chapters = [] current = [] for line in lines: if CHAPTER_PATTERN.match(line.strip()) and current: chapters.append('\n'.join(current)) current = [line] else: current.append(line) if current: chapters.append('\n'.join(current)) return chapters

这个正则覆盖了大部分中文技术文档的章节格式。实际用的时候,你得先拿几份真实文档跑一遍,看看漏了哪些格式,再补正则。

4.3 块大小与重叠的取舍

章节切完之后,如果某个章节还是太长,就得再切。这时候块大小怎么定?

我的经验值是300 到 500 字。低于 300 字,语义太碎,向量表达不完整;高于 500 字,一个块里可能混了多个子话题,检索精度下降。这个区间是多次实测下来的甜点区。

重叠(overlap)也要设。相邻块之间留 50 到 100 字的重叠,防止关键信息正好卡在切分边界上。比如一个参数说明跨了两个块,有重叠的话,至少有一个块包含完整信息。

但重叠不是越多越好。重叠太多,向量库里全是重复内容,检索时返回一堆相似片段,浪费上下文窗口。50 到 100 字足够了。

4.4 给每个块打上章节路径标签

这一步很多人会忽略,但对检索准确率提升很明显。每个块除了原文,还要记录它的章节路径,比如“第三章 维护保养 > 3.2 滤芯更换”。

为什么要这个?因为检索时可以用章节路径做过滤或加权。比如用户问“滤芯更换”,如果某个块的章节路径里包含“滤芯”,那它的相关性天然更高,可以给它加个权重。另外,喂给大模型时,带上章节路径,模型更容易理解这段内容的上下文位置,生成的答案也更靠谱。

实现上就是在切分时维护一个路径栈,遇到章节标题就入栈,切到具体块时把当前栈拼成路径字符串存进元数据。

5. 完整链路串起来:从对话到答案

5.1 一次完整请求的处理流程

把前面三块串起来,一次用户请求的处理流程是这样的:

  1. 接收用户输入,取出最近 N 轮对话历史。
  2. 判断是否需要重写,检测代词、句子完整性。
  3. 执行 query 重写,用小模型把当前 query 还原成自包含形式。
  4. 重写失败兜底,如果无法消解,触发澄清反问。
  5. 向量化 query,调云端 embedding 接口。
  6. 检索向量库,top_k 取 3 到 5,带相似度阈值过滤。
  7. 组装上下文,把检索到的块按章节路径排序,拼成 prompt。
  8. 调用大模型生成答案,prompt 里带上对话历史和检索片段。
  9. 返回答案,同时把本轮对话存入历史。

这个流程里,第 3 步和第 5 步是两次模型调用,加上第 8 步的生成,一次请求至少三次模型交互。延迟会比单轮 RAG 高,但准确率的提升值得。如果延迟敏感,可以把重写和向量化并行,或者用更小的模型做重写。

5.2 上下文组装的技巧

检索到片段之后,怎么拼进 prompt 也有讲究。我一般按这个模板:

你是一个知识库问答助手。请根据以下参考资料回答用户问题。如果参考资料中没有相关信息,请明确说明“资料中未提及”,不要编造。 参考资料: [片段1] 章节:第三章 维护保养 > 3.2 滤芯更换 内容:... [片段2] 章节:第三章 维护保养 > 3.1 保养周期 内容:... 对话历史: 用户:XX-200 的保养周期是多久? 助手:建议每 500 小时保养一次。 用户问题:那它的滤芯呢? 请回答:

关键点有三个:一是明确要求“不知道就说不知道”,这是防幻觉的底线;二是片段带章节路径,帮模型定位;三是把对话历史也带上,让模型理解当前问题的语境。

5.3 效果对比:改前改后差多少

我拿同一批 50 个多轮追问测试用例跑过对比。改之前,也就是不做指代消解、本地 embedding、固定字数切分,准确率大概 52%。改之后,指代消解加云端向量加章节切分,准确率到了 81%。提升最明显的是含代词的追问,从 30% 出头涨到接近 80%。

这个提升不是靠换大模型来的,模型全程没变。纯粹是检索环节的优化。所以如果你现在的 RAG 答不准,先别急着换模型,把检索链路捋一遍,收益可能更大。

6. 踩过的坑与排查清单

6.1 指代消解把 query 改歪了

最常见的问题是重写模型过度发挥。比如用户问“这个多少钱”,上文提的是 A 产品,但用户其实在问 B 产品。模型硬把“这个”消解成 A,检索就错了。

排查方法:把每轮重写前后的 query 都打日志,定期人工抽查。发现改歪的案例,补充到提示词的 few-shot 示例里。我一般会放 3 到 5 个正例和反例在提示词里,效果比纯指令好很多。

另一个技巧是限制重写幅度。如果重写后的 query 和原 query 的编辑距离太大,就标记为可疑,走澄清反问。这样能拦住大部分过度改写。

6.2 云端向量接口超时或限流

批量建库时,云端接口偶尔会超时或返回 429。这时候不能直接崩,要有重试机制。我的做法是指数退避重试,第一次等 1 秒,第二次 2 秒,第三次 4 秒,最多重试 3 次。同时把失败的批次记下来,最后统一补跑。

另外,建库最好分批进行,别一次性把几千个块全塞进去。每批处理完存一次盘,万一中途挂了,不用从头再来。

6.3 章节切分把表格切碎了

TXT 里的表格是切分重灾区。一个参数表如果被从中间切开,检索到的片段就是残缺的。我的处理办法是在切分前先识别表格区域,连续多行包含制表符或对齐空格的,标记为表格块,整块不切。如果表格太长超过块大小上限,就按行切,但每块都带上表头。

识别表格可以用简单的启发式:连续 3 行以上包含多个连续空格或制表符,就认为是表格。

6.4 常见问题速查表

现象可能原因排查方向
追问答非所问指代未消解看重写日志,检查代词是否还原
答案缺一半切分把内容切断检查块边界,加重叠
检索结果全是无关片段向量模型不匹配换云端模型,或调 top_k 和阈值
建库特别慢没批量、没缓存上批量接口和哈希缓存
模型编造答案prompt 没约束加“不知道就说不知道”指令
同义术语搜不到embedding 语义弱换中文能力强的云端模型

6.5 几个我踩过的具体坑

坑一:对话历史存太多。一开始我把全部历史都存下来喂给重写模型,结果模型被早期无关话题干扰。后来改成只取最近 3 轮,效果反而更好。历史不是越多越好,够用就行。

坑二:向量维度选太高。试过 3072 维,检索没快多少,存储和内存占用翻倍。后来退回 1536 维,性价比最高。

坑三:章节正则太严。一开始正则只匹配“第X章”,结果文档里用“一、”“1.1”的章节全漏了。后来把常见格式都加上,覆盖率才上来。建议拿真实文档跑一遍,看漏了什么再补。

坑四:忘了给块去重。文档里有重复内容,切完块之后向量库里一堆重复,检索时返回好几个一样的片段,浪费上下文。后来在建库时加了去重,按内容哈希过滤。

7. 后续还能怎么优化

这套方案跑通之后,还有几个方向可以继续打磨。

一是重写模型换更小的。现在用 7B 做重写,其实有点浪费。试过 1.5B 的模型,在指代消解这个特定任务上,配合好的提示词,效果差距不大,但速度快了一倍多。如果你的硬件紧张,可以往这个方向试。

二是加一层重排序(Rerank)。向量检索召回 top_k 之后,再用一个重排序模型对候选片段精排。这一步能进一步提升精度,尤其是 top_k 设得比较大的时候。重排序模型也有云端和本地可选,本地的小模型就够用。

三是章节路径做加权检索。现在章节路径只是拼在 prompt 里,其实还可以参与检索打分。比如 query 里出现“滤芯”,那章节路径含“滤芯”的块加权。这个改动不大,但效果立竿见影。

四是建一个 badcase 库。每次发现答错的案例,把 query、检索到的片段、正确答案都存下来。积累到一定量,可以用来微调重写模型,或者优化切分规则。这个习惯我从做第一个 RAG 项目就保持到现在,回报很大。

最后分享一个我自己的体会:本地 RAG 做准这件事,八成的功夫在检索前和检索中,不在生成。很多人把精力花在换更大的生成模型上,但检索拿到的上下文是错的,再大的模型也救不回来。把指代消解、向量模型、切分策略这三块打磨好,哪怕生成模型小一点,答案的准确率也能上一个台阶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询