实战项目三:语义搜索与问答系统

📂 所属阶段:第六阶段 — 工业级 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 模型和向量索引质量。


🔗 扩展阅读