
简介本资源为基于知识库的智能对话助手系统完整项目源码面向具备Python与Java基础、希望实践大模型检索增强问答的开发者与学习者。项目以文心千帆大语言模型负责语义理解与回答生成结合Milvus向量数据库实现文档向量的高效相似度检索并借助ApacheTika统一解析PDF、Office、HTML等多格式文档构建可动态更新的知识库同时支持用户上传zip压缩包批量导入文档解决多格式资料难以结构化检索与智能问答的问题。压缩包共50个文件约22.64MB以39个Java源码为主体辅以yml配置、xml依赖、html页面、json数据及docx说明文档等覆盖从文档解析、向量入库到问答检索的完整链路。目前已有97人学习下载适合作为课程设计、毕业项目或企业知识库问答原型的参考实现帮助读者快速理解大模型与向量检索协同工作的工程结构。1. 从一份 PDF 到一句靠谱回答知识库对话助手到底在解决什么你手里有一堆产品手册、内部规范、历史工单格式横跨 PDF、Word、PPT、Excel甚至还有扫描件。同事问你「XX 流程第三步的审批阈值是多少」你翻了三分钟才找到而对方已经等得不耐烦。基于知识库的智能对话助手系统要解决的正是这件事把散落的多格式文档解析成纯文本切块后向量化存入 Milvus用户提问时先检索最相关的片段再交给文心千帆大语言模型组织成自然语言回答。它适合有私有文档、又不想把原文直接喂给公网模型的中小团队也适合想理解 RAG检索增强生成完整链路的开发者。整条链路里ApacheTika 负责「读得进来」Milvus 负责「找得准」文心千帆负责「说得像人话」三者缺一不可。2. 文档解析与切块ApacheTika 怎么把十种格式变成干净文本2.1 为什么选 Tika 而不是自己写解析器多格式文档解析是个典型的「看起来简单、做起来全是坑」的活。PDF 有文本层和扫描层之分Word 有 doc 和 docx 两代格式PPT 里文字藏在文本框和备注里Excel 还有合并单元格。自己写解析器意味着你要维护一套格式分支每加一种格式就多一份技术债。ApacheTika 的价值在于它用统一的AutoDetectParser接口屏蔽了底层差异内部通过服务加载机制自动匹配解析器你只需要给它一个 InputStream 和一个 ContentHandler它就能吐出 XHTML 或纯文本。常见做法是用 Tika 的BodyContentHandler配合Metadata一次性拿到正文和元数据作者、创建时间、页数元数据后续可以做过滤和溯源。选型上还有一个现实考量Tika 是 Java 生态里维护最久、格式覆盖最广的开源解析库之一和后面要用的 Milvus Java SDK、文心千帆 HTTP 接口能放在同一个 JVM 进程里减少跨语言调用的胶水代码。如果你团队是 Python 栈也可以用 Tika 的 REST 服务模式起一个tika-server容器Python 侧发 HTTP 请求拿文本代价是多一跳网络开销。2.2 用 Tika 解析 PDF、Word、PPT 的最小可跑代码下面这段 Java 代码演示如何把一个文件解析成纯文本并打印元数据。依赖只需要tika-core和tika-parsers-standard-package两个坐标。import org.apache.tika.metadata.Metadata; import org.apache.tika.parser.AutoDetectParser; import org.apache.tika.parser.ParseContext; import org.apache.tika.sax.BodyContentHandler; import java.io.FileInputStream; import java.io.InputStream; public class TikaExtractDemo { public static String extractText(String filePath) throws Exception { // AutoDetectParser 会根据文件魔数和扩展名自动选择解析器 AutoDetectParser parser new AutoDetectParser(); // BodyContentHandler 只保留 body 部分-1 表示不限制字符数 BodyContentHandler handler new BodyContentHandler(-1); Metadata metadata new Metadata(); ParseContext context new ParseContext(); try (InputStream stream new FileInputStream(filePath)) { parser.parse(stream, handler, metadata, context); } // 元数据里常用来做过滤的字段 System.out.println(Content-Type: metadata.get(Content-Type)); System.out.println(Author: metadata.get(Author)); return handler.toString(); } }逻辑说明AutoDetectParser是入口它内部维护了一个解析器列表按优先级匹配。BodyContentHandler的构造参数传-1表示不限制输出长度传正数则会截断生产环境建议传一个上限比如 5_000_000防止超大文档撑爆内存。Metadata对象在 parse 之后会被填充Content-Type能告诉你 Tika 实际识别成了什么格式如果识别成application/octet-stream说明魔数没匹配上需要检查文件是否损坏。参数说明ParseContext默认即可但如果你要解析加密 PDF需要往 context 里塞PasswordProvider。另外 Tika 2.x 之后解析器包拆分了只引tika-core会报No parser found必须把tika-parsers-standard-package一起引入。2.3 切块策略固定长度、按段落还是按语义解析出全文后不能整篇丢给模型上下文窗口有限而且检索粒度太粗会引入噪声。切块chunking是 RAG 里最容易被低估的一步。常见三种策略固定字符数切块、按段落/标题切块、按语义相似度切块。固定长度实现最简单但会把一句话拦腰截断按段落切块保留了语义完整性适合结构清晰的文档语义切块效果最好但需要额外跑一个嵌入模型计算相邻句相似度成本高。我一般用「按标题层级 最大长度兜底」的混合策略先按 Markdown 标题或文档大纲切一级块如果某个块超过 800 字符再按句号、换行符二次切分块之间保留 100 字符重叠overlap防止答案正好落在边界上被切掉。重叠不是越多越好100 到 150 字符是经验值再多会显著增加 Milvus 里的向量数量和检索耗时。def split_by_heading(text, max_len800, overlap100): # 先按双换行粗切模拟段落 paragraphs [p.strip() for p in text.split(\n\n) if p.strip()] chunks, buffer [], for para in paragraphs: if len(buffer) len(para) max_len: buffer para \n else: if buffer: chunks.append(buffer.strip()) # 保留尾部 overlap 字符作为下一块开头 buffer buffer[-overlap:] para \n if buffer: chunks.append(buffer.strip()) return chunks这段 Python 是切块逻辑的参考实现实际项目里我会把它和 Tika 的 Java 输出对接或者直接用 Python 的tika包。注意buffer[-overlap:]在 buffer 短于 overlap 时会取到全部不会报错但会导致重复内容偏多可以在前面加一个长度判断。3. 向量化与 Milvus 入库把文本变成可检索的数字3.1 嵌入模型选型文心千帆 Embedding 还是本地模型文本块要存进 Milvus 之前必须先转成向量。文心千帆平台提供了 Embedding 接口调用方便中文语义效果在通用场景下够用适合不想自己维护 GPU 的团队。代价是每次向量化都要走网络批量入库时要注意 QPS 限制和重试。另一种做法是本地部署开源嵌入模型比如 BGE 系列的中文模型优点是离线、可控、无调用费用缺点是要有 GPU 或者忍受 CPU 推理的慢速。选型判断标准很简单文档量在十万块以内、更新频率低用文心千帆 Embedding 最省事文档量百万级、每天增量大本地模型摊薄成本更划算。我一般会先用千帆接口跑通链路等检索效果验证后再决定要不要换本地模型避免一上来就陷进环境配置。3.2 Milvus 集合设计与索引参数怎么定Milvus 2.x 里数据组织在 Collection 下Collection 的 schema 决定了字段。一个典型的文档块 schema 包含主键 idint64 或 varchar、向量字段 embeddingfloat_vector、原文 chunk_textvarchar、来源文件 sourcevarchar、块序号 chunk_indexint64。向量维度必须和嵌入模型输出一致千帆 Embedding 常见维度是 384 或 1024建 Collection 前一定要确认维度写错后面插入会直接报错。索引方面Milvus 支持 FLAT、IVF_FLAT、HNSW、DISKANN 等。数据量小于十万条用 FLAT 暴力检索就够召回率 100%延迟也能接受。超过十万条建议 HNSW参数 M 控制图中每个节点的连接数efConstruction 控制建索引时的搜索范围两者越大索引越准但内存占用越高。我一般设 M16、efConstruction200 作为起点检索时 ef 设 64 到 128在召回和延迟之间取平衡。from pymilvus import CollectionSchema, FieldSchema, DataType, Collection, connections connections.connect(hostlocalhost, port19530) fields [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idTrue), FieldSchema(nameembedding, dtypeDataType.FLOAT_VECTOR, dim1024), FieldSchema(namechunk_text, dtypeDataType.VARCHAR, max_length4096), FieldSchema(namesource, dtypeDataType.VARCHAR, max_length512), FieldSchema(namechunk_index, dtypeDataType.INT64), ] schema CollectionSchema(fields, descriptionknowledge base chunks) 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()逻辑说明auto_idTrue让 Milvus 自己生成主键省去维护 id 的麻烦。max_length对 VARCHAR 是必填项设太小插入长文本会报错设太大浪费内存4096 对大多数块够用。metric_type选 COSINE 是因为文本嵌入通常做过归一化余弦距离比欧氏距离更符合语义相似度直觉。create_index之后必须load()才能检索这一步容易忘忘了会报 collection not loaded。参数说明HNSW 的 M 建议范围 8 到 64越大越准越吃内存efConstruction 建议 100 到 500建索引慢但一次性的。检索时的 ef 要大于等于 top_k否则可能返回不足条数。3.3 批量插入与去重别让同一份文档进库两次实际运维里最常见的问题是重复入库。用户上传了一份文档解析、切块、向量化、插入第二天又上传了同一份如果不去重Milvus 里就有两份一模一样的块检索时返回重复内容模型回答也会啰嗦。去重可以在插入前用 source 字段做一次查询或者对 chunk_text 算一个哈希存到 MySQL 里做唯一约束。批量插入时注意 Milvus 单次插入的数据量官方建议每批不超过 1000 条向量太大容易超时。插入后要flush()才能保证数据落盘可检索但 flush 是重操作不要每插一条就 flush 一次攒够一批再 flush。def insert_chunks(collection, chunks, embeddings, source): # 构造与 schema 字段顺序一致的列式数据 entities [ embeddings, # embedding [c for c in chunks], # chunk_text [source] * len(chunks), # source list(range(len(chunks))), # chunk_index ] # 分批插入每批 500 条 batch_size 500 for i in range(0, len(chunks), batch_size): collection.insert([col[i:ibatch_size] for col in entities]) collection.flush()逻辑说明Milvus 的insert接收的是列式数据每个子列表对应一个字段顺序必须和 schema 里除 auto_id 主键外的字段顺序一致。分批是为了避免单次请求体过大。flush放在所有批次之后调用一次即可。4. 检索与生成文心千帆怎么把片段组织成回答4.1 检索阶段top_k、相似度阈值与重排序用户提问后先用同一个嵌入模型把问题转成向量再去 Milvus 做 ANN 检索。top_k 设多少合适太小可能漏掉关键片段太大引入噪声让模型分心。我一般先取 top_k10然后用相似度阈值过滤掉低于 0.5 的结果COSINE 距离下如果过滤后不足 3 条就放宽阈值到 0.4 再取一次。更讲究的做法是加一个重排序模型对 top_k 结果做精排把最相关的 3 到 5 条送给大模型这一步能明显提升回答准确率代价是多一次模型推理。检索时还要注意一个问题用户问的是「审批阈值」但文档里写的是「审批金额上限」字面不重合纯向量检索可能召回不到。解决办法是在切块时把标题和小节名拼进块内容里比如「第三章 审批流程 审批阈值金额上限为……」这样向量里就带了标题的语义信息。4.2 提示词组装把检索片段塞进文心千帆的正确姿势文心千帆的对话接口接收 messages 数组system 角色用来定人设和约束user 角色放用户问题检索到的片段一般拼在 system 或 user 里。我习惯把片段放在 system 里格式是「以下是与问题相关的资料请基于资料回答不要编造\n[片段1]\n[片段2]」然后在 user 里只放原始问题。这样模型更容易区分「资料」和「问题」减少把资料当问题回答的情况。提示词里必须加一句「如果资料中没有相关信息请回答不知道」否则模型会用自己的预训练知识硬编这在企业知识库场景里是致命的。另外要限制回答长度避免模型把整个片段复述一遍。import requests def ask_qianfan(question, contexts, api_key): context_text \n.join([f[片段{i1}] {c} for i, c in enumerate(contexts)]) system_prompt ( 你是一个企业知识库助手。请严格基于以下资料回答问题 如果资料中没有相关信息直接回答「资料中未找到相关内容」。\n f资料\n{context_text} ) payload { messages: [ {role: system, content: system_prompt}, {role: user, content: question}, ], temperature: 0.1, # 知识问答场景要低温度减少发挥 max_output_tokens: 512, } headers {Content-Type: application/json, Authorization: fBearer {api_key}} resp requests.post(https://qianfan.baidubce.com/v2/chat/completions, jsonpayload, headersheaders, timeout30) return resp.json()[choices][0][message][content]逻辑说明temperature设 0.1 是为了让回答稳定、少发挥知识问答不需要创意。max_output_tokens限制在 512 防止模型长篇大论。接口地址和字段名以文心千帆当前文档为准不同版本可能有差异接入前先看官方说明。参数说明contexts是检索返回的片段列表建议控制在 3 到 5 条总长度不超过模型上下文窗口的 60%留出空间给问题和回答。如果片段太长可以先做一次摘要压缩再拼进去。4.3 引用溯源让回答带上来源文件企业场景里用户看到回答后往往要确认「这是哪份文件说的」。实现溯源不难检索时每个片段都带 source 和 chunk_index生成回答后把这些来源附在末尾。更精细的做法是让模型在回答里用[1][2]标注引用前端再把标注映射回文件链接。这一步能显著提升用户信任度也是知识库助手和普通聊天机器人的核心区别。5. 避坑与排查这条链路上最容易翻车的五个地方5.1 解析出来是空文本现象Tika 解析 PDF 返回空字符串元数据里 Content-Type 是application/pdf。原因这份 PDF 是扫描件没有文本层Tika 只能拿到图片。解决接入 OCR 环节常见做法是先用 Tika 判断文本长度小于阈值就转成图片走 OCR 引擎把 OCR 结果作为正文。注意 OCR 有错误率关键数字要人工复核。5.2 Milvus 插入报维度不匹配现象insert时报The dim of field embedding is not equal to schema。原因嵌入模型换了输出维度从 1024 变成 768但 Collection 还是按 1024 建的。解决换模型必须重建 Collection或者建一个按维度命名的 Collection 如kb_chunks_768不要在原 Collection 上硬改。血泪经验是建库前把维度写进配置中心代码里读配置别硬编码。5.3 检索结果全是同一份文档现象不管问什么返回的片段都来自同一份 PDF。原因那份文档被切成了几千个块向量数量碾压其他文档ANN 检索时被它霸榜。解决在检索时加 source 维度的过滤或配额比如每个 source 最多返回 3 条或者在切块时控制单文档块数上限超长文档做层级摘要。5.4 模型回答「资料中未找到」但资料明明有现象检索返回的片段里确实有答案但模型说没找到。原因片段被截断答案正好在截断处之后或者片段顺序太乱模型没读到。解决检查切块时的 overlap 是否生效把最相关的片段排在前面并在提示词里明确「资料按相关度排序」。另外温度太低有时会让模型过于保守可以试 0.2。5.5 批量入库跑到一半超时现象插入几千条向量时连接断开。原因单次请求体太大或者 Milvus 的 gRPC 消息大小限制。解决把批次降到 200 到 500 条检查 Milvus 服务端的grpc.max_receive_message_length配置必要时调大。另外网络抖动要用重试重试时注意幂等别把同一批插两次。6. 进阶技巧用混合检索和查询改写把准确率再提一档纯向量检索在中文短查询上有个天然短板专有名词、编号、代码这类 token嵌入模型未必能区分开。比如「GB-2024-001 的生效日期」和「GB-2024-002 的生效日期」向量可能非常接近检索容易串。解决办法是混合检索Milvus 2.4 之后支持稀疏向量字段可以把 BM25 或 SPLADE 的稀疏向量和稠密向量放同一个 Collection检索时用hybrid_search做加权融合。稠密向量管语义稀疏向量管关键词两者互补。具体做法是给 schema 加一个sparse_vector字段插入时同时写入稠密和稀疏向量检索时构造两个 AnnSearchRequest一个查稠密一个查稀疏再用 RRFRanker 融合。RRF 的好处是不需要调权重对两路结果做排名倒数融合工程上省心。参数上稠密路 top_k 取 10稀疏路 top_k 取 10融合后取 5 条送模型。另一个技巧是查询改写。用户问「报销怎么弄」直接检索可能召回不到「费用报销流程」的块。可以在检索前先用文心千帆做一次查询扩展让模型生成 2 到 3 个同义问法分别检索后合并去重。这一步会增加一次模型调用延迟多几百毫秒但对召回率的提升在实测里比较明显。我一般只在首轮检索结果相似度低于阈值时才触发改写避免每次都多花钱。验证效果不能靠感觉。我会准备一个 50 到 100 条的问题-答案对作为评测集每次调整切块大小、top_k、提示词后跑一遍统计命中率检索结果包含答案的比例和回答准确率。命中率低于 80% 就先调检索命中率够了但回答不对再调提示词。这个习惯让我少走了很多「改了提示词但其实是检索没召回」的弯路。最后说一个我踩过的坑不要一上来就追求全自动。文档解析、切块、入库这条链路前期一定要留人工抽检环节随机抽 20 个块看内容是否完整、有没有乱码、标题有没有丢。等抽检通过率稳定在 95% 以上再放开批量。知识库的质量下限由解析决定上限由检索和生成决定下限没守住后面再优化都是白费。希望帮到你。本文还有配套的精品资源点击获取