1. 多跳问答的痛点与现有方案局限
多跳问答(Multi-hop QA)是自然语言处理领域的一个经典难题,它要求模型能够像人类一样进行多步骤推理。比如回答"青霉素发现者的出生地"这个问题,需要先识别出"亚历山大·弗莱明"这个关键人物,再查找他的出生信息。这种跨文档的推理能力,正是当前大语言模型(LLM)应用中的关键挑战。
传统迭代式RAG方案(如Self-Ask、IRCoT)采用"检索-推理-再检索"的循环模式,每跳都需要调用LLM,导致三个显著问题:
1.1 成本随跳数线性增长
在典型的迭代式RAG中:
- 2跳问题需要2次LLM调用
- 4跳问题就需要4次调用
- 每次调用都涉及prompt构造、上下文拼接和结果解析
以GPT-4-32k模型为例,按官方定价:
- 输入token:$0.06/1k tokens
- 输出token:$0.12/1k tokens
假设每次调用平均消耗2k tokens,4跳问题就需要8k tokens,单次查询成本就高达$0.48。对于企业级应用每天处理数万次查询的场景,月成本可能突破六位数。
1.2 实体漂移问题
在多跳推理过程中,子问题往往包含隐式指代。例如:
- 初始问题:"谁发现了青霉素?" → 答案:"亚历山大·弗莱明"
- 生成子问题:"他出生在哪里?"
这里的"他"需要正确关联到弗莱明,但传统方法在检索时:
- 缺乏显式的实体绑定机制
- 容易受上下文相似词干扰(如其他科学家名字)
- 最终导致检索结果偏离真实目标
1.3 原始文档的检索效率低下
原始知识库通常存在:
- 段落冗长(包含无关细节)
- 信息密度低(关键事实被埋没)
- 表述形式与查询不匹配(文档用叙述体,用户用问答体)
例如查询"《Lilli的婚姻》的导演是谁",可能需要检索包含该电影完整介绍的段落,而真正需要的只是"Jaap Speyer"这个名字。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. CompactRAG架构设计精要
南京大学团队提出的CompactRAG采用"离线预处理+在线轻量推理"的双阶段架构,其核心创新在于将计算密集型任务前移,使在线查询时的LLM调用次数恒定为2次。
2.1 离线预处理阶段
2.1.1 原子化QA知识库构建
使用LLM(如GPT-4或LLaMA-3.1-8B)将原始文档转化为细粒度QA对:
python复制def generate_qa_pairs(text):
prompt = f"""将以下文本转化为问答对列表:
要求:
1. 每个事实生成独立的Q&A
2. 使用SpaCy提取的实体作为问题主语
3. 答案保持简洁
文本:{text}"""
response = llm.generate(prompt)
return parse_qa_pairs(response)
处理示例:
输入原文:
"《Lilli的婚姻》是1919年德国无声电影,由Jaap Speyer执导"
输出QA对:
- Q: "谁执导了《Lilli的婚姻》?" A: "Jaap Speyer"
- Q: "《Lilli的婚姻》是哪一年的电影?" A: "1919年"
- Q: "《Lilli的婚姻》是什么类型的电影?" A: "德国无声电影"
2.1.2 关键技术设计
-
实体锚定:使用SpaCy的命名实体识别确保问题包含明确主语
python复制import spacy nlp = spacy.load("en_core_web_lg") def anchor_entities(text): doc = nlp(text) return [ent.text for ent in doc.ents] -
语义编码优化:将QA对拼接为"[q;a]"格式再嵌入,提升检索相关性
python复制from sentence_transformers import SentenceTransformer encoder = SentenceTransformer('all-mpnet-base-v2') def encode_qa(q, a): return encoder.encode(f"[{q};{a}]") -
增量更新机制:设计版本控制系统,支持知识库动态扩展
python复制class QABank: def __init__(self): self.version = 0 self.embeddings = [] def update(self, new_qas): self.version += 1 # 增量嵌入计算...
2.2 在线推理阶段
2.2.1 问题分解器(首次LLM调用)
将复杂问题解析为依赖图结构:
python复制def decompose_question(question):
prompt = f"""将问题分解为子问题依赖图:
示例:
输入:青霉素发现者的出生地
输出:
- 主问题:发现青霉素的科学家是谁?
- 子问题:<答案1>的出生地是哪里?
当前问题:{question}"""
return llm.generate(prompt)
2.2.2 轻量级处理模块
-
子问题重写器(Flan-T5-small):
python复制from transformers import T5ForConditionalGeneration rewriter = T5ForConditionalGeneration.from_pretrained("google/flan-t5-small") def rewrite_subq(subq, context): input_text = f"根据上下文重写问题:{subq} 上下文:{context}" outputs = rewriter.generate(input_text) return outputs[0]['generated_text'] -
答案抽取器(RoBERTa-base):
python复制from transformers import RobertaForQuestionAnswering extractor = RobertaForQuestionAnswering.from_pretrained("roberta-base") def extract_answer(question, qa_pair): inputs = tokenizer(question, qa_pair, return_tensors="pt") outputs = extractor(**inputs) # 处理输出logits...
2.2.3 答案合成器(第二次LLM调用)
汇总子答案生成最终回复:
python复制def synthesize_answers(main_q, sub_answers):
prompt = f"""根据以下信息回答问题:
主问题:{main_q}
子答案:{" ".join(sub_answers)}
请生成连贯的最终答案:"""
return llm.generate(prompt)
3. 性能优化与工程实践
3.1 离线预处理优化技巧
-
批量并行处理:
python复制from concurrent.futures import ThreadPoolExecutor def batch_process(texts, batch_size=32): with ThreadPoolExecutor() as executor: results = list(executor.map(generate_qa_pairs, texts)) return [item for sublist in results for item in sublist] -
嵌入索引优化:
- 使用FAISS进行向量相似度搜索
- 采用HNSW图结构加速最近邻查找
python复制import faiss def build_index(embeddings): dim = embeddings.shape[1] index = faiss.IndexHNSWFlat(dim, 32) index.add(embeddings) return index
3.2 在线阶段性能保障
-
缓存机制设计:
python复制from functools import lru_cache @lru_cache(maxsize=1000) def retrieve_qa(question_embedding): # FAISS检索逻辑... -
异步流水线:
python复制import asyncio async def process_query(query): decomposition = await decompose_async(query) sub_tasks = [process_subq(subq) for subq in decomposition] return await synthesize_async(await asyncio.gather(*sub_tasks))
4. 效果验证与案例分析
4.1 基准测试结果
在HotpotQA数据集上的对比实验:
| 方法 | 准确率 | Tokens/查询 | 延迟(ms) |
|---|---|---|---|
| IRCoT (GPT-4) | 65.2% | 10240 | 3200 |
| Iter-RetGen | 72.4% | 4700 | 2100 |
| CompactRAG (LLaMA) | 70.4% | 1900 | 900 |
| CompactRAG (GPT-4) | 77.2% | 1900 | 1100 |
关键发现:
- Token消耗降低60-81%
- 使用LLaMA-8B即可达到GPT-4迭代方案的准确率
- 延迟降低50%以上
4.2 典型问题处理流程
案例:查询"莫奈创作《睡莲》时使用的绘画风格"
-
离线预处理:
- 原始文档:"印象派画家克劳德·莫奈在1899年至1926年间创作了约250幅《睡莲》系列油画"
- 生成QA对:
- Q: "莫奈属于哪个艺术流派?" A: "印象派"
- Q: "《睡莲》系列的创作时间?" A: "1899-1926年"
-
在线阶段:
- 第一次LLM调用:分解为
- 主问题:"《睡莲》的作者是谁?"
- 子问题:"<作者>属于什么艺术流派?"
- 轻量模块:
- 检索到作者是"克劳德·莫奈"
- 重写子问题为"克劳德·莫奈属于什么艺术流派?"
- 从QA库检索到答案"印象派"
- 第二次LLM调用:生成最终答案"莫奈在创作《睡莲》时使用的是印象派风格"
- 第一次LLM调用:分解为
5. 实施建议与避坑指南
5.1 知识库构建注意事项
-
领域适配:
- 科技文献:侧重专业术语和概念关系
- 产品手册:关注功能参数和使用场景
- 历史资料:强调时间线和人物关联
-
QA对质量控制:
python复制def validate_qa(q, a): # 检查问题是否包含明确主语 # 验证答案是否直接回应问题 # 过滤主观性强或模糊的表述
5.2 线上服务调优
-
性能监控指标:
- 子问题重写准确率
- QA检索命中率
- 答案合成相关性评分
-
容错机制:
python复制def fallback_strategy(query): if detect_ambiguity(query): return ask_clarifying_question() elif retrieval_failed(): return default_response()
6. 扩展应用场景
6.1 企业知识管理
- 产品知识库:解决"X型号是否支持Y功能"类多跳查询
- 客服系统:处理"订单延迟的补偿政策"等复合问题
6.2 学术研究辅助
- 文献综述:回答"方法A在领域B中的应用效果"
- 技术调研:追踪"X技术的演进历程"
6.3 个性化推荐
- 内容平台:"喜欢A作品的用户也关注的B类型创作者"
- 电商场景:"适合X场景的Y价位Z品牌产品"
在实际部署中发现,当知识库规模超过100万QA对时,采用分层索引结构(先粗筛类别再细查)可以使检索延迟稳定在200ms以内。对于时效性要求高的场景,建议建立基于最后修改时间的增量更新机制,而非全量重建。
