效果展示(环境依赖请参看上一篇文章):
qa.json(示例知识):
测试结果:
引言
在构建智能问答系统时,常常遇到用户提问方式多样化的问题。即使问题本质相同,表达方式可能千差万别。为了提高搜索和匹配的准确性,我们需要对原始问题进行扩展,即生成多个同义表达。这有助于增强向量检索的召回能力,从而提升问答系统的性能。
Demo的目标是基于 Hugging Face 预训练模型和 FAISS 向量数据库,实现高效的知识检索,支持对用户问题的同义扩展,并通过向量搜索提供最佳答案。
问题分析
在传统的基于关键字匹配的问答系统中,用户的查询必须与数据库中的问题文本完全匹配,否则无法找到答案。这样的方法存在以下缺陷:
- 用户表达多样性:相同的问题可以有多种不同的提问方式,例如“天气怎么样?”与“今天的天气如何?”
- 查询灵活性不足:固定的问答库难以覆盖所有可能的提问方式,导致问答系统容易失配。
- 检索准确度低:单纯的关键字匹配容易受到语序、停用词等因素影响。
为了解决这些问题,我们需要构建一个基于语义的查询扩展和高效的向量检索系统。
解决方案
本项目采用以下方法来优化问答系统的性能:
- 生成同义表达:
- 使用 Hugging Face 预训练的大模型(Qwen1.5-1.8B)生成不同的问法。
- 通过 prompt 设定规则,确保生成的问法保持原意但表达不同。
- 构建向量数据库:
- 采用 BAAI/bge-large-zh 作为嵌入模型,将问题转换为向量。
- 使用 FAISS 存储和索引这些向量,提高检索效率。
- 语义匹配优化:
- 通过向量相似度搜索,而非简单的关键字匹配,来提高查询命中率。
- 设定合理的相似度阈值,减少无关答案。
为什么选择这些技术?
- Qwen1.5-1.8B 生成同义问法
- 该模型在中文生成任务上表现优异,能够生成多样化但语义一致的问法。
- 通过温度调整提高生成质量,保证问题扩展的合理性。
- BAAI/bge-large-zh 作为嵌入模型
- 专为中文优化,相较于通用的嵌入模型效果更好。
- 支持归一化嵌入,提高向量检索的精确度。
- FAISS 作为向量存储与检索工具
- 高效支持海量数据的相似性搜索。
- 通过索引优化,提高查询速度。
替换方案
对于不同的应用场景,可以考虑如下替换方案:
- 数据存储替换:
- 如果 JSON 存储方式不适用,可替换为数据库(如 MySQL, PostgreSQL, MongoDB)。
- 模型替换:
- 可替换为更轻量级的 Qwen 模型(如 Qwen1.5-0.5B)以适应资源受限环境。
- 也可以用 ChatGLM、GPT-4 等大模型,根据任务需求选择合适的生成模型。
- 嵌入方法替换:
- 可使用 OpenAI 的 text-embedding-ada-002 进行文本向量化。
- 可选用 Sentence-BERT 等其他嵌入模型。
代码实现(包含详细注释)
import os
import json
import torch
import logging
import re
from datetime import datetime
from transformers import pipeline
from langchain_huggingface.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.schema import Document
# 设置日志
logging.basicConfig(filename='retrieval.log', level=logging.INFO)
# 认证和环境配置
os.environ["HF_TOKEN"] = "you_huggingface_token"
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 读取 JSON 数据,避免损坏
def load_json_safe(file_path):
"""安全加载 JSON 文件,避免文件损坏导致崩溃"""
if not os.path.exists(file_path):
return {}
try:
with open(file_path, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
print(f"⚠️ 文件 {file_path} 解析失败,已重置为空!")
return {}
qa_data = load_json_safe("qa.json")
# 备份原始 JSON,防止数据丢失
synonyms_cache_file = "synonyms_cache.json"
synonyms_cache = load_json_safe(synonyms_cache_file)
# 使用 Hugging Face 生成同义扩展
generator = pipeline("text-generation", model="Qwen/Qwen1.5-1.8B", device=0)
def generate_synonyms(question, num_variations=10):
# 优化后的生成提示词
prompt = (
f"请为以下问题生成 {num_variations} 个不同的问法,但要保持意思不变:\n"
f"- 只调整句式,不改变核心含义\n"
f"- 必须保留原问题的核心主题\n"
f"- 直接返回问法,每行一个\n\n"
f"原问题:{question}\n"
)
# 调整生成参数
response = generator(
prompt,
max_new_tokens=200,
do_sample=True, # 启用采样模式
temperature=0.5, # 提高创造性
num_return_sequences=3 # 生成多组结果增加多样性
)
# 合并多组结果并去重
raw_output = " ".join([res["generated_text"] for res in response])
raw_output = raw_output.replace(prompt, "").strip()
# 强化过滤规则
synonyms = list(set(
q[1].strip().rstrip("??") + "?" # 只取匹配到的第二个元素(实际的问题)
for q in re.findall(r"(\d+\.\s*)?(.+?)", raw_output)
if len(q[1]) > 3 and not any(w in q[1] for w in ["注意事项", "常用语", "技巧"])
))
print(f"\n🔹【原问题】{question}\n🔹【同义扩展】{synonyms}\n") # ✅ 打印同义扩展
return synonyms[:num_variations] # 确保返回数量准确
# 生成所有问题的扩展
question_expansions = {q: generate_synonyms(q) for q in qa_data.keys()}
# **保存到 JSON 文件**
with open("synonyms_cache.json", "w", encoding="utf-8") as f:
json.dump(question_expansions, f, ensure_ascii=False, indent=4)
print("✅ 同义扩展数据已保存到 synonyms_cache.json")
# 构建向量数据库
def create_documents(qa_data, question_expansions):
documents = []
for question, answer in qa_data.items():
# 确保答案格式统一
answer = answer[0] if isinstance(answer, list) else answer
# 添加原始问题(重要!)
documents.append(Document(
page_content=question.rstrip("??") + "?", # 统一问号
metadata={"answer": answer}
))
# 添加扩展问题
for syn in question_expansions.get(question, []):
syn = syn.rstrip("??") + "?"
documents.append(Document(
page_content=syn,
metadata={"answer": answer}
))
return documents
documents = create_documents(qa_data, question_expansions)
# 选择适合中文的嵌入模型
model_name = "BAAI/bge-large-zh"
embeddings = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs={"device": device},
encode_kwargs={'normalize_embeddings': True}
)
vectorstore = FAISS.from_documents(documents, embeddings)
# 查询函数
def query_ai_knowledge_base(query):
# 预处理查询语句
processed_query = query.strip().rstrip("??") + "?"
# 放宽阈值并增加返回数量
similar_docs = vectorstore.similarity_search_with_relevance_scores(
processed_query,
k=10, # 扩大搜索范围
score_threshold=0.3 # 降低阈值
)
# 加权得分排序
answers = {}
for doc, score in similar_docs:
ans = doc.metadata["answer"]
answers[ans] = answers.get(ans, 0) + score
# 选取最高分答案
if not answers:
return "未找到相关信息"
best_answer = max(answers.items(), key=lambda x: x[1])[0]
return best_answer
# 带日志记录的查询函数
def query_with_logging(query):
result = query_ai_knowledge_base(query)
logging.info(f"Query: {query} | Result: {result} | Timestamp: {datetime.now()}")
return result
# 交互式测试查询
while True:
user_query = input("\n请输入查询问题 (输入 'exit' 结束): ").strip()
if user_query.lower() in ["退出", "q", "exit"]:
print("🔚 退出查询")
break
print("回答:", query_with_logging(user_query))
结论
通过这个简单的示例,展示了通过智能生成同义问法、向量化查询和快速检索,提供相关的答案,极大地提升了查询效率和准确性。
如果你觉得这篇博客对你有帮助,别忘了点赞、收藏并转发给你的朋友!更多精彩内容,敬请关注!