
在 LLM 应用开发中“把大模型连接到数据”很容易被理解为“让模型能回答数据里的问题”。很多团队在 Demo 阶段把 PDF、数据库、网页内容一股脑交给模型发现确实能回答几句于是认为项目已经完成了大半。但实际经验往往是数据是接上了回答质量、召回覆盖、答案可信度、权限边界、成本控制却都还没有完成。业内有一种说法是“连接 LLM 到你的数据”只解决 21% 的问题。这个 21% 不是精确统计而是一种比例提醒接入数据只是起点上下游的检索、上下文构造、评估、监控、安全和迭代才是主要工作量。这篇博客会围绕这一观点展开。先拆解为什么连接数据只占一小部分然后从零搭建一条最小可运行的文档问答链路再解释为什么检索质量和上下文构造决定最终效果最后介绍评估、排错和生产环境的必要补充。适合刚接触 RAG检索增强生成的开发者、正在做企业知识库的工程师以及想把数据接入 LLM 的算法或后端团队参考。1. 先理解“21%”指什么LLM 数据应用的完整工作量1.1 连接数据解决了哪些真实问题大模型本身存在两个天然限制一是训练数据有截止时间二是没有接触企业内部私有数据。用户问“我们公司最新版本的手册里导出功能在哪里”通用模型只能根据类似文档猜测很难给出准确答案。把数据连接到 LLM本质上是在模型能力之外补上一块“事实来源”。常见的做法是 RAG先把文档切块、向量化、存入索引收到问题后用检索得到相关片段再把这些片段作为上下文发送给模型。这样做可以降低幻觉概率也能让答案带上业务语境。但这只解决了“模型看不到数据”的问题。数据接入后模型看到的到底是哪几段内容、这几段内容是否覆盖了问题、拼接后的上下文是否符合模型的阅读习惯、回答错了如何发现这些都不在“连接数据”这一步内。1.2 完整工作量不是“加载-生成”两步如果只做一个最小 Demo确实只需要两步加载文档生成回答。但进入实际项目后完整链路会拉长很多。工作项解决什么问题常见被忽视程度数据接入与解析从 PDF、Word、数据库、网页中抽取正文低多数人会做权限与脱敏防止用户 A 检索到用户 B 的文档高容易被忽视切分与预处理决定哪些片段适合作为上下文高向量化与索引构建决定相似内容能否被快速找到中查询改写与多路召回提升问题与文档之间的匹配率高重排序从召回的候选里选出真正有用的片段高上下文组织与 Prompt 设计决定模型是否理解并使用这些资料中生成策略与引用标注决定答案是否可验证、可追溯中离线评估与回归测试决定修改后质量是变好还是变坏高在线监控与日志决定线上问题能否被发现和定位高数据更新与版本管理决定内容变更后回答是否同步更新高从这个表格能看出数据连接只是第一行后面还有大量和“质量、安全、运维”相关的工程工作。团队如果把精力全部放在“多加载几种数据源”而对检索和评测不加投入项目越往后越容易返工。1.3 上下文工程才是剩余工作的主战场“连接数据”之后真正决定模型回答质量的是进入 Prompt 的那一小段上下文。LLM 一次能阅读的 token 有限企业数据可能几十万份文档不可能全部塞给模型。系统必须在几百毫秒内从海量数据中选出几百到几千字的片段并且保证这些片段有序、准确、没有噪声。围绕“如何组织上下文”展开的工作也就是上下文工程Context Engineering通常比单纯的接入更难。它依赖检索质量也依赖 Prompt 结构。同样是检索增强有的系统能做到“每个回答都给出文档编号”有的系统却经常答非所问差别往往不在模型而在上下文链路。这也是为什么说接入数据只是 21% 的解决方案。文档读到内存、向量写入数据库、模型能引用资料这条路只打通了从“数据”到“模型”的物理通道。真正让业务方满意的是“给定问题稳定、准确地返回基于资料的答案”这需要把 1.2 里的表格逐项做扎实。1.4 什么样的项目适合这篇博客的路线如果你的需求是“把十几页帮助文档做成问答机器人”最小链路就够用如果你的需求是“企业知识库、产品手册、客服知识中心”则必须考虑检索、评估和权限。这篇博客采用一条适合入门的路线先读取本地txt或md文本切分成块用中文 Embedding 模型向量化再用 FAISS 建立索引最后由兼容 OpenAI API 接口的模型生成答案。它不会覆盖所有生产细节但足以让你建立“连接数据只是起点”的体感。2. 搭建最小链路把一份本地文档真正接入 LLM2.1 环境准备与依赖选择开发环境建议使用 Python 3.10 或更高版本。下面四个依赖是这条链路的核心依赖作用说明sentence-transformers生成文本向量使用开源 Embedding 模型本地推理不需要外部 APIfaiss-cpu构建向量索引和检索CPU 版本即可满足学习和小规模使用openai调用大模型生成回答可对接 OpenAI 服务也可通过base_url对接本地兼容服务numpy处理向量数组FAISS 需要 float32 数组安装命令pip install sentence-transformers faiss-cpu openai numpy如果你还要处理 PDF可以额外安装pypdf。不同操作系统对faiss-cpu的安装方式略有差异遇到安装失败时先确认 Python 版本和 pip 版本。注意实际项目中不要把所有依赖写死版本。建议先按当前日期附近的新版本安装跑通后锁定requirements.txt避免后续环境不可复现。2.2 项目目录结构最小项目可以这样组织llm-data-21/ ├── ingest.py # 加载、切分、向量化、写索引 ├── ask.py # 读取索引检索并生成回答 ├── data/ │ ├── intro.md │ └── faq.txt └── knowledge/ ├── index.faiss └── chunks.txtingest.py负责把data/下的文档处理成向量索引ask.py负责接收问题、检索片段并调用模型回答。knowledge/是产物目录可以手动创建也可以在脚本里自动创建。2.3 文档加载与切分先写最基础的文档加载函数。这里以txt和md为例避免引入过多解析逻辑from pathlib import Path def load_text_files(folder: str) - list[str]: texts [] for pattern in (*.txt, *.md, *.markdown): for path in Path(folder).glob(pattern): texts.append(path.read_text(encodingutf-8)) return texts切分函数按固定字符数切片并带重叠窗口。重叠的作用是避免一句话被从中间截断后语义丢失import re def split_text(text: str, chunk_size: int 400, overlap: int 50) - list[str]: text re.sub(r\s, , text.strip()) chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) if end len(text): break start end - overlap return chunks这里按字符切分是入门演示方案。实际项目中建议优先按句子边界、标题或段落边界切分避免把语义切碎。中英文混排时固定字符切分很容易切断术语和数值后面检索阶段会出现“好像有点相关但不完整”的问题。2.4 嵌入与向量索引使用中文语义向量模型生成文本向量import numpy as np import faiss from sentence_transformers import SentenceTransformer def build_index(folder: str, index_dir: str): docs load_text_files(folder) chunks [] for doc in docs: chunks.extend(split_text(doc)) if not chunks: raise RuntimeError(没有读取到任何文本片段) model SentenceTransformer(BAAI/bge-small-zh-v1.5) vectors model.encode(chunks, normalize_embeddingsTrue) vectors np.asarray(vectors, dtypenp.float32) dimension vectors.shape[1] index faiss.IndexFlatIP(dimension) index.add(vectors) Path(index_dir).mkdir(parentsTrue, exist_okTrue) faiss.write_index(index, f{index_dir}/index.faiss) with open(f{index_dir}/chunks.txt, w, encodingutf-8) as f: for chunk in chunks: f.write(chunk.replace(\n, ) \n\n) print(ftext chunks: {len(chunks)}) print(fvector dimension: {dimension})这里使用内积索引IndexFlatIP并且对向量做了归一化所以相似度计算可以近似等价于余弦相似度。模型BAAI/bge-small-zh-v1.5第一次运行时会从模型仓库下载权重需要网络连接生产环境建议提前下载并固化模型路径。2.5 检索并组装上下文ask.py中先加载索引和原始片段然后实现检索import numpy as np import faiss from sentence_transformers import SentenceTransformer from openai import OpenAI model SentenceTransformer(BAAI/bge-small-zh-v1.5) index faiss.read_index(knowledge/index.faiss) with open(knowledge/chunks.txt, r, encodingutf-8) as f: raw f.read() chunks [c.strip() for c in raw.split(\n\n) if c.strip()] def search(query: str, top_k: int 4) - list[str]: query_vec model.encode([query], normalize_embeddingsTrue) query_vec np.asarray(query_vec, dtypenp.float32) scores, indices index.search(query_vec, top_k) results [] for idx in indices[0]: if 0 idx len(chunks): results.append(chunks[idx]) return results生成回答时把检索结果放入 Promptdef ask(query: str): context \n\n.join(search(query)) client OpenAI() resp client.chat.completions.create( modelgpt-4o-mini, messages[ { role: system, content: 只根据提供的资料回答问题。资料中没有的信息明确说明不知道。回答末尾列出资料来源片段编号。, }, { role: user, content: f资料\n{context}\n\n问题{query}, }, ], temperature0.2, ) return resp.choices[0].message.content if __name__ __main__: import sys query sys.argv[1] if len(sys.argv) 1 else 这个项目支持哪些数据格式 print(ask(query))代码里的OpenAI()会读取环境变量OPENAI_API_KEY。如果你使用的是本地模型服务或兼容 OpenAI 协议的中间层可以改成client OpenAI(base_urlhttp://localhost:8000/v1, api_keyEMPTY)不要把 API Key 硬编码在代码里。2.6 学习环境如何验证把文档放进data/依次运行python ingest.py python ask.py 你的问题正常结果应能看到一个基于资料内容的回答。为了确认检索链路本身是否正常你也可以先单独打印search()的返回值检查返回的片段是否确实和问题相关。这一步很重要因为后续很多质量问题都出在“检索结果已经不对模型再怎么生成也难以纠正”。如果你使用的是本地模型首次推理可能较慢建议先用一个小模型验证流程再切换到正式模型。3. 为什么数据已连接仍回答不好检索与上下文组织是关键3.1 检索不到与检索到但不相关是两类问题数据接入后第一个常见问题是“答非所问”。原因分两类一类是相关片段确实在索引里但检索算法没有找出来另一类是相关片段被找出来了但进入 Prompt 后模型没有正确使用。检索阶段主要看两点向量模型是否理解“用户问法”和“文档表达”之间的语义关系以及索引结构是否支持我们想要的召回方式。比如用户问“上季度营收”文档里写的是“2024 年 Q2 财务数据”如果 Embedding 模型没有把这组语义映射到相近位置仅靠关键词匹配会失败仅靠向量相似度也可能失败。解决思路不是迷信某一个模型而是先跑几个真实问题观察召回的 top 片段。如果片段完全不沾边先换 Embedding 模型或增加查询改写如果片段沾边但不够完整再考虑切分和重排序。3.2 切分、重叠、元数据与重排序的影响切分参数对检索质量影响很大用一个表格说明常见关系参数取值过小的影响取值过大的影响推荐方向chunk_size语义片段不完整缺少上下文噪声多向量表示不聚焦300-800 字按结构边界调整overlap首尾信息可能被切断索引冗余重复片段多50-100 字或一句话长度是否保留元数据难以做来源过滤检索结果无法定位记录文档名、章节、权限、时间是否重排序依赖向量相似度前几名额外增加延迟top 20 召回后重排序取 top 3-5生产项目通常采用“两阶段检索”先用向量检索或关键词检索召回较多候选再用重排序模型对候选精排。重排序可以显著提升“最终进入 Prompt 的片段”的准确率但会带来额外延迟和计算成本。元数据同样重要。给每个片段附上文档来源、章节、权限组既能做过滤也能在生成时告诉模型“这些片段来自哪个文档”方便溯源。3.3 系统 Prompt 要明确边界但不能代替检索质量生成阶段常见的错误是把检索结果塞进 Prompt但没有告诉模型“只能依据资料回答”。系统提示词至少应包含三层约束你是企业内部知识助手。 回答规则 1. 只依据给定资料回答不依赖模型自身经验。 2. 资料中找不到答案时直接说“资料中未提到”不要编造。 3. 答案尽量使用资料中的术语和表达。 4. 需要在回答末尾列出参考片段编号。这段提示词不能完全杜绝幻觉但能显著降低模型自由发挥的概率。更关键的是它要求模型“在无法回答时承认不知道”这对内部知识场景非常重要因为业务方宁可得不到答案也不希望得到错误答案。3.4 长上下文不是万能有些团队觉得既然模型支持 128K 甚至更长上下文不如把所有资料一次性塞进去。这个思路在小数据量下可行但会带来三个问题成本高、延迟高、噪声干扰大。模型对长上下文不同位置的注意力并不均匀关键答案夹在大段无关文字中间时反而不容易被提取。更好的做法是保留“检索”这一层只把最相关片段交给模型。即使未来上下文窗口扩大到几百万 token检索依旧能降低成本和延迟也能让答案来源更清晰。4. 用评估把“看起来行”变成“可度量、可回归”4.1 先离线构建一个小的评测集评估 RAG 应用最经济的方法是准备一组带标准答案的问题集。数量不需要很大但必须覆盖三个层次问题类型说明示例直接命中答案集中在某一个片段“联系人管理页面在哪里”多片段拼接答案分散在多个文档或章节“开通流程需要哪几步需要哪些权限”不可回答资料中不存在答案“系统是否支持英文语音输入”建议从 20 到 50 个问题开始。重点是让评测集覆盖真实用户会问的问题不要只写你希望模型回答的问题。4.2 核心指标要看得懂离线评测至少关注四类指标指标计算对象含义Recallk检索结果前 k 个片段是否包含标准答案所在的片段MRR检索结果第一个正确答案出现在第几位的倒数平均Answer Correctness生成结果生成答案和标准答案在语义上是否一致Faithfulness生成结果生成答案中的内容是否都能由资料片段支撑可以用一个非常简单的函数评估检索命中率def evaluate_recall(questions, expected_chunk_ids, retrieve_fn, top_k5): hit 0 for question, expected_ids in zip(questions, expected_chunk_ids): retrieved_ids retrieve_fn(question, top_ktop_k) if set(retrieved_ids) set(expected_ids): hit 1 return hit / len(questions)这里需要你能把“标准答案所在片段”对应到具体的 chunk id。实际操作中可以先让检索系统跑一遍人工标注哪些问题是检索对、哪些不对比一开始就写完整自动化评估更可行。4.3 生成质量评估可以用“模型评判”辅助回答正确性评估比较主观常见做法是用一个更强的模型当裁判对“生成答案”和“标准答案”打分。需要注意模型评判本身可能不稳定更适合作为筛选器最终结果仍要人工抽检。示例提示词你是一个评估助手。请对比生成答案和标准答案从内容一致性、是否包含关键信息、是否有额外错误三个维度打分打分范围 0-10。只需输出分数和一句理由。这种形式适合在离线脚本中批量跑但不要把它当成绝对标准。更稳妥的是把模型评分低的问题抓出来让人工查看“为什么低”。4.4 建立回归阈值和回测机制有了评测集和指标后续每次修改都要回归。建议团队约定一个最低指标例如“检索 Recall5 不低于 80%不可回答问题的正确拒绝率不低于 90%”。不达标的修改不要合并到主线。回归机制可以在 CI 中执行每次提交ingest.py、prompt.py或索引构建脚本时自动跑一遍评测集输出指标对比。这个环节不需要一开始做得特别重先跑通“修改一次看一次分数”再逐步增加数据集规模。4.5 线上评估要记录日志和用户反馈离线评估不能覆盖所有用户表达因此线上日志同样重要。每次问答至少记录用户问题检索到的片段 ID 和相似度分数构建后的 Prompt可脱敏模型输出耗时和 token 消耗用户后续行为是否点击来源、是否继续编辑这些日志是下一步优化检索和提示词的最有力依据。5. 排查链路数据接上了回答仍然不达标5.1 先按四段链路定位排查 RAG 问题不要一上来就改 Prompt。推荐顺序是数据解析 - 切分索引 - 检索召回 - 生成回答。每一个环节都有对立的检查方式。5.2 问题一检索不出相关内容现象问“导出功能在哪里”返回的片段都是“导入”相关。可能原因切分切坏了关键句子被割裂Embedding 模型对领域术语不敏感查询问法和文档表达差异太大索引没有包含新文档权限过滤把可用片段过滤掉了检查与处理打印search()返回片段肉眼确认是否相关。直接用文档里的原句作为查询看能否检索到如果能说明是查询改写问题。尝试换一个 Embedding 模型或同时加入关键词召回。检查切分后的片段是否保留标题、编号、表格结构。5.3 问题二检索出来了生成答案却没有采用现象把相关片段打印出来内容是对的但模型回答里没有使用。可能原因Prompt 没有明确要求“只依据资料”进入 Prompt 的片段太多关键片段被无关片段淹没片段顺序不合理关键内容放得太靠后模型对中文指令的理解力不足检查与处理先减少 top_k比如从 5 降到 3观察是否改善。在 Prompt 中强调“优先使用第一个片段”。把context明确标记为资料并把问题放在最后减少上下文遗忘。换更强的模型测试排除生成能力瓶颈。5.4 问题三答案出现资料中没有的信息现象模型回答了一段看似合理但资料里完全没有的内容。可能原因检索结果不完整模型只能用“记忆”补齐Prompt 没有拒绝权限模型默认要给出答案资料片段本身含有猜测或过时内容检查与处理确认检索结果是否完整覆盖问题。在系统提示中明确“无法从资料回答时直接说资料未提到”。对生成答案增加验证步骤比如抽取关键实体检查是否都在资料片段中出现。上线前对“不可回答”问题单独测试。5.5 问题四权限和隐私边界现象用户能检索到没有权限看到的文档片段。原因很多入门架构只做了单一向量索引没有把用户身份和文档权限注入检索链路。处理方式每个片段保存allowed_roles、owner、tenant_id等元数据。检索时先按用户权限过滤元数据再进行向量相似度搜索。对敏感文档建立独立索引禁止跨库检索。不要只依赖“把敏感内容从结果里删掉”因为向量检索阶段就可能泄露片段信息。5.6 排查清单现象优先检查常见解检索结果不相关切分、Embedding、查询语句调整 chunk换模型增加关键词召回相关但排不在前面检索排序策略增加重排序调整 top_k答案不采用资料Prompt、上下文长度强化指令减少片段提高关键片段位置答案过度自由发挥检索完整性、Prompt 边界明确拒绝规则增加验证用户访问到无权内容元数据、权限过滤按权限过滤分索引隔离实际排查时应该把每一环输出保存下来。只保留最终答案无法定位问题保留 query、检索片段、Prompt、answer 四份数据才能快速判断是哪一环出错。6. 从 21% 走向完整方案生产级注意点与扩展方向6.1 数据权限和服务端过滤不能后置生产环境里的“连接数据”不只是文档读入更是权限模型的接入。一个合格的知识库问答系统至少要做到以下三点文档进入索引前完成脱敏和权限标注。检索过程执行权限过滤而不是在生成结果后删除。敏感数据不能进入公共模型 Prompt除非确认模型服务满足数据合规要求。如果数据量不大可以考虑按权限组建立多个索引。数据量大时在向量库中保存 metadata并在检索时传入过滤条件。无论哪种方式都需要测试“一个普通用户能否通过构造问题绕过权限拿到其他租户的内容”。6.2 数据更新与索引重建企业文档是动态变化的。数据更新最简单可靠的方式是全量重建索引适合数据量不大、更新频率低的情况。数据量大时全量重建耗时长需要考虑增量更新。增量更新有几个复杂点删除文档时如果未删除对应向量检索会继续返回过期内容。文档改动后旧片段和新片段会同时存在。Embedding 模型的版本升级后新旧向量分布不一致最好全量重建。推荐做法是发布前先跑一次评测集用指标确认索引更新没有导致召回下降。6.3 成本、延迟与监控每一次问答都会产生 embedding 和生成模型两部分成本。常见优化方向手段效果注意结果缓存高频问题直接命中缓存缓存 key 要考虑用户权限减少 top_k降低上下文 token可能降低覆盖率使用更小模型降低生成成本需要评估质量批量向量化降低文档更新成本不影响查询延迟监控指标建议包括检索平均耗时、生成耗时、每次问答 token 数、缓存命中率、检索 Top1 相似度分布、用户反馈率。当某个问题的检索相似度长期很低时说明可能缺少对应文档或切分不合理。6.4 发布前检查清单以下清单可以直接用于项目提测或发布前检查文档解析是否包含全部目标格式PDF 中的表格和页眉页脚是否处理。切分结果是否保存能否按片段 ID 回溯到原文。索引是否与当前数据版本一致删除的数据是否已经移除。检索结果是否经过权限过滤是否有越权样例。评测集是否覆盖“直接回答、多片段、不可回答”三类问题。是否记录 query、检索片段、Prompt、answer、耗时、token 消耗。是否有缓存缓存策略是否考虑权限隔离。是否设置模型输出长度和温度上限。是否对不可回答的问题做了拒绝测试。是否有回滚方案例如数据更新后质量下降时如何快速回到旧索引。6.5 扩展方向多路召回、Rerank、Agent如果当前链路已经稳定下一步可以按以下方向扩展多路召回向量检索加 BM25 关键词检索再合并结果提升长尾表达覆盖。重排序使用交叉编码器对召回片段精排提升上下文质量。多轮对话让用户能连续追问需要把历史对话转化为新的检索条件。Agent 化当问题需要查数据库、调用 API、读多个文档时可以让 LLM 决定调用哪些工具。结构化数据接入文档只是数据的一种SQL 数据库、API 返回值同样可以进入上下文管道。这些方向共同指向一个事实LLM 只是推理引擎数据接入完成之后工程上的检索质量、评估闭环和安全边界才是项目能否长期稳定运行的关键。回到文章开头的那个 21%。连接 LLM 到你的数据解决的是“模型能否看见数据”的问题让模型准确、可信、可追溯地使用数据才是更困难、也更有长期价值的部分。实际项目中可以在第一版 Demo 落地的同时就建立评测集和日志记录。哪怕只记录 20 个问题也能在后续迭代中提前发现那些“看起来能用一上线就打脸”的隐患。