
全栈学习走到第五周我把目标锁定在 RAGRetrieval-Augmented Generation检索增强生成知识库问答系统上。不是因为它“热门”而是因为它是我眼中把 AI 技术落地产出的最佳载体既不要求从零训练大模型又能在真实业务里直接解决“模型不懂我的私有资料”这个痛点。更关键的是从文档加载、文本切分、向量化、检索、重排序到大模型生成再到后端 API、前端聊天界面、容器化部署这一条链路完整覆盖了我前面五周积累的所有技能点。这一周的实践总结我会按我真实的推进顺序来写从背景和架构设计讲起然后是环境选型、核心链路实现、评估与调优、生产级部署最后是避坑记录。这篇文章适合那些已经掌握 Python 和后端基础、想用一个大而全的项目把 AI 应用开发全流程走一遍的开发者。如果你对 RAG 只有概念没有实操经验跟着这篇文章走完你也能把手里的知识库变成一个可以上线用的问答服务。1. 为什么第五周的里程碑项目必须是 RAG 知识库问答1.1 背景从裸调大模型到必须回答“私有知识”的问题前几周我都在做相对单点的事情调用大模型 API 做文本总结、用 LangChain 搭一个简单的对话链、给模型加些 Prompt 模板。做到第四周时一个很现实的问题开始反复出现我自己整理的技术笔记、团队内部的产品文档、公司积累的故障复盘报告这些资料根本不在大模型的训练数据里。直接把这些文档拼进 Prompt又会遇到两个绕不过去的坎——上下文窗口有限以及“我又没说让你回答这个你凭什么瞎编”。我试过把一份 3 万字的内部文档直接塞进 Prompt结果模型回答得一本正经但内容完全对不上号。那一刻我意识到如果不引入检索机制私有知识库就永远只能停留在“聊天玩具”这个层面。RAG 的核心思路其实不复杂与其让模型硬背所有知识不如给它一个“开卷考试”的资格先从外部知识库中检索出与问题最相关的几段文本再把这些文本作为参考资料交给模型组织答案。这样既绕开了训练成本又能保证答案来源可追溯。1.2 这个项目的“全栈”到底全在哪里我之所以把这一周定义为全栈实践而不是单纯的大模型应用开发是因为这个项目天然横跨多个技术层次。数据层需要处理 PDF、Markdown、Word、TXT 等多种格式的文档做解析、清洗、切分。算法层需要了解嵌入模型Embedding Model怎么把文本变成向量向量之间的距离又意味着什么。检索层需要掌握向量数据库的选型、索引类型、相似度检索、重排序策略。应用层需要写后端服务把检索和生成串联成一条完整的问答链路。前端层需要一个简洁可用的聊天交互界面让用户能真正“用”起来。运维层Docker 容器化、环境变量管理、接口监控、日志采集、性能压测。工程化版本管理、依赖锁定、配置文件管理、CI 发布思路。前几周的知识点在这个项目里全部变成了刚需。比如我之前觉得 Python 虚拟环境管理很麻烦但在这个项目里装 torch、sentence-transformers、chromadb、fastapi、langchain 这些重依赖时良好的虚拟环境隔离直接帮我避免了一整天的版本冲突问题。1.3 项目的核心需求边界动手之前我先花了一些时间把事情说清楚这个系统要解决什么问题不解决什么问题。输入侧用户上传一批内部文档系统需要完成解析入库。问答侧用户对这批文档进行自然语言提问系统返回有依据的回答并附上引用来源。边界不做多轮复杂对话状态管理不做用户权限系统不做大规模分布式检索不训练自有模型。明确边界太重要了。因为“从零到生产级”并不等于“从零到谷歌”我给自己设的目标是在单机条件下用容器化的方式把系统跑成一个可对外提供服务、可观测、可扩展但并不堆砌无必要复杂度的小型生产应用。如果一开始就想把微服务、K8s、多租户全部装进去项目大概率会烂尾。2. 架构设计与技术选型先画好图再写代码2.1 整体流程一次完整问答背后发生了什么我习惯先把核心调用链画清楚再动手写代码。一次完整的 RAG 问答请求从我实现后的视角看包含下面这条路径用户输入问题。后端调用查询改写模块判断是否需要对问题进行补充或改写。查询文本通过 Embedding 模型向量化。请求进入向量数据库执行相似度检索召回 Top-K 候选文本块。对候选文本块做重排序选出最相关的几段。将问题与所选文本块组装成 Prompt。将 Prompt 发送给大模型生成最终回答。把回答、引用来源、耗时指标返回给前端。从实现顺序上看我又把它拆成了离线处理和在线服务两条链路。离线链路负责文档解析、清洗、切分、向量化和入库在线链路负责问答时实时完成检索与生成。2.2 技术栈选型我为什么没有无脑上 LangChain技术选型阶段我对比了几套常见方案。LangChain生态最全内置了 Document Loader、Text Splitter、VectorStore、Retriever、QA Chain 等组件上手快。但它抽象层级太多一旦出了问题排查链路很长有时一个简单的参数错误会被一堆 wrapper 掩盖。LlamaIndex在知识库场景的数据索引、检索策略上做得更专业文档切分和索引结构设计很灵活。但我当时对它的熟悉度不够遇到问题社区资料相对少一些。自研胶水代码 必要组件的组合用 LangChain 只拿少量工具能力核心检索逻辑自己写。虽然代码更多但每个环节都在自己掌控之下。最终我的选择是用 LangChain 但只用它做文档加载和文本切分向量化调用开源模型向量数据库用 Chroma检索逻辑和后端服务自己实现。这样做的好处是我可以彻底搞清楚每一步到底在做什么遇到问题能直接定位到具体模块。比如早期 LangChain 版本更新频繁很多时候不是你的代码错了而是底层 API 变了自研胶水代码能让我快速绕开这些坑。2.3 核心组件对比向量数据库与嵌入模型怎么选向量数据库这个环节我需要认真比较因为它直接决定检索效果和系统性能。我对比了 Chroma、FAISS、Qdrant、Milvus 这几种。名称部署难度单机性能功能丰富度我的适用性判断Chroma很低嵌入式运行也可中小规模足够基础检索、元数据过滤、集合管理作为第一版首选FAISS中需自行封装持久化高检索能力强但缺少服务化能力适合离线批量处理Qdrant中需独立服务高过滤、payload、分布式支持适合生产自托管Milvus较高组件多很高大规模分布式能力强适合数据量大且团队有运维能力我的数据量级在几万条文本块以内单机完全能扛住所以第一版选择了 Chroma把它跑在 Docker 容器里持久化目录挂载到宿主机。如果以后单机不够用检索逻辑抽象成一个接口层替换成 Qdrant 并不难。嵌入模型的选择上我比较了几类方案OpenAI 的 text-embedding-3-small、国产的开源模型 BAAI/bge-base-zh-v1.5、以及多语言模型 multilingual-e5。考虑到我主要处理中文资料并且希望本地化部署不依赖外部 API我最终选了 bge-base-zh-v1.5。它在中文语义匹配上的效果不错模型文件也就 400MB 左右单机 CPU 也能跑实际推理速度在可接受范围内。2.4 生成模型API 调用与本地部署的取舍生成模型我用了外部大模型 API主要是 OpenAI 兼容接口。考虑到生产环境可能部署在内网我把 LLM 调用也抽象成了一层接口内部支持多种配置来源。这样做依赖了外部 API但好处是生成质量稳定、不需要额外的 GPU 资源。我也尝试过本地跑 ChatGLM 或 Qwen 的开源版本但一台普通开发机要同时跑嵌入模型和 7B 级别的生成模型非常吃力即使量化后响应延迟也容易超过十秒。实际权衡下来第一版生产部署用 API 调用是性价比最高的方案。3. 核心链路实现解析每个环节都有看不见的坑3.1 文档加载与解析格式适配是第一道门槛我先准备了一批真实数据包括我自己的技术笔记Markdown、几份产品说明书PDF、一些会议纪要TXT。在 LangChain 里对应的 Loader 分别是 TextLoader、MarkdownHeaderTextSplitter 和 PyPDFLoader但实际用起来远远没这么简单。PDF 解析是最麻烦的。很多 PDF 中文字体是嵌入式的直接从 PyPDFLoader 提取出来的文本是乱码或者缺少换行。最终我用了 PyMuPDF 作为 PDF 解析引擎对扫描版 PDF 则建议使用 OCR 工具但 OCR 会引入额外误差我实际项目里避免使用扫描版。我总结的文档清洗规则值得你直接抄去除多余空行、制表符和不可见字符。统一换行符为\n。如果段落间存在大量重复页眉页脚用正则过滤。Markdown 文件需要移除图片链接因为图片描述对检索没有帮助。文档若带有目录页跳过去避免把目录内容也当正文。清洗完成后文档会被转换成统一的字符串列表每项代表一个逻辑段落。3.2 文本切分不同切分策略如何影响检索效果文本切分是 RAG 中影响落地效果最明显但又最容易被新手忽视的环节。切得太碎语义不完整检索起来容易召回一堆只包含碎片信息的片段切得太大每块充满噪声超出模型上下文后还要截断回答质量也会下降。我测试了两种常用策略。固定长度切分按字符数固定切分比如 500 字一段相邻块有 50 字重叠。实现简单但容易把一个完整知识点从中间劈开。递归字符切分先按段落分隔再按句子分隔再到固定长度。这是 LangChain 推荐的做法效果明显更好。实际代码中我使用了一个递归切分器from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size400, chunk_overlap80, separators[\n\n, \n, 。, , , , , , ], ) chunks splitter.split_text(cleaned_text)选 400 这个值是因为对于中文一个字大约对应一个 token400 字对应上下文窗口的占用约 300 到 400 tokens加上问题本身和后续组装的其他内容不至于让模型一次性处理太多无关信息。80 字的重叠用来保证跨块语义的连续性避免一个知识点恰好被截断在边界上。另外我加了元数据记录逻辑每一个 chunk 都记录它来自哪个文件、属于哪个一级标题、在原始文档中的位置。这些元数据在后续做引用来源展示和过滤时非常有用。3.3 向量化与索引结构同义词都能查到的秘密接下来是把切分好的文本块变成向量。bge 模型的使用需要接上 sentence-transformers 库from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-base-zh-v1.5) embeddings model.encode(chunks, normalize_embeddingsTrue)normalize_embeddingsTrue必须打开。因为后续计算余弦相似度时对归一化后的向量直接点积就等价于余弦相似度效率更高这也是很多向量数据库默认采用的操作方式。向量送入 Chroma 时必须携带文档内容和元数据import chromadb client chromadb.PersistentClient(path./data/vectordb) collection client.get_or_create_collection( namecompany_kb, metadata{hnsw:space: cosine}, ) collection.add( ids[fchunk-{i} for i in range(len(chunks))], documentschunks, metadatasmetadata_list, embeddingsembeddings.tolist(), )为什么要显式在 metadata 里指定hnsw:space: cosine因为 Chroma 的默认距离可能是 L2而我在 embedding 阶段做了归一化配合 cosine 距离才能真实反映语义相似度。这个问题在官方文档中常被一笔带过但它直接决定了检索结果是否符合直觉。3.4 检索与重排序Top-K 召回不是结束第一次跑通全链路后我发现的明显问题是向量检索召回的 Top-5 结果虽然语义接近但真正能直接支撑回答的内容不一定排在最前面。原因是向量相似度只能衡量“整体语义接近”而回答一个问题往往需要精确匹配某个关键实体或数字。我的解决方案是分层检索 重排序第一步向量检索召回 Top-20 候选块。 第二步用重排序模型对候选块做精细打分。这里我用了bge-reranker-base它对“问题和段落的相关性”建模比单纯 embedding 相似度要准得多。 第三步从重排序结果取前 3 块作为最终上下文。重排序的具体实现类似from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-base) pairs [[question, chunk] for chunk in top20_chunks] scores reranker.predict(pairs)这个处理让我的回答准确率有了非常明显的提升尤其是针对“某个功能的参数是多少”这类精确查找型问题。重排序的代价是多了一次模型推理但数据量级小延迟增加通常不到 200 毫秒完全可接受。3.5 Prompt 设计与大模型生成把“开卷资料”喂对Prompt 对于 RAG 的生成质量影响也很大。我一开始只简单地把检索结果拼在问题后面结果模型经常会直接复述原文而不作总结归纳。后来我设计了更明确的结构化 Prompt你是一个企业内部知识库问答助手。 请基于“参考资料”回答问题。 要求 1. 如果参考资料中找不到答案请明确说“知识库中未找到相关信息”不要编造。 2. 优先使用参考资料中的原话或数据并保持口语化组织。 3. 回答末尾列出参考来源的文档名和原始位置。 参考资料 {document_text} 问题 {question}这套 Prompt 的设计逻辑是给模型明确边界让它知道“不知道”是合法答案避免幻觉。另一个考虑是模型如果不加约束会在回答中掺杂自己的常识这对内部知识库场景可能是灾难性的比如把旧版本的产品参数当成了正确答案。4. 从原型到可评测的工程化改造不能只“能跑”4.1 后端服务用 FastAPI 把 RAG 链路包成可调用的 API代码跑通 Jupyter 原型之后第一步工程化是用 FastAPI 封装成 REST API。我提供了三个接口POST /ingest上传文档并触发解析入库。POST /query提交问题返回回答、引用来源、检索耗时。GET /health健康检查供部署探活使用。/query的核心实现如下app.post(/query) async def query_knowledge_base(request: QueryRequest): start time.perf_counter() question request.question top_chunks retrieve_top_k(question, k20) best_chunks rerank(question, top_chunks, top_n3) prompt build_prompt(question, best_chunks) answer llm_client.generate(prompt) sources [{file: c.metadata[source], position: c.metadata[index]} for c in best_chunks] return { answer: answer, sources: sources, latency_ms: int((time.perf_counter() - start) * 1000), }这里有个实践心得一定要在接口层记录每个环节的耗时并在返回结构里带出来。否则后期做性能优化时你根本不知道瓶颈是在检索、重排序还是模型生成上。我给每个阶段都用time.perf_counter()做了埋点。4.2 前端界面不写花哨但要让可用性成立前端部分我用的是一个相对轻量的方案FastAPI 服务静态文件前端用原生 HTML JavaScript加上一个 Markdown 渲染库。没有引入 React 或 Vue因为对这种内部工具型应用原生页面足够快更重要的是不需要引入 Node 构建链路。页面逻辑并不复杂顶部上传按钮调用/ingest接口上传文档。中间聊天区把用户输入和系统回答按时间线渲染。回答内容下方展示引用来源方便点击跳转到原文。为了良好体验我做了流式输出。大模型完整回答往往需要 4 到 8 秒如果前端一直等用户早就失去耐心了。我用 SSEServer-Sent Events实现后端到前端的流式传输后端在生成过程中逐步把文本块推送过去前端收到就立即渲染这样首字延迟从六七秒降到了一点几秒。SSE 在 FastAPI 中实现from fastapi.responses import StreamingResponse app.post(/query_stream) async def query_stream(request: QueryRequest): async def event_generator(): async for token in llm_client.stream_generate(prompt): yield fdata: {token}\n\n return StreamingResponse(event_generator(), media_typetext/event-stream)前端用EventSource或fetch读取流式响应。这里踩了个小坑浏览器原生EventSource只支持 GET 请求但我需要发送问题文本所以实际用了fetch的ReadableStream来处理 POST 请求的流式输出。4.3 评估体系RAG 测评怎么做才算数这周我体会最深的一点是RAG 系统没有评估就没有优化方向。一开始我只能肉眼抽查几个回答感觉“看起来不错”但这根本不是工程化标准。为此我搭建了一套简单的离线评估流程。评估思路是准备一组测试集每条样本包含问题标准答案或至少包含的必须要点期望检索到的文档范围然后跑两个维度的指标检索质量指标RecallK也就是检索出的 K 个片段里有多少是真正相关的内容。生成质量指标忠实度回答是否严格基于参考资料、相关度是否回答了问题、完整性要点是否覆盖。针对生成质量的自动评估我使用大模型作为裁判LLM-as-a-Judge让它根据评分规则对回答逐项打分请根据以下评价维度对回答打分1-5分 - 回答是否忠实于参考资料有无明显编造 - 回答是否准确覆盖问题的关键要求 - 回答是否简洁清晰无冗余表达 参考资料 {reference_chunks} 问题 {question} 回答 {answer}评估集不大只有 50 条但这个动作让我把优化从“靠感觉”变成了“看分数”。比如调整切分大小后忠实度从 3.8 分升到 4.5 分我才放心继续下一步。4.4 调优记录三个参数救回了检索效果整个调优过程中有三处改动对最终效果贡献最大。第一点是切分策略。从固定 800 字符改成递归切分 400 字符加 80 重叠后相关内容的精确匹配率提升了大一截。因为原始切分将同一主题的论述拆进了不同块检索时明明内容都在库里就是匹配不上。第二点是重排序的引入。不加重排序时虽然向量召回的前几个结果通常相关但第 3 名往往已经是无关内容。加了重排序之后最终 Top-3 的质量稳定了很多。第三点是查询改写。有些用户提问很口语化比如“那个新版的产品咱们支持远程升级吗”直接拿这句话去检索效果并不好。我加了一个查询改写模块让 LLM 先把问题改写成适合检索的关键词组合比如“新版 产品 远程升级 支持”检索效果明显提升。改写可以用 GPT 或本地小模型我用的是外部 API每次改写多花 200 毫秒但收益很高。5. 生产级部署从“在我电脑上能跑”到“别人也能用”5.1 Docker 化一次构建到处运行把服务部署到生产环境的第一步是 Docker 化。我写了一个多阶段构建的 Dockerfile构建阶段负责安装 Python 依赖运行阶段只保留必要文件和模型缓存。FROM python:3.11-slim as builder WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --prefix/install -r requirements.txt FROM python:3.11-slim WORKDIR /app COPY --frombuilder /install /usr/local COPY . . EXPOSE 8000 CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]这里要注意的是国内环境下拉取 Python 基础镜像容易超时我用的是配置好的镜像源pip 安装也指定了国内源否则安装sentence-transformers那一堆依赖可能要等一个小时。模型文件的处理上我选择在构建镜像时直接从模型仓库下载但这会导致镜像体积膨胀。后来我改成用环境变量指定模型缓存目录并把模型目录挂载进容器这样模型文件固定存在宿主机上容器重建不用重新下载。这对开发迭代非常友好。5.2 配置管理与敏感信息保护生产环境不能把 API Key 写在代码里。我把所有配置都放到了环境变量并通过pydantic-settings进行统一读取。示例配置from pydantic_settings import BaseSettings class Settings(BaseSettings): embedding_model_name: str BAAI/bge-base-zh-v1.5 reranker_model_name: str BAAI/bge-reranker-base vector_db_path: str ./data/vectordb llm_api_key: str llm_base_url: str llm_model: str class Config: env_file .env settings Settings()这样一来开发环境使用.env文件生产环境直接注入环境变量即可代码里完全看不到敏感信息。5.3 日志、监控与可观测性被忽略却最救命的部分上线第一天最容易慌的时候就是你根本不知道系统为什么回答错了。如果日志里只有一句“Internal Server Error”排查起来会非常痛苦。我在项目里加了结构化日志采用 JSON 格式输出每条日志都包含请求 ID、阶段耗时、检索 Top-K 来源、模型返回状态等关键信息。logger.info( query completed, extra{ request_id: request_id, retrieval_ms: retrieval_ms, rerank_ms: rerank_ms, llm_ms: llm_ms, sources: [s[file] for s in sources], }, )生产环境我用 Prometheus 拉取指标Grafana 展示面板。重点指标包括QPS每秒查询数P50/P95 响应延迟向量检索耗时、重排序耗时、LLM 调用耗时内部错误率这些数据让我在上线后能快速判断是模型 API 抖动还是检索链路性能下降。有一天 P95 延迟突然从 3 秒涨到 8 秒我通过指标定位到是 LLM 服务端限流导致的重试等待而不是自己的检索逻辑问题。5.4 上线部署拓扑与资源规划由于我是单机部署实际拓扑比较简单Nginx 监听 80/443 端口做 TLS 终结和反向代理到 FastAPI。FastAPI 容器负责 API 服务。Chroma 容器提供向量数据库数据持久化到挂载卷。模型缓存目录挂载到宿主机一个固定目录容器内只读加载。资源规划方面embedding 模型使用 CPU 推理需要约 2GB 内存reranker 模型需要约 1GB 内存FastAPI 服务本身加上 Python 运行环境约 500MB。整个系统在 8GB 内存的云主机上跑得比较从容CPU 占用高峰在文档批量入库时会比较高但问答阶段的 CPU 负载压力不大。我还做了简单的并发控制因为 embedding 模型和 reranker 模型在 CPU 上是串行推理的如果多个请求同时进来可能会把 CPU 打满。我通过一个信号量限制了同时间最多两个请求执行重排序其他的排队等待。这个处理让接口在并发场景下表现更加稳定。6. 实测效果与典型的失败案例复盘6.1 效果实测常见的几类问题它怎么回答我把系统部署完后用 20 个来自真实工作场景的问题做了现场测试。按问题类型分类效果差异很大。第一类“标准参数查询”。比如“网关设备的默认端口是多少”。这类问题只要文档里有明确记载重排序模型基本都能精准召回对应段落回答准确率非常高。第二类“方案对比”。比如“方式 A 和方式 B 有什么区别”。这类问题需要综合多个段落的信息模型回答质量受限于切分是否完整。如果相关信息被切到不同块里召回只能拿到一部分回答就会显得不全面。第三类“无中生有问题”。比如问“知识库里完全没有提到的某个功能怎么配置”。在 Prompt 约束下模型多数时候会回答“知识库中未找到相关信息”这是令我满意的表现。第四类“口语化问题”。比如“那个能管设备的东西叫啥来着”。查询改写模块在这类问题上发挥了巨大作用改写后的关键词远比原问题更容易命中。6.2 失败案例 1文档切分导致关键信息被截断有一个案例很有代表性。我上传了一份产品功能列表里面有句话是“远程升级功能仅支持 PLC-300 及以上型号且固件版本不低于 V2.1。”这句话被切分器从“且”字前后切成了两块第一块剩下“远程升级功能仅支持 PLC-300 及以上型号”第二块是“固件版本不低于 V2.1”。检索“远程升级对固件版本的要求”时向量召回只拿到了第二块模型回答就变成了“固件版本不低于 V2.1”但丢掉了“PLC-300 及以上型号”这个设备范围限制。用户如果只看到前半句很容易得出错误结论。这个问题提醒我文本切分不是按固定字数机械处理就够了需要对文档结构有感知。我把这条数据加入评估集后调大了递归切分里的段落分隔优先级并在切分时尽量以句子和分句为单位此类截断问题明显减少。6.3 失败案例 2检索到了正确答案但模型视而不见另一种失败更隐蔽检索出的 Top-3 明明是正确内容但模型回答依旧编造了一个不存在的答案。排查后发现问题出在我的 Prompt 没有足够强调“只能依据参考资料回答”。有时模型会根据自己训练时的知识来补充“常识”而这份“常识”和知识库的正确答案冲突。我的解决办法是在 Prompt 中增加对抗性的表达“即使你认为某个答案符合常识只要参考资料中没有出现就不能写入回答。回答必须逐句有依据。”这个约束加上之后此类幻觉问题显著减少。6.4 失败案例 3多轮对话中上下文干扰原本设计时我说不做多轮对话但实际测试中发现用户天然会问“那它的功耗呢”这类指代性问题。最终我实现了一个轻量级的上下文记忆机制把当前问题和最近两轮的问答摘要一起传给 LLM 做查询改写从而把指代转换成明确的查询词。测试“那它的功耗呢”之前如果用户刚问过“智能网关的特性”改写模块会把它改写成“智能网关 功耗 参数”检索效果就正常了。这个方案比维护完整的多轮对话状态要务实得多。7. 本次项目的经验教训与后续演进方向7.1 五个让我少加班的好习惯这次项目周期不长但有几个习惯确实帮了大忙。第一每个环节单独写测试小脚本。加载完文档先打印看看内容切分完先抽查几个块向量化完先跑一个相似度检索测试。不要一次性把所有代码写完再调试否则错误定位成本太高。第二把所有参数配置化。切分大小、重叠大小、Top-K 数量、重排序 Top-N、模型名称、超时时间全部放在配置文件中。调参的时候你就知道这有多幸福。第三评估集和调参分离。调整参数前先跑一次评估得出基线分数调整后再跑一次对比用数据说话。第四对外部 LLM 调用设置超时和重试。外部 API 一定会有抖动不设置超时通常会卡住整个请求设置超时并做一次重试体验会好很多。第五上线前做一次空知识库测试。没有文档时系统应该给出“知识库为空请先上传文档”的明确提示而不是报 500 错误。7.2 未来可扩展的方向这个项目还有不少可以继续加深的方向。比如引入增量更新机制文档定期自动重爬重向量化比如把向量检索和关键词检索做混合召回用 BM25 弥补向量检索对精确匹配的不足再比如使用 Graph RAG在实体关系层做检索应对需要多跳推理的问题。现阶段我对 Graph RAG 还属于学习和调研阶段它更适合知识图谱特征明显的场景对大部分文档型知识库向量检索加重排序仍然是性价比更高的方案。另外版权和权限控制是生产环境绕不开的话题。我现在只在本地做了文件来源标记如果要真正面向多部门使用必须引入用户权限、数据隔离和审计日志。7.3 给同样在走全栈 AI 学习路线的人几句经验这一周最大的感受是RAG 不是模型调参问题而是工程问题。从数据清洗、切分、索引、检索、重排序到生成任何一环出了偏差整个系统的回答质量都会崩。只去研究大模型本身而不关注数据管道和检索质量是学不好 RAG 的。我也越来越认同一个判断AI 全栈开发者的核心竞争力在于能快速把模型能力和工程基础设施整合成一个稳定的产品。这周我可以把 Jupyter 里的原型代码变成 Docker 镜像跑在云服务器上靠的就是前几周打下的后端、网络、容器、监控基础。如果你也在规划自己的全栈学习路线建议每一周都选一个能“上线”的项目作为结课作业而不是只做代码练习。最后分享一个小 tips训练自己的评估意识。不管项目多小在动手写代码前先想清楚“什么叫做好了”用指标去定义“好”。你后面会发现这句话对整个技术成长路线的价值远大于任何具体的框架和工具。