零成本搭建RAG智能客服:Ollama + Milvus + DeepSeek全程实战

基于 Milvus + Ollama(BGE-M3) + DeepSeek 的智能客服 RAG 实战

一、项目背景

在社保、医保、就业等公共服务领域,每天都有大量群众拨打热线咨询相似问题。传统人工客服成本高、效率低,而基于关键词匹配的机器人又难以理解用户的真实意图。

本项目基于 RAG(Retrieval-Augmented Generation,检索增强生成) 架构,搭建了一套智能客服系统,实现:

  • 用户问题语义理解(非关键词匹配)
  • 从知识库中精准检索相关问答
  • 基于 LLM 生成自然语言回答
  • 自动推荐相关问题引导用户继续咨询
  • 支持语音输入和语音播报

二、整体架构

复制代码
用户(语音/文本输入)
        │
        ▼
   ┌─────────────┐
   │   Flask服务   │  ← HTTPS部署
   │   (后端API)   │
   └──────┬──────┘
          │
          ▼
   ┌──────────────────┐     ┌──────────────────┐
   │   Ollama本地服务   │     │   Milvus向量数据库  │
   │ (bge-m3 向量化)   │────▶│ (知识库语义检索)    │
   └──────────────────┘     └────────┬─────────┘
                                     │ Top-20 相似问答
                                     ▼
                           ┌──────────────────┐
                           │  DeepSeek API     │
                           │ (LLM推理生成回答)  │
                           └────────┬─────────┘
                                    │
                                    ▼
                             回答 + 推荐问题
                           (SSE流式 / 普通返回)

三、技术栈

组件 技术选型 说明
Embedding模型 Ollama + bge-m3 本地部署,免费,支持中英文混合
向量数据库 Milvus 开源高性能,支持COSINE相似度
大语言模型 DeepSeek API (deepseek-chat) 推理能力强,性价比高
后端服务 Flask + flask_cors 轻量级,支持HTTPS
前端 HTML + JS + jQuery 支持语音输入(讯飞) + Markdown渲染

四、环境准备

4.1 安装 Ollama 并拉取模型

bash 复制代码
# 安装Ollama(参考官方文档 https://ollama.com)
# 拉取中文Embedding模型
ollama pull bge-m3

验证模型是否可用:

bash 复制代码
curl http://localhost:11434/api/embeddings -d '{
  "model": "bge-m3:latest",
  "prompt": "社保缴费年限不够怎么办"
}'

返回结果中包含 embedding 字段即为成功。

4.2 部署 Milvus

推荐使用 Docker 部署:

bash 复制代码
# 下载docker-compose.yml
wget https://github.com/milvus-io/milvus/releases/download/v2.4.0/milvus-standalone-docker-compose.yml -O docker-compose.yml

# 启动
docker compose up -d

4.3 安装Python依赖

bash 复制代码
pip install flask flask-cors pymilvus requests chardet

五、知识库构建

5.1 创建Milvus集合

python 复制代码
from pymilvus import (
    MilvusClient, FieldSchema, CollectionSchema, DataType
)

client = MilvusClient(
    uri="http://your_milvus_host:19530",
    token="your_token",
    db_name="default"
)

fields = [
    FieldSchema(name="uid", dtype=DataType.INT64, is_primary=True, auto_id=False, max_length=100),
    FieldSchema(name="Question", dtype=DataType.VARCHAR, max_length=5000),
    FieldSchema(name="Answer", dtype=DataType.VARCHAR, max_length=5000),
    FieldSchema(name="Vector", dtype=DataType.FLOAT_VECTOR, dim=1024),
]

schema = CollectionSchema(fields, "社保知识库向量存储")
client.create_collection(
    collection_name="SI_knowledge",
    schema=schema,
    dimension=1024
)

注意 :bge-m3 模型输出的向量维度为 1024 ,创建集合时 dim 必须一致。

5.2 创建向量索引

python 复制代码
def create_index(client: MilvusClient, collection_name: str):
    index_para = client.prepare_index_params()
    index_para.add_index(
        field_name="Vector",
        index_type="IVF_FLAT",
        metric_type="COSINE",
        params={"nlist": 1024}
    )
    client.create_index(collection_name=collection_name, index_params=index_para)

