#实战项目三:语义搜索与问答系统
📂 所属阶段:第六阶段 — 工业级 NLP 项目实战
🔗 相关章节:参数高效微调 (PEFT) · Prompt Engineering 基础
#1. 项目概述
目标:构建 FAQ 智能问答系统
用户提问:"密码忘记了怎么办?"
系统回答:根据 FAQ 知识库返回最相关的答案
核心技术:
- 语义向量:将文本编码为向量
- 向量检索:找最相似的 FAQ 条目
- RAG:结合检索 + 生成#2. 架构设计
┌──────────────────────────────────────────────────────┐
│ 用户提问 │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Step 1: 语义向量编码 │
│ Embedding Model(Sentence-BERT) │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Step 2: 向量数据库检索 │
│ FAISS / Milvus / Chroma │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Step 3: 构建 Prompt │
│ 检索结果 + 问题 → Prompt │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ Step 4: LLM 生成回答 │
│ GPT-4o / 本地 LLaMA │
└──────────────────────────────────────────────────────┘
↓
最终回答#3. 数据准备
# data_prepare.py
import pandas as pd
# FAQ 数据示例
faq_data = [
{
"question": "密码忘记了怎么办?",
"answer": "请访问登录页面,点击"忘记密码",输入注册邮箱,系统会发送重置链接到您的邮箱。或者联系客服人工处理。",
"category": "账户"
},
{
"question": "如何修改个人信息?",
"answer": "登录后点击右上角头像 → "个人中心" → "编辑资料",即可修改昵称、头像、简介等信息。",
"category": "账户"
},
{
"question": "订阅如何取消?",
"answer": "登录后进入"我的订阅"页面,点击"取消订阅"按钮即可。取消后服务将在当前周期结束后停止。",
"category": "订阅"
},
{
"question": "支持哪些支付方式?",
"answer": "我们支持微信支付、支付宝、信用卡(Visa/Mastercard)、PayPal 等多种支付方式。",
"category": "支付"
},
# ... 更多 FAQ 条目
]
df = pd.DataFrame(faq_data)
df.to_json("faq.jsonl", orient="records", lines=True, force_ascii=False)#4. 向量化与索引
# vectorize.py
from sentence_transformers import SentenceTransformer
import numpy as np
import faiss
# 加载 Sentence-BERT 模型
model = SentenceTransformer("all-MiniLM-L6-v2") # 轻量,中文可用 paraphrase-multilingual
# 向量化 FAQ
questions = df["question"].tolist()
question_embeddings = model.encode(questions, show_progress_bar=True)
embeddings = np.array(question_embeddings).astype("float32")
# L2 归一化(余弦相似度等价于内积)
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
embeddings = embeddings / norms
# 构建 FAISS 索引
d = embeddings.shape[1] # 向量维度
index = faiss.IndexFlatIP(d) # 内积索引(需归一化)
index.add(embeddings)
# 保存
faiss.write_index(index, "faq.index")
df.to_pickle("faq.pkl")
print(f"FAQ 条目数:{len(questions)},向量维度:{d}")#5. 语义检索
# retrieve.py
from sentence_transformers import SentenceTransformer
import faiss
import pandas as pd
model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
index = faiss.read_index("faq.index")
faq_df = pd.read_pickle("faq.pkl")
def semantic_search(query, top_k=3):
"""语义检索"""
# 编码查询
query_vec = model.encode([query]).astype("float32")
query_vec = query_vec / np.linalg.norm(query_vec)
# 检索
distances, indices = index.search(query_vec, top_k)
# 返回结果
results = []
for dist, idx in zip(distances[0], indices[0]):
if idx >= 0: # 有效结果
results.append({
"question": faq_df.iloc[idx]["question"],
"answer": faq_df.iloc[idx]["answer"],
"score": float(dist),
})
return results
# 示例
results = semantic_search("密码忘了怎么重置")
for r in results:
print(f"问题:{r['question']}")
print(f"答案:{r['answer']}")
print(f"相似度:{r['score']:.4f}\n")#6. RAG 问答
# rag_qa.py
from openai import OpenAI
client = OpenAI()
def rag_answer(question, top_k=3):
"""检索 + 生成(RAG)"""
# 1. 检索相关 FAQ
results = semantic_search(question, top_k)
if not results:
return {"answer": "抱歉,没有找到相关信息。", "sources": []}
# 2. 构建上下文
context = "\n\n".join([
f"FAQ {i+1}:{r['question']}\n答案:{r['answer']}"
for i, r in enumerate(results)
])
# 3. 构建 Prompt
prompt = f"""你是一个智能客服助手。根据以下 FAQ 信息回答用户问题。
FAQ 信息:
{context}
用户问题:{question}
要求:
1. 如果 FAQ 中有相关信息,用自己的话总结回答
2. 如果没有相关信息,礼貌地说明无法解答
3. 回答控制在 100 字以内
4. 回答后注明参考的 FAQ 编号
"""
# 4. 调用 LLM
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
max_tokens=300,
)
answer = response.choices[0].message.content
return {
"answer": answer,
"sources": [{"question": r["question"], "score": r["score"]} for r in results]
}
# 示例
result = rag_answer("密码忘记了怎么处理")
print(result["answer"])
print("参考来源:", result["sources"])#7. FastAPI 部署
# app.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(title="FAQ 智能问答系统")
class QuestionRequest(BaseModel):
question: str
top_k: int = 3
@app.post("/qa")
def qa_endpoint(req: QuestionRequest):
return rag_answer(req.question, req.top_k)
@app.post("/search")
def search_endpoint(req: QuestionRequest):
results = semantic_search(req.question, req.top_k)
return {"results": results}
@app.get("/health")
def health():
return {"status": "ok"}
# 启动:uvicorn app:app --host 0.0.0.0 --port 8000#8. Streamlit 前端
# ui.py
import streamlit as st
import requests
st.title("FAQ 智能问答系统")
question = st.text_input("请输入您的问题:", placeholder="例如:如何取消订阅?")
if st.button("提问"):
if question:
with st.spinner("思考中..."):
resp = requests.post(
"http://localhost:8000/qa",
json={"question": question}
)
result = resp.json()
st.success(result["answer"])
with st.expander("查看参考来源"):
for i, src in enumerate(result["sources"]):
st.write(f"**{i+1}. {src['question']}** (相似度: {src['score']:.3f})")#9. 小结
FAQ 智能问答系统流程:
1. 准备 FAQ 数据(问答对)
2. Sentence-BERT 向量化
3. FAISS 构建向量索引
4. 语义检索 → top_k 相关问答
5. Prompt Engineering → 注入上下文
6. LLM 生成回答
2026 年进阶方向:
- 支持多轮对话
- 集成私有知识库(PDF/Word)
- 流式输出(打字机效果)
- 多模态(支持图片提问)💡 最佳实践:RAG 系统的核心是"检索质量"。如果检索到不相关内容,再强大的 LLM 也无法给出正确答案。优先优化 Embedding 模型和向量索引质量。
🔗 扩展阅读

