ARTICLE DETAIL

资讯详情

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

RAG数据接入实战:从21%到完整落地的关键工程

RAG数据接入实战:从21%到完整落地的关键工程 在很多 LLM 应用项目里都会出现这样一个阶段RAG 的 Demo 跑通了向量库建好了模型也接上了演示效果不错但一上真实业务数据回答质量就明显下降。问题往往不是模型不够强而是“连接数据”这件事被严重低估了。有一篇讨论 LLM 数据接入的技术文章标题写得非常直接《Connecting an LLM to Your Data Is the 21% Solution》。它想表达的核心观点是把 LLM 连接到你的数据只完成了整个解决方案的 21%。剩下的 79%才是决定项目能否真正落地的关键。本文会围绕这个观点展开。先解释 21% 到底指什么再给出完整的 RAG 数据接入实战包含可运行的 Python 代码最后总结数据接入工程化的最佳实践、编排框架选型思路和常见问题排查方法。适合正在做 LLM 应用、RAG 检索、企业知识库开发的开发者阅读。1. 为什么说“连接数据”只是 21% 的解1.1 21% 是一个精力分配的隐喻先说明一点21% 不是一个精确统计出来的数值而是一个帮助团队重新分配精力的隐喻。从实现路径上看把 LLM 连接到业务数据通常会经历这么几步选一个 Embedding 模型把文档切块写入向量数据库然后写一个检索函数把检索结果拼到 Prompt 里交给 LLM 生成答案。每一步都有成熟的开源组件一个熟悉 Python 的开发者两三天就能跑通。但真实项目里这套链路只是冰山一角。文档格式五花八门、同一条知识在多个文档里重复、切块大小影响召回率、检索到的内容与问题语义不匹配、模型输出无法溯源、权限控制缺失、线上效果无法评估。这些问题不会在 Demo 阶段暴露却会在生产环境里一个接一个出现。所以21% 的隐喻可以这样理解模型调用与向量检索这类“连接性”代码只占整个 LLM 数据应用工作量的一小部分数据治理、检索调优、评测闭环、安全管控这些“非连接性”工作才是真正决定效果上限的大头。1.2 完整的 LLM 数据应用链路在进入代码之前先建立一张完整的链路图。无论是 RAG、Agent 还是知识库问答底层都离不开这条数据流水线。原始文档 │ ① 加载 ▼ 清洗与转换 │ ② 切分 ▼ 文本分块 │ ③ 向量化 ▼ 向量数据库 ────── ④ 相似度检索 ────── 用户问题 │ ▼ 上下文组装 │ ⑤ 生成 ▼ LLM 回答每一步都会影响最终质量加载阶段要处理 PDF、Word、Markdown、HTML、数据库导出文件等不同格式。清洗阶段要去除页眉页脚、统一编码、过滤广告和无效内容。切分阶段要决定块大小、重叠长度、按什么分隔符切。向量化阶段要选择适合业务语言和领域的 Embedding 模型。检索阶段要设置返回条数、相似度阈值必要时加 Reranker 重排。生成阶段要设计 Prompt约束模型只基于检索内容回答。很多团队把精力集中在向量化和检索上却忽略了加载、清洗、切分这些看似简单、实际坑最多的环节。这也是为什么数据连接看似容易却很难做好的原因。1.3 剩下 79% 的工作长什么样如果把“连接数据”之外的工程内容拆开大致是以下几块工作方向具体内容数据接入与治理多格式解析、编码统一、去重、敏感信息识别、元数据提取切分与索引优化块大小调优、语义切分、父子分块、多级索引检索质量调优Embedding 选型、混合检索、Reranker、阈值校准评测体系构建 Golden Set、离线指标、在线反馈闭环权限与安全数据隔离、最小权限访问、脱敏、合规边界可观测性日志、Trace、调用链、成本监控、失败重试运维与迭代增量更新、版本回滚、模型升级、效果回归这些内容每一项都不“性感”却是生产系统绕不开的硬骨头。后面第 6 章会专门讨论工程化建议。2. 环境准备与版本说明2.1 技术栈选择本文的实战案例使用 Python 生态技术栈如下Python 3.10 及以上版本。LangChain 生态负责文档加载、切分、向量库封装和 LLM 调用。Chroma本地向量数据库轻量、无需单独部署服务。OpenAI Embedding 与 ChatOpenAI示例中用于向量化和生成答案。python-dotenv管理 API Key 等环境变量。需要说明的是版本迭代非常快。langchain-openai、chromadb这类库几乎每个月都在发新版本接口也可能调整。本文示例以常见稳定版本为主重点演示配置思路和代码结构实际使用时请根据你本地的依赖情况微调。如果你所在的企业不允许调用外部模型 API可以把 OpenAI 相关组件替换为本地部署的 Embedding 模型和 LLM整体架构不变只是接口地址和模型名不同。2.2 依赖清单创建项目后先准备requirements.txtlangchain-core0.2,0.4 langchain-community0.2,0.4 langchain-openai0.1,0.3 chromadb0.5,1.0 python-dotenv1.0 pymupdf1.24其中pymupdf是用来解析 PDF 的如果只处理 TXT 和 Markdown可以暂时不装。安装命令pip install -r requirements.txt2.3 示例项目结构实战案例是一个本地文档问答系统目录结构如下llm-data-rag/ ├── requirements.txt ├── .env ├── data/ │ └── tech_manual.txt ├── ingest.py ├── query.py └── evaluate.pydata/tech_manual.txt模拟业务方提供的操作手册文档。ingest.py负责加载、清洗、切分、写入向量库。query.py负责检索和问答。evaluate.py负责简单的检索效果评估。.env存放 API Key 和模型配置。.env文件内容如下OPENAI_API_KEYsk-xxxxxxxx EMBEDDING_MODELtext-embedding-3-small LLM_MODELgpt-4o-mini注意.env文件不要提交到 Git 仓库API Key 一旦泄露会产生费用和安全风险。生产环境推荐使用密钥管理服务。3. 核心原理拆解LLM 连接数据的完整链路3.1 RAG 为什么是主流方案RAG 的全称是 Retrieval-Augmented Generation检索增强生成。它把“检索外部数据”和“生成回答”两件事组合在一起。相对于直接让 LLM 回答RAG 有几个明显优势知识更新成本低。更新索引比重新微调模型代价小得多。减少幻觉。模型被要求基于检索到的片段回答而不是凭空发挥。支持权限控制。可以在检索阶段按用户权限过滤数据。可溯源。能够把回答映射回原始文档片段。理解 RAG 的关键在于不要把向量库当成一个简单的“数据容器”它是一个需要精细调优的检索系统。检索质量直接决定生成质量也就是业界常说的“垃圾进垃圾出”。3.2 数据接入的五个关键层从工程视角看LLM 数据接入可以拆成五层第一层文档加载。不同的文件格式对应不同的 Loader。TXT 和 Markdown 最简单PDF 需要处理排版和扫描件Word 需要处理表格和样式HTML 需要剔除标签。选错加载器后面的模型再好也白搭。第二层清洗与转换。主要工作是统一编码、去掉无意义字符、合并断行、识别重复内容。如果文档里包含手机号、身份证号等敏感信息还要在这里做脱敏。第三层切分与索引。切分是数据接入里最容易被低估的环节。块太大检索精度下降块太小上下文信息不完整。常见做法是用递归字符切分器按段落、句子、标点逐级切分并设置一定的重叠区间。第四层向量化与存储。先选一个稳定的 Embedding 模型把文本块转成向量连同原文、元数据一起写入向量库。元数据至少应该包含来源文件、页码、标题等方便后续过滤和溯源。第五层检索与生成。用户问题先向量化再到向量库做相似度检索取 Top-K 片段组装成 Prompt最后交给 LLM 生成答案。这一层还包括 Reranker 重排、阈值过滤、引用标记等优化手段。3.3 检索质量为什么是瓶颈很多 RAG 项目效果不好问题都出在检索这一环。首先是 Embedding 模型的语言和领域匹配问题。中文业务文档如果使用纯英文优化的 Embedding 模型语义匹配效果会明显下降。需要选择支持中文、对领域术语有较好理解能力的模型。其次是切分对召回的影响。一段业务规则如果被拦腰切开检索到的片段语义不完整LLM 就无法正确回答。这时候需要考虑语义切分或父子分块。再次是相似度阈值问题。Chroma 返回的 score 可能是距离也可能是相似度取决于distance_function配置。距离越小越相似相似度则相反。很多开发者不区分这两者直接用固定阈值结果线上误召回一堆无关内容。最后是缺少重排环节。向量检索擅长召回但排序能力一般。Top-K 结果里混入不相关内容时Reranker 可以基于交叉编码器做二次排序显著提升最终回答质量。3.4 编排框架与 MCP 的定位数据接入只是 LLM 应用的一部分。在复杂场景里还需要编排框架把“数据检索、工具调用、多步推理”串起来。编排框架解决的是以下几个问题多步流程管理先检索再判断是否需要调用工具最后生成回答。记忆管理多轮对话中如何保留上下文避免 Token 超限。工具注册与调用LLM 决定调用哪个函数、传什么参数框架负责执行。可观测性记录每步的输入输出方便排查。目前常见的方案有 LangChain、LlamaIndex、Spring AI 等。其中 Spring AI 在 Java 生态里比较流行组合方式常见的是 Spring AI MCP RAG Agent。MCPModel Context Protocol是另一种值得关注的趋势。它把数据源、数据库、API 等外部能力封装成标准协议让 LLM 客户端通过统一的接口访问数据。它的价值在于标准化以前每个数据源都要写一套自定义工具调用有了 MCP 之后模型和工具之间的连接方式可以复用。如果你的应用只需要固定文档问答纯 RAG 就够了。但如果需要查库、调接口、操作文件就要引入工具调用和 Agent 编排。4. 完整实战案例用 RAG 把本地文档接入 LLM下面用一个完整案例演示“把本地文档接入 LLM”的全过程。示例数据是一份模拟的订单系统操作手册。4.1 准备示例数据在data/tech_manual.txt中放入以下内容订单系统操作手册 1. 订单状态说明 订单状态包括待支付、已支付、已发货、已完成、已取消。 待支付订单超过 30 分钟未支付系统自动取消。 2. 查询订单 进入订单管理模块点击“订单查询”。 支持按订单号、用户手机号、下单时间范围筛选。 查询结果默认按下单时间倒序排列。 3. 物流信息 订单发货后在订单详情页点击“查看物流”即可查看物流轨迹。 物流信息由第三方物流接口同步最多延迟 30 分钟。 4. 退款处理 已支付订单可申请退款退款原路返回。 已发货订单需要先确认收货再申请售后退款。这份文档包含了状态、查询、物流、退款四类业务知识足够演示检索效果。4.2 数据清洗与文档加载首先编写ingest.py从加载文档开始。代码如下# ingest.py import os from dotenv import load_dotenv from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma load_dotenv() DATA_PATH data/tech_manual.txt PERSIST_DIR ./chroma_db EMBEDDING_MODEL os.getenv(EMBEDDING_MODEL, text-embedding-3-small) def clean_text(text: str) - str: 简单文本清洗统一全角空格去掉空白行。 text text.replace(\u3000, ) lines [line.strip() for line in text.splitlines()] lines [line for line in lines if line] return \n.join(lines) def load_documents(): loader TextLoader(DATA_PATH, encodingutf-8) documents loader.load() for doc in documents: doc.page_content clean_text(doc.page_content) return documents这里的关键点是在把文本交给切分器之前先做一次清洗。实际业务文档里全角空格、空行、页眉页脚、特殊符号都很常见不处理会污染向量索引。4.3 分块策略切分参数直接影响检索召回效果。本案例使用递归字符切分器def split_documents(documents): splitter RecursiveCharacterTextSplitter( chunk_size300, chunk_overlap50, separators[\n\n, \n, 。, , , , ], keep_separatorTrue, ) return splitter.split_documents(documents)参数含义如下chunk_size300每个块最多约 300 个字符。业务手册类文档建议 200 到 500 之间。chunk_overlap50相邻块之间保留 50 个字符重叠避免关键信息恰好在切分边界被切断。separators切分优先级先按段落再按换行再按句号。中文场景一定要把“。”、“”、“”放进去否则会按空格或单个字符切语义被肢解。4.4 构建向量库向量库写入逻辑如下def build_vectorstore(chunks): embeddings OpenAIEmbeddings(modelEMBEDDING_MODEL) vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directoryPERSIST_DIR, ) return vectorstore if __name__ __main__: docs load_documents() chunks split_documents(docs) build_vectorstore(chunks) print(f文档加载完成共切分为 {len(chunks)} 个分块已写入 {PERSIST_DIR})执行命令python ingest.py预期输出为文档加载完成共切分为 12 个分块已写入 ./chroma_db注意persist_directory指定了向量库落盘目录。第一次运行会创建目录并写入索引后续再运行会往同一个库追加数据。如果文档更新了建议先删除旧目录再重建避免旧分块残留。4.5 检索与问答接下来编写query.py实现“检索 生成”# query.py import os from dotenv import load_dotenv from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_community.vectorstores import Chroma load_dotenv() PERSIST_DIR ./chroma_db EMBEDDING_MODEL os.getenv(EMBEDDING_MODEL, text-embedding-3-small) LLM_MODEL os.getenv(LLM_MODEL, gpt-4o-mini) def retrieve_context(question: str, k: int 4): embeddings OpenAIEmbeddings(modelEMBEDDING_MODEL) vectorstore Chroma( persist_directoryPERSIST_DIR, embedding_functionembeddings, ) return vectorstore.similarity_search_with_score(question, kk) def build_prompt(question: str, docs) - str: context \n\n.join( f[片段{i 1}]\n{doc.page_content} for i, (doc, _) in enumerate(docs) ) return f请根据下面的资料回答用户问题。 如果资料中没有答案请明确回答“根据现有资料无法回答”不要编造。 资料 {context} 问题{question} 回答 def main(): question input(请输入问题).strip() docs retrieve_context(question) print(\n--- 检索结果 ---) for i, (doc, score) in enumerate(docs): print(f[{i 1}] score{score:.4f} | {doc.page_content[:60]}...) prompt build_prompt(question, docs) llm ChatOpenAI(modelLLM_MODEL, temperature0.2) answer llm.invoke(prompt).content print(\n--- 回答 ---) print(answer) if __name__ __main__: main()这段代码做了三件事把用户问题向量化并检索 Top-K 片段把检索结果组装成带编号的上下文 Prompt调用 LLM 生成回答并限制模型在没有资料依据时不能乱编。4.6 运行与验证执行python query.py输入问题请输入问题订单发货后如何查看物流预期输出实际值取决于文档切分和模型返回--- 检索结果 --- [1] score0.3892 | 3. 物流信息 订单发货后在订单详情页点击“查看物流”即可查看物流轨迹... [2] score0.7123 | 2. 查询订单 进入订单管理模块点击“订单查询”... ... --- 回答 --- 根据操作手册订单发货后可以在订单详情页点击“查看物流”查看物流轨迹。这里需要特别注意 score 的含义。Chroma 默认使用 L2 距离score 越小表示越相似。如果你在创建集合时指定了distance_functioncosine那么 score 表示余弦距离同样是越小越相似。千万不要把 score 和“相似度百分比”混为一谈。4.7 检索效果评估只跑通一个问答不代表系统可用。建议写一个简单的评估脚本用一组“问题 期望命中文档”的测试集来衡量检索效果。# evaluate.py 核心片段 def precision_at_k(questions, golden_docs, retriever, k4): 计算 Top-K 命中率。 hit 0 for question, golden in zip(questions, golden_docs): results retriever.invoke(question)[:k] retrieved_sources {doc.metadata.get(source, ) for doc in results} if golden in retrieved_sources: hit 1 return hit / len(questions) if __name__ __main__: test_questions [ 订单超过多久未支付会自动取消, 如何查询订单物流, 已发货订单如何退款, ] golden_docs [ 1. 订单状态说明, 3. 物流信息, 4. 退款处理, ] # 实际使用时把 golden 换成文档 ID 或来源文件路径评测是关键一步。没有评测你就无法判断参数调整到底是在变好还是变坏。建议在项目一开始就维护一份 Golden Set每次调整切分参数、换 Embedding 模型、改检索逻辑后都跑一遍。5. 高频问题与排查思路5.1 常见问题对照表问题现象常见原因解决思路检索结果与问题完全无关Embedding 模型不适合中文或领域更换支持中文、领域适配的 Embedding 模型回答仍然出现幻觉检索为空或上下文不完整增加 Top-K、调整切分大小、在 Prompt 中强制“无答案”约束报错“LLM 文本向量 API 未配置”没有正确配置 Embedding 的 API Key 或 Base URL检查.env确认 API Key 与模型名称请求报 413 Request Entity Too Large单次传给模型的上下文太长限制检索片段数量、压缩 Prompt、使用 Token 截断文档更新后回答仍是旧内容向量库没有增量更新或旧分块未删除删除旧索引并重建或按文档 hash 做增量同步score 阈值过滤后查不到结果误把距离当相似度阈值方向反了先打印 score 分布再决定过滤方向和阈值检索命中但回答错误上下文被切分破坏语义不完整增大 chunk_overlap 或使用父子分块策略5.2 系统化排查顺序如果回答质量差不要急着调 Prompt先按下面的顺序排查第一步打印检索结果。确认 Top-K 片段里有没有正确答案。如果没有问题在检索侧和 LLM 无关。第二步检查片段完整性。如果答案信息正好被切分边界切断调整切分参数。第三步检查 Prompt 组装。确认检索片段真的被传入了模型而不是代码 bug 导致上下文为空。第四步检查模型行为。用同一段 Prompt 直接调用 LLM看看是否因为模型没有遵循“只基于资料回答”的指令。第五步补充日志。记录问题、检索片段、score、模型回答、耗时方便定位是哪一层的锅。6. 工程最佳实践把 21% 变成 80%6.1 数据质量永远是第一优先级向量检索的上限由数据质量决定。接数据之前先回答几个问题这份文档有没有过期内容同一条知识是否存在多个冲突版本敏感信息有没有混在普通文档里格式是否适合切分建议在数据接入阶段做三件事去重、版本标记、敏感信息识别。把“数据接入”当成一个独立的数据工程任务来做而不是一次性的脚本。6.2 评测先行参数后调没有评估指标的 RAG 调优本质上是靠感觉碰运气。项目启动时就应该建立 Golden Set至少包含 50 到 100 条“问题 期望文档 期望答案”的测试数据。离线阶段关注三个指标Top-K 命中率正确答案是否出现在检索结果前 K 条。回答准确率生成答案与标准答案是否一致。拒答率不确定时是否能够正确说“不知道”而不是硬编。6.3 权限与安全边界LLM 数据接入最常见的生产事故是用户通过检索问到了本不该看到的数据。正确的做法是在检索层做权限隔离而不是在生成层做内容过滤。具体来说每条文档在写入向量库时都要带上department、level、owner等权限元数据用户查询时先根据用户身份生成允许访问的元数据过滤条件再执行检索。另外如果数据包含个人隐私或商业机密要评估是否适合发送到外部模型 API。不能确定安全边界时优先选择私有化部署的模型或先做脱敏处理。6.4 成本与性能优化Embedding 和 LLM 调用都是成本大头。可以从几个方向优化检索结果缓存。相同问题在短时间内直接命中缓存减少重复调用。合理的 Top-K。K 从 4 调到 10成本不一定增加太多但召回率可能提升反之如果 K 过大Prompt 变长成本也会上升。对检索片段做 Reranker。先用廉价向量召回 Top-20再用 Reranker 取 Top-5效果通常优于直接取 Top-5。控制日志中的敏感内容。日志不要完整打印文档内容容易泄密也会增大存储成本。6.5 从固定问答到 Agent 的演进如果业务不再满足于“查文档”而是要“查数据库、调接口、批量处理任务”就需要引入工具调用和 Agent 编排。演进路径通常是这样的第一阶段纯 RAG回答固定文档里的问题。第二阶段RAG 工具调用LLM 根据问题决定是否查询订单库、调用物流接口。第三阶段多步 AgentLLM 自主规划“先查订单再查物流再判断是否需要发起售后”。这里有一个很容易踩的坑不要一上来就上 Agent。Agent 的不可控性比 RAG 高很多工具越多越容易出现“工具滥用”或“循环调用”。建议先用固定链路跑通业务再逐步开放 Agent 能力并且给工具调用加上白名单和超时控制。7. 总结与下一步建议回到开头的问题为什么说“把 LLM 连接到数据只是 21% 的解决方案”因为连接本身只是一个起点。如果你只完成了 Embedding、向量库和检索代码那你大概率只拥有了一个能演示的 Demo而不是一个能上线的系统。真正决定项目成败的是数据质量、切分策略、检索调优、评测体系、权限管控和可观测性。本文的实战案例演示了最基础的 RAG 闭环你可以在此基础上继续做三件事第一把手里的真实业务文档接入管线建立一份自己的 Golden Set跑一次评测看看当前 Top-K 命中率是多少。 第二尝试调整切分参数和 Embedding 模型用评测数据对比效果变化感受一下“21% 之外”的调优空间。 第三把权限元数据加到向量库中实现按用户过滤检索结果这是从 Demo 走向生产环境的第一步。如果这篇文章对你有帮助欢迎收藏备用。也欢迎在评论区聊聊你在 LLM 数据接入时踩过的坑尤其是切分和检索阈值这两块相信很多人都有共鸣。
返回列表