5.3 导入知识库数据

将业务问答数据向量化并存入Milvus。核心逻辑:先检索是否已存在语义相似的问答(去重),再插入新数据。

python 复制代码
def vectorize_text(text):
    url = "http://localhost:11434/api/embeddings"
    headers = {"Content-Type": "application/json", "Accept": "application/json"}
    data = {
        "model": "bge-m3:latest",
        "prompt": text
    }
    response = requests.post(url, json=data, headers=headers)
    if response.status_code == 200:
        return response.json().get('embedding', [])
    return None

def import_knowledge(client, question, answer, uid):
    vector = vectorize_text(question)

    # 去重:先检索是否存在语义高度相似的问题
    search_params = {
        "metric_type": "COSINE",
        "params": {"radius": 0.87}
    }
    res = client.search(
        collection_name="SI_knowledge",
        data=[vector],
        limit=10,
        output_fields=["uid", "Question"],
        search_params=search_params
    )

    if len(res[0]) > 0:
        print(f"已存在相似问题: {res[0][0]['entity']['Question']}")
        return

    # 插入新数据
    data = {"uid": uid, "Question": question, "Answer": answer, "Vector": vector}
    client.insert(collection_name="SI_knowledge", data=data)
    print(f"插入成功: uid={uid}")

六、核心服务实现

6.1 Flask服务主框架

python 复制代码
from flask import Flask, request, render_template, Response, stream_with_context
from pymilvus import MilvusClient
from flask_cors import CORS
import requests, json, time

app = Flask(__name__)
CORS(app)

# Milvus连接配置
client = MilvusClient(
    uri="your_milvus_host:19530",
    token="your_token",
    db_name="default"
)

# DeepSeek API配置
DEEPSEEK_URL = "https://api.deepseek.com/chat/completions"
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY")  # 建议使用环境变量

headers = {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Authorization": f"Bearer {DEEPSEEK_API_KEY}"
}

# 向量检索参数
search_params = {
    "metric_type": "COSINE",
    "params": {"radius": 0.5}
}

6.2 本地Embedding向量化

python 复制代码
def vectorize_text(text):
    """调用本地Ollama的bge-m3模型进行文本向量化"""
    url = "http://localhost:11434/api/embeddings"
    headers = {"Content-Type": "application/json", "Accept": "application/json"}
    data = {"model": "bge-m3:latest", "prompt": text}
    response = requests.post(url, json=data, headers=headers)
    if response.status_code == 200:
        return response.json().get('embedding', [])
    return None

为什么用本地Ollama做Embedding?

  • 免费调用,无API费用
  • 数据不出内网,安全可控
  • bge-m3 对中文语义理解效果优秀

6.3 普通问答接口(同步返回)

python 复制代码
@app.route('/getAnser', methods=['GET'])
def get_answer():
    question = request.args.get('q')

    # 1. 问题向量化
    vector = vectorize_text(question)

    # 2. Milvus语义检索,获取Top-20相关问答
    res = client.search(
        collection_name="SI_knowledge",
        data=[vector],
        limit=20,
        output_fields=["uid", "Question", "Answer"],
        search_params=search_params
    )

    # 3. 构建DeepSeek的请求
    data = {
        "model": "deepseek-chat",
        "messages": [],
        "stream": False,
        "temperature": 0.3,
        "max_tokens": 8192
    }

    # 用户问题(带Prompt约束)
    data["messages"].append({
        "role": "user",
        "content": (
            question
            + "?只能使用提供的上下文进行逻辑推理,不要提示根据提供的上下文。"
            "回答问题后,推荐相关的3个问题且不含回答及序号,"
            "且答案与推荐的问题之间用<separate>分开且问题的答案在前面"
        )
    })

    # 将检索到的知识库答案作为System上下文注入
    for item in res[0]:
        data["messages"].append({
            "role": "system",
            "content": item["entity"]["Answer"]
        })

    # 4. 调用DeepSeek生成回答
    start_time = time.time()
    response = requests.post(DEEPSEEK_URL, json=data, headers=headers)
    print(f"耗时: {time.time() - start_time:.2f}s")

    if response.status_code == 200:
        result = response.json()
        content = result["choices"][0]["message"]["content"]

        # 5. 解析回答和推荐问题
        answer, recommend = parse_answer_and_recommend(content)
        return answer + "<separate>" + recommend
    return ""

