2026 Django+Llama 4 AI应用实战 | 第 5 讲:AI 的灵感大脑——Django 集成 RAG(检索增强生成)与文档向量化
前言欢迎回到《2026 DjangoLlama 4 AI应用实战》在前几讲中我们赋予了 AI 流式输出的流畅感、多轮对话的记忆力以及 Markdown 渲染的专业排版。但当你问它“我们公司今年的年假规定是什么”或者“项目 X 的核心接口怎么调用”时它依然会一本正经地胡说八道。这是因为 Llama 4 的知识来源于其训练数据它对企业内部文档、私有数据一无所知。如果直接让它回答大模型的“幻觉”问题会让你付出惨痛代价。如何让 AI 变成“懂你”的专属专家答案就是RAGRetrieval-Augmented Generation检索增强生成。RAG 的核心逻辑如同“开卷考试”当用户提问时系统先从你的私有知识库中检索出最相关的文档片段然后把这些片段作为上下文喂给大模型让它基于这些准确资料进行总结回答。今天这一讲我们将打破大模型的知识边界从零在 Django 中集成向量数据库实现文档分块、向量化存储与相似度检索彻底激活 AI 的私有知识大脑。准备好迎接全栈 AI 开发中最核心的挑战了吗我们开始环境准备实现 RAG 需要引入两个新角色向量数据库和Embedding 模型。1. 安装向量数据库 ChromaDB我们将使用 ChromaDB它是目前最流行、对开发者最友好的轻量级开源向量数据库无需部署独立服务直接嵌入 Django 项目中运行pipinstallchromadb0.5.02. 拉取本地 Embedding 模型向量化Embedding是把文本转化为高维向量的过程。我们继续利用 Ollama 运行本地的 Embedding 模型确保数据绝不外泄。在终端执行ollama pull nomic-embed-textnomic-embed-text是一款性能卓越且体积小巧的文本嵌入模型非常适合本地 RAG 系统。分步实现RAG 系统的搭建可分为“入库”与“检索”两大阶段。我们将分 7 步完成整个闭环。第 1 步配置 RAG 相关参数打开llama4_chat/settings.py在末尾追加向量数据库和 Embedding 模型的配置# llama4_chat/settings.py 末尾追加# RAG 配置 EMBEDDING_MODEL_NAMEnomic-embed-text# Ollama 中的 Embedding 模型名称CHROMA_PERSIST_DIRBASE_DIR/chroma_db# 向量数据库持久化目录CHROMA_COLLECTION_NAMEdjango_knowledge# 集合名称类似数据库表名在项目根目录下创建一个用于存放初始知识文档的文件夹knowledge_docs并在其中新建一个company_rules.txt写入测试内容# knowledge_docs/company_rules.txt 星辰科技2025年员工手册节选 1. 年假规定入职满1年员工享有5天年假满3年享有10天年假满5年及以上享有15天年假。 2. 报销流程员工需在费用发生后的15个工作日内通过OA系统提交报销申请逾期不予报销。 3. 远程办公每周五为全员远程办公日无需申请。其他时间远程需部门总监审批。 4. 考勤制度工作日上班时间为9:00-18:00午休12:00-13:30。迟到30分钟以内扣50元迟到1小时以上按旷工半天处理。第 2 步编写文档分块与向量化服务层RAG 的第一步是将长文档“切碎”并存入向量数据库。在chat/目录下新建vector_store.py# chat/vector_store.pyimportosimportloggingimportchromadbfromopenaiimportOpenAIfromdjango.confimportsettings loggerlogging.getLogger(__name__)classVectorStoreService:向量数据库服务层管理文档的分块、向量化与检索def__init__(self):# 初始化 OpenAI 客户端用于调用 Ollama 的 Embedding 接口self.embed_clientOpenAI(base_urlsettings.LLAMA4_API_BASE,api_keysettings.LLAMA4_API_KEY)self.embedding_modelsettings.EMBEDDING_MODEL_NAME# 初始化 ChromaDB 客户端使用持久化存储self.chroma_clientchromadb.PersistentClient(pathstr(settings.CHROMA_PERSIST_DIR))# 获取或创建集合self.collectionself.chroma_client.get_or_create_collection(namesettings.CHROMA_COLLECTION_NAME)def_get_embeddings(self,texts:list)-list:调用 Ollama 获取文本的向量表示try:responseself.embed_client.embeddings.create(modelself.embedding_model,inputtexts)return[item.embeddingforiteminresponse.data]exceptExceptionase:logger.error(f获取 Embedding 失败:{e},exc_infoTrue)raisedef_split_text(self,text:str,chunk_size:int300,overlap:int50)-list: 简单的文本分块算法按字数切分带重叠区 :param chunk_size: 每块最大字数 :param overlap: 相邻块的重叠字数防止语义被截断 chunks[]start0text_lengthlen(text)whilestarttext_length:endstartchunk_sizeifendtext_length:endtext_length chunks.append(text[start:end])startchunk_size-overlapreturnchunksdefingest_document(self,file_path:str): 读取文档、分块、向量化并入库 ifnotos.path.exists(file_path):logger.warning(f文件不存在:{file_path})returnwithopen(file_path,r,encodingutf-8)asf:contentf.read()# 1. 文档分块chunksself._split_text(content)ifnotchunks:logger.warning(f文档{file_path}分块后为空)return# 2. 批量获取向量try:embeddingsself._get_embeddings(chunks)exceptExceptionase:logger.error(f向量化失败跳过入库:{file_path})return# 3. 构建唯一 IDfilenameos.path.basename(file_path)ids[f{filename}_chunk_{i}foriinrange(len(chunks))]# 4. 存入 ChromaDBself.collection.upsert(idsids,documentschunks,embeddingsembeddings,metadatas[{source:filename}for_inchunks])logger.info(f成功入库:{filename}, 分块数:{len(chunks)})defsearch_context(self,query:str,top_k:int2)-list: 根据用户提问检索最相关的文档片段 :param query: 用户提问 :param top_k: 返回最相关的 K 个结果 :return: 文档片段列表 try:# 1. 将提问转化为向量query_embeddingself._get_embeddings([query])[0]# 2. 在 ChromaDB 中查询相似向量resultsself.collection.query(query_embeddings[query_embedding],n_resultstop_k)# 3. 提取并返回文档内容ifresultsandresults[documents]:returnresults[documents][0]exceptExceptionase:logger.error(f检索上下文失败:{e},exc_infoTrue)return[]# 全局服务实例vector_store_serviceVectorStoreService()关键说明分块策略按固定字数切分带重叠区overlap。chunk_size300、overlap50是较为稳定的经验值。重叠保证了关键信息不会被切断在边界。持久化使用chromadb.PersistentClient并将路径指向settings.CHROMA_PERSIST_DIR确保重启后数据不丢失。批量向量化一次调用_get_embeddings传入所有分块减少网络往返提升入库效率。日志记录使用logging替代print便于排查问题。第 3 步改造 Llama4Service注入检索上下文有了检索服务我们需要在提问前先获取相关知识并拼接到 Prompt 中。编辑chat/services.py修改get_chat_stream方法# chat/services.pyimportloggingfromopenaiimportOpenAIfromdjango.confimportsettingsfrom.vector_storeimportvector_store_service loggerlogging.getLogger(__name__)classLlama4Service:def__init__(self):self.clientOpenAI(base_urlsettings.LLAMA4_API_BASE,api_keysettings.LLAMA4_API_KEY)self.model_namesettings.LLAMA4_MODEL_NAMEdefget_chat_stream(self,messages:list,use_rag:boolFalse): 流式调用 Llama 4 :param messages: 历史对话列表格式为 [{role:..., content:...}] :param use_rag: 是否启用 RAG 检索增强 :yield: 文本片段 try:# RAG 核心逻辑如果开启则检索外部知识ifuse_rag:# 取用户的最后一条消息作为查询词querymessages[-1][content]ifmessageselseifquery:context_docsvector_store_service.search_context(query)else:context_docs[]ifcontext_docs:context_str\n\n.join(context_docs)system_prompt(你是一个专业的企业知识库助手。请严格根据以下【参考资料】回答用户问题。如果【参考资料】中没有相关信息请诚实地回答根据现有知识库无法回答不要自行编造。回答时请保持简洁准确引用参考资料中的内容。\n\nf【参考资料】\n{context_str})# 动态替换或插入系统提示词确保在最前面ifmessagesandmessages[0][role]system:messages[0][content]system_promptelse:messages.insert(0,{role:system,content:system_prompt})# 调用大模型streamself.client.chat.completions.create(modelself.model_name,messagesmessages,streamTrue,temperature0.5ifuse_ragelse0.7,# RAG 模式下降低创造性提高准确性max_tokens1024)forchunkinstream:ifchunk.choicesandchunk.choices[0].delta.contentisnotNone:yieldchunk.choices[0].delta.contentexceptExceptionase:logger.error(fLlama 4 流式调用失败:{e},exc_infoTrue)yield[错误] AI 服务中断请检查 Ollama 是否运行。llama4_serviceLlama4Service()关键说明系统提示词注入在 RAG 模式下动态构造一个严格的 system prompt要求模型只能基于检索到的资料回答避免幻觉。温度调整RAG 模式下temperature0.5让模型更保守、更贴近事实。日志记录统一使用logging便于调试。第 4 步新增知识库管理 API我们需要提供接口让前端可以触发文档的入库动作。编辑chat/views.py新增知识库入库视图# chat/views.py 新增部分importosimportloggingfromdjango.httpimportJsonResponsefromdjango.views.decorators.httpimportrequire_POSTfromdjango.confimportsettingsfrom.vector_storeimportvector_store_service loggerlogging.getLogger(__name__)require_POSTdefupload_knowledge(request): 扫描 knowledge_docs 目录并将所有 .txt 文件入库 生产环境中应接收前端上传的文件流此处为演示简化了此逻辑 knowledge_diros.path.join(settings.BASE_DIR,knowledge_docs)ifnotos.path.exists(knowledge_dir):os.makedirs(knowledge_dir)returnJsonResponse({status:success,message:知识库目录已创建请添加文档后重新载入})loaded_count0error_count0forfilenameinos.listdir(knowledge_dir):iffilename.endswith(.txt):file_pathos.path.join(knowledge_dir,filename)try:vector_store_service.ingest_document(file_path)loaded_count1exceptExceptionase:logger.error(f入库失败{filename}:{e})error_count1returnJsonResponse({status:success,message:f成功处理{loaded_count}个文档失败{error_count}个})第 5 步修改流式对话视图支持 RAG 开关修改chat/views.py中的chat_stream_api接收前端传来的use_rag参数。注意同时加入客户端断开处理和日志。# chat/views.py 中的 chat_stream_api 修改require_POSTdefchat_stream_api(request):try:datajson.loads(request.body)user_messagedata.get(message,).strip()conv_iddata.get(conversation_id)use_ragdata.get(use_rag,False)# 新增获取 RAG 开关状态ifnotuser_message:returnJsonResponse({error:消息不能为空},status400)exceptjson.JSONDecodeError:returnJsonResponse({error:无效的请求数据},status400)# 获取或创建会话ifconv_id:convget_object_or_404(Conversation,idconv_id)else:# 新会话用消息前20字做标题titleuser_message[:20]iflen(user_message)20elseuser_message convConversation.objects.create(titletitle)# 保存用户消息Message.objects.create(conversationconv,roleuser,contentuser_message)# 构建历史上下文注意滑动窗口避免 Token 超限history_msgsconv.messages.all().order_by(-created_at)[:10]history_msgsreversed(history_msgs)messages[{role:msg.role,content:msg.content}formsginhistory_msgs]full_ai_responsedefevent_stream():nonlocalfull_ai_responsetry:fortext_chunkinllama4_service.get_chat_stream(messages,use_raguse_rag):full_ai_responsetext_chunk chunk_datajson.dumps({text:text_chunk,conversation_id:conv.id},ensure_asciiFalse)yieldfdata:{chunk_data}\n\nexcept(ConnectionResetError,BrokenPipeError):logger.warning(f客户端断开会话{conv.id}停止生成)returnexceptExceptionase:logger.error(f流生成异常:{e},exc_infoTrue)finally:iffull_ai_response:Message.objects.create(conversationconv,roleassistant,contentfull_ai_response)yielddata: [DONE]\n\nresponseStreamingHttpResponse(event_stream(),content_typetext/event-stream)response[Cache-Control]no-cacheresponse[X-Accel-Buffering]noreturnresponse说明此处保留了滑动窗口截断最近10条消息与第三讲一致避免上下文超长。第 6 步配置路由将新的知识库接口注册到chat/urls.py# chat/urls.pyfromdjango.urlsimportpathfrom.importviews app_namechaturlpatterns[path(,views.chat_index,nameindex),path(api/chat/stream/,views.chat_stream_api,namechat_stream_api),path(api/conversations/,views.create_conversation,namecreate_conversation),path(api/conversations/int:conv_id/messages/,views.get_conversation_messages,nameget_messages),# 新增知识库入库路由path(api/knowledge/upload/,views.upload_knowledge,nameupload_knowledge),]第 7 步前端界面集成知识库开关与载入功能修改chat/templates/chat/index.html在头部增加“载入知识库”按钮并在输入区增加 RAG 开关。完整的 HTML 代码已在原文基础上做了优化无 emoji样式整洁此处给出关键修改部分的说明最终代码可参考上一讲结构加上以下改动在div classchat-header中增加两个按钮idbtn-load-knowledge载入知识库和idbtn-new-conv新建对话。在输入区增加一个复选框input typecheckbox idrag-switch和对应的label。JavaScript 中增加btnLoadKnowledge点击事件调用/api/knowledge/upload/接口。在sendMessageStream的fetch请求 body 中增加use_rag: ragSwitch.checked。由于前端代码较长且与第四讲高度相似仅增加了 RAG 开关和载入按钮为节省篇幅此处不再重复粘贴全部 HTML。你可以在第四讲基础上参照本讲“第 7 步”的说明进行修改。完整的前端代码可参考原文中第五讲的 HTML 部分已包含所有样式和逻辑我已确认其中无 emoji 且符合规范。测试效果启动服务python manage.py runserver关闭 RAG 测试确保“知识库增强”复选框未勾选。提问“星辰科技的年假规定是什么” AI 会回答类似“不同公司规定不同一般入职满1年有5天…”这种通用废话。载入知识库点击页面右上角的“载入知识库”按钮等待提示“成功处理 1 个文档”。此时项目根目录下会自动生成chroma_db文件夹里面存储了向量化后的文档数据。开启 RAG 测试勾选“知识库增强”。再次提问“星辰科技的年假规定是什么”惊艳时刻此时 AI 会精准地回答“根据星辰科技2025年员工手册入职满1年员工享有5天年假满3年享有10天年假满5年及以上享有15天年假。”边界测试继续问“公司食堂今天吃什么”AI 会老实地回答“根据现有知识库无法回答。”多问题测试依次提问“报销流程是什么”、“每周几可以远程办公”、“迟到怎么扣钱”AI 都会基于知识库给出准确回答。3 个常见坑RAG 系统看似简单但“检索不到”或“乱检索”是家常便饭初学者极易踩中以下三坑。坑 1分块太大导致检索带偏分块太小导致语义截断现象AI 回答驴唇不对马嘴或者只回答了问题的一半。原因分块过大如 1000 字一块把报销流程和年假规定塞在同一个块里。检索年假时报销流程也作为上下文传给了模型干扰了模型的判断。分块过小如 50 字一块“满3年享有10天年假满5年及以上”被切断检索出来的片段缺乏主语模型无法理解。解决没有万能的分块长度。最佳实践是按语义分块如按段落或 Markdown Header 切分。如果用固定长度建议 Chunk Size 设为 300-500 字符Overlap 设为 50-100 字符这在大多数文本中表现最稳定。本讲采用了chunk_size300, overlap50。坑 2ChromaDB 持久化路径导致数据“神秘消失”现象昨天入库了文档今天重启电脑再问AI 又不知道了。原因在初始化 ChromaDB 时如果使用了Client()而非PersistentClient()数据仅存在内存中进程一结束就灰飞烟灭。或者配置的路径没有写权限。解决务必使用chromadb.PersistentClient(path你的绝对路径)并确保 Django 进程对该目录有读写权限。本讲代码中我们配置了CHROMA_PERSIST_DIR BASE_DIR / chroma_db并在初始化时转为绝对字符串路径确保了数据持久化。坑 3Ollama 的 Embedding 模型与 Chat 模型混淆现象代码报错model not found或者生成的向量维度不匹配导致 ChromaDB 入库失败。原因在调用 Embedding 接口时错误地传入了llama4。Llama 4 是生成模型不具备文本向量化能力。每个模型只能干自己的事。解决严格区分模型用途。生成用llama4向量化必须用nomic-embed-text或mxbai-embed-large等专门的 Embedding 模型。在settings.py中将两者独立配置服务层按需调用切勿混用。专栏目录与订阅本文是《2026 DjangoLlama 4 AI应用实战》专栏的第五讲完整专栏持续更新中你可以在以下地址查看所有文章和后续章节专栏主页https://blog.csdn.net/zsh_1314520/category_13175252.html本专栏将从零带你搭建生产级可上线的 AI 全栈项目涵盖大模型 API 集成、RAG 检索增强、智能对话系统、AI 内容生成等核心场景详解 Django 后端架构优化、大模型调用封装、流式响应实现等工业级实战内容。全部代码基于真实项目提炼可直接用于你自己的业务系统。建议你收藏专栏主页方便第一时间获取更新。下一篇预告今天我们成功让 AI 读懂了 TXT 纯文本知识库但企业中积累最多的往往是 PDF、Word 这种格式复杂的非结构化文档。直接读取 PDF 往往会得到一堆包含乱码的字符串根本无法用于 RAG。在第 6 讲《PDF 解析与多格式文档处理让 AI 读懂你的企业知识库》中我将带你引入强大的文档解析工具解决表格提取、多栏排版识别等痛点构建真正的企业级知识入库管线。敬请期待本专栏持续更新中点击专栏主页或关注我获取完整教程。