资讯动态

从零搭建企业级RAG检索主链路:LangGraph+Milvus+Ollama实现首次问答闭环

发布时间:2026/10/5 8:49:27 来源:尧图企业网站定制
1. 检索主链路到底在搭什么先把“第一次问答闭环”这件事说透很多人做RAG项目卡住的地方从来不是“模型不会回答”而是“链路根本没跑通”。尤其是从零到一搭企业级智能问答系统到了检索主链路这一章意味着你已经把文档入库、向量化、索引构建这些前置工作做完了现在要面对的是最核心的一件事用户问一句话系统怎么把这句话变成一次完整的、可观测的、可复现的问答闭环。我先把这件事的定义说清楚。所谓“第一次问答闭环”指的是从用户输入问题开始经过查询理解、向量检索、上下文组装、大模型生成、流式输出直到前端完整收到答案并正确渲染的整条链路。这条链路里任何一个环节断了用户看到的就是转圈、超时、空回答或者半截话。热搜词里出现的“stream disconnected before completion: idle timeout waiting for sse”就是典型的链路断点问题后面我会专门拆。为什么这一章特别关键因为在此之前你做的都是离线批处理数据进得去就行。但从这一刻开始系统要面对真实用户的实时请求延迟、并发、超时、流式中断、检索召回质量所有问题会同时爆发。我见过太多项目索引建得漂漂亮亮一到问答环节就原形毕露。所以这一章的目标不是“能回答”而是“稳定地、可观测地、可调试地回答”。技术选型上这套链路我采用的是LangGraph 编排 Milvus 向量检索 Ollama 本地推理 SSE 流式输出的组合。LangGraph 负责把检索、生成、工具调用这些节点串成有状态图Milvus 负责向量召回Ollama 提供本地大模型推理能力SSE 负责把生成结果实时推给前端。这个组合的好处是每一层都可替换、可观测、可单独调试不会出现“黑盒一锅端”的情况。适合谁来参考如果你已经跑通了文档切分和向量入库正准备把问答链路接起来这篇内容就是给你写的。如果你还在纠结 Milvus 装 standalone 还是集群、Ollama 模型放哪个盘也能在这里找到答案。下面我按实际搭建顺序把每个环节的决策逻辑、参数计算、踩坑经验全部摊开讲。2. 链路整体设计与选型逻辑为什么是 LangGraph 而不是一条直线2.1 从“线性链”到“状态图”的思维转变最早我做 RAG 用的是最朴素的线性链问题进来直接拿去检索检索结果拼进 prompt丢给模型生成。这条链在 demo 阶段没问题但一上企业场景就崩。原因很简单真实问题不是每一句都需要检索。用户问“你好”“谢谢”“你叫什么”你还要去 Milvus 里捞一遍向量纯属浪费算力还拖慢响应。LangGraph 的价值就在这里。它把问答过程建模成一张有状态图每个节点是一个处理步骤边决定下一步走向。你可以根据查询意图做条件路由闲聊类问题直接走生成节点知识类问题先走检索节点。这个“条件边”的能力是线性链给不了的。我实际用的图结构大致是这样入口节点做查询预处理然后一个路由节点判断是否需要检索。需要检索的走“向量检索 → 上下文组装 → 生成”不需要的直接“生成”。生成节点内部再挂工具调用能力为后续扩展留口子。这个结构的好处是你后面加“多轮改写”“重排序”“答案校验”这些节点时不用推翻重来往图里插节点就行。提示LangGraph 的节点函数尽量保持“纯函数”风格输入状态、输出状态增量不要在节点里做全局副作用操作否则调试时会非常痛苦。2.2 Milvus 选 standalone 还是集群先看数据量再决定热搜里“milvus standalone模式”和“在 mac 上使用 docker 安装 milvus”出现频率很高说明很多人卡在部署这一步。我的建议很直接企业级项目在验证阶段一律先用 standalone。原因有三点。第一standalone 模式把 etcd、minio、milvus 三个组件打包在一个进程里部署成本极低一条 docker compose 就能起来。第二standalone 在千万级向量以内性能完全够用绝大多数企业知识库根本到不了这个量级。第三等你真的需要集群时代码层的 Milvus 客户端调用方式几乎不用改只是连接地址从本地变成集群地址。这里有个细节要注意。热搜里提到“服务器linux上使用 milvus_uri: str ./data/milvus.db 本地加载 milvus”这是 Milvus Lite 的用法适合本地快速验证但它和 standalone 是两套东西。Milvus Lite 把数据存成单个本地文件不支持并发写入也不支持完整的索引类型。如果你只是想在 mac 上跑通链路Lite 够用但只要涉及多人访问或者数据量上去必须切到 standalone。部署方式适用场景数据上限并发能力迁移成本Milvus Lite本地验证、单机 demo百万级单写入低Standalone企业验证、中小生产千万级中等并发低Cluster大规模生产亿级以上高并发中2.3 Ollama 的角色定位本地推理的性价比之选选 Ollama 做推理层核心考量是数据不出内网和成本可控。企业知识库往往涉及内部文档走外部 API 有合规风险Ollama 本地部署能规避这个问题。热搜里“ollama离线安装包”“ollama下载慢”“ollama安装到其他盘”这些词说明大家最关心的就是安装和模型存储。我的经验是Ollama 的模型默认存在用户目录下动辄几十 GB系统盘很容易爆。安装前先把模型存储路径改到大容量数据盘通过环境变量OLLAMA_MODELS指定。离线安装的话提前在有网机器上把模型 pull 下来整个models目录打包拷过去即可比在线拉取稳得多。至于模型选择问答场景我一般用 7B 到 14B 参数区间的指令微调模型。太小了回答质量差太大了推理慢且显存吃紧。具体选哪个要结合你的硬件和延迟要求实测没有标准答案。3. 核心细节拆解检索、组装、生成三段各自的坑3.1 查询向量化别小看这一步的一致性检索链路的第一环是把用户问题转成向量。这里最容易犯的错误是入库和查询用了不同的 embedding 模型或不同的归一化方式。我踩过这个坑入库时用的是某个模型的默认输出查询时手贱加了归一化结果余弦相似度全乱召回的全是无关内容。Milvus 里做余弦相似度检索索引类型和度量方式要匹配。用IP内积度量时向量必须归一化用COSINE度量时Milvus 内部会处理。热搜里“milvus余弦值”这个词说明有人已经在关注这个点。我的建议是统一用COSINE省心不容易出错。# 查询向量化示例注意与入库保持完全一致 from ollama import Client client Client(hosthttp://localhost:11434) def embed_query(text: str) - list[float]: resp client.embeddings(modelnomic-embed-text, prompttext) return resp[embedding]注意embedding 模型一旦确定入库和查询必须锁死同一个版本。换模型等于重建整个索引没有捷径。3.2 上下文组装token 预算怎么算检索回来一堆文档片段不能全塞进 prompt。大模型有上下文窗口限制塞太多不仅慢还会稀释关键信息。我一般按这个公式估算预算可用上下文 token 模型窗口 - 系统提示 token - 历史对话 token - 输出预留 token假设模型窗口 8192系统提示占 300历史对话占 1000输出预留 1500那留给检索上下文的就只有约 5400 token。按每段 300 token 算最多塞 18 段。实际我会控制在 10 段以内给重排序和格式留余量。组装时我习惯给每段加上来源标记比如[文档1]这样生成时模型能引用来源前端也能做溯源展示。这个细节在企业场景很重要用户需要知道答案是从哪份文档来的。3.3 生成节点流式输出与工具调用的衔接生成节点是整条链路最复杂的地方因为它同时要处理流式输出和工具调用。LangGraph 里工具调用是通过条件边实现的模型输出里如果包含工具调用请求就路由到工具节点执行执行完再回到生成节点。这里有个坑流式输出和工具调用天然冲突。流式是一边生成一边推工具调用需要等模型完整输出才能解析。我的处理方式是首轮生成不开流式先判断是否需要工具调用确认是纯文本回答后再走流式生成。这样虽然多一次模型调用但逻辑清晰不会出现半截工具调用 JSON 推给前端的尴尬。热搜里“langgraph 工具调用”和“封装sse 流式接口调用逻辑”这两个词正好对应这个环节。工具调用的封装要独立成模块SSE 的封装也要独立两者通过生成节点的状态传递衔接不要耦合在一起。4. 实操过程从 Milvus 启动到前端收到第一个字4.1 Milvus standalone 启动与集合创建先起 Milvus。用 docker compose 是最省事的官方仓库里有现成的 standalone 配置。启动后默认端口 19530 是 gRPC9091 是健康检查。# 下载官方 compose 文件后启动 docker compose -f docker-compose-standalone.yml up -d # 验证是否起来 curl http://localhost:9091/healthz集合创建时字段设计要提前想好。我一般包含这几个字段主键 id、向量字段 embedding、原文 chunk_text、来源 source、以及可选的元数据字段。索引类型用HNSW度量方式COSINE这两个参数在千万级以内表现都很稳。from pymilvus import CollectionSchema, FieldSchema, DataType, Collection fields [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idTrue), FieldSchema(nameembedding, dtypeDataType.FLOAT_VECTOR, dim768), FieldSchema(namechunk_text, dtypeDataType.VARCHAR, max_length4096), FieldSchema(namesource, dtypeDataType.VARCHAR, max_length512), ] schema CollectionSchema(fieldsfields) collection Collection(namekb_chunks, schemaschema) index_params { index_type: HNSW, metric_type: COSINE, params: {M: 16, efConstruction: 200}, } collection.create_index(field_nameembedding, index_paramsindex_params) collection.load()M和efConstruction这两个参数控制索引质量和构建速度。M 越大召回越好但内存占用越高16 是常用平衡点。efConstruction 影响构建精度200 是稳妥值。4.2 LangGraph 图构建节点与条件边图构建的核心是把每个处理步骤写成节点函数然后用边连起来。我先把状态结构定义清楚状态里包含问题、检索结果、上下文、答案、是否需要检索等字段。from typing import TypedDict, List class QAState(TypedDict): question: str need_retrieval: bool retrieved_docs: List[dict] context: str answer: str路由节点判断是否需要检索我用的规则是关键词加轻量分类。简单说问题里包含“是什么”“怎么做”“为什么”这类词或者长度超过一定阈值就走检索。这个规则不完美但胜在快且可解释后续可以换成小模型分类。条件边根据need_retrieval决定走向这是 LangGraph 最直观的能力。整个图编译后就是一个可调用的对象输入初始状态输出最终状态。4.3 SSE 流式接口封装让前端逐字收到答案SSE 这块是热搜重灾区“stream disconnected before completion: idle timeout waiting for sse”这个报错我太熟了。根因通常是服务端在生成间隙没有发送任何数据连接被中间层判定为空闲超时。解决办法有两个。一是设置合理的超时时间Nginx 里把proxy_read_timeout调大比如 300 秒。二是服务端在生成间隙发送心跳注释行SSE 协议里以冒号开头的行会被客户端忽略但能保活连接。# FastAPI 里封装 SSE 生成器 from fastapi.responses import StreamingResponse async def sse_generator(question: str): yield : heartbeat\n\n # 立即发一个心跳避免首字节超时 async for token in stream_answer(question): yield fdata: {token}\n\n yield data: [DONE]\n\n app.get(/qa/stream) async def qa_stream(q: str): return StreamingResponse( sse_generator(q), media_typetext/event-stream, headers{Cache-Control: no-cache, X-Accel-Buffering: no}, )X-Accel-Buffering: no这个头很关键它告诉 Nginx 不要缓冲响应否则前端会一次性收到全部内容流式就失去意义了。热搜里“nginx 代理 ollama 设置”也涉及类似问题代理层不关缓冲流式体验直接废掉。4.4 端到端联调第一次完整问答联调时我建议按这个顺序验证先单独测 Milvus 检索确认能召回相关片段再单独测 Ollama 生成确认模型能正常输出最后把两者接进 LangGraph跑完整链路。这样出问题时能快速定位是哪一层。第一次跑通时我习惯在关键节点打日志记录检索耗时、生成首 token 耗时、总耗时。这三个指标是后续优化的基准。首 token 耗时尤其重要它直接决定用户感知的响应速度。5. 常见问题与排查技巧实录5.1 检索召回不准先查向量一致性再查索引召回不准是最常见的问题。排查顺序我固定为三步。第一步确认入库和查询用的是同一个 embedding 模型和同一套预处理逻辑。第二步手动拿一个已知答案的问题去检索看返回的 top 结果里有没有正确片段。第三步如果前两步都正常但召回还是差考虑加重排序环节用交叉编码器对召回结果重新打分。热搜里“rag瓶颈”和“rag检索增强”说的就是这类问题。检索质量是 RAG 的天花板生成模型再好也救不回错误的召回。我的经验是与其花时间调生成 prompt不如先把检索召回率提上去。5.2 流式中断超时、缓冲、心跳三件套流式中断的排查我整理成一张表按现象对原因。现象可能原因排查方向首字节迟迟不来生成节点阻塞检查检索和模型调用耗时中途断开代理层空闲超时调大 proxy_read_timeout一次性收到全部代理层缓冲关闭 buffering偶发断开无心跳保活生成间隙发心跳注释提示SSE 连接断开后前端要能自动重连重连时带上最后收到的事件 ID服务端从断点继续。这个机制在企业场景是刚需用户不会容忍答案看一半没了。5.3 Ollama 推理慢模型大小与硬件匹配Ollama 推理慢通常不是软件问题是模型和硬件不匹配。7B 模型在纯 CPU 上跑每秒可能只有几个 token体验很差。有 GPU 的话优先用 GPU显存不够就用量化版本。热搜里“ollama部署私有大模型”和“ollama离线安装包”说明很多人在这块折腾我的建议是先确认硬件再选模型别反过来。另外Ollama 默认会加载模型到显存并常驻多个模型切换时会反复加载卸载很慢。生产环境建议固定一个模型或者用keep_alive参数控制驻留时间。5.4 工具调用不触发检查模型能力和 promptLangGraph 工具调用不触发八成是模型本身不支持 function calling或者 prompt 里没把工具描述清楚。不是所有 Ollama 模型都支持工具调用选模型时要确认这一点。prompt 里工具的名称、参数、用途要写得明确模型才知道什么时候该调。我一般会先用一个明确的测试问题验证工具调用链路比如“帮我查一下今天的天气”确认能触发后再接真实业务工具。这样能把模型能力和业务逻辑分开调试。6. 链路可观测性与后续扩展方向6.1 埋点让每一次问答都可追溯企业级系统和 demo 最大的区别就是可观测性。我在链路的每个节点都埋了耗时和状态埋点记录问题、检索到的文档 ID、生成的答案、各阶段耗时。这些数据存下来既能做问题排查也能做效果分析。具体做法是在 LangGraph 的状态里加一个trace字段每个节点往里追加自己的执行记录。链路结束后把 trace 落库。这样任何一个线上问题都能还原出当时的完整执行路径。6.2 扩展多轮对话与知识库更新第一次闭环跑通后扩展方向主要有两个。一是多轮对话把历史对话纳入状态让模型能理解指代和上下文。二是知识库增量更新新文档进来后增量入库不用重建整个索引。多轮对话的难点是历史对话的 token 管理不能无限累积。我的做法是保留最近若干轮更早的做摘要压缩。知识库更新则要注意 Milvus 的删除和插入操作删除是标记删除需要定期 compact 回收空间。6.3 性能优化缓存与并发性能优化上查询向量化结果可以缓存相同问题不用重复算。检索结果也可以做短时缓存热点问题直接命中。并发方面Milvus 和 Ollama 都支持一定程度的并发但要注意资源竞争尤其是 GPU 显存。我实测下来单机 standalone 加一个 7B 模型支撑几十路并发问题不大。再往上就要考虑模型服务拆分和 Milvus 集群了。这个量级判断要基于实际压测别拍脑袋。这套链路我从零搭过好几遍每次都会在细节上踩新坑。第一次闭环跑通的那一刻看着前端一个字一个字蹦出答案那种感觉确实不一样。后面要做的就是把这条链路打磨稳让它经得起真实用户的折腾。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