6.4 流式问答接口(SSE实时返回)

python 复制代码
def safe_decode(byte_data: bytes, encoding='utf-8') -> str:
    """兼容多种编码格式的字节解码"""
    try:
        encoding = requests.compat.chardet.detect(byte_data)['encoding']
        return byte_data.decode(encoding)
    except UnicodeDecodeError:
        return byte_data.decode(encoding, errors='replace')

@app.route('/getAnserStream', methods=['GET'])
def get_answer_stream():
    question = request.args.get('q')

    # 1. 向量化 + 检索(同上)
    vector = vectorize_text(question)
    res = client.search(
        collection_name="SI_knowledge",
        data=[vector],
        limit=20,
        output_fields=["uid", "Question", "Answer"],
        search_params=search_params
    )

    # 2. 构建请求(stream=True)
    data = {
        "model": "deepseek-chat",
        "messages": [],
        "stream": True,
        "temperature": 0.3,
        "max_tokens": 8192
    }

    data["messages"].append({
        "role": "user",
        "content": (
            question
            + "?只能使用提供的上下文进行逻辑推理,不要提示根据提供的上下文,"
            "如果发现是问了多个问题,拆分问题。"
            "回答问题后,从上下文中推荐相关的3个问题且不含回答及序号,"
            "且答案与推荐的问题之间用<separate>分开且问题的答案在前面"
        )
    })

    for item in res[0]:
        data["messages"].append({
            "role": "system",
            "content": f"问:{item['entity']['Question']}?答:{item['entity']['Answer']}"
        })

    # 3. 流式调用DeepSeek
    response = requests.post(DEEPSEEK_URL, json=data, headers=headers, stream=True)

    if response.status_code == 200:
        def generate():
            for line in response.iter_lines():
                if line:
                    text = safe_decode(line).replace("data: ", "")
                    try:
                        chunk = json.loads(text)
                        content = chunk["choices"][0]["delta"]["content"]
                        yield content
                    except Exception:
                        pass
        return Response(stream_with_context(generate()), mimetype='text/plain')
    return ""

6.5 解析回答和推荐问题

python 复制代码
def parse_answer_and_recommend(raw_content: str):
    """从LLM输出中分离答案和推荐问题"""
    # 处理DeepSeek-R1的思考标记
    char_think = ">\u25b8"
    position = raw_content.find(char_think)
    start = position + 8 if position > 0 else 0
    content = raw_content[start:]

    # 分离答案和推荐问题
    sep = "<separate>"
    sep_pos = content.find(sep)

    if sep_pos > 0:
        answer = content[:sep_pos]
        recommend_text = content[sep_pos + len(sep):]
        # 清洗推荐问题文本
        lines = recommend_text.split('\n')
        cleaned = []
        for line in lines:
            for noise in ["相关推荐问题", "相关问题推荐", "推荐相关问题", "*", ":", ":"]:
                line = line.replace(noise, "")
            if len(line) >= 4:
                cleaned.append(line)
        recommend = "<separate>".join(cleaned)
        return answer, recommend

    return content, ""

七、前端集成

7.1 流式请求与Markdown渲染

javascript 复制代码
function askllm(question) {
    var allResult = '';
    fetch('/getAnserStream?q=' + encodeURIComponent(question))
    .then(response => {
        const reader = response.body.getReader();
        const decoder = new TextDecoder();

        function readChunk() {
            return reader.read().then(({ done, value }) => {
                if (done) {
                    // 流结束,解析推荐问题
                    const sepIndex = allResult.indexOf("<separate>");
                    const answer = allResult.slice(0, sepIndex);
                    // 使用marked.js渲染Markdown
                    document.getElementById("result").innerHTML =
                        marked(allResult.substring(0, sepIndex));
                    // 解析并渲染推荐问题...
                    return;
                }
                const text = decoder.decode(value);
                allResult += text;
                document.getElementById('result').innerHTML += text;
                readChunk();
            });
        }
        readChunk();
    });
}

