
做 RAG 项目的人十个里有八个在检索不准的时候第一反应是换个更强的 Embedding 模型。我见过太多团队把 bge-large 换成 bge-m3再换成某个榜单上排名更高的模型折腾一圈下来召回率只涨了两三个点该找不到的还是找不到。问题出在哪出在大家默认了一个前提——所有文件都该用同一套方式切分、同一套方式入库、同一套方式检索。这个前提本身就是错的。我做过一个内部知识库项目文档类型横跨产品需求文档、API 接口说明、运维手册、会议纪要、还有一堆 Excel 配置表。一开始我用统一的 RecursiveCharacterTextSplitterchunk_size 设 512overlap 设 50全部灌进 pgvector检索效果惨不忍睹。后来我按文件类型拆成四套入库策略召回率从 61% 直接拉到 89%Embedding 模型一个字没换。这篇文章就把这套分文件类型设计入库方案的思路完整拆开讲包括每种类型为什么这么切、元数据怎么设计、检索时怎么路由、以及我在实操中踩过的那些坑。不管你是刚接触 RAG 的新手还是已经在调优检索命中率的老手应该都能从里面找到能直接抄的东西。1. 为什么统一入库策略是 RAG 检索不准的头号元凶1.1 不同文件的语义粒度根本不在一个量级先想清楚一件事Embedding 模型是把一段文本映射成一个向量这个向量代表的是这段文本的语义中心。如果一段文本里塞了三个不同的主题那这个向量就变成了三个主题的平均值跟任何一个具体问题都不太像。这就是为什么 chunk 的切分方式直接决定了检索质量。但问题在于不同类型的文件它的自然语义单元大小完全不一样。一份 API 文档里一个接口的说明可能就三五句话你把三个接口合并成一个 chunk检索某个接口的参数含义时向量里混进了另外两个接口的信息相似度自然被稀释。反过来一份产品需求文档里一个完整的需求描述可能有两三千字你硬切成 512 字符的小块每块都只是需求的一部分检索时你拿到的是碎片LLM 拼出来的答案也是残缺的。我做过一个粗略的统计在我们那个知识库里文件类型平均单文档字数自然语义单元大小统一 512 切分的后果API 接口文档8000200-400 字多个接口混在一个 chunk产品需求文档150001500-3000 字需求被切碎上下文丢失运维手册20000500-1000 字步骤和前置条件分离会议纪要3000整篇一个议题议题被拦腰截断Excel 配置表不定一行一条记录行结构被破坏你看同样是 512 字符对 API 文档来说太大了对需求文档来说又太小了。用一套参数打天下本质上是在用一个平均值去适配所有分布结果就是每种类型都不讨好。1.2 向量检索的相似度到底在比什么很多人对向量检索有个误解以为它是在做语义理解。其实它做的事情很朴素把你的 query 也变成一个向量然后算它和库里每个 chunk 向量的余弦相似度取 top-k。所以检索准不准取决于两件事——query 向量和 chunk 向量是不是在同一个语义空间里可比以及 chunk 向量是不是干净地代表了你想找的那个信息。这里有个反直觉的点chunk 越短向量越纯但可能丢失上下文导致 LLM 答不好chunk 越长上下文越全但向量越糊检索时越难精准命中。这是一个 trade-off没有银弹。而不同文件类型对这个 trade-off 的最优解是不一样的这就是为什么必须分类处理。我举个具体的例子。我们知识库里有一份运维手册里面有一段是数据库主从切换的标准操作流程大概 1200 字包含前置检查、切换步骤、回滚方案。如果用 512 切分前置检查和切换步骤会被切到两个 chunk 里。用户问主从切换前要检查什么检索命中的是前置检查那个 chunk看起来没问题。但如果用户问主从切换怎么做命中的可能是切换步骤那个 chunkLLM 拿到的时候看不到前置检查给出的答案就是不完整的。而如果整段 1200 字作为一个 chunk检索切换前检查什么时向量里混了切换步骤的信息相似度会被拉低可能就排不到 top-k 里了。1.3 元数据缺失让检索失去了过滤维度还有一个被严重低估的问题大部分人的 RAG 入库时除了文本内容和向量几乎不存任何结构化元数据。这意味着检索时你只能靠向量相似度硬排没有任何过滤和加权的余地。但实际上用户的问题里往往带着明确的过滤意图。上个月产品评审会上定的那个方案是什么——这里有时间过滤上个月和来源过滤产品评审会。订单服务的超时配置是多少——这里有服务名过滤订单服务。如果你的 chunk 里没有存这些元数据向量检索只能靠语义碰运气命中率自然上不去。我在项目里给每个 chunk 都挂了这么一组元数据来源文件、文件类型、章节路径、创建时间、最后更新时间、涉及的服务/模块名、文档版本。别小看这些字段检索时先按元数据做一轮粗筛再在候选集里做向量精排命中率能提升一大截。这个后面会详细讲怎么落地。2. 按文件类型拆解四套入库方案的设计逻辑2.1 结构化文档API 文档、配置说明按语义边界切宁小勿大API 文档、配置说明这类文件的特点是结构极其规整每个接口/配置项就是一个独立的语义单元彼此之间几乎没有语义依赖。处理这类文件核心原则是一个接口一个 chunk绝不合并。具体怎么做我的做法是先按 Markdown 标题层级切分。大部分 API 文档都是用 Markdown 写的二级标题是接口名三级标题是参数说明、返回值、示例。我会以二级标题为边界切分每个接口的所有内容参数、返回、示例作为一个完整 chunk。如果某个接口的内容超过 1500 字再按三级标题二次切分但会在每个子 chunk 的开头拼接上接口名作为上下文锚点。from langchain.text_splitter import MarkdownHeaderTextSplitter headers_to_split_on [ (##, api_name), (###, section), ] splitter MarkdownHeaderTextSplitter( headers_to_split_onheaders_to_split_on, strip_headersFalse ) chunks splitter.split_text(api_doc_content) # 对超长接口做二次切分并拼接接口名作为上下文 final_chunks [] for chunk in chunks: if len(chunk.page_content) 1500: sub_splitter RecursiveCharacterTextSplitter( chunk_size800, chunk_overlap100 ) sub_chunks sub_splitter.split_text(chunk.page_content) for sub in sub_chunks: sub f接口{chunk.metadata[api_name]}\n{sub} final_chunks.append(sub) else: final_chunks.append(chunk.page_content)为什么要在子 chunk 开头拼接口名因为切分之后子 chunk 可能只剩参数说明这几个字向量里完全没有接口名的信息检索XX 接口的参数时根本命中不了。拼上接口名之后向量里就有了这个锚点命中率立刻不一样。这个技巧我在好几个项目里都用过简单但极其有效。元数据方面API 文档的 chunk 我会存接口名、HTTP 方法、路径、所属服务、版本号。检索时如果用户问题里提到了服务名或接口路径可以直接用元数据过滤把候选集缩小到几个接口再算向量相似度精度高得多。2.2 长文档需求文档、方案设计父子分块检索用子块生成用父块产品需求文档、技术方案设计这类文件是最难处理的。它们的语义单元很大一个完整需求可能两三千字但检索时用户的问题往往很具体这个需求的验收标准是什么。如果整段入库向量太糊如果切碎入库LLM 拿到的上下文不全。我的解法是父子分块Parent-Child Chunking。具体来说把文档按语义单元切成父块比如一个完整需求然后把父块再切成更小的子块比如需求的背景、目标、验收标准各一个子块。入库时只把子块做 Embedding 存进向量库但每个子块都记录它属于哪个父块。检索时用子块去匹配 query命中后把对应的父块完整取出来喂给 LLM。# 伪代码示意父子分块逻辑 parent_chunks split_by_semantic_unit(doc) # 按需求/章节切父块 for parent in parent_chunks: parent_id generate_id(parent) # 父块存进文档库可以是关系库或文档存储 doc_store.save(parent_id, parent) # 子块切分 child_chunks RecursiveCharacterTextSplitter( chunk_size400, chunk_overlap50 ).split_text(parent) for child in child_chunks: # 子块做 Embedding 存进向量库带上 parent_id vector_store.add( textchild, embeddingembed(child), metadata{parent_id: parent_id, type: child} )检索流程就变成了query 向量匹配子块 → 拿到 parent_id → 从文档库取父块 → 喂给 LLM。这样既保证了检索的精准度子块向量纯又保证了生成的上下文完整父块信息全。这个方案我在一个需求管理系统的 RAG 里用过效果非常明显。之前用户问XX 功能的验收标准经常检索到的是需求背景那段因为背景里也提到了功能名。用了父子分块之后验收标准那个子块因为语义纯粹命中率大幅提升而且 LLM 拿到的是完整需求答案质量也上去了。2.3 操作手册、流程文档按步骤切保留前置条件运维手册、操作流程这类文档有个特殊性它的价值在于步骤的完整性和顺序性。你切碎了步骤就断了你整段存检索又不精准。我的做法是按操作步骤为边界切分每个步骤作为一个 chunk但强制在每个 chunk 里保留前置条件和所属流程名。具体来说一份运维手册通常长这样## 数据库主从切换流程 ### 前置检查 1. 确认主库无写入... 2. 检查从库延迟... ### 切换步骤 1. 停止应用写入... 2. 提升从库为主库... ### 回滚方案 1. ...我会把前置检查切换步骤回滚方案各切一个 chunk但每个 chunk 的开头都拼上数据库主从切换流程这个流程名并且把前置检查的内容也附加到切换步骤的 chunk 里作为上下文。def split_ops_manual(doc): sections split_by_headers(doc) # 按三级标题切 flow_name extract_flow_name(doc) # 提取流程名 precheck sections.get(前置检查, ) chunks [] for section_name, content in sections.items(): # 每个 chunk 拼接流程名 enriched f流程{flow_name}\n环节{section_name}\n{content} # 切换步骤额外附加前置检查 if section_name 切换步骤 and precheck: enriched f流程{flow_name}\n前置条件{precheck}\n环节{section_name}\n{content} chunks.append(enriched) return chunks为什么要给切换步骤附加前置检查因为用户问主从切换怎么做的时候LLM 如果只拿到切换步骤给出的答案会漏掉前置检查这在运维场景里是危险的。把前置条件带上LLM 生成的答案才是完整可执行的。这是运维类 RAG 和普通问答 RAG 最大的区别——它要求答案具备可操作性不能有信息缺失。2.4 表格、配置清单一行一 chunk保留表头Excel 配置表、参数清单这类文件是最容易被忽视的。很多人直接把整个表格转成文本然后按字符数切分结果就是表头和内容分离检索出来的 chunk 完全看不懂。正确的做法是一行数据作为一个 chunk但每一行都要带上表头。比如一张配置表参数名默认值说明所属模块timeout3000请求超时毫秒订单服务retry3重试次数订单服务入库时每一行变成参数名timeout默认值3000说明请求超时毫秒所属模块订单服务。这样检索订单服务的超时配置时这一行的向量里包含了所有关键信息能精准命中。import pandas as pd def table_to_chunks(df, source_name): chunks [] headers df.columns.tolist() for _, row in df.iterrows(): # 每行拼成字段名值的形式 parts [f{h}{row[h]} for h in headers] content f来源{source_name}\n .join(parts) chunks.append({ text: content, metadata: { source: source_name, type: table_row, # 把关键列也存成元数据方便过滤 module: row.get(所属模块, ) } }) return chunks这里有个细节如果表格很大几千行一行一 chunk 会导致向量库膨胀。我的经验是对于纯查询类的配置表一行一 chunk 是值得的因为检索精度提升明显。但如果表格只是作为参考附件可以按模块聚合一个模块的所有行合成一个 chunk。具体怎么选看你的检索场景是查具体某个参数还是了解某个模块的配置全貌。3. 混合检索向量不是万能的关键词该上场就上场3.1 纯向量检索在什么情况下会翻车向量检索擅长的是语义相似但它在两类场景下会翻车。第一类是精确匹配场景用户问错误码 E5021 是什么意思向量检索可能给你返回一堆语义相近但错误码不同的条目因为 E5021 和 E5022 在向量空间里几乎挨着。第二类是专有名词场景用户问XX 内部框架怎么配置如果这个框架名在训练语料里没出现过Embedding 模型根本不知道它是什么向量表示就是随机的。我实测过一个案例知识库里有 200 多个错误码用户问E5021纯向量检索的 top-5 里只有 2 个是对的。换成 BM25 关键词检索top-5 里 5 个全对。原因很简单错误码这种精确 token关键词匹配比语义匹配靠谱得多。所以正确的做法是混合检索向量检索和关键词检索各跑一遍然后融合结果。融合算法我推荐用 RRFReciprocal Rank Fusion它不需要调权重对两路结果的排名做倒数加权简单又稳。def rrf_fusion(vector_results, keyword_results, k60): scores {} for rank, doc_id in enumerate(vector_results): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) for rank, doc_id in enumerate(keyword_results): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank 1) # 按融合分数排序 return sorted(scores.items(), keylambda x: x[1], reverseTrue)RRF 里的 k 一般取 60这是原论文的推荐值我试过 40 到 80差别不大60 是个稳妥的选择。融合之后取 top-k 作为最终候选集再喂给 LLM。3.2 pgvector 里怎么同时做向量和关键词检索如果你用的是 pgvector混合检索其实很好实现因为 PostgreSQL 本身就支持全文检索。你可以在同一张表里既存向量又存 tsvector一条 SQL 就能同时跑两路。-- 建表时同时建向量索引和全文索引 CREATE TABLE chunks ( id BIGSERIAL PRIMARY KEY, content TEXT, embedding vector(1024), content_tsv tsvector, metadata JSONB ); CREATE INDEX ON chunks USING ivfflat (embedding vector_cosine_ops); CREATE INDEX ON chunks USING GIN (content_tsv); -- 混合检索向量 全文 WITH vector_search AS ( SELECT id, content, 1 - (embedding $1) AS vec_score FROM chunks ORDER BY embedding $1 LIMIT 20 ), keyword_search AS ( SELECT id, content, ts_rank(content_tsv, plainto_tsquery($2)) AS kw_score FROM chunks WHERE content_tsv plainto_tsquery($2) ORDER BY kw_score DESC LIMIT 20 ) SELECT COALESCE(v.id, k.id) AS id, COALESCE(v.content, k.content) AS content, COALESCE(v.vec_score, 0) AS vec_score, COALESCE(k.kw_score, 0) AS kw_score FROM vector_search v FULL OUTER JOIN keyword_search k ON v.id k.id;拿到两路结果后在应用层做 RRF 融合。这套方案的好处是不用引入额外的检索引擎pgvector 一个库全搞定运维成本低。缺点是全文检索对中文支持一般需要装 zhparser 或者 pg_jieba 扩展来做中文分词。如果不想折腾扩展也可以用 simple 配置按字符切但效果会打折扣。3.3 检索路由让不同类型的问题走不同的检索路径混合检索是基础但更进一步的做法是检索路由——根据 query 的特征决定走哪条检索路径。我总结了几条路由规则Query 特征路由策略理由包含错误码、ID、版本号关键词检索优先精确 token 匹配更靠谱包含怎么如何等操作词向量检索 操作手册过滤语义匹配找步骤包含服务名、模块名元数据过滤 向量检索先缩小范围再精排包含时间词上个月、最近时间元数据过滤时间维度硬过滤短 query5 字关键词检索为主向量信息量不足路由的实现可以很简单用正则匹配 query 里的特征词命中哪条规则就走哪条。不需要上什么复杂的分类模型规则就够了。我在项目里用这套路由把错误码类问题的命中率从 40% 提到了 90% 以上。import re def route_query(query): # 错误码/ID 模式 if re.search(r[A-Z]\d{3,}, query): return keyword_first # 操作类问题 if re.search(r怎么|如何|步骤|流程, query): return vector_with_ops_filter # 时间词 if re.search(r上个月|最近|上周|今天, query): return time_filtered # 短 query if len(query) 5: return keyword_first return hybrid4. 元数据设计被九成人忽略的检索加速器4.1 每个 chunk 该挂哪些元数据元数据是 RAG 里投入产出比最高的东西但大部分人只存了个 source。我建议每个 chunk 至少挂这几类元数据来源类文件名、文件路径、文件类型、文档版本结构类章节路径如第三章 3.2 节 参数说明、chunk 在文档中的位置时间类文档创建时间、最后更新时间、生效时间业务类涉及的服务/模块、负责人、标签关系类parent_id父子分块用、关联文档 id这些字段不是随便挂的每一个都有明确的检索用途。来源类用于过滤和溯源结构类用于理解上下文时间类用于时效性过滤业务类用于精准定位关系类用于父子分块和文档关联。4.2 元数据过滤怎么和向量检索配合元数据过滤有两种模式前置过滤和后置过滤。前置过滤是先按元数据筛出候选集再在候选集里做向量检索后置过滤是先做向量检索再按元数据筛掉不符合的。前置过滤的优点是快候选集小向量计算量少缺点是如果元数据筛错了好结果直接被排除。后置过滤的优点是召回全缺点是向量计算量大而且可能 top-k 里全是该被过滤的。我的经验是强过滤条件用前置弱过滤条件用后置。什么是强过滤比如用户明确说了订单服务那服务名就是强过滤前置筛掉其他服务完全合理。什么是弱过滤比如用户说最近的文档这个最近是模糊的用后置过滤先召回再按时间加权排序更稳妥。def retrieve(query, filtersNone, modepre): if mode pre and filters: # 前置过滤先按元数据缩小范围 candidate_ids metadata_filter(filters) results vector_search(query, restrict_tocandidate_ids) else: # 后置过滤先向量检索再筛 results vector_search(query, top_k50) if filters: results [r for r in results if match_filters(r, filters)] results results[:10] return results4.3 时间衰减让新文档在检索里占优势知识库有个特点新文档通常比旧文档更准确。一份 2023 年的配置说明很可能已经被 2024 年的版本取代了。如果检索时不考虑时间用户可能拿到过时的信息。我的做法是在相似度分数上叠加一个时间衰减因子。公式很简单final_score similarity * decay_factor decay_factor exp(-λ * days_since_update)λ 是个可调参数我一般取 0.001 到 0.005 之间。取 0.001 的话一年前的文档衰减到 0.69取 0.005 的话一年前的文档衰减到 0.16。具体取多少看你的知识库更新频率。更新快的比如运维手册λ 取大一点更新慢的比如产品架构文档λ 取小一点。这个衰减因子不要直接乘在向量相似度上做排序而是作为 rerank 阶段的一个加权项。因为如果直接乘可能会把一些虽然旧但仍然正确的结果排掉。我的做法是向量检索召回 top-50然后用相似度 * 0.8 时间衰减 * 0.2重新排序取 top-10。这样既考虑了时效性又不会完全忽略旧文档。5. 实操中踩过的坑和验证方法5.1 切分参数不是拍脑袋定的要用数据说话我见过太多人 chunk_size 直接抄别人的 512 或者 1000从来不验证。这是大忌。chunk_size 应该由你的文档特性和检索场景决定而且必须用数据验证。我的验证方法是准备一组标注好的问答对至少 50 条每条标注了正确答案所在的文档和段落。然后用不同的 chunk_size 跑一遍检索看命中率。具体来说看两个指标Hit Ratetop-k 里有没有正确答案和 MRR正确答案排在第几位。def evaluate_chunk_size(qa_pairs, chunk_sizes): results {} for size in chunk_sizes: # 用这个 size 重新入库 rebuild_index(chunk_sizesize) hit_count 0 mrr_sum 0 for qa in qa_pairs: retrieved retrieve(qa[question], top_k10) for rank, doc in enumerate(retrieved): if doc[source] qa[answer_source]: hit_count 1 mrr_sum 1 / (rank 1) break results[size] { hit_rate: hit_count / len(qa_pairs), mrr: mrr_sum / len(qa_pairs) } return results我实测下来不同文件类型的最优 chunk_size 差异很大。API 文档 300-500 最好需求文档 800-1200 最好运维手册 600-800 最好。统一用 512 的话需求文档的命中率会掉 20 个点以上。所以别偷懒该分类就分类该验证就验证。5.2 中文分词的坑别用默认配置如果你用 pgvector 做关键词检索中文分词是个大坑。PostgreSQL 默认的全文检索配置是按空格分词的中文会被当成一整个 token检索效果极差。必须装 zhparser 或 pg_jieba 扩展。-- 安装 zhparser 后创建配置 CREATE TEXT SEARCH CONFIGURATION chinese (PARSER zhparser); ALTER TEXT SEARCH CONFIGURATION chinese ADD MAPPING FOR n,v,a,i,e,l WITH simple; -- 建索引时指定中文配置 CREATE INDEX idx_chinese ON chunks USING GIN (to_tsvector(chinese, content));装完扩展之后还要注意一点zhparser 的词典可能不包含你的业务专有名词。比如你的产品叫星云zhparser 可能把它切成星和云。这时候需要自定义词典把业务词加进去。这个工作看起来琐碎但对检索命中率影响很大值得花时间做。5.3 检索结果去重别让相似 chunk 霸屏父子分块和重叠切分都会导致一个问题检索出来的 top-k 里有好几个 chunk 来自同一个父块或同一段内容。这会导致 LLM 拿到的上下文冗余浪费 token 还影响生成质量。我的做法是在 rerank 之后做一次去重如果两个 chunk 的 parent_id 相同只保留分数最高的那个。如果是重叠切分导致的重复用文本相似度比如 Jaccard判断相似度超过 0.8 的只留一个。def deduplicate(results, sim_threshold0.8): seen_parents set() deduped [] for r in results: # 父块去重 if r.get(parent_id) and r[parent_id] in seen_parents: continue # 文本相似度去重 if any(jaccard(r[content], d[content]) sim_threshold for d in deduped): continue deduped.append(r) if r.get(parent_id): seen_parents.add(r[parent_id]) return deduped这个去重步骤看起来简单但对最终答案质量影响很大。我做过对比去重前 LLM 经常因为上下文里有重复信息而产生冗余回答去重后答案明显更精炼准确。5.4 怎么判断检索不准是切分问题还是 Embedding 问题最后分享一个排查思路。当检索不准时怎么判断是切分的问题还是 Embedding 的问题我的方法是拿一个检索失败的 case把正确答案所在的原文段落手动切出来单独做 Embedding然后算它和 query 的相似度。如果相似度很高比如 0.8 以上说明 Embedding 没问题是切分把正确答案切碎了导致检索不到如果相似度很低说明 Embedding 模型对这个领域的语义理解不够需要考虑换模型或者做微调。这个排查方法我用了很多次十次里有七八次问题都出在切分上而不是 Embedding。这也印证了标题那句话——检索不准九成的锅不在向量。6. 一套可复用的入库方案配置模板把上面的东西串起来我给出一套可以直接抄的配置模板。假设你用 LangChain pgvector按文件类型分四套 pipelinePIPELINE_CONFIG { api_doc: { splitter: markdown_header, chunk_size: 400, chunk_overlap: 50, context_prefix: True, # 子块拼接接口名 metadata_fields: [api_name, service, version, method], retrieval: hybrid, rerank: True }, requirement_doc: { splitter: parent_child, parent_size: 2000, child_size: 400, child_overlap: 50, metadata_fields: [doc_name, section_path, update_time], retrieval: vector, rerank: True, time_decay: 0.001 }, ops_manual: { splitter: step_based, chunk_size: 700, chunk_overlap: 100, attach_precheck: True, # 步骤块附加前置条件 metadata_fields: [flow_name, step_type, service], retrieval: hybrid, rerank: True }, config_table: { splitter: row_based, include_header: True, metadata_fields: [source, module, param_name], retrieval: keyword_first, rerank: False } }这套配置的核心思想是入库时就按文件类型分好检索时按 query 特征路由。不要指望一套参数打天下也不要指望换个 Embedding 模型就能解决所有问题。检索路由的部分把 query 分类到对应的 pipeline然后走对应的检索策略def smart_retrieve(query): # 判断 query 类型 query_type classify_query(query) # 路由到对应的检索策略 if query_type config_lookup: return keyword_search(query, filter{type: table_row}) elif query_type api_question: return hybrid_search(query, filter{type: api_doc}) elif query_type how_to: return hybrid_search(query, filter{type: ops_manual}) else: return hybrid_search(query)这套方案我在两个项目里落地过检索命中率相比统一入库提升了 25 到 30 个百分点。最关键的是它不需要换 Embedding 模型不需要上昂贵的 rerank 服务纯粹靠入库策略和检索路由的优化就能拿到这个提升。最后说个我自己的体会RAG 这个领域大家都在卷模型、卷框架但真正决定效果的往往是这些脏活累活——怎么切分、怎么存元数据、怎么路由检索。这些工作不性感但它们是检索质量的地基。地基没打好上面堆再多的模型也是白搭。我见过太多团队花大价钱买 Embedding API却在切分策略上偷懒最后效果还不如一个切分讲究的开源方案。所以下次检索不准的时候先别急着换模型回头看看你的入库方案是不是该按文件类型拆一拆了。