首页 / 资讯中心 / 文章详情

零成本搭建私有化RAG智能客服:Ollama+Milvus+DeepSeek完整实战

零成本搭建私有化RAG智能客服:Ollama+Milvus+DeepSeek完整实战 ★ FEATURED ARTICLE
如果你手头有一个客服知识库但预算不够去买大模型 API 套餐也担心把内部资料丢到外部服务上不安全那这套方案应该能帮到你。我最近按“尽量不花钱”的思路把 Ollama 和 Milvus 以及 DeepSeek 搭成了一个完整的 RAG 智能客服从环境搭建到代码落地全程没有产生一分钱 API 费用。这篇文章把整个过程中的架构思路、实现细节、调优经验和踩坑记录完整写出来目标是让你跟着操作也能跑通。这个方案适合有基本 Python 基础的开发者或者刚接触 RAG、想自己搞一个私有知识库问答系统的小团队。不需要租 GPU不需要买向量数据库服务一台 16G 内存的普通电脑就能跑起来。我会解释每个环节为什么这么选也会把参数背后的逻辑讲清楚避免你只知道复制粘贴。1. 先理清楚这个RAG客服到底怎么工作1.1 RAG解决什么问题RAG 全称是 Retrieval-Augmented Generation中文叫检索增强生成。它的核心想法很简单大模型不是万能的尤其对私有领域的知识一无所知那就先在知识库里找出和用户问题最相关的资料再把资料一起喂给大模型让它结合资料来回答。你可以把 RAG 理解成“开卷考试”。大模型是考生Milvus 是书架Ollama 负责把考生带到考场。考生不需要把所有知识点背下来只需要在书架上快速翻到相关章节然后照着章节内容答题。这就解决了客服场景里最实际的问题知识库会频繁更新如果每次更新都去微调大模型成本和时间都受不了。用 RAG新增一份文档向量化后塞进 Milvus 就行了模型本身不用动。为什么要自己做 RAG 而不是直接调用大模型 API原因有两个。一是按 token 计费在客服高频场景下是持续成本问答一旦多起来账单很容易失控。二是客户咨询中难免涉及订单、售后、价格策略等敏感信息员工或者用户提出问题后这些数据如果经过外部 API就存在泄漏风险。本地化部署意味着模型和向量库运行在自己电脑或内网服务器上数据不出门采购流程和合规审查都能省掉一大截。1.2 三个组件各扮演什么角色Ollama一个本地大模型运行工具把 DeepSeek 等开源模型下载到本地并提供 HTTP 接口。它封装了模型下载、推理调度、内存管理你不需要手动配置复杂的 Python 推理环境。Milvus开源向量数据库存的是“文本的数学表示”也就是 embedding 向量。它的作用是快速找出和用户问题语义最接近的知识片段。相比用 CSV 文件暴力扫描或者简单关键词匹配向量检索能理解近义词和口语化表达。DeepSeek这里指通过 Ollama 拉取到的 DeepSeek 系列模型比如 deepseek-r1:7b。它是真正生成回答的“大脑”接收检索出来的资料组合成最终话术。这组合最香的地方是三个组件全都有免费可用的开源版本不涉及按量计费也没有限流。你只需要付出硬件电费和时间。1.3 整体链路与选型理由整个流程可以拆成两条链路一条是知识入库一条是检索问答。知识入库就是把客服文档切分成小段用 embedding 模型把每段文本变成向量再把向量和原文一起写入 Milvus。检索问答则是用户发来问题后把问题用同一个 embedding 模型变成向量去 Milvus 里用相似度搜索取回 Top K 条资料最后把问题和资料拼成提示词交给 Ollama 里的 DeepSeek 模型生成答案。我选了 Milvus 而不是其他向量数据库是因为它在大规模向量检索上性能很稳而且有 Milvus Lite 这种轻量模式开发阶段可以像用 SQLite 一样直接用文件不用先部署一套服务。等将来数据量大了再平滑切到 Docker 单机版或者集群版。Ollama 则是目前本地跑模型最省事的工具一条命令拉模型一条命令起服务对新手相当友好。2. 从零开始准备环境2.1 安装Ollama并拉取模型如果你的系统是 Linux 或者 macOS打开终端执行curl -fsSL https://ollama.com/install.sh | shWindows 用户直接到官方仓库下载安装包双击安装即可。装完后执行ollama -v看是否成功。然后拉取两个模型一个是生成回答用的 DeepSeek一个是生成向量用的 BGE-M3ollama pull deepseek-r1:7b ollama pull bge-m3我为什么选 deepseek-r1:7b因为它的体量和效果比较平衡。7B 参数量的模型经过量化后在 CPU 上也能跑只是速度慢一点如果有 NVIDIA 显卡Ollama 会默认用 GPU 加速体验会好很多。如果你电脑内存只有 8G也可以换更小的 deepseek-r1:1.5b牺牲一点回答质量换速度。BGE-M3 是专门做 embedding 的模型输出 1024 维向量对中文的支持比很多通用模型好。这里有个关键点知识入库和查询提问必须用同一个 embedding 模型否则向量的空间不一致检索结果一定不准。拉完模型后Ollama 默认监听 11434 端口。可以用ollama list查看已有模型用curl http://localhost:11434/api/tags验证服务是否正常。2.2 安装并启动一个MilvusMilvus 有两种跑法我建议开发阶段用最简单的那种。最简单的方式是安装 Milvus Lite它可以把数据库保存在一个本地文件里非常适合学习和测试。你先创建虚拟环境然后安装依赖python -m venv rag_env source rag_env/bin/activate # Windows 下执行 rag_env\Scripts\activate pip install -U pymilvus milvus-lite requests之后在代码里这样初始化客户端from pymilvus import MilvusClient # 开发模式数据保存在本地文件 client MilvusClient(knowledge.db)如果用 Docker 部署正式环境那就下载官方的单机部署文件wget https://github.com/milvus-io/milvus/releases/download/v2.4.16/milvus-standalone-docker-compose.yml -O docker-compose.yml docker compose up -dDocker 模式会启动 Milvus、etcd 和 MinIO 三个服务实际生产中建议用这种方式。连接方式改成# 生产模式连接 Docker 服务 client MilvusClient(urihttp://localhost:19530, tokenroot:Milvus)要注意Milvus 默认端口是 19530docker-compose 启动后如果发现端口被占用先看是不是有别的服务占着端口。开发模式和生产模式代码差别很小这也是我用 pymilvus 的 MilvusClient 封装的原因切换环境不用改逻辑。2.3 准备知识库数据与切分思路数据质量决定 RAG 质量这句话怎么强调都不过分。我先造一个模拟的电商售后知识库内容大概是退换货规则、物流时效、退款说明这些常见客服问题。你可以用自己的业务文档替换。推荐的数据格式是纯文本或者 Markdown关键是将每一条知识整理成“一问一答”或“标题正文”的结构化形式。比如【退货政策】自签收之日起7天内可申请无理由退货退回商品需保持完好且不影响二次销售运费由买家承担。 【换货规则】产品自收到次日起30天内若出现非人为质量问题可以申请换货无需承担往返运费。 【退款时效】退货完成后退款会在1-3个工作日内原路返回具体到账时间取决于支付渠道。 【物流查询】商品发货后物流单号会在24小时内更新若超过48小时未更新可能正在转运中。拿到原始文档后先做清洗把 PDF 复制出来的断行去掉、把全角半角标点统一、把明显乱码段落删掉。清洗完再切分切分这一步容易被忽略但直接影响检索效果。我推荐按段落语义切分而不是死板地按字数硬切。可以写一个简单的按空行分段、再按最大长度合并的函数def split_text(text, max_len500, overlap50): paragraphs [p.strip() for p in text.split(\n\n) if p.strip()] chunks [] buffer for para in paragraphs: if len(buffer) len(para) max_len: buffer \n para else: if buffer: chunks.append(buffer) if len(para) max_len: # 超长段落按句号二次切分 start 0 while start len(para): end min(start max_len, len(para)) cut para.rfind(。, start, end) if cut start max_len // 2: chunks.append(para[start:cut1]) start cut 1 else: chunks.append(para[start:end]) start end buffer if buffer: chunks.append(buffer) return chunks这里设置max_len500是因为 BGE-M3 对长文本的编码能力虽然不错但单段超过一定长度后向量会被“稀释”检索精度下降。overlap参数我其实没有在函数里写进去如果你希望相邻片段有重叠可以每次保留上一段末尾的 50 个字再拼到下一段开头能避免跨段信息被截断。3. 核心代码一向量化与数据入库3.1 设计集合Schema在 Milvus 里表叫 Collection每条记录叫 Entity。我们要建一个叫knowledge_base的集合字段设计如下字段名类型说明idINT64主键自增或手动指定textVARCHAR存切分后的文本原文embeddingFLOAT_VECTOR1024维向量来自 BGE-M3创建集合的代码如下from pymilvus import MilvusClient, DataType client MilvusClient(knowledge.db) if client.has_collection(collection_nameknowledge_base): client.drop_collection(collection_nameknowledge_base) schema client.create_schema(auto_idFalse, enable_dynamic_fieldTrue) schema.add_field(field_nameid, datatypeDataType.INT64, is_primaryTrue) schema.add_field(field_nametext, datatypeDataType.VARCHAR, max_length1024) schema.add_field(field_nameembedding, datatypeDataType.FLOAT_VECTOR, dim1024) index_params client.prepare_index_params() index_params.add_index(field_nameembedding, index_typeFLAT, metric_typeCOSINE) client.create_collection( collection_nameknowledge_base, schemaschema, index_paramsindex_params )选metric_typeCOSINE是因为余弦相似度更适合衡量语义向量的方向一致性而不是绝对距离。对于文本场景两个向量方向越接近语义就越相关。FLAT索引是暴力计算小数据量下没必要用高级索引将来超过百万条再改成 IVF_FLAT 或 HNSW 不迟。3.2 用BGE-M3生成向量并写入调用 Ollama 的接口生成 embedding封装一个函数import requests OLLAMA_URL http://localhost:11434 def embed_texts(texts): resp requests.post( f{OLLAMA_URL}/api/embed, json{model: bge-m3, input: texts} ) resp.raise_for_status() return resp.json()[embeddings]然后把文本切分好的每一段转成向量逐批写入 Milvusdef ingest_docs(doc_text, start_id0): chunks split_text(doc_text) vectors embed_texts(chunks) data [] for i, (chunk, vec) in enumerate(zip(chunks, vectors)): data.append({ id: start_id i, text: chunk, embedding: vec }) client.insert(collection_nameknowledge_base, datadata) return len(data)这里有个容易踩的坑当你批量传入很多文本给 Ollama 时它返回的 embeddings 顺序和输入的顺序是一致的。但如果你在做并发请求就得自己保证顺序否则数据会对不上号。我建议先单请求批量传等数据量大了再拆并发并发加 seq 字段标识顺序。写入后可以查一下集合里的条数print(client.get_collection_stats(collection_nameknowledge_base))如果返回{row_count: 12}之类的数据就说明入库成功。3.3 验证入库效果光入库不算完先手动检索一下确保向量库真的能返回相关内容test_question 退货需要满足什么条件 test_vec embed_texts([test_question])[0] res client.search( collection_nameknowledge_base, data[test_vec], limit2, output_fields[text] ) for hit in res[0]: print(hit[entity][text], score:, hit[distance])这一步很关键。如果相似度分数很高说明 embedding 模型和切分策略没问题。如果分数普遍很低或者检索结果完全无关那一定得回头检查数据和切分逻辑不要急着往下搭生成环节。4. 核心代码二召回 大模型生成回答4.1 查询向量化与相似度检索线上问答时用户输入的问题要先转成向量然后去 Milvus 里搜索。完整函数如下def search_knowledge(question, top_k3): q_vec embed_texts([question])[0] res client.search( collection_nameknowledge_base, data[q_vec], limittop_k, output_fields[text] ) results [hit[entity][text] for hit in res[0]] scores [hit[distance] for hit in res[0]] return results, scorestop_k3是我调下来比较稳的参数。取太少可能漏掉关键资料取太多无关内容混进来反而干扰模型。实际项目中可以先设 3后面根据准确率调整。4.2 构造提示词模板提示词模板是整个 RAG 生成质量的核心。我用的模板长这样def build_prompt(question, context): system ( 你是某线上商城的智能客服助手。 请依据知识库资料用自然、简洁、专业的中文回答用户问题。\n 规则\n 1. 优先使用资料原文信息回答。\n 2. 如果资料中没有答案不要编造请告知用户并建议联系人工客服。\n 3. 不要提及你是AI模型。\n 4. 回答控制在200字以内。\n ) user f知识库资料\n{context}\n\n用户问题{question} return system, user把检索出的片段用换行拼接成一个 context如果片段太长可以只保留开头一小段。提示词设计有一条铁律一定要告诉模型“不知道就直说”。否则模型会用训练积累的常识脑补答案这在客服场景里是灾难。4.3 接上DeepSeek模型输出最终答案获取答案时我推举用 Ollama 的/api/chat接口因为可以分别传 system 和 user 消息比拼接在同一个 prompt 里更规范def chat_with_knowledge(question): context_list, scores search_knowledge(question, top_k3) # 过滤低分结果 context \n.join(context_list) system, user build_prompt(question, context) resp requests.post( f{OLLAMA_URL}/api/chat, json{ model: deepseek-r1:7b, messages: [ {role: system, content: system}, {role: user, content: user} ], stream: False, options: { temperature: 0.3, top_p: 0.8 } } ) resp.raise_for_status() return resp.json()[message][content]把temperature调到 0.3是因为客服回复需要稳定性和准确性不需要太多创造性。如果温度太高同一个问题每次回答文案都会有明显差别而且更容易出现胡说八道的情况。写一个简单循环测试跑通if __name__ __main__: while True: q input(请输入问题输入exit退出) if q exit: break print(chat_with_knowledge(q))实际运行效果大概是输入“退货需要满足什么条件”系统返回“自签收之日起7天内可申请无理由退货商品需保持完好且不影响二次销售运费由买家承担。” 这种结果比直接用 DeepSeek 裸奔要靠谱得多因为每个回答都有知识库依据。5. 调优与避坑从“能用”到“好用”5.1 召回质量怎么调如果是这个方案可能被召回质量不好这个问题所困扰多半是以下几个原因。第一个是分块大小。块太大向量里塞进太多无关信息检索出来的片段就不聚焦块太小语义不完整模型找不到完整上下文。经验值是在 300 到 600 字之间客服文档偏短问答可以适当缩到 200 字左右。第二个是 top_k 和相似度阈值。可以看检索结果的 score不同 embedding 模型的 score 分布不一样。BGE-M3 的余弦相似度一般 0.6 以上才算靠谱低于 0.5 的基本可以直接丢弃。我建议先打印前 10 条检索结果人工看一眼 score 分布再定阈值。第三个是查询改写。用户口语化严重时比如“东西坏了怎么办”可能和知识库里的“产品质量问题换货规则”匹配不太上。这时候可以先用一个轻量模型把用户问题改写成语义更完整的查询比如“产品出现质量问题如何处理”再去做 embedding 搜索。这个技巧很有效但也会增加一次模型调用只能算锦上添花。5.2 让回答更可控的几个技巧有几次我发现模型回答虽然正确但太啰嗦一个问题能扯出一大段。后来在 system prompt 里加了一条“回答不超过 200 字”效果立竿见影。如果你想要更严格的控制可以加 few-shot 示例给模型一个标准问答样例输出风格会更稳定。另外DeepSeek-R1 系列本身是推理模型它会先把推理过程写在回答里。客服场景不需要展示推理链可以在提示词里加一句“直接输出最终回答不要输出思考过程”。否则用户会看到一大段自言自语体验很差。实测下来这一条不发不行。如果售后问题涉及多个知识片段比如用户既问退货又问退款模型可能需要综合两段资料。这时在 system prompt 里允许模型“分点作答”会更清晰我遇到过模型把所有内容糊成一段的情况后来提示词里明确“若涉及多个要点请分条列出”可读性提升很多。5.3 性能与并发注意事项本地部署最大的瓶颈是硬件。7B 量化模型在 CPU 上生成一个回答可能要 10 秒以上如果业务量不大倒也能接受。有 GPU 的话Ollama 会自动检测显存一般 8G 显存就能跑 7B 模型。并发方面Ollama 默认支持多个请求排队但我建议在实际服务里加一个简单的互斥锁避免用户集中问答时 OOM。生产做法是把 Ollama 的服务封装成一个 FastAPI 接口请求进来先查缓存再进 RAG 流程。向量检索的性能反而是小数据量下最不用担心的。几千条知识的本地向量库FLAT 索引的查询时间在毫秒级。真正要注意的是不要每来一个请求都往 Milvus 写数据新增知识可以用定时任务批量入库。5.4 数据更新策略知识库不是静态的每隔一段时间就要更新。最简单的更新方案是按 id 删除旧数据再插入新数据client.delete( collection_nameknowledge_base, filterid in [1, 2, 3] ) client.upsert(collection_nameknowledge_base, datanew_data)Milvus 2.4 之后的 pymilvus 客户端支持 upsert 操作按主键更新。实际项目中我会在文档里加一个source字段在更新时先把某个来源的所有记录删掉再重新插入这样不会把不同文档的旧版本残留在库里。我在处理过一份更新频率高的 FAQ 表格后意识到与其频繁全量重灌不如把知识库分成几个集合比如售后规则一个集合、商品信息一个集合检索时分开查再合并结果。这样更新某个模块时互不影响也能降低整体故障风险。6. 常见问题与排查实录6.1 Milvus连接失败症状是代码报RuntimeError: fail to connect to server。原因基本就是三种Milvus 服务没启动、URI 写错、端口被占用。如果是本地文件模式检查knowledge.db路径是否有写权限如果是 Docker 模式先执行docker compose ps看 Milvus 是否在运行。还有一点容易被忽略Milvus Lite 模式不能用http://localhost连接必须用本地文件路径反过来也一样。这种连接问题在我自己的部署中出现的频率很低但每次切换环境都容易踩。建议在入口处做一次连通性测试失败就打印明确提示不要等查询时报一堆底层异常。6.2 Ollama模型加载慢或内存不足症状是第一次请求等很久然后报library cuda错误或者直接 OOM。Ollama 第一次加载模型确实会慢因为它要把权重读入内存之后会缓存在内存中速度就快很多。如果是 CPU 机器内存至少得有 16G建议用 7B 量化版但预留充足的内存。如果爆内存换deepseek-r1:1.5b或者清理掉不用的模型。显存不够时可以设置 OLLAMA_MAX_LOADED_MODELS1让 Ollama 同时只加载一个模型。这个参数在系统环境变量里配置不会影响多模型支持只是减少内存占用。6.3 检索结果为空或相关度低检索结果为空先确认集合里有没有数据再看查询向量维度是否和集合维度一致。维度不一致时Milvus 会直接报错这属于低级问题但也容易在切换 embedding 模型后发生。相关度低的话先降低分块大小再检查是不是原始文档本身表述和用户问法差异太大。最有效的排查手段是打印检索出的原文人工判断是不是知识库压根没有覆盖这个知识点。如果知识库里真的没有那 RAG 是救不回来的任何优化都于事无补只能补充文档。6.4 回答明显错误或胡编乱造如果系统给出的答案看起来内容正确但实际上是没有根据的那就是典型的模型幻觉。出现这种情况说明两点要么检索的上下文不够要么系统提示词里没有明确禁止模型自由发挥。我的修正思路是先加一条“如果资料没有相关内容只能说不知道不能推测”。然后把temperature降到 0.1 再试。如果还不行就得看看是不是检索到了不相关的片段。因为模型不会区分哪段资料可信你把噪声片段也塞进 context它就会一本正经地用错误资料作答。最后加一个兜底分支相似度最高的一条如果分数低于阈值直接返回“未找到相关知识建议人工介入”不进入生成阶段。这样虽然回答“死板”但至少不会误导用户。6.5 向量维度设置错误BGE-M3 的输出维度是 1024这是它的模型结构决定的不能随意改。有次我在创建集合时写错成 768报错信息非常隐晦查了半天才发现是维度不匹配。建议写代码时把维度写成配置项并和 embedding 模型名称绑定这样别人接手或者自己日后调整模型时不容易改错。一个小技巧是在创建集合前先跑一下len(embed_texts([测试]))打印真实维度然后再拿去建表。这比背数字靠谱得多尤其是换不同 embedding 模型的时候。个人体会与扩展建议这套系统搭完之后我的直观感受是RAG 项目的复杂度不在模型而在检索质量。DeepSeek 和 Ollama 都只是执行工具真正决定客服答得好不好的是知识库怎么清洗、怎么切分、怎么检索。把数据这块磨好哪怕是 7B 的小模型也能做出像样的问答效果。如果后续想把系统做得更完整我建议朝三个方向扩展一是给每个请求接一个日志系统记录用户问题和回答方便抽检质量二是做一个简单后台管理页运营人员可以直接在网页上添加和修改知识片段不用改代码三是把 Ollama 推理改成流式返回用户打字时能实时看到回复过程体验会更接近商业客服系统。零成本只是起点把流程跑顺之后这套架构完全有能力撑起一个小型团队的日常客服需求。
阅读完成 · 觉得有帮助?
咨询建站