7.2 语音输入(讯飞语音识别)

前端集成了讯飞实时语音转写(iat),用户可以通过点击麦克风按钮直接语音提问,语音识别结果自动发送给后端进行问答。

7.3 语音播报(讯飞TTS)

回答生成完成后,自动调用讯飞TTS进行语音播报,实现完整的语音交互闭环。

八、HTTPS部署

python 复制代码
if __name__ == '__main__':
    app.run(
        host='0.0.0.0',
        port=443,
        ssl_context=('path/to/server.crt', 'path/to/server.key')
    )

生产环境建议使用 Nginx 反向代理 + Let's Encrypt 证书,而非直接在Flask中启用HTTPS。

九、关键技术细节

9.1 Prompt工程

本项目的Prompt设计核心思路:

复制代码
用户问题 + 约束条件 → 要求LLM:
1. 只能使用提供的上下文推理(防止幻觉)
2. 不提示"根据提供的上下文"(更自然)
3. 拆分多问题场景
4. 输出答案 + 推荐问题(用特殊分隔符分隔)

将检索到的知识库答案以 system 角色注入,格式为 问:xxx?答:xxx,让LLM理解上下文结构。

9.2 相似度阈值调优

参数 说明
radius 0.5 检索阈值,越高越严格
limit 20 检索返回条数
metric_type COSINE 余弦相似度
temperature 0.3 LLM温度值,偏低以保持准确

知识库去重时使用 radius=0.87,确保高语义相似的问题不会重复入库。

9.3 选择bge-m3的原因

  • 中英文混合支持:bge-m3 多语言效果好
  • 维度适中:1024维,Milvus检索效率高
  • 本地部署零成本:通过Ollama本地运行,无需API调用

十、效果展示

用户提问示例:

用户: "职工缴费年限不够,怎么办"

系统回答:

  • 从知识库检索到Top-20相关问题及答案
  • DeepSeek基于上下文生成结构化回答
  • 自动推荐3个相关问题:如"养老保险缴费标准是什么"、"灵活就业人员如何参保"等

十一、优化方向

  1. Prompt输出结构化 :当前使用 <separate> 分隔符解析推荐问题,可改为JSON格式输出更稳定
  2. Rerank重排序:在Milvus检索后增加Rerank模型(如bge-reranker),提升Top-K准确率
  3. 对话历史管理:当前为单轮对话,可增加session管理实现多轮上下文
  4. 缓存层:高频问题可加Redis缓存,减少重复的Embedding和LLM调用
  5. 知识库更新机制:定时从业务系统同步最新问答数据

十二、总结

本项目通过 Ollama本地Embedding + Milvus向量检索 + DeepSeek推理 的RAG架构,实现了:

  • 零Embedding API成本的语义检索
  • 基于知识库的准确回答(减少幻觉)
  • 流式输出 + 语音交互的良好用户体验

整套方案技术栈开源、部署简单,适合政府、企业等有私有知识库的智能客服场景。


相关技术文档

相关推荐
ZPC82104 小时前
自定义action server 接收arm_controller 指令
人工智能·机器人
迷茫的启明星4 小时前
各职业在当前发展阶段,使用AI的舒适区与盲区
大数据·人工智能·职场和发展
Liqiuyue5 小时前
Transformer:现代AI革命背后的核心模型
人工智能·算法·机器学习
桂花饼5 小时前
AI 视频生成:sora-2 模型快速对接指南
人工智能·音视频·sora2·nano banana 2·claude-opus-4-6·gemini 3.1
GreenTea6 小时前
AI Agent 评测的下半场:从方法论到落地实践
前端·人工智能·后端
冬奇Lab7 小时前
一天一个开源项目(第73篇):Multica - 把 AI 编程智能体变成真正的团队成员
人工智能·开源·资讯
天地沧海7 小时前
AI知识库集问答
人工智能
冬奇Lab7 小时前
大模型就是你雇的员工:从职场管理学看 AI 协作范式的三次进化
人工智能