ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

企业私有RAG知识库实战:从原理选型到代码实现

企业私有RAG知识库实战:从原理选型到代码实现 1. 从模型不会到模型会RAG 到底解决了什么问题先讲一个我这两年听得最多的诉求老板说GPT 很聪明但一问到我们公司的业务细节就胡编或者我们有一堆制度文档、技术方案、项目周报员工天天在群里问能不能做个机器人自动答。这个场景的本质是让大模型基于企业私有资料回答问题。官方一点的说法叫 RAG即 Retrieval-Augmented Generation检索增强生成。原理并不复杂用户提问后系统先从你的私有文档里检索出相关片段再把问题片段一起丢给大模型让模型只依据这些材料作答。那为什么不能直接把所有文档塞给模型因为模型有上下文窗口限制几千页 PDF 塞不进去也因为模型的记忆是训练时形成的你的私有资料它压根没见过硬答只能靠编。RAG 把知识存储从模型的参数里搬到了外部的向量数据库中模型只负责理解和组织语言这样既解决了知识时效性问题也解决了知识私有化问题。这套方案特别适合下面几类人企业技术负责人想把内部知识资产盘活让员工、客服、售前能快速找到答案个人开发者手里有一堆产品文档、笔记、论文想本地搭个问答工具想入门大模型应用的技术人员RAG 是当前落地最多、门槛最低、效果最可控的大模型应用方向没有之一。顺带说一句很多人会问 RAG 和微调Fine-tuning的区别。我给的判断标准很简单如果你的知识需要频繁更新或者你要求模型引用原文、不能胡说那就用 RAG如果你希望模型学会某种固定的输出风格、特定领域的表述习惯再考虑微调。RAG 改知识库像换数据库内容微调像换模型大脑前者适合知识类场景后者适合能力类场景。2. 技术选型模型、向量库、框架怎么定关键看私有两个字这个项目标题里最重要的词是企业私有。也就是说数据不能出内网不能依赖外部 API 调用至少核心链路要可控。所以我在选型时定的三条原则就是模型能本地跑、组件尽量开源、链路尽量简单。2.1 大模型选型本地部署是底线Ollama 是真香私有化的第一步是把大模型跑在自己的服务器上。本地部署模型有很多工具实测下来最省事的是 Ollama——一条命令就能启动一个完整的模型服务兼容 OpenAI 的 API 格式对后续代码接入非常友好。可选模型方面我按硬件条件给三个档次硬件水平推荐模型说明8GB 内存的笔记本qwen2.5:3b / qwen2.5:7b能跑但速度一般适合个人验证24GB 显存起步的服务器qwen2.5:14b / glm4-chat效果和速度的平衡点是我目前主力多卡或 A100 级别qwen2.5:72b效果接近闭源模型适合严肃业务实际选择时参数量往上走准确率和逻辑性都会有明显提升尤其是指令遵循能力。7B 以下的模型经常出现问东答西的情况用在生产环境里很容易被用户吐槽。我建议至少用 14B 级别起步哪怕用 CPU 推理慢一点效果也比小模型硬扛强得多。Ollama 下载模型时走的是官方仓库这一步在国内网络环境下不太稳定。一个变通思路是先从模型托管平台下载 GGUF 格式文件再用 Modelfile 手动导入整个过程不依赖任何非常规手段就是正常的文件导入操作。具体方法官方文档有照着做就行。2.2 向量数据库与 Embedding 模型被低估的两个关键组件很多初次接触 RAG 的人把注意力都放在大模型上结果搭出来的系统一问三不知问题往往出在检索环节——要么向量化做得烂要么向量库选得随意。Embedding向量化模型负责把文本转成数字向量它的质量直接决定能不能搜到。国内场景我推荐BAAI/bge-large-zh-v1.5或m3e-base中文效果比 OpenAI 的 text-embedding-ada-002 好而且是完全本地化的不涉及任何数据外送问题。这里有一个细节Embedding 模型的输出维度要和向量库的配置保持一致bge-large-zh-v1.5 是 1024 维m3e-base 是 768 维。向量数据库的选择我用过的方案有 Chroma、FAISS、Milvus、Qdrant 这几种简单给大家一个选型结论Chroma零配置文件pip 装完直接用适合几百份文档、单机部署的轻量场景。我大部分个人项目和初期原型都用它。FAISSMeta 出的库更像是一个检索算法库灵活性高但需要自己管理索引的持久化。Milvus / Qdrant真正的分布式向量数据库支持多租户、高并发、权限管理适合企业级。如果你的知识库文档超过几万份或者有多个人同时用直接上 Qdrant部署起来比 Milvus 简单不少。我给企业做方案时第一版往往用 Chroma 跑通等真的出现性能瓶颈再迁移到 Qdrant。没必要一开始就上重武器RAG 系统的瓶颈通常不在向量库本身。2.3 框架LangChain 可以学但别一头扎进去LangChain 是 RAG 方向最出名的框架封装了文档加载、切片、向量化、检索、Prompt 管理等全套流程官方还提供了大量与 Ollama/Chroma 集成的示例它能让一个新手在半小时内把 Demo 跑起来。但我的真实体验是别把 LangChain 当成项目的骨架只把它当成工具箱。原因有两个LangChain 的抽象层级太多你要排查问题的时候报错栈会绕好几层很难判断是你代码的问题还是框架的坑RAG 的核心逻辑其实非常简单就是切片、向量化、检索、拼接 Prompt、调模型你自己写一遍对系统的掌控力和排查问题的能力会完全不一样。所以下面的实战部分我将先给一套手写最小实现的代码从零构建整个流程。这套代码理解了之后你再回头用 LangChain 做工程化封装会从容得多。3. 从 0 到 1 实战手写一套完整可用的 RAG 系统下面进入正题。这个实战教程基于以下环境你按步骤操作即可复现操作系统Windows / Linux / macOS 均可示例以 Linux 为主Python3.10 或更高版本模型服务Ollama本地运行 qwen2.5:14b向量化bge-large-zh-v1.5通过 sentence-transformers 加载本地模型向量库Chromapip 直接安装无需额外服务3.1 环境准备先把地基打牢第一步安装 Ollama 并拉取模型。Ollama 的安装包到官网下载即可Windows 和 macOS 都有安装包Linux 执行安装脚本后会自动注册为系统服务。# 检查 ollama 是否安装成功 ollama --version # 拉取对话模型以 14B 为例大约 9GB 磁盘空间 ollama pull qwen2.5:14b # 启动模型服务默认端口 11434 ollama serve提示Ollama 模型默认存放在系统盘如果你的服务器系统盘空间有限可以设置OLLAMA_MODELS环境变量指向大容量磁盘再重启服务。第二步安装 Python 依赖。我建议新建一个干净的虚拟环境别和系统环境混在一起省的后面依赖冲突搞到头大。# 创建虚拟环境 python3 -m venv rag_env source rag_env/bin/activate # Windows: rag_env\Scripts\activate # 安装核心依赖 pip install chromadb sentence-transformers langchain-community pip install pypdf docx2txt flask这里装langchain-community不是为了用它的链式调用而是为了复用它的文档加载器省得自己写 PDF 解析。pypdf是 PDF 解析库docx2txt负责 Word 文档flask用来在最后做一个 Web 交互界面。3.2 文档加载与文本切分RAG 效果好坏的分水岭很多人以为 RAG 的难点在模型或向量库其实真正的分水岭在最容易被忽略的文本切分环节。你的文档不会自动变成整齐的段落PDF 里有页眉页脚、表格、多栏排版Word 里还有各种样式切分策略直接决定了检索召回的质量。先写文档加载部分把 PDF 和 Word 统一成先是文本的格式from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader import glob def load_documents(folder_path: str): docs [] # 扫描目录下的 PDF 和 Word 文件 for pdf_file in glob.glob(f{folder_path}/**/*.pdf, recursiveTrue): loader PyPDFLoader(pdf_file) docs.extend(loader.load()) for docx_file in glob.glob(f{folder_path}/**/*.docx, recursiveTrue): loader Docx2txtLoader(docx_file) docs.extend(loader.load()) print(f共加载 {len(docs)} 个文档片段) return docs切分这一步我直接给结论长文本按固定大小切分还需要去头去尾裁剪上下文短文本按段落切分即可。我推荐一个组合策略——先按文档结构初步切分再把过长的块按 500~800 字符的窗口二次切分相邻块之间保留 80~120 字符的重叠。from langchain.text_splitter import RecursiveCharacterTextSplitter def split_documents(docs): text_splitter RecursiveCharacterTextSplitter( chunk_size600, # 每块最大字符数 chunk_overlap100, # 相邻块重叠 100 字符保证上下文衔接 separators[\n\n, \n, 。, , , ., 空格, ], # 注意separators 顺序很重要优先按段落切其次按句子切 ) chunks text_splitter.split_documents(docs) print(f切分后共 {len(chunks)} 个文本块) return chunks为什么 chunk_size 定在 600 而不是 300 或 1000这里有个经验平衡太长会混入无关信息降低检索精准度太短则上下文不完整模型理解不了。中文场景下 500~800 字符是检索效果最稳的区间600 是我在多种文档类型上做对比测试后的一个稳妥值。chunk_overlap 为 100 则确保被切开的关键句不会被拦腰截断。注意切分后的每个块应当尽量保持语义完整。如果某个块恰好从一个句子的中间开始检索时模型的输入就是残缺的输出自然容易前言不搭后语。这也是RecursiveCharacterTextSplitter的递归特性在发挥作用它会先按段落切段落太长再按句子切句子太长才会按字符硬切。3.3 向量化与存储构建知识库的核心链路文本切分完成后下一步把每个块转成向量并存入向量数据库。这一步涉及两个关键选择Embedding 模型和向量库。我在生产环境里用bge-large-zh-v1.5效果最稳但模型文件有 1.3GB 左右首次加载需要一点时间。你也可以先用 m3e-base约 400MB做验证后面再换。为了避免每次启动都重新下载模型直接用 huggingface 的本地缓存机制即可。from sentence_transformers import SentenceTransformer import chromadb import hashlib # 加载中文向量化模型本地路径不涉及数据外送 embedding_model SentenceTransformer(BAAI/bge-large-zh-v1.5) def get_embedding(text: str): # sentence-transformers 可以直接对文本生成向量 return embedding_model.encode(text).tolist() # 初始化 Chroma 客户端数据持久化到本地目录 client chromadb.PersistentClient(path./chroma_db) collection client.get_or_create_collection( nameenterprise_kb, metadata{hnsw:space: cosine} # 使用余弦相似度计算距离 ) def build_knowledge_base(chunks): ids [] embeddings [] documents [] metadatas [] for idx, chunk in enumerate(chunks): # 用 hash 生成稳定 ID避免重复写入 chunk_id hashlib.md5(chunk.page_content.encode(utf-8)).hexdigest() ids.append(chunk_id) documents.append(chunk.page_content) # 记录来源文件方便溯源 metadatas.append({source: chunk.metadata.get(source, fchunk_{idx})}) embeddings.append(get_embedding(chunk.page_content)) collection.upsert( idsids, documentsdocuments, embeddingsembeddings, metadatasmetadatas, ) print(f知识库构建完成共写入 {len(documents)} 条数据)这里有两个非常关键的细节第一个细节相似度算法选 cosine 而不是 L2。向量距离的度量方式直接影响检索结果。L2 距离受向量长度影响较大对于长短不一的文本块长度本身会影响得分排序而余弦相似度只关注方向不受长度干扰在文本检索场景中效果普遍更好。第二个细节document 和 embedding 要分别传入。Chroma 支持在 upsert 时直接传原始文本和对应的向量。这里有个容易踩的坑如果你只传 documents 让 Chroma 自己调用默认的 embedding 函数那么每次启动都会重新计算一遍向量或者用默认的英文模型——那你的中文检索基本就废了。必须预先算好 embedding 再写入。3.4 检索与生成把相关知识变成可靠回答知识库索引完成后RAG 系统的另一半就是检索 生成。检索的质量直接决定了生成的天花板。先来看检索部分。这里同样有一个容易被忽略但极其关键的细节在传给 Embedding 模型之前先对用户问题做预处理。我遇到很多次效果不佳的情况最后定位到的问题竟然是用户提问措辞太口语化和知识库里的书面语表达距离太大导致向量相似度不高。解决方法是为 Embedding 模型加一个简单的指令前缀这也是 bge 系列模型的官方建议用法。def search_knowledge_base(query: str, top_k: int 5): # bge 模型建议为检索任务添加指令前缀 query_embedding get_embedding(f为这个句子生成表示以用于检索相关文章{query}) results collection.query( query_embeddings[query_embedding], n_resultstop_k, include[documents, metadatas, distances] ) return results然后进入生成环节。这里我直接用requests调用 Ollama 的 OpenAI 兼容接口避免引入额外的 SDK。重点在于构建 Prompt 的方式。import requests import json OLLAMA_URL http://localhost:11434/v1/chat/completions MODEL_NAME qwen2.5:14b def generate_answer(query: str, top_k: int 5): # 先从知识库检索相关片段 results search_knowledge_base(query, top_ktop_k) contexts results[documents][0] # 命中的文本块列表 sources results[metadatas][0] # 构造上下文文本 context_text \n\n---\n\n.join(contexts) # 构造 Prompt明确告诉模型只依据资料回答 prompt f你是一个企业知识库助手。请严格依据下面提供的资料回答用户问题。 如果资料中没有相关内容请直接回答“资料库中未找到相关信息”不要编造。 资料 {context_text} 用户问题{query} 请基于资料给出简洁、准确的回答。回答中不要提及“根据资料”或引用编号。 payload { model: MODEL_NAME, messages: [ {role: system, content: 你是企业知识库助手回答必须基于给定资料。}, {role: user, content: prompt} ], temperature: 0.1, stream: False, } resp requests.post(OLLAMA_URL, jsonpayload, timeout120) result resp.json() return result[choices][0][message][content] f\n\n参考来源{sources}这里有几个生成环节的实操要点temperature 必须调低。知识问答类场景要的是准确、稳定而不是发散。0.1 是一个保守但好用的值模型会倾向于严格基于上下文作答不会自由发挥。如果你用它写营销文案那 temperature 可以调高到 0.8 左右但知识库场景千万别。Prompt 里的不要编造约束不是可有可无的。大模型的习惯是有问必答哪怕资料里没有相关信息它也会硬着头皮编一段。加上这句话之后模型会更倾向于承认自己不知道。当然这不是 100% 可靠的后面我会讲如何用后处理来兜底。参考来源必须跟着答案一起返回。企业内部用的时候员工会质疑答案的准确性甚至需要追溯到原始文档做二次确认。所以在返回里有参考来源字段是刚需不是加分项。3.5 一个完整的可执行脚本与 Web 问答接口把上面所有环节组合到一起就是一个完整可运行的最小 RAG 系统。我把它整理成一个单一脚本方便你直接跑通全流程。# rag_system.py - 企业私有知识库完整示例 import glob import hashlib import requests import chromadb from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from sentence_transformers import SentenceTransformer # 1. 全局配置 DOC_FOLDER ./data # 你的文档放这里 DB_PATH ./chroma_db # 向量库持久化目录 COLLECTION_NAME enterprise_kb OLLAMA_URL http://localhost:11434/v1/chat/completions MODEL_NAME qwen2.5:14b EMBEDDING_MODEL BAAI/bge-large-zh-v1.5 # 2. 初始化组件 embedding_model SentenceTransformer(EMBEDDING_MODEL) client chromadb.PersistentClient(pathDB_PATH) collection client.get_or_create_collection( nameCOLLECTION_NAME, metadata{hnsw:space: cosine} ) def load_and_split(): 加载文档并切分为文本块 docs [] for pdf_file in glob.glob(f{DOC_FOLDER}/**/*.pdf, recursiveTrue): docs.extend(PyPDFLoader(pdf_file).load()) for docx_file in glob.glob(f{DOC_FOLDER}/**/*.docx, recursiveTrue): docs.extend(Docx2txtLoader(docx_file).load()) splitter RecursiveCharacterTextSplitter( chunk_size600, chunk_overlap100, separators[\n\n, \n, 。, , , ., , ], ) chunks splitter.split_documents(docs) return chunks def build_knowledge_base(): 全量构建知识库适用首次或者全量更新 chunks load_and_split() ids, documents, metadatas, embeddings [], [], [], [] for idx, chunk in enumerate(chunks): chunk_id hashlib.md5(chunk.page_content.encode(utf-8)).hexdigest() ids.append(chunk_id) documents.append(chunk.page_content) metadatas.append({source: chunk.metadata.get(source, )}) embeddings.append(embedding_model.encode(chunk.page_content).tolist()) collection.upsert(idsids, documentsdocuments, metadatasmetadatas, embeddingsembeddings) print(f知识库构建完成共 {len(documents)} 条) def retrieve(query: str, top_k: int 5): 向量检索 query_emb embedding_model.encode( f为这个句子生成表示以用于检索相关文章{query} ).tolist() results collection.query( query_embeddings[query_emb], n_resultstop_k, include[documents, metadatas], ) return results[documents][0], results[metadatas][0] def ask(query: str, top_k: int 5): 端到端问答 contexts, sources retrieve(query, top_k) source_texts [s.get(source, ) for s in sources] context_text \n\n---\n\n.join(contexts) prompt f你是一个企业知识库助手。请严格依据下面提供的资料回答用户问题。 如果资料中没有相关内容请直接回答“资料库中未找到相关信息”不要编造。 资料 {context_text} 用户问题{query} 请基于资料给出简洁、准确的回答不要提及“根据资料”等字样。 payload { model: MODEL_NAME, messages: [ {role: system, content: 你是企业知识库助手回答必须基于给定资料。}, {role: user, content: prompt}, ], temperature: 0.1, stream: False, } resp requests.post(OLLAMA_URL, jsonpayload, timeout120) answer resp.json()[choices][0][message][content] return answer, source_texts if __name__ __main__: import sys if len(sys.argv) 1 and sys.argv[1] --build: build_knowledge_base() else: while True: q input(\n请输入问题输入 exit 退出) if q.lower() exit: break ans, src ask(q) print(f\n回答{ans}) print(f来源{src})如果需要给团队用还可以直接用 Flask 封装一个最简单的问答接口几行代码就能搞定from flask import Flask, request, jsonify app Flask(__name__) app.route(/ask, methods[POST]) def handle_ask(): data request.get_json() query data.get(query, ) ans, src ask(query) return jsonify({answer: ans, sources: src}) if __name__ __main__: app.run(host0.0.0.0, port8000)跑完上面所有代码你就拥有了一套完整的企业私有知识库问答系统。而整套系统的运行完全不依赖任何外部 API所有组件都是本地部署真正做到了数据不出内网。4. 从 Demo 到生产容器化部署与知识库运维Demo 能在自己的电脑上跑通和能在企业生产环境里稳定运行中间还有不小的距离。这一节主要讲三件事部署方式、知识库更新策略、权限与审计。4.1 容器化部署把整个系统变成可交付的产品生产环境部署时我不建议在裸机上一股脑装 Python 环境。直接用 Docker Compose 把 Ollama、应用服务、向量库编排起来这样无论迁移到哪台服务器一条命令就能拉起全部服务。一个可用的 docker-compose.yml 长这样基于前面代码稍作调整version: 3.8 services: ollama: image: ollama/ollama:latest container_name: ollama ports: - 11434:11434 volumes: - /data/ollama:/root/.ollama restart: unless-stopped rag-api: build: . container_name: rag-api ports: - 8000:8000 volumes: - /data/docs:/app/data - /data/chroma_db:/app/chroma_db depends_on: - ollama environment: OLLAMA_BASE_URL: http://ollama:11434 EMBEDDING_MODEL: BAAI/bge-large-zh-v1.5 restart: unless-stopped容器化之后有几个小细节要多留意一是模型文件的挂载。Ollama 下载的模型动辄好几个 GB如果不挂载到宿主机目录容器一删模型就没了下次又得重新下载。把/root/.ollama挂载出来是一个必须操作。二是向量库目录的持久化。Chroma 的 PersistentClient 是把数据写在本地目录里的容器重启不能丢所以也需要挂载卷。三是首次启动时需要注意依赖加载顺序。Ollama 服务启动后模型需要先被拉取到本地才能响应请求。建议在 rag-api 启动脚本里加一个健康检查循环确认 Ollama 已经就绪后再加载知识库避免出现连接拒绝的报错。4.2 知识库更新与权限控制容易被忽略的两件大事知识库不是建完就万事大吉它是需要持续供养的活系统。企业里的新制度、新方案、新项目成果都必须定期同步进知识库否则系统会慢慢变成过时问答机。更新策略我推荐增量 upsert 而不是全量重建。Chroma 的upsert天然支持按 ID 覆盖只要 chunk 内容没变ID 就是稳定的重复执行不会产生重复数据。可以写一个定时任务每天扫描一次文档目录对新文件做加载和向量化对已存在的文件跳过。# 定时增量更新的简化逻辑 def incremental_update(): chunks load_and_split() need_add [] # 计算所有 chunk 的 ID for chunk in chunks: chunk_id hashlib.md5(chunk.page_content.encode(utf-8)).hexdigest() existing collection.get(ids[chunk_id]) if not existing[ids]: # 不存在就新增 need_add.append((chunk_id, chunk)) # 只对新增部分做向量化 for chunk_id, chunk in need_add: collection.upsert( ids[chunk_id], documents[chunk.page_content], embeddings[embedding_model.encode(chunk.page_content).tolist()], metadatas[{source: chunk.metadata.get(source, )}], ) print(f本次新增 {len(need_add)} 条其余未变动)权限控制方面企业场景下我会建议这样分级普通员工只能通过 Web 界面提问不能查看向量库原始内容不能触发知识库更新知识库管理员可以上传文档、触发增量更新、查看更新日志系统运维可以管理向量数据库本身、备份恢复、监控日志。如果系统接入 SSO单点登录直接在 API 层做角色校验就行。这块在企业落地时是必须做好的因为知识库里存的是公司核心资料权限泄漏等于资料泄漏。还有一件事很多人会忽略审计日志。系统上线后谁问了什么问题、模型返回了什么答案、引用了哪些文档这些都应该记录下来。一方面是出了问题能回溯另一方面也是知识产权保护的证据链。5. 实践中的坑与排查RAG 系统效果不好99% 的问题出在这里这部分我想把实操中遇到的最典型问题罗列出来这些内容在我做过的项目里几乎必踩帮大家提前排掉。5.1 召回效果差的排查清单先给一个观点大多数 RAG 系统效果不佳问题根本不在于模型而是检索阶段就没把正确的资料捞出来。模型再聪明喂给它的资料是错的它也只能一本正经地胡说。遇到答非所问或者回答笼统的情况我建议按下面的顺序排查第一步看切分质量。把你的文档切片随机抽 10 条出来读一遍看每条切片是不是一个语义完整的片段。如果发现切片经常在句子中间断开、大量内容是页眉页脚、或者一个切片里混了完全无关的几个主题那先调整切分策略而不是去调模型。第二步看召回数量。top_k设置在 3~5 是经验值但也要看你的 chunk 大小。chunk 越小需要的 top_k 越大chunk 越大top_k 可以越小。如果召回结果里混入了大量无关片段问题可能出在向量相似度上。第三步看相关性排序。Chroma 默认按距离升序返回结果距离越小越相关。打印一下distances看看如果前几名的距离差距不明显说明知识库里本身就没有强相关的资料或者切分方式让关键信息被稀释了。第四步看 Embedding 模型是否匹配。国内文档必须用中文 Embedding 模型这一点我重复多少次都不为过。如果用默认的 all-MiniLM-L6-v2中文内容基本等于瞎检索。另外注意 bge 系列在向量化时不要截断文本bge-large 支持 512 token中文大约 300~400 字刚好覆盖我们的 chunk 大小。5.2 关于图片、表格与多轮对话RAG 的边界到底在哪我经常被问到两个问题RAG 知识库能存储图片吗和RAG 能做多轮对话吗这需要把 RAG 的边界讲清楚。第一个问题能不能存图片。直接回答传统的 RAG 流程里图片本身不能被直接检索因为 Embedding 模型处理的是文本。但企业里大量知识是以图片形式存在的比如产品截图、流程图、拍照的合同页。我的处理思路是两条路图片里的文字用 OCR 工具如 PaddleOCR把文字提取出来再作为文本 chunk 存入知识库这样检索和问答都正常图片本身如果确实需要原始图片展示可以把文件路径当作元数据存在 chunk 里回答时返回路径让前端展示原图。这在企业内部系统里是完全够用的。真正多模态的看图问答现阶段还不适合私有化场景因为多模态模型的资源消耗高出一个量级性价比不划算。第二个问题多轮对话。纯 RAG 的每次问答之间是互相独立的它没有记忆。上一轮你问了报销流程是什么这一轮你追问需要什么材料模型并不知道这个指的是报销流程。解决办法也很简单就是用对话历史来重写用户提问def rewrite_query_with_history(history: list[dict], current_query: str) - str: 把最近两轮对话和当前问题一起交给模型让模型改写为带完整语义的独立问题 messages [{role: system, content: 你是查询改写助手根据对话历史把当前问题改写为独立完整的问题。}] messages.extend(history[-2:]) messages.append({role: user, content: f请改写当前问题{current_query}}) resp requests.post(OLLAMA_URL, json{ model: MODEL_NAME, messages: messages, temperature: 0, }) return resp.json()[choices][0][message][content]改写后再做向量检索上下文就完整了。这本质上是在 RAG 外面套了一层对话管理在企业场景中效果非常明显。5.3 RAG、KG 与结构知识库到底怎么选最后聊一个选型层面的问题。我发现很多人搞不清 RAG检索增强生成、KG知识图谱和传统关系型数据库到底各自解决什么问题。关系型数据库 查询适合的是确定性查询员工查去年 Q3 的销售额是多少这是明确的结构化问题答案在数据库里有精确字段通过 SQL 查询拿到即可。知识图谱适合的是关系推理比如A 部门和 B 部门的项目有哪些关联谁为谁提供数据这类问题需要沿着实体关系做推理和跳转RAG 做不了因为向量检索只能找相似文本没法做多跳推理。RAG适合的是语义检索 开放问答比如报销流程是什么、X 项目的技术架构怎么设计的这些知识往往散落在非结构化文档里没有固定的字段可以查询只能通过语义匹配找到相关段落。三者的关系不是替代而是互补。我给企业的建议是先用 RAG 做非结构化文档问答这是覆盖面最广、见效最快的场景。当用户开始频繁问谁和谁有什么关系这类问题时再考虑在核心业务数据上构建知识图谱两者共存即可。6. 最后补充一些个人经验这个项目从最初的简单原型到后来在企业内部真正落地我最大的体会是RAG 的技术门槛不高难的是把每个细节打磨到位。切分的粒度、Embedding 模型的选择、Prompt 的约束、上下文窗口的管理任何一个环节偷懒最后的结果就是模型给你一本正经地胡说八道而用户会因此对整个系统失去信任。还有一个小技巧想分享。如果你希望模型的回答更可信可以考虑在生成答案之前加上一个相关性判定步骤先判断检索回来的文本块和用户问题是否真的相关如果最高相似度低于某个阈值比如 0.35就直接返回未在知识库中找到相关信息而不是硬让模型生成答案。这能极大减少胡编乱造的情况代价只是多一次向量计算。另外如果你以后要接入 LangChain 或其他框架我的建议是让框架去处理协议对接、格式转换这类繁琐问题但核心的切分策略、Prompt 设计、检索逻辑一定要自己掌控这样才能灵活调优。框架是为你服务的不是让你为框架服务的。
返回列表