ARTICLE DETAIL

资讯详情

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

从零搭建最小RAG系统:Embedding+Chroma+DeepSeek实战

从零搭建最小RAG系统:Embedding+Chroma+DeepSeek实战 我一直觉得RAG是目前大模型落地到业务里性价比最高的一条路。先别急着反驳。你想想微调模型要准备训练集、要买显卡或者烧API费用、要处理过拟合和灾难性遗忘一个项目没几个月下不来。而RAG呢本质上是给大模型配了一个“可以随时翻书的助手”白天写好文档晚上就能让模型回答里面的内容。我见过不少团队第一天搭好最小链路第二天就开始给内部同事做知识库问答了这个速度是微调完全比不上的。Day 8这个安排其实很妙。前面几天我们玩了提示词、API调用、模型参数调整今天终于要把这些散件串成一个完整系统用Embedding把文本变成向量用Chroma把这堆向量存起来并做相似度检索最后把检索结果连同用户问题一起丢给DeepSeek让它生成带依据的回答。这篇文章我尽量把每一步为什么这么做、换一种方案会有什么代价都讲透代码也给全你照着敲一遍就能拥有一个逻辑完整的最小RAG。很多教程上来就贴代码跑通了就完事但你换个版本、换个场景就抓瞎。我们不搞那套。先把三个参与者的角色搞清楚后面所有操作都会变得顺理成章。1. 为什么先搞明白RAG的三个参与角色RAG这东西名字叫“检索增强生成”听起来很高端拆开看就三件事找资料、存资料、写答案。任何一个环节掉链子整个系统的体验都会崩。但实际项目里大部分人把精力全放在了“写答案”这一步也就是调Prompt、调模型参数结果发现资料找得不准模型再聪明也白搭。1.1 模型知识有“保质期”RAG是给模型配外挂记忆先聊一个最基本的认知问题大模型的参数里存的是训练时的知识。DeepSeek的训练数据截止日期是固定的你问它今年新发布的政策文件、你们公司内部的操作手册、某个产品最新的参数表它只能靠“猜”或者“编”。这不是模型笨是它的知识库根本没这些东西。RAG的解决思路很直接用户提问的时候先从你的文档库里把相关片段捞出来拼到Prompt里一起发给模型。模型看到了原文片段就能照着原文回答而不是凭空发挥。这个过程相当于每次考试都允许模型开卷你只需要保证“书”里确实有答案并且模型能翻到正确的那一页。这里有个很重要的认知RAG解决的不是“模型不够聪明”的问题而是“模型不知道你的事”的问题。如果问题本身需要复杂推理或者文档里根本没有相关内容RAG救不了你。所以在项目启动前先想清楚你的文档库里到底有没有能回答用户问题的内容这决定了RAG项目的天花板。1.2 Embedding、向量库、LLM各司其职缺一不可三个角色我打个比方吧。Embedding模型像一个“翻译官”把人类语言翻译成计算机能算距离的向量。向量库像一个“图书馆管理员”负责把海量向量存得井井有条你问它“哪本书讲这个”它能马上指给你。大模型像一个“写手”拿到管理员递给它的几页书把它组织成一段通顺的、有依据的回答。流程走一遍用户问“DeepSeek支持哪些API接口”。Embedding先把这句话翻译成向量Chroma拿这个向量去库里找最接近的几段文档向量把这几段原文从Chroma里取出来和用户问题一起拼进Prompt发给DeepSeekDeepSeek写出回答。这套流程里有意思的地方在于Embedding和向量检索本身完全不理解语义。它们只是把文本映射到高维空间里然后算欧氏距离或者余弦相似度而已。所谓“语义相似”其实是同义句在向量空间里挨得比较近这个统计规律的体现。理解了这一点你就能明白为什么Embedding模型的效果会直接决定RAG的上限——翻译官如果水平差图书馆管理员再怎么勤快递给写手的也可能是毫不相干的内容。1.3 RAG vs 微调什么时候该用哪个我经常被问到一个问题既然RAG这么好是不是不用学微调了答案当然不是。这两者解决的其实是不同层面的问题。微调改变的是模型的“行为方式”比如让它输出特定的格式、学习某种说话风格、掌握某个领域的推理逻辑。RAG改变的是模型的“知识范围”让它能引用你提供的具体资料。说得再直白一点微调是改变一个人的思维习惯RAG是往他书架上塞书。你不可能通过塞书让一个人变得更会推理也不可能通过训练让一个人知道一本他从未读过的书的内容。实际项目里RAG和微调常常配合使用。比如你给客服机器人做了微调让它说话更礼貌、更简洁同时用RAG让它能查到每个客户的具体订单信息。Day 8这节课先不做微调把RAG这条腿走稳了后面再谈另一条腿。2. 技术选型Embedding与向量库怎么定做最小RAG最怕的不是代码写不出来而是选型选了一堆重家伙半天搭不起来热情全被磨没了。我在这里坚持一个原则能轻则轻先把链路跑通再考虑性能和规模化。所以Embedding选本地开源模型向量库用API最友好的Chroma生成端选DeepSeek的API。2.1 Embedding模型怎么选为什么不上OpenAI如果你做英文RAGOpenAI的text-embedding-3-small确实是省心之选效果稳定API调用也简单。但我们是中文场景而且从成本角度考虑我建议直接用智源开源的bge系列尤其是BAAI/bge-small-zh-v1.5这个模型。先看数据。在C-MTEB中文语义文本相似度基准上bge-small-zh-v1.5的分数大概在60多分看着不起眼但你要知道它的体积才不到100MBCPU就能跑对一台普通笔记本来说完全没压力。相比之下OpenAI的API要联网、要付费、每次调用有延迟虽然效果略好一点但对一个学习项目来说不是最优解。还有一个重要的点bge系列在中文上的表现经过专门调优。很多人拿英文Embedding模型硬跑中文出来的向量检索效果惨不忍睹就是因为英文模型的中文语义空间没被充分训练。选型的时候Embedding模型必须跟你的语料语言匹配这是我在多个项目里踩出来的教训。如果你手头没有GPU也没关系bge-small-zh-v1.5这种小模型在CPU上编码一段几百字的文本也就几十毫秒索引几百篇文档完全可接受。学习阶段不需要追求大模型把流程跑通比什么都重要。如果你以后要上生产再换bge-m3或者混用多路召回也不迟代码结构是兼容的只需要改一行模型名。2.2 Chroma版本的坑一堆老教程跑不通就是这个原因这是我必须单独拿出来说的一个坑。你如果在网上搜Chroma教程会看到很多代码写成这样import chromadb client chromadb.Client()或者是from chromadb.config import Settings client chromadb.Client(Settings(chroma_db_implduckdbparquet, persist_directory./data))这两个写法在今天已经跑不通了。Chroma在2024年初更新了一个大版本把旧的Client()和chroma_db_impl这个参数全部废弃了现在统一的入口是PersistentClient。你要是照抄老教程大概率会报TypeError: __init__() got an unexpected keyword argument persist_directory之类的错。正确写法是import chromadb client chromadb.PersistentClient(path./chroma_data)这个PersistentClient出来之后向量库的持久化变得特别省事。你只要在初始化的时候指定一个本地目录之后每次写数据、查数据都会自动同步到磁盘下次启动程序还能接着用。不需要手动调persist()方法那个方法在新版本里也没了。版本差异还体现在collection.add()的返回值上。新版本里add()方法返回一个AddResult对象里面包含ids和uris等字段而不是什么都没返回。很多人没注意这个变化但本质上不影响使用按新写法来就好。2.3 DeepSeek在系统里只是“写手”不是“搜索器”DeepSeek在这里的角色定位一定要清晰它只负责把检索到的文档片段组织成通顺的回答不负责从自己的脑子里找答案。为什么因为一旦你让模型“自由发挥”它就有可能在文档内容不足的时候开始编造。RAG系统的可靠性恰恰来自于“只有文档里有模型才说”这个约束。那我为什么选DeepSeek而不是其他模型两个理由。第一DeepSeek的推理能力和中文表达能力在同等价位里是顶尖的。尤其是deepseek-chat这个模型处理中文长文本的时候条理很清晰很少出现车轱辘话。第二它的API风格跟OpenAI完全兼容这意味着你以后想换其他模型代码只需要改base_url和model名称其他都不用动。第三成本确实是香。学习阶段调用几百次API花费可以忽略不计。不过我要提醒你一个容易忽略的事DeepSeek API文档里明确说了知识截止时间。你在调用的时候不要指望它能认出你刚写进Chroma的那些文档。它的任务很简单——你给它什么文本它就怎么回答。这也是为什么我们把检索质量放在第一位因为生成端再优秀也弥补不了检索端的缺失。DeepSeek API的接入有两种方式一是直接用OpenAI SDK把base_url指向DeepSeek的地址二是用requests库自己拼HTTP请求。我建议学习阶段用前者代码量少逻辑清晰。你以后想接OpenAI或者其他兼容API只需要改两个常量。3. 最小闭环代码逐段拆解好了理论聊得差不多了开始写代码。我会把整个流程拆成几个模块每个模块讲清楚职责和关键细节。整个项目的目录结构非常轻量只有两个Python文件和一堆测试用的文本文件。rag_mini/ ├── data/ │ ├── deepseek_intro.txt │ ├── rag_concepts.txt │ └── chroma_usage.txt ├── vector_store.py └── query_engine.py先说你准备好测试文本要注意的事。这一步很多人会草草了事随便从网上复制几段就塞进来结果检索效果一塌糊涂。我建议你准备3到5个主题不同的文档每个文档500到2000字主题越分散越好这样测试检索精度时才有区分度。我当时用的三个文档分别是DeepSeek模型的介绍、RAG概念详解、Chroma的安装和使用方法。3.1 环境准备别急着写代码先把版本锁死Python环境我默认你是3.10或者3.113.9也能跑但某些依赖可能会要求更高。建议新建一个虚拟环境不要污染全局Python。命令行操作如下python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate装依赖pip install chromadb sentence-transformers openai这里说一个版本坑。sentence-transformers在2024年底有一波新版本它依赖的transformers版本会变动可能导致旧代码的SentenceTransformer加载方式失效。装的时候如果遇到怪异的报错先试试指定版本pip install sentence-transformers3.1.1 transformers4.46.3chromadb同样建议锁一个大版本。我写这篇文章时用的是1.0.x系列如果你的版本变成2.x了API可能会有微调那就以官方文档为准。然后去DeepSeek开放平台申请一个API Key。这个流程比较常规注册账号、开通API服务、创建密钥网页上有详细引导我就不赘述了。拿到Key之后我建议你建一个.env文件把它存起来不要硬编码在代码里这样以后上传Git仓库的时候不容易泄露。我用python-dotenv来管理环境变量先装上pip install python-dotenv3.2 文档加载与切分RAG的血肉第一步文档加载这部分我先用最原始的open()函数读文本文件。生产环境里你可能会面对PDF、Word、HTML、Markdown等各式各样的格式那时候就需要引入unstructured这类库来做格式解析了。但对于最小RAG纯文本已经足够表达核心概念。真正值得动脑的是“切分”这一步。有人图省事把整个文档作为一个整体扔进去做一个向量。这样做的问题很明显当文档很长比如10页用户问一个细节问题时整篇文档的向量会被很多无关内容“稀释”检索相似度会偏低而且就算被检索到了把10页内容全塞给大模型既浪费token又容易干扰模型注意力。另一种极端做法是切得特别碎比如按句子切分。这样做的结果是语义被切断了。一个完整的观点可能分在几个句子里检索到一句话根本看不懂前后文。我建议在最小RAG阶段使用滑动窗口式的切分方式块大小设为300到500字块与块之间有50到100字的重叠。重叠的目的是保证一句话不会被硬生生切成两段那些跨边界的语义信息得以保留。这里我直接用一个简单的切分函数def split_text(text, chunk_size400, overlap80): if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) if end len(text): break start end - overlap return chunks这个函数逻辑很朴素每次前进chunk_size - overlap个字符切成一段。如果你以后要用更智能的切分方式比如按句号、按Markdown标题层级来切可以在这个函数的基础上改进。最小RAG阶段别追求完美先跑通。3.3 向量化与入库这步慢是正常的别慌向量化的核心代码非常短难点在于理解每个API的参数。先把vector_store.py这个文件完整写出来import os import glob from dotenv import load_dotenv import chromadb from sentence_transformers import SentenceTransformer load_dotenv() def load_documents(data_dir./data): docs [] for file_path in glob.glob(os.path.join(data_dir, *.txt)): with open(file_path, r, encodingutf-8) as f: content f.read() docs.append({source: file_path, text: content}) return docs def vectorize(texts): model SentenceTransformer(BAAI/bge-small-zh-v1.5) return model.encode(texts, normalize_embeddingsTrue) def main(): # 1. 初始化 Chroma指定持久化目录 client chromadb.PersistentClient(path./chroma_data) # 2. 创建或获取 collection collection client.get_or_create_collection(namerag_docs) # 3. 加载文档并切分 docs load_documents() chunked_data [] for doc in docs: chunks split_text(doc[text]) for i, chunk in enumerate(chunks): chunked_data.append({ id: f{doc[source]}_{i}, text: chunk, metadata: {source: doc[source], chunk_index: i} }) # 4. 向量化 texts [item[text] for item in chunked_data] embeddings vectorize(texts) # 5. 写入 Chroma collection.add( ids[item[id] for item in chunked_data], documents[item[text] for item in chunked_data], embeddingsembeddings.tolist(), metadatas[item[metadata] for item in chunked_data] ) print(f成功写入 {len(chunked_data)} 个文档块) def split_text(text, chunk_size400, overlap80): if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) if end len(text): break start end - overlap return chunks if __name__ __main__: main()我猜你看到collection.add()那一段会有点发怵为什么需要同时传ids、documents、embeddings、metadatas四个参数因为Chroma那套API就是这种“全列”风格它希望你一次性把所有内容给它而不是像传统数据库那样一行一行插入。还有个值得说的地方vectorize()函数做了normalize_embeddingsTrue这是为了让向量在做余弦相似度计算时性能更好。Chroma在做向量检索时默认就是用余弦距离而余弦相似度在向量归一化之后跟内积计算是等价的能提升检索速度。这是个很小的细节但涉及底层原理我多提一嘴。另外model.encode()出来的是一个numpy数组但Chroma要求传Python原生的list所以加了.tolist()。这一步千万别漏否则会报类型错误。我在第一次写的时候就被这个坑卡了十分钟。运行python vector_store.py你会看到控制台打印出成功写入的文档块数量。第一次运行的时候SentenceTransformer会自动下载模型需要等一会儿。如果网络比较慢你会看到半天没反应这是正常的别中途关掉进程。3.4 检索查询相似度排序的背后逻辑向量库写入完成之后最核心的流程就是这个查询环节。我在query_engine.py里把检索和生成分开写方便你单独调试每一步。import os from dotenv import load_dotenv import chromadb from sentence_transformers import SentenceTransformer from openai import OpenAI load_dotenv() EMBED_MODEL BAAI/bge-small-zh-v1.5 DEEPSEEK_API_KEY os.getenv(DEEPSEEK_API_KEY) DEEPSEEK_BASE_URL https://api.deepseek.com DEEPSEEK_MODEL deepseek-chat def embed_query(text): model SentenceTransformer(EMBED_MODEL) return model.encode(text, normalize_embeddingsTrue) def search_chroma(query, top_k3, collection_namerag_docs): client chromadb.PersistentClient(path./chroma_data) collection client.get_or_create_collection(namecollection_name) query_embedding embed_query(query).tolist() result collection.query( query_embeddings[query_embedding], n_resultstop_k, include[documents, metadatas, distances] ) return result def build_prompt(query, search_result): documents search_result[documents][0] metadatas search_result[metadatas][0] context_parts [] for i, doc in enumerate(documents): source metadatas[i][source] context_parts.append(f[{i1}] (来源: {source})\n{doc}) context \n\n.join(context_parts) prompt f请根据以下资料回答问题。 资料 {context} 问题{query} 要求 1. 优先使用资料中的信息回答。 2. 如果资料中没有答案直接说明“资料中未找到相关信息”不要编造。 3. 回答时在句末用[1]、[2]标出参考的资料编号。 return prompt def call_deepseek(prompt): client OpenAI(api_keyDEEPSEEK_API_KEY, base_urlDEEPSEEK_BASE_URL) response client.chat.completions.create( modelDEEPSEEK_MODEL, messages[ {role: system, content: 你是一个严谨的文档问答助手。}, {role: user, content: prompt} ], temperature0.3 ) return response.choices[0].message.content if __name__ __main__: query input(请输入问题) result search_chroma(query, top_k3) prompt build_prompt(query, result) answer call_deepseek(prompt) print(\n--- 回答 ---) print(answer)你注意到没有我在构建Prompt的时候特意把来源文件名也加进去了。这看起来是个很不起眼的操作但作用很大当模型在多个资料之间做取舍时有明确的来源标注有助于它组织回答结构。更重要的是如果你以后要做“答案溯源”这个来源信息是关键钩子。还要说一个参数选择top_k3。为什么是3不是5不是10因为对最小RAG来说Top 3既能保证信息覆盖又不会给模型塞入太多噪音片段。你试过就知道Top 10的内容往往有一半以上跟问题不太相关这些不相关的内容反而会干扰模型判断。生产环境里这个值需要用验证集调但学习阶段3是个稳妥的起点。相似度阈值是个很好用的参数但我在最小RAG里故意没用它。原因在于collection.query()只返回相似度最高的几个结果哪怕这些结果跟问题完全不沾边它也会返回。学习阶段让你肉眼看到“明明没有答案但系统还是硬找了几段出来”这种体验比帮你加一个过滤逻辑更有教育意义。等你对相似度分布有了直观感知再去加where子句或者自定义过滤条件也不迟。3.5 组装Prompt调用DeepSeek模型调用的细节坑Prompt模板设计这件事我觉得才是RAG系统里最考验功力的地方。很多人以为RAG的Prompt就是把资料拼接在问题前面结束。结果模型回答的时候东拉西扯一会儿用了资料里的内容一会儿又开始自由发挥。为什么因为你的Prompt没有给模型立规矩。我在build_prompt里加了三条要求优先级从高到低第一“优先使用资料中的信息回答”。这句话是给模型一个明确的指令资料是权威。第二“如果资料中没有答案直接说明资料中未找到相关信息不要编造”。这句话是给模型一个“安全出口”。模型有强烈的讨好用户倾向你逼它必须回答它就可能编。给它一个被允许的“我不知道”的选项它反而更容易老实。第三“回答时在句末用[1]、[2]标出参考的资料编号”。这个要求既能让回答更可信也为你后续做答案溯源做铺垫。DeepSeek的模型调用这里有几个容易踩的坑我提前说。第一个坑是base_url。DeepSeek的API地址是https://api.deepseek.com不是https://api.deepseek.com/v1也不是其他奇奇怪怪的地址。如果你用OpenAI SDKbase_url只填主域名就可以。如果你自己写HTTP请求路径要拼成/chat/completions。第二个坑是model参数的名称。DeepSeek的官方模型名是deepseek-chat和deepseek-reasoner不是deepseek也不是deepseek-v3。很多人拿第三方教程里的模型名直接填结果请求报错Model Not Exist。第三个坑是max_tokens的默认值。DeepSeek API的max_tokens默认值比较小如果你的检索片段比较长模型可能在回答中途就截断了。我在代码里虽然没显式设置但建议你在实际使用中加上max_tokens1024之类的配置宁可多写一点也不要看一半被掐断的回答。温度参数temperature我设了0.3这是RAG场景里比较推荐的值。太低了比如0回答会显得机械、只会复述原文太高了比如1.0模型容易发挥过头开始编内容。0.3左右是“尊重原文但不死板”的平衡点。运行一下试试。输入“什么是RAG”如果顺利你会看到DeepSeek基于你准备的文档内容给出了回答并且标注了[1]、[2]这样的引用编号。4. 实测效果与三个关键调优点整条链路跑通之后大概率你会有一种“好像也没那么难”的感觉这是好事。但如果你以为这样就叫会了RAG那就太早了。我把这套最小系统拿到真实场景里去测了一轮暴露出几个非常典型的问题这里一个一个说。4.1 测试集设计你不能只用一道题考验系统我先准备好三个测试问题覆盖三种不同情况第一类是文档里明确有答案的问题比如“bge-small-zh-v1.5是基于什么训练的”这个问题用来检验基本检索能力。第二类是文档里有相关内容但需要模型归纳总结的问题比如“Chroma持久化需要注意什么”这个问题用来检验模型的归纳能力。第三类是文档里完全没有答案的问题比如“明天的天气怎么样”这个问题用来检验系统会不会“不懂装懂”。你猜结果是什么第一类问题答得漂亮第二类问题勉强凑合第三类问题翻车了——模型没有说“资料中未找到相关信息”反而根据自己训练时的知识硬生生编了一段天气预测出来。这说明什么逻辑层面检索端没有得分特别低的候选结果系统认为“总能找到点东西”所以就把噪音片段也发给模型生成端了。模型收到一段大概率没用的资料加上一个“你可以回答”的暗示就顺水推舟编了答案。这就是为什么我在前面强调RAG系统两个端的约束缺一不可。检索端要做相似度过滤生成端的Prompt必须明确允许“不知道”。别小看这句“允许不知道”它在很大程度上决定了系统是可靠的助手还是胡编的骗子。4.2 分块大小怎么调它决定检索精度的上限我在前面把分块大小设成400字、重叠80字这套参数放在我的测试文档上表现良好。但换成你的文档可能就不是最优的了。这里我说说调参的思路。分块越短检索粒度越细但单块信息量不足模型容易“只见树木不见森林”。分块越长上下文越完整但相似度计算会被稀释精准度下降。这是一个先天矛盾没有绝对正确的答案只能根据你的文档场景去权衡。看两个极端情况。如果你的文档是操作手册信息点密度高每个步骤都对应一个独立操作那么分块可以短一点比如150到200字这样检索到一个步骤就是一个完整建议。如果你的文档是行业分析报告一个核心观点可能要用两三段才能阐述完分块就应该长一点比如800字甚至考虑按段落分组来做父子块。还有个实用技巧重叠区间的长度最好能覆盖一个完整句子的平均长度。这样即使一句话在切分边界附近也至少有一个分块包含完整的这句话不会出现“一句话被拦腰斩断两边各剩一半”的尴尬。中文一句话平均20到40字重叠80字其实已经比较宽裕了。我想再提一点就是分块后要不要做“清洗”。比如你的文档里有表格、有标题、有页眉页脚这些内容切出来之后可能是无意义噪声。生产环境里我会加一个规则空块、纯符号块、长度过短的块直接丢弃不进入向量库。最小RAG阶段你可以先不管但要留个心眼。4.3 检索Top-K与相似度阈值系统可靠性的两道闸门top_k3这个参数看起来只是“取几条候选”但它对回答质量的影响非常大。我做个对比实验你就清楚了。拿同一个问题分别用top_k1、top_k3、top_k5去跑。结果top_k1的时候如果检索到的那一段不是特别相关生成的回答就是在硬圆场。top_k3最均衡材料够用且噪音可控。top_k5的时候噪音开始增加模型偶尔会把边角料信息也写进回答显得重点不突出。我的建议是最小RAG阶段先固定top_k3以后加入rerank环节之后再把top_k放大。至于相似度阈值Chroma的查询结果里会带有distances字段你可以打印出来观察一下“相关问题”和“无关问题”的分数分布然后卡在合适的边界上。这里补充一个概念Chroma返回的distances默认是余弦距离值越小表示越相似。很多教程讲的“相似度分数越高越好”是余弦相似度的逻辑千万别混淆。如果你看到返回的距离值在0到2之间那是余弦距离如果你希望显示成“0到1之间越大越相关”可以在collection.query()里设置distance_function为余弦相似度做转换但底层逻辑不受影响。4.4 Prompt里加引文让答案敢于说“我不知道”我做的最后一项调整是在Prompt里加了“允许不知道”的开关和引文要求。这东西说是个调优点其实是系统性设计的一部分。测试第三类问题明天的天气时我发现即便检索到的资料完全不相关只要Prompt里不明确说“可以回答不知道”模型就会尽力把不相关内容扯到答案里。但一旦加了这句“如果资料中没有答案直接说明资料中未找到相关信息”模型的编造行为立刻收敛了。这不是玄学。模型天生倾向于迎合用户你在Prompt里给一个合法退路它就不会被逼着胡编。引文编号的设计也是一样的逻辑。要求模型在句末标出资料编号本质上是强制模型跟“被引用的那段原文”对齐。模型要标[1]就必须真正基于第1段资料来写这句话。这一个小小的格式要求能把模型的自由发挥空间压缩一大截回答的可信度提高一个量级。我最终版的Prompt模板长这样请根据以下资料回答问题。 资料 [1] (来源: xxx) 内容... [2] (来源: yyy) 内容... 问题xxx 要求 1. 严格基于资料回答不要使用资料之外的知识。 2. 如果资料中没有答案请直接回答“资料中未找到相关信息”。 3. 回答时在句末用[1][2]标注引用来源。这套模板我在好几个项目里改来改去最后留下来的就是最朴素的这版。你以后如果觉得模型回答太啰嗦就在Prompt里加“用三句话以内回答”如果觉得回答太生硬就加“在回答的开头用一句自然的话引入”。Prompt这东西没有标准答案但你得知道改哪一行会产生什么效果。5. 这套最小实现下一步怎么长成生产级系统我知道你早晚不会满足于一个跑在命令行里的玩具。但好消息是从最小实现走向生产级系统路是清晰的而且每一步都是在这个基础上做增量不需要推翻重来。这里聊几个方向。5.1 服务化从脚本到API姿势要优雅目前这套代码是同步阻塞的跑在命令行里没问题但一旦要接Web前端或者同时处理多个用户的并发请求就必须服务化。最省力的方案是直接把query_engine.py里的逻辑套进FastAPI或Flask的一个路由里。POST请求进来解析用户问题检索Chroma拼Prompt调DeepSeek返回结果。这个过程需要把search_chroma函数里的Chroma Client初始化、Embedding模型加载这些定时开销挪到模块加载时完成不要每次请求都重新初始化一次。否则模型加载的几十秒会被摊到每个用户头上用户直接等哭。另一个细节是服务化之后你要面对的是并发安全。Chroma的PersistentClient在单进程里是安全的但如果你用多进程部署就要小心并发写的问题。生产环境里我一般会给Chroma加一层Redis缓存高频问题直接走缓存减少对向量库的重复检索。5.2 索引更新新增文档不是简单append很多人上线RAG之后做的第一件事就是把新文档往库里塞。然后发现检索结果越来越乱。为什么因为你的切分函数会把新文档切成块但如果你修改了旧文档那些旧块还留在库里新旧内容互相打架。解决思路有两种。第一种是轻量的增量更新上传新文档时用文档的哈希值做ID前缀同名文档重新上传时先删除旧前缀的块再插入新的。第二种是定期重建索引每天定时跑一次入库脚本全量重新切分、重新向量化确保索引和文档库保持一致。对Python代码来说删除旧块的操作在Chroma里是collection.delete(where{source: file_path})。不过这里的source字段是metadata里的值删除条件是精确匹配不要试图用通配符。我踩过这个坑where子句里写{source: {$contains: deepseek}}是不支持的得用精确的文件路径。5.3 检索质量跃升混合检索与重排序到了这一步你把基础RAG玩熟之后会开始对检索质量提出更高要求。最常见的进阶方案是混合检索也就是把关键词检索BM25跟向量检索结合起来。BM25擅长精确匹配专业术语向量检索擅长语义扩展两者取并集再去重能显著提升召回率。取回来的候选集可能需要几十条这时候就需要一个重排序模型比如bge-reranker。它的作用是对候选文档做精细的逐条打分把最相关的排到前面。top_k的选择会在重排序之后进行这样既保证了召回率又保证了精度。我提这两个方案的时候并没有给详细代码因为那是Day 12、Day 15的课程内容。但你要知道它们不是推翻今天的代码而是在search_chroma这一步做增强。RAG是分层打怪的游戏今天你已经完成了从0到1的突破剩下的都是往这条骨架上添肌肉。回到开头那句话。RAG是目前大模型落地到业务里性价比最高的一条路。从最小实现出发你会越来越清楚它强在哪里、弱在哪里、哪些问题值得优化、哪些问题根本不需要解决。这份判断力是光看教程得不到的你得亲手把这三件套跑起来让它出错再修好它。
返回列表