
1. 项目概述与核心痛点最近在折腾大模型应用落地的朋友估计没少被“幻觉”问题折磨。你精心构建了一个问答系统用户满怀期待地问了一个问题结果大模型给你编造了一个有鼻子有眼、但完全是胡说八道的答案。比如你问“公司2025年的年假政策是什么”它可能煞有介事地告诉你“根据最新规定司龄满1年可享受15天年假”而实际上公司政策是10天。这种一本正经地胡说八道就是所谓的“大模型幻觉”。幻觉问题在大模型应用尤其是基于私有知识库的问答场景中几乎是致命的。它直接摧毁了用户对系统的信任。单纯依靠提示词工程Prompt Engineering去约束模型“不要瞎编”效果非常有限模型在缺乏足够信息时依然倾向于“自信地”生成看似合理的文本。这正是RAG检索增强生成技术要解决的核心问题。RAG的核心思想很简单让模型在回答之前先去你的知识库比如文档、数据库里查一查。它把用户的问题转换成一个搜索查询从知识库中检索出最相关的文档片段然后将这些片段和原始问题一起交给大模型指令模型“基于以下参考信息来回答问题”。这样模型的回答就有了事实依据大大降低了信口开河的概率。然而实现一个能“彻底解决幻觉”的RAG系统远不是把检索和生成拼起来那么简单。市面上很多教程只展示了最简单的流程但在真实业务中你会遇到检索不准、切片Chunking不合理、上下文长度限制、重排序Re-ranking缺失等一系列问题任何一个环节的短板都会导致最终的答案依然包含幻觉或信息不全。所以这个项目的目的就是打造一套实战级、高可用的RAG系统。我们将从前端到后端构建一个完整的、可复现的解决方案。前端提供一个简洁的交互界面后端则集成检索、重排序、提示词工程等关键环节。我会把每一步的代码、配置、以及我踩过的坑都摊开来讲目标是让你拿到这套代码后稍作配置就能在自己的环境里跑起来真正用于业务场景。2. 技术栈选型与架构设计为什么选择这套技术栈这是经过实际项目权衡后的结果。一个稳定、易维护且性能可接受的RAG系统需要几个核心组件协同工作。2.1 后端技术栈Node.js LangChain FastAPI看到标题里的Node.js和LangChain你可能会疑惑LangChain不是Python生态的吗没错LangChain的核心和丰富生态确实在Python。但对于一个需要快速部署、前后端语言统一尤其当团队前端技术栈是Node.js、并且对复杂AI管道要求不是极端苛刻的场景Node.js版本的LangChainlangchain/langgraph是一个值得考虑的轻量级选择。它提供了构建Agent和链Chain的核心抽象足以支撑一个中等复杂度的RAG流程。不过为了追求更高的开发效率和更成熟的AI开发生态本项目的后端将采用Python FastAPI。这是一个高性能的异步Web框架完美契合需要调用多个可能阻塞的IO服务如向量数据库查询、大模型API调用的场景。同时Python拥有无与伦比的AI库支持。最终后端技术栈确定如下Web框架: FastAPI。负责提供RESTful API接口处理前端请求。RAG框架: LangChain。它是我们的“总指挥”负责编排整个RAG流程文档加载、文本分割、向量化、检索、提示词组装、调用大模型。向量数据库: Chroma本地模式。选择它是因为其轻量、易用无需额外部署服务适合快速原型和中小规模知识库。对于生产环境可以考虑Qdrant、Weaviate或Pinecone。嵌入模型: OpenAI的text-embedding-3-small。生成文本的向量表示Embedding。选择它是因为质量稳定、API易用。如果考虑成本或数据隐私可以替换为开源的BGE-M3或nomic-embed-text-v1.5通过Ollama等工具本地部署。大语言模型: OpenAI的gpt-3.5-turbo。负责最终的答案生成。在检索到相关上下文后由它来合成最终答案。同样可根据需要替换为 Claude、DeepSeek或本地部署的 Llama 3.1 等模型。重排序模型: Cohere的rerank-english-v3.0或BAAI/bge-reranker-large。这是一个关键但常被忽略的组件。初步向量检索返回的Top K个片段可能并非全部高度相关。重排序模型会对这K个片段根据问题进行二次相关性打分和排序只保留最相关的几个能显著提升最终上下文的质量。2.2 前端技术栈React Vite Tailwind CSS前端的目标是构建一个轻快、美观的聊天界面。React: 主流前端框架生态丰富组件化开发方便。Vite: 下一代前端构建工具启动和热更新速度极快开发体验好。Tailwind CSS: 实用优先的CSS框架可以快速构建出美观的UI无需在样式文件间来回切换。Axios: 用于调用后端FastAPI接口。2.3 系统架构流程图整个系统的数据流如下知识库构建离线原始文档PDF/TXT/Markdown - LangChain文档加载器 - 文本分割器 - 嵌入模型 - 向量存储Chroma。问答流程在线用户在前端界面提出问题。前端通过Axios将问题发送到FastAPI后端。FastAPI接收到问题调用LangChain构建的RAG链。RAG链首先使用嵌入模型将问题向量化。在Chroma向量库中进行相似性搜索检索出Top K个相关片段。可选但推荐调用重排序模型对Top K片段进行精排筛选出Top N个最相关片段。将筛选后的片段和原始问题通过精心设计的提示词模板组合成最终提示。调用大语言模型如GPT-3.5传入提示生成答案。大模型返回的答案连同引用的文档片段用于溯源一并返回给FastAPI。FastAPI将答案和引用源返回给前端。前端渲染答案并可以展示答案引用了哪些原始文档片段。这个架构清晰地将离线处理与在线服务分离并且每个环节都可替换例如换用不同的向量库、大模型或重排序器具备了良好的扩展性。3. 后端核心实现从零搭建抗幻觉RAG链后端是整个系统的大脑。我们将在backend目录下进行开发。3.1 环境准备与依赖安装首先创建并激活Python虚拟环境这是管理项目依赖的最佳实践。mkdir rag-anti-hallucination cd rag-anti-hallucination mkdir backend frontend cd backend python -m venv venv # Windows: venv\Scripts\activate # Mac/Linux: source venv/bin/activate安装核心依赖。requirements.txt文件内容如下fastapi0.104.1 uvicorn[standard]0.24.0 langchain0.1.0 langchain-community0.0.10 langchain-openai0.0.2 chromadb0.4.22 pypdf3.17.4 python-dotenv1.0.0 cohere5.2.2 sentence-transformers2.2.2 pydantic2.5.0执行安装pip install -r requirements.txt。接下来创建.env文件来管理敏感信息如API密钥。切记不要将此文件提交到版本控制系统在.gitignore中添加它。OPENAI_API_KEYsk-your-openai-key-here COHERE_API_KEYyour-cohere-key-here # 如果使用Cohere重排序注意如果你使用开源的重排序模型如BGE Reranker则不需要Cohere的API密钥但需要确保有足够的GPU内存或使用CPU推理。3.2 知识库构建文档加载与智能切片知识库的质量直接决定了检索的上限。糟糕的切片会导致检索出无关信息或丢失关键信息。我们创建一个knowledge_base.py文件来处理这部分。import os from langchain_community.document_loaders import PyPDFLoader, TextLoader, UnstructuredMarkdownLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma from dotenv import load_dotenv load_dotenv() class KnowledgeBaseBuilder: def __init__(self, persist_directory./chroma_db): self.embeddings OpenAIEmbeddings(modeltext-embedding-3-small) self.persist_directory persist_directory # 关键选择合适的分割器 self.text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个片段的字符数目标 chunk_overlap100, # 片段之间的重叠字符数 separators[\n\n, \n, 。, , , , , , ] # 分割优先级 ) def load_documents(self, file_path): 根据文件后缀选择加载器 ext os.path.splitext(file_path)[-1].lower() if ext .pdf: loader PyPDFLoader(file_path) elif ext .txt: loader TextLoader(file_path, encodingutf-8) elif ext .md: loader UnstructuredMarkdownLoader(file_path) else: raise ValueError(fUnsupported file type: {ext}) return loader.load() def build_and_persist(self, file_paths): 加载文档分割生成向量并持久化存储 all_docs [] for fp in file_paths: print(fLoading {fp}...) docs self.load_documents(fp) all_docs.extend(docs) print(Splitting documents into chunks...) splits self.text_splitter.split_documents(all_docs) print(fCreated {len(splits)} chunks.) print(Creating vector store...) vectordb Chroma.from_documents( documentssplits, embeddingself.embeddings, persist_directoryself.persist_directory ) vectordb.persist() print(fVector store persisted to {self.persist_directory}) return vectordb if __name__ __main__: # 示例构建知识库 builder KnowledgeBaseBuilder() # 假设你的文档放在 ./docs 目录下 doc_paths [./docs/employee_handbook.pdf, ./docs/company_policy.txt] vectordb builder.build_and_persist(doc_paths)实操心得文本分割的“艺术”chunk_size不是越大越好。过大的片段可能包含多个主题稀释了核心信息的向量表示过小的片段则可能丢失上下文。500-1000字符是一个常见的起点需要根据你的文档类型技术文档、法律条文、对话记录进行调整。chunk_overlap至关重要。它确保了上下文信息不会在分割点被生硬切断。例如一个重要的定义刚好在片段末尾重叠部分可以确保它在下一个片段的开头再次出现提高了检索到完整信息的概率。separators的顺序决定了分割策略的优先级。这里我们优先按段落分再按句子分最后按词语分这符合大多数文本的自然结构。3.3 RAG链核心检索、重排序与生成这是抗幻觉的核心。我们创建rag_chain.py。import os from typing import List, Dict, Any from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate from langchain_openai import ChatOpenAI from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings from langchain.schema import Document from dotenv import load_dotenv import cohere # 用于Cohere重排序 # 或者使用开源的sentence-transformers做重排序 # from sentence_transformers import CrossEncoder load_dotenv() class EnhancedRAGChain: def __init__(self, persist_directory./chroma_db, use_rerankercohere): self.embeddings OpenAIEmbeddings(modeltext-embedding-3-small) self.vectordb Chroma( persist_directorypersist_directory, embedding_functionself.embeddings ) self.llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) # temperature0减少随机性 self.use_reranker use_reranker if use_reranker cohere: self.cohere_client cohere.Client(os.getenv(COHERE_API_KEY)) elif use_reranker bge: # 初始化开源重排序模型首次使用会下载模型 # 注意这可能需要较大的内存/显存 # from sentence_transformers import CrossEncoder # self.rerank_model CrossEncoder(BAAI/bge-reranker-large) pass # 定义抗幻觉提示词模板 self.prompt_template PromptTemplate( input_variables[context, question], template你是一个专业的问答助手请严格根据以下提供的参考信息来回答问题。如果参考信息中没有明确包含答案或者信息不足以回答问题请直接说“根据现有信息我无法回答这个问题”不要编造任何信息。 参考信息 {context} 问题{question} 请基于以上参考信息回答 ) def rerank_documents(self, query: str, documents: List[Document], top_n: int 3) - List[Document]: 对检索到的文档进行重排序保留最相关的top_n个 if not documents or self.use_reranker is None: return documents[:top_n] doc_texts [doc.page_content for doc in documents] if self.use_reranker cohere: # 使用Cohere API重排序 results self.cohere_client.rerank( modelrerank-english-v3.0, queryquery, documentsdoc_texts, top_ntop_n, ) reranked_indices [r.index for r in results.results] reranked_docs [documents[i] for i in reranked_indices] return reranked_docs elif self.use_reranker bge: # 使用本地BGE模型重排序 # pairs [[query, doc] for doc in doc_texts] # scores self.rerank_model.predict(pairs) # scored_docs list(zip(scores, documents)) # scored_docs.sort(keylambda x: x[0], reverseTrue) # reranked_docs [doc for _, doc in scored_docs[:top_n]] # return reranked_docs return documents[:top_n] # 暂未实现返回原样 else: return documents[:top_n] def get_retriever(self, k10): 创建检索器并包装成可重排序的检索链 base_retriever self.vectordb.as_retriever(search_kwargs{k: k}) def _enhanced_retriever(query: str) - List[Document]: # 1. 初步向量检索 preliminary_docs base_retriever.get_relevant_documents(query) # 2. 重排序 reranked_docs self.rerank_documents(query, preliminary_docs, top_n4) # 最终保留4个片段 return reranked_docs return _enhanced_retriever def create_chain(self): 创建最终的RAG问答链 retriever self.get_retriever(k10) qa_chain RetrievalQA.from_chain_type( llmself.llm, chain_typestuff, # “stuff”策略将所有上下文塞入提示词。对于长文档可考虑“map_reduce”或“refine” retrieverretriever, chain_type_kwargs{prompt: self.prompt_template}, return_source_documentsTrue # 关键返回源文档用于溯源 ) return qa_chain def query(self, question: str) - Dict[str, Any]: 执行查询 qa_chain self.create_chain() result qa_chain.invoke({query: question}) return { answer: result[result], source_documents: [ {content: doc.page_content, metadata: doc.metadata} for doc in result[source_documents] ] } # 全局实例供API调用 rag_engine EnhancedRAGChain(use_rerankercohere) # 或 bge, None核心要点解析抗幻觉提示词模板中明确指令模型“严格根据参考信息”回答并设置了“无法回答”的兜底条款。这是约束模型行为的第一道防线。重排序集成get_retriever方法不仅做了简单的向量检索还集成了重排序步骤。先检索出较多的候选片段k10再用更精细的交叉编码器Cross-Encoder模型进行精排选出最相关的几个top_n4。这步能有效过滤掉向量相似但语义不匹配的噪声。溯源功能return_source_documentsTrue让链返回用于生成答案的源文档片段。这是建立信任的关键前端可以展示这些片段让用户自己判断答案的依据是否可靠。3.4 FastAPI接口封装创建main.py作为后端服务的入口。from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import List, Optional from rag_chain import rag_engine # 导入上面创建的RAG引擎 app FastAPI(title抗幻觉RAG问答系统API) # 配置CORS允许前端跨域请求 app.add_middleware( CORSMiddleware, allow_origins[http://localhost:5173], # 前端开发服务器地址 allow_credentialsTrue, allow_methods[*], allow_headers[*], ) # 定义请求/响应模型 class QueryRequest(BaseModel): question: str class SourceDoc(BaseModel): content: str metadata: dict class QueryResponse(BaseModel): answer: str sources: List[SourceDoc] success: bool error_message: Optional[str] None app.get(/) def read_root(): return {message: 抗幻觉RAG问答系统后端服务已启动} app.post(/query, response_modelQueryResponse) async def query_knowledge_base(request: QueryRequest): try: result rag_engine.query(request.question) return QueryResponse( answerresult[answer], sources[ SourceDoc(contentdoc[content], metadatadoc[metadata]) for doc in result[source_documents] ], successTrue ) except Exception as e: raise HTTPException(status_code500, detailf查询处理失败: {str(e)}) if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8000)现在后端核心就完成了。通过python main.py启动服务它将在http://localhost:8000监听提供一个/query的POST接口。4. 前端实现构建简洁交互界面前端的目标是提供一个干净、直观的聊天式界面。我们在frontend目录下操作。4.1 项目初始化与依赖安装使用Vite快速创建React TypeScript项目。cd ../frontend npm create vitelatest . -- --template react-ts npm install npm install axios tailwindcss postcss autoprefixer npx tailwindcss init -p修改tailwind.config.js/** type {import(tailwindcss).Config} */ export default { content: [ ./index.html, ./src/**/*.{js,ts,jsx,tsx}, ], theme: { extend: {}, }, plugins: [], }在src/index.css中引入Tailwindtailwind base; tailwind components; tailwind utilities;4.2 核心组件实现聊天界面我们创建两个主要组件ChatInterface.tsx和Message.tsx。首先定义类型src/types.tsexport interface Message { id: string; content: string; isUser: boolean; timestamp: Date; sources?: SourceDoc[]; // 仅AI消息有来源 } export interface SourceDoc { content: string; metadata: Recordstring, any; } export interface ApiResponse { answer: string; sources: SourceDoc[]; success: boolean; error_message?: string; }然后实现src/components/ChatInterface.tsximport React, { useState, useRef, useEffect } from react; import axios from axios; import Message from ./Message; import { Message as MessageType, ApiResponse } from ../types; import { Send, Bot, User, AlertCircle } from lucide-react; const API_BASE_URL http://localhost:8000; // 后端地址 const ChatInterface: React.FC () { const [messages, setMessages] useStateMessageType[]([ { id: 1, content: 你好我是一个基于知识库的智能助手。请向我提问我会尽力从提供的资料中寻找答案。, isUser: false, timestamp: new Date(), } ]); const [input, setInput] useState(); const [isLoading, setIsLoading] useState(false); const [error, setError] useStatestring | null(null); const messagesEndRef useRefHTMLDivElement(null); const scrollToBottom () { messagesEndRef.current?.scrollIntoView({ behavior: smooth }); }; useEffect(() { scrollToBottom(); }, [messages]); const handleSend async () { if (!input.trim() || isLoading) return; const userMessage: MessageType { id: Date.now().toString(), content: input, isUser: true, timestamp: new Date(), }; setMessages(prev [...prev, userMessage]); setInput(); setIsLoading(true); setError(null); try { const response await axios.postApiResponse(${API_BASE_URL}/query, { question: input, }); const aiMessage: MessageType { id: (Date.now() 1).toString(), content: response.data.answer, isUser: false, timestamp: new Date(), sources: response.data.sources, }; setMessages(prev [...prev, aiMessage]); } catch (err: any) { console.error(Query failed:, err); setError(err.response?.data?.detail || 请求失败请检查网络或后端服务); const errorMessage: MessageType { id: (Date.now() 1).toString(), content: 抱歉处理您的请求时出现错误${err.response?.data?.detail || 未知错误}, isUser: false, timestamp: new Date(), }; setMessages(prev [...prev, errorMessage]); } finally { setIsLoading(false); } }; const handleKeyPress (e: React.KeyboardEvent) { if (e.key Enter !e.shiftKey) { e.preventDefault(); handleSend(); } }; return ( div classNameflex flex-col h-screen bg-gray-50 {/* 头部 */} header classNamebg-white shadow-sm border-b p-4 div classNamecontainer mx-auto flex items-center justify-between div classNameflex items-center space-x-3 div classNamep-2 bg-blue-100 rounded-lg Bot classNamew-6 h-6 text-blue-600 / /div div h1 classNametext-xl font-bold text-gray-800抗幻觉RAG知识库助手/h1 p classNametext-sm text-gray-500基于检索增强生成答案有据可查/p /div /div div classNametext-sm text-gray-500 已连接后端服务 /div /div /header {/* 聊天区域 */} main classNameflex-1 overflow-y-auto p-4 md:p-6 div classNamecontainer mx-auto max-w-4xl space-y-6 {messages.map((msg) ( Message key{msg.id} message{msg} / ))} {isLoading ( div classNameflex items-center space-x-3 div classNamep-2 bg-green-100 rounded-full Bot classNamew-5 h-5 text-green-600 / /div div classNameflex space-x-1 div classNamew-2 h-2 bg-gray-400 rounded-full animate-bounce style{{ animationDelay: 0ms }}/div div classNamew-2 h-2 bg-gray-400 rounded-full animate-bounce style{{ animationDelay: 150ms }}/div div classNamew-2 h-2 bg-gray-400 rounded-full animate-bounce style{{ animationDelay: 300ms }}/div /div /div )} div ref{messagesEndRef} / /div /main {/* 错误提示 */} {error ( div classNamemx-4 md:mx-auto max-w-4xl div classNamebg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg flex items-center AlertCircle classNamew-5 h-5 mr-2 / span classNametext-sm{error}/span /div /div )} {/* 输入区域 */} footer classNameborder-t bg-white p-4 div classNamecontainer mx-auto max-w-4xl div classNameflex space-x-3 textarea classNameflex-1 border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none placeholder输入您的问题例如公司的年假政策是怎样的 rows{2} value{input} onChange{(e) setInput(e.target.value)} onKeyDown{handleKeyPress} disabled{isLoading} / button onClick{handleSend} disabled{isLoading || !input.trim()} classNameself-end px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors {isLoading ? 思考中... : Send classNamew-5 h-5 /} /button /div p classNametext-xs text-gray-500 mt-2 text-center 助手将严格依据知识库内容回答避免编造信息。答案下方会显示引用的来源片段。 /p /div /footer /div ); }; export default ChatInterface;接着实现src/components/Message.tsximport React, { useState } from react; import { Bot, User, ChevronDown, ChevronUp, FileText } from lucide-react; import { Message as MessageType } from ../types; interface MessageProps { message: MessageType; } const Message: React.FCMessageProps ({ message }) { const [showSources, setShowSources] useState(false); const isAI !message.isUser; return ( div className{flex ${isAI ? justify-start : justify-end}} div className{flex max-w-[80%] ${isAI ? flex-row : flex-row-reverse}} {/* 头像 */} div className{flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${isAI ? bg-green-100 mr-3 : bg-blue-100 ml-3}} {isAI ? Bot classNamew-5 h-5 text-green-600 / : User classNamew-5 h-5 text-blue-600 /} /div {/* 消息气泡 */} div div className{rounded-2xl px-4 py-3 ${isAI ? bg-white border border-gray-200 text-gray-800 : bg-blue-600 text-white}} div classNamewhitespace-pre-wrap{message.content}/div /div {/* 时间戳 */} div className{text-xs text-gray-500 mt-1 ${isAI ? text-left : text-right}} {message.timestamp.toLocaleTimeString([], { hour: 2-digit, minute: 2-digit })} /div {/* AI消息的引用来源 */} {isAI message.sources message.sources.length 0 ( div classNamemt-2 button onClick{() setShowSources(!showSources)} classNameinline-flex items-center text-xs text-blue-600 hover:text-blue-800 font-medium FileText classNamew-3 h-3 mr-1 / {showSources ? 隐藏引用来源 : 显示${message.sources.length}个引用来源} {showSources ? ChevronUp classNamew-3 h-3 ml-1 / : ChevronDown classNamew-3 h-3 ml-1 /} /button {showSources ( div classNamemt-2 space-y-2 border border-gray-200 rounded-lg p-3 bg-gray-50 p classNametext-xs font-semibold text-gray-700 mb-2答案基于以下信息生成/p {message.sources.map((source, idx) ( div key{idx} classNametext-sm border-l-4 border-blue-400 pl-3 py-1 bg-white rounded p classNametext-gray-700{source.content}/p {source.metadata ( p classNametext-xs text-gray-500 mt-1 来源: {source.metadata.source || 未知} (第{source.metadata.page || ?}页) /p )} /div ))} /div )} /div )} /div /div /div ); }; export default Message;最后修改src/App.tsx和src/main.tsx来使用我们的组件。src/App.tsx:import ChatInterface from ./components/ChatInterface; function App() { return ChatInterface /; } export default App;src/main.tsx:import React from react import ReactDOM from react-dom/client import App from ./App import ./index.css ReactDOM.createRoot(document.getElementById(root)!).render( React.StrictMode App / /React.StrictMode, )4.3 启动前端在frontend目录下运行npm run dev前端开发服务器将在http://localhost:5173启动。确保后端服务也在运行 (http://localhost:8000)现在你就可以在浏览器中打开前端与你的抗幻觉RAG助手对话了。5. 部署与优化实战指南代码能跑通只是第一步要让系统稳定、可靠地服务还需要考虑部署和深度优化。5.1 生产环境部署考量后端部署服务器选择一台具有公网IP的云服务器如阿里云ECS、腾讯云CVM。环境使用supervisor或systemd来管理你的FastAPI进程确保崩溃后自动重启。反向代理使用Nginx作为反向代理处理静态文件、SSL加密HTTPS和负载均衡如果你部署了多个后端实例。进程管理对于高并发场景使用Gunicorn或Uvicorn搭配多个工作进程Worker。例如gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000前端部署构建运行npm run build生成静态文件在dist目录。托管可以将dist目录的内容上传到对象存储如阿里云OSS、腾讯云COS并开启静态网站托管或者直接通过Nginx提供静态文件服务。向量数据库升级Chroma生产模式上述代码使用了Chroma的本地持久化模式。对于生产环境建议部署Chroma的独立服务器后端通过HTTPClient连接实现多实例后端共享同一个向量库。替代方案评估Qdrant、Weaviate或Milvus。它们提供了更好的分布式支持、更丰富的查询功能和监控指标。5.2 高级优化技巧混合检索Hybrid Search问题纯向量检索对于关键词匹配、缩写、特定术语可能效果不佳。方案结合稀疏向量检索如BM25和稠密向量检索。先用BM25进行关键词检索再用向量检索进行语义检索最后合并结果并去重重排序。LangChain的EnsembleRetriever可以简化这个流程。查询理解与改写Query Understanding/Expansion问题用户问题可能表述模糊、简短或包含错别字。方案在检索前先用一个小模型如GPT-3.5对原始查询进行改写或扩展。例如将“年假怎么算”扩展为“员工年假计算规则、年假天数规定、年假政策”。示例代码片段def query_expansion(original_query): prompt f请将以下用户问题改写或扩展成2-3个更全面、更适合用于知识库检索的查询语句用分号分隔。 原问题{original_query} 扩展后 response llm.invoke(prompt) expanded_queries response.content.split(;) return [original_query] [q.strip() for q in expanded_queries if q.strip()] # 对每个扩展查询进行检索然后合并结果元数据过滤问题知识库包含多种类型文档如HR政策、技术手册、会议纪要检索时希望限定范围。方案在文档切片时为其添加元数据如doc_type: hr_policy,year: 2024。检索时可以通过self.vectordb.as_retriever(search_kwargs{k: 5, filter: {doc_type: hr_policy}})进行过滤大幅提升检索精度。上下文窗口与摘要问题当检索到的相关片段总长度超过大模型的上下文限制时直接“stuff”会失败。方案Map-Reduce将每个相关片段单独发送给大模型生成子答案再汇总所有子答案生成最终答案。适合文档间独立性强的场景。Refine迭代处理文档将上一个答案与下一个文档片段结合不断精炼答案。适合需要深度推理、信息连贯的场景。上下文压缩使用一个单独的LLM步骤对检索到的冗长上下文进行摘要只保留最核心的信息再交给答案生成模型。6. 常见问题排查与性能调优在实际运行中你肯定会遇到各种问题。这里记录了一些典型问题及其解决方案。6.1 检索相关性问题问题检索到的文档片段似乎不相关。检查嵌入模型尝试不同的嵌入模型。对于中文BGE-M3或nomic-embed-text-v1.5可能比OpenAI的默认模型效果更好。确保你的文档语言和嵌入模型训练语言匹配。调整切片策略回顾第3.2节。尝试不同的chunk_size和chunk_overlap。对于结构化文档如Markdown标题可以尝试MarkdownHeaderTextSplitter。启用重排序确保重排序环节已启用且工作正常。检查Cohere API密钥或本地重排序模型是否加载成功。重排序是提升相关性的最有效手段之一。增加检索数量k在重排序前先检索更多的候选片段例如k20给重排序模型更多选择。6.2 答案质量与幻觉问题问题即使有相关上下文模型依然在编造细节或答非所问。强化提示词优化你的提示词模板。明确、强硬的指令是关键。可以尝试加入“如果信息不足必须明确说明”等约束甚至让模型先判断信息是否充足。检查上下文注入打印出最终发送给大模型的完整提示词确认检索到的上下文是否正确、完整地嵌入到了提示词中。降低模型“创造力”将temperature参数设为0或接近0的值如0.1减少模型生成中的随机性。使用更强的模型如果条件允许将生成模型从gpt-3.5-turbo升级到gpt-4-turbo或claude-3-sonnet。更强大的模型在遵循指令和基于上下文推理方面通常表现更好。6.3 性能与延迟问题问题查询响应速度慢。向量检索优化确保Chroma数据库建立了索引。对于大规模数据考虑使用支持更高效索引的向量数据库如Qdrant的HNSW索引。异步处理FastAPI本身支持异步。确保你的向量检索、模型调用等IO密集型操作使用异步客户端如httpx.AsyncClient调用API或使用支持异步的向量数据库客户端。缓存对于常见、重复的问题可以在应用层如Redis或向量数据库层某些向量库支持缓存引入缓存机制。重排序模型轻量化Cohere的API调用有网络延迟。如果对延迟极度敏感可以考虑使用更小、更快的本地重排序模型或在特定场景下牺牲一些精度关闭重排序。6.4 系统稳定性问题问题服务偶尔崩溃或无响应。超时与重试在调用外部APIOpenAI, Cohere时务必设置合理的超时时间和重试策略使用指数退避。速率限制遵守各API提供商的速率限制Rate Limit在代码中实现限流或使用队列。健康检查为FastAPI服务添加/health端点用于监控服务状态。日志记录使用logging模块记录关键步骤、错误和慢查询便于问题追踪。这套从零开始的RAG实战代码已经囊括了对抗幻觉的核心武器高质量的文本处理、精准的检索与重排序、强约束的提示词工程以及可溯源的答案呈现。它不是一个玩具而是一个可以直接作为内部知识库助手、智能客服原型或文档问答系统基石的解决方案。技术细节上的微调比如嵌入模型的选择、切片大小的优化、提示词的打磨需要你根据自己具体的文档内容和业务需求进行持续的迭代和测试。记住解决幻觉没有银弹但通过这样一套严谨的工程化流程你已经能够将它控制在一个可接受、可解释、可管理的范围内了。