文章目录
- RAG初阶
-
- [1. 开发环境](#1. 开发环境)
-
- [1.1 创建开发环境](#1.1 创建开发环境)
- [1.2 设置运行环境](#1.2 设置运行环境)
- [1.3 LlamaIndex](#1.3 LlamaIndex)
-
- [1.3.1 什么是LlamaIndex?](#1.3.1 什么是LlamaIndex?)
- [1.3.2 LlamaIndex 与 LangChain](#1.3.2 LlamaIndex 与 LangChain)
- [2. 知识库的构建](#2. 知识库的构建)
-
- [2.1 数据加载](#2.1 数据加载)
-
- [2.1.1 数据解析](#2.1.1 数据解析)
- [2.1.2 数据清洗](#2.1.2 数据清洗)
- [2.2 文本分块](#2.2 文本分块)
-
- [2.2.1 固定大小分块](#2.2.1 固定大小分块)
- [2.2.2 句子分块](#2.2.2 句子分块)
- [2.2.3 语义分块](#2.2.3 语义分块)
- [2.2.4 递归分块](#2.2.4 递归分块)
- [2.2.5 基于文档结构的分块](#2.2.5 基于文档结构的分块)
- [2.2.6 基于LLM的分块](#2.2.6 基于LLM的分块)
-
- [2.2.6.1 接入大模型](#2.2.6.1 接入大模型)
- [2.2.6.2 大模型功能封装](#2.2.6.2 大模型功能封装)
- [2.2.6.3 大模型分块代码](#2.2.6.3 大模型分块代码)
- [2.3 向量嵌入](#2.3 向量嵌入)
-
- [2.3.1 余弦相似度](#2.3.1 余弦相似度)
- [2.3.2 欧氏距离](#2.3.2 欧氏距离)
- [2.3.3 点积距离](#2.3.3 点积距离)
- [2.4 向量数据库](#2.4 向量数据库)
-
- [2.4.1 什么是向量数据库?](#2.4.1 什么是向量数据库?)
- [2.4.2 Milvus与MySQL核心概念对比](#2.4.2 Milvus与MySQL核心概念对比)
- [2.4.3 Milvus与MySQL的区别](#2.4.3 Milvus与MySQL的区别)
- [2.4.4 部署Milvus 向量数据库](#2.4.4 部署Milvus 向量数据库)
- [2.4.5 向量数据库常见操作](#2.4.5 向量数据库常见操作)
-
- [2.4.5.1 核心概念](#2.4.5.1 核心概念)
- [2.4.5.2 连接数据库](#2.4.5.2 连接数据库)
- [2.4.5.3 获取client操作db](#2.4.5.3 获取client操作db)
- [2.4.5.4 操作collection](#2.4.5.4 操作collection)
- [2.4.5.5 数据常见操作](#2.4.5.5 数据常见操作)
- [2.4.5.6 向量相似度检索](#2.4.5.6 向量相似度检索)
- [3. 数据检索](#3. 数据检索)
-
- [3.1.1 用户查询](#3.1.1 用户查询)
- [3.1.2 查询向量化](#3.1.2 查询向量化)
- [3.1.3 向量检索](#3.1.3 向量检索)
- [3.1.4 代码实现](#3.1.4 代码实现)
- [4. 提示词增强](#4. 提示词增强)
-
- [4.1 提示词模板化](#4.1 提示词模板化)
- [RAG 中的提示词模板化](#RAG 中的提示词模板化)
- [4.2 上下文压缩与过滤](#4.2 上下文压缩与过滤)
-
- [4.2.1 背景](#4.2.1 背景)
- [4.2.2 什么是上下文压缩](#4.2.2 什么是上下文压缩)
- [4.2.3 什么是过滤](#4.2.3 什么是过滤)
- [4.3 角色设定](#4.3 角色设定)
- [4.4 代码实现](#4.4 代码实现)
- [5. LLM 生成](#5. LLM 生成)
-
- [5.1 LLM 概念](#5.1 LLM 概念)
- [5.2 代码实现](#5.2 代码实现)
- [6. Chatgpt风格医疗助手](#6. Chatgpt风格医疗助手)
-
- [6.1 数据获取](#6.1 数据获取)
- [6.2 数据简化](#6.2 数据简化)
- [6.3 数据分块](#6.3 数据分块)
-
- [6.3.1 数据分块](#6.3.1 数据分块)
- [6.4 数据入库](#6.4 数据入库)
-
- [6.4.1 初始化数据库](#6.4.1 初始化数据库)
- [6.4.2 数据向量化](#6.4.2 数据向量化)
- [6.4.3 向量数据入库](#6.4.3 向量数据入库)
- [6.5 数据检索](#6.5 数据检索)
- [6.6 生成提示词](#6.6 生成提示词)
- [6.7 LLM生成](#6.7 LLM生成)
- [6.8 web实现](#6.8 web实现)
RAG初阶
1. 开发环境
1.1 创建开发环境
PyCharm 中进入 设置 → Python → 解释器 → 添加解释器 → 添加本地解释器 → Conda 环境 → 新建环境 → 选择 Python 版本 → 确定。
随后在终端中创建环境,这里使用最新版本的python:
conda create -n RAG python=3.13
conda activate RAG
1.2 设置运行环境

1.3 LlamaIndex
1.3.1 什么是LlamaIndex?
#mermaid-svg-PsybE05gmdR1CJG7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PsybE05gmdR1CJG7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PsybE05gmdR1CJG7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PsybE05gmdR1CJG7 .error-icon{fill:#552222;}#mermaid-svg-PsybE05gmdR1CJG7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PsybE05gmdR1CJG7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PsybE05gmdR1CJG7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PsybE05gmdR1CJG7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PsybE05gmdR1CJG7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PsybE05gmdR1CJG7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PsybE05gmdR1CJG7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PsybE05gmdR1CJG7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PsybE05gmdR1CJG7 .marker.cross{stroke:#333333;}#mermaid-svg-PsybE05gmdR1CJG7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PsybE05gmdR1CJG7 p{margin:0;}#mermaid-svg-PsybE05gmdR1CJG7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PsybE05gmdR1CJG7 .cluster-label text{fill:#333;}#mermaid-svg-PsybE05gmdR1CJG7 .cluster-label span{color:#333;}#mermaid-svg-PsybE05gmdR1CJG7 .cluster-label span p{background-color:transparent;}#mermaid-svg-PsybE05gmdR1CJG7 .label text,#mermaid-svg-PsybE05gmdR1CJG7 span{fill:#333;color:#333;}#mermaid-svg-PsybE05gmdR1CJG7 .node rect,#mermaid-svg-PsybE05gmdR1CJG7 .node circle,#mermaid-svg-PsybE05gmdR1CJG7 .node ellipse,#mermaid-svg-PsybE05gmdR1CJG7 .node polygon,#mermaid-svg-PsybE05gmdR1CJG7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PsybE05gmdR1CJG7 .rough-node .label text,#mermaid-svg-PsybE05gmdR1CJG7 .node .label text,#mermaid-svg-PsybE05gmdR1CJG7 .image-shape .label,#mermaid-svg-PsybE05gmdR1CJG7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PsybE05gmdR1CJG7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PsybE05gmdR1CJG7 .rough-node .label,#mermaid-svg-PsybE05gmdR1CJG7 .node .label,#mermaid-svg-PsybE05gmdR1CJG7 .image-shape .label,#mermaid-svg-PsybE05gmdR1CJG7 .icon-shape .label{text-align:center;}#mermaid-svg-PsybE05gmdR1CJG7 .node.clickable{cursor:pointer;}#mermaid-svg-PsybE05gmdR1CJG7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PsybE05gmdR1CJG7 .arrowheadPath{fill:#333333;}#mermaid-svg-PsybE05gmdR1CJG7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PsybE05gmdR1CJG7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PsybE05gmdR1CJG7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PsybE05gmdR1CJG7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PsybE05gmdR1CJG7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PsybE05gmdR1CJG7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PsybE05gmdR1CJG7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PsybE05gmdR1CJG7 .cluster text{fill:#333;}#mermaid-svg-PsybE05gmdR1CJG7 .cluster span{color:#333;}#mermaid-svg-PsybE05gmdR1CJG7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PsybE05gmdR1CJG7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PsybE05gmdR1CJG7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PsybE05gmdR1CJG7 .icon-shape,#mermaid-svg-PsybE05gmdR1CJG7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PsybE05gmdR1CJG7 .icon-shape p,#mermaid-svg-PsybE05gmdR1CJG7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PsybE05gmdR1CJG7 .icon-shape .label rect,#mermaid-svg-PsybE05gmdR1CJG7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PsybE05gmdR1CJG7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PsybE05gmdR1CJG7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PsybE05gmdR1CJG7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 私有数据
LlamaIndex
AI的数据管家
大语言模型 LLM
LlamaIndex 是一个专门用于构建 LLM + 数据应用 的框架,尤其适合学习和开发 RAG(检索增强生成) 。它可以把 PDF、数据库、API 等数据加载进来,经过切分、Embedding 和索引后,建立知识库;用户提问时,再检索相关内容交给 LLM 生成答案。除此之外,LlamaIndex 还提供查询引擎、聊天引擎、Agent、Workflow、评估和可观测性等功能。简单来说,LlamaIndex 更关注"如何让大模型高效使用你的数据",因此在 RAG 场景中非常常用。
1.3.2 LlamaIndex 与 LangChain
# 安装 LlamaIndex
pip install llama-index
# 查看 LlamaIndex版本
pip show llama-index
| 框架 | 核心擅长 | 重心 |
|---|---|---|
| LlamaIndex | 数据全链路:加载、解析、分块、索引、精准检索 | RAG、知识库、文档问答、混合检索优化 |
| LangChain | 流程编排:Chain、Agent 多步骤逻辑、工具调用、复杂工作流 | 智能体、多工具串联、复杂任务调度 |
LlamaIndex 和 LangChain 都是 LLM 应用开发框架。
- LlamaIndex :更侧重 数据接入和 RAG,擅长文档加载、切分、索引、检索。
- LangChain :更偏向 整体 LLM 应用编排,除了 RAG,还重点支持 Agent、Tool、工作流等。
两者可以互相嵌套混用:用 LlamaIndex 做数据检索底座,LangChain 编排上层业务逻辑。
2. 知识库的构建
#mermaid-svg-BkMo7jw0q2zKrlIi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BkMo7jw0q2zKrlIi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BkMo7jw0q2zKrlIi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BkMo7jw0q2zKrlIi .error-icon{fill:#552222;}#mermaid-svg-BkMo7jw0q2zKrlIi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BkMo7jw0q2zKrlIi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BkMo7jw0q2zKrlIi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BkMo7jw0q2zKrlIi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BkMo7jw0q2zKrlIi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BkMo7jw0q2zKrlIi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BkMo7jw0q2zKrlIi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BkMo7jw0q2zKrlIi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BkMo7jw0q2zKrlIi .marker.cross{stroke:#333333;}#mermaid-svg-BkMo7jw0q2zKrlIi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BkMo7jw0q2zKrlIi p{margin:0;}#mermaid-svg-BkMo7jw0q2zKrlIi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BkMo7jw0q2zKrlIi .cluster-label text{fill:#333;}#mermaid-svg-BkMo7jw0q2zKrlIi .cluster-label span{color:#333;}#mermaid-svg-BkMo7jw0q2zKrlIi .cluster-label span p{background-color:transparent;}#mermaid-svg-BkMo7jw0q2zKrlIi .label text,#mermaid-svg-BkMo7jw0q2zKrlIi span{fill:#333;color:#333;}#mermaid-svg-BkMo7jw0q2zKrlIi .node rect,#mermaid-svg-BkMo7jw0q2zKrlIi .node circle,#mermaid-svg-BkMo7jw0q2zKrlIi .node ellipse,#mermaid-svg-BkMo7jw0q2zKrlIi .node polygon,#mermaid-svg-BkMo7jw0q2zKrlIi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BkMo7jw0q2zKrlIi .rough-node .label text,#mermaid-svg-BkMo7jw0q2zKrlIi .node .label text,#mermaid-svg-BkMo7jw0q2zKrlIi .image-shape .label,#mermaid-svg-BkMo7jw0q2zKrlIi .icon-shape .label{text-anchor:middle;}#mermaid-svg-BkMo7jw0q2zKrlIi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BkMo7jw0q2zKrlIi .rough-node .label,#mermaid-svg-BkMo7jw0q2zKrlIi .node .label,#mermaid-svg-BkMo7jw0q2zKrlIi .image-shape .label,#mermaid-svg-BkMo7jw0q2zKrlIi .icon-shape .label{text-align:center;}#mermaid-svg-BkMo7jw0q2zKrlIi .node.clickable{cursor:pointer;}#mermaid-svg-BkMo7jw0q2zKrlIi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BkMo7jw0q2zKrlIi .arrowheadPath{fill:#333333;}#mermaid-svg-BkMo7jw0q2zKrlIi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BkMo7jw0q2zKrlIi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BkMo7jw0q2zKrlIi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BkMo7jw0q2zKrlIi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BkMo7jw0q2zKrlIi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BkMo7jw0q2zKrlIi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BkMo7jw0q2zKrlIi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BkMo7jw0q2zKrlIi .cluster text{fill:#333;}#mermaid-svg-BkMo7jw0q2zKrlIi .cluster span{color:#333;}#mermaid-svg-BkMo7jw0q2zKrlIi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BkMo7jw0q2zKrlIi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BkMo7jw0q2zKrlIi rect.text{fill:none;stroke-width:0;}#mermaid-svg-BkMo7jw0q2zKrlIi .icon-shape,#mermaid-svg-BkMo7jw0q2zKrlIi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BkMo7jw0q2zKrlIi .icon-shape p,#mermaid-svg-BkMo7jw0q2zKrlIi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BkMo7jw0q2zKrlIi .icon-shape .label rect,#mermaid-svg-BkMo7jw0q2zKrlIi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BkMo7jw0q2zKrlIi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BkMo7jw0q2zKrlIi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BkMo7jw0q2zKrlIi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 文本 Text
切分 Splitter
文本分块 Text Chunk
嵌入模型 Embedding Model
向量嵌入 Vector Embedding
存储和索引 Store & Index
知识库
#mermaid-svg-YPEhTkJIOBIrQOwq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YPEhTkJIOBIrQOwq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YPEhTkJIOBIrQOwq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YPEhTkJIOBIrQOwq .error-icon{fill:#552222;}#mermaid-svg-YPEhTkJIOBIrQOwq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YPEhTkJIOBIrQOwq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YPEhTkJIOBIrQOwq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YPEhTkJIOBIrQOwq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YPEhTkJIOBIrQOwq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YPEhTkJIOBIrQOwq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YPEhTkJIOBIrQOwq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YPEhTkJIOBIrQOwq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YPEhTkJIOBIrQOwq .marker.cross{stroke:#333333;}#mermaid-svg-YPEhTkJIOBIrQOwq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YPEhTkJIOBIrQOwq p{margin:0;}#mermaid-svg-YPEhTkJIOBIrQOwq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YPEhTkJIOBIrQOwq .cluster-label text{fill:#333;}#mermaid-svg-YPEhTkJIOBIrQOwq .cluster-label span{color:#333;}#mermaid-svg-YPEhTkJIOBIrQOwq .cluster-label span p{background-color:transparent;}#mermaid-svg-YPEhTkJIOBIrQOwq .label text,#mermaid-svg-YPEhTkJIOBIrQOwq span{fill:#333;color:#333;}#mermaid-svg-YPEhTkJIOBIrQOwq .node rect,#mermaid-svg-YPEhTkJIOBIrQOwq .node circle,#mermaid-svg-YPEhTkJIOBIrQOwq .node ellipse,#mermaid-svg-YPEhTkJIOBIrQOwq .node polygon,#mermaid-svg-YPEhTkJIOBIrQOwq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YPEhTkJIOBIrQOwq .rough-node .label text,#mermaid-svg-YPEhTkJIOBIrQOwq .node .label text,#mermaid-svg-YPEhTkJIOBIrQOwq .image-shape .label,#mermaid-svg-YPEhTkJIOBIrQOwq .icon-shape .label{text-anchor:middle;}#mermaid-svg-YPEhTkJIOBIrQOwq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YPEhTkJIOBIrQOwq .rough-node .label,#mermaid-svg-YPEhTkJIOBIrQOwq .node .label,#mermaid-svg-YPEhTkJIOBIrQOwq .image-shape .label,#mermaid-svg-YPEhTkJIOBIrQOwq .icon-shape .label{text-align:center;}#mermaid-svg-YPEhTkJIOBIrQOwq .node.clickable{cursor:pointer;}#mermaid-svg-YPEhTkJIOBIrQOwq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YPEhTkJIOBIrQOwq .arrowheadPath{fill:#333333;}#mermaid-svg-YPEhTkJIOBIrQOwq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YPEhTkJIOBIrQOwq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YPEhTkJIOBIrQOwq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YPEhTkJIOBIrQOwq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YPEhTkJIOBIrQOwq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YPEhTkJIOBIrQOwq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YPEhTkJIOBIrQOwq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YPEhTkJIOBIrQOwq .cluster text{fill:#333;}#mermaid-svg-YPEhTkJIOBIrQOwq .cluster span{color:#333;}#mermaid-svg-YPEhTkJIOBIrQOwq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YPEhTkJIOBIrQOwq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YPEhTkJIOBIrQOwq rect.text{fill:none;stroke-width:0;}#mermaid-svg-YPEhTkJIOBIrQOwq .icon-shape,#mermaid-svg-YPEhTkJIOBIrQOwq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YPEhTkJIOBIrQOwq .icon-shape p,#mermaid-svg-YPEhTkJIOBIrQOwq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YPEhTkJIOBIrQOwq .icon-shape .label rect,#mermaid-svg-YPEhTkJIOBIrQOwq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YPEhTkJIOBIrQOwq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YPEhTkJIOBIrQOwq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YPEhTkJIOBIrQOwq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户查询 Query
嵌入模型 Embedding Model
向量嵌入 Vector Embedding
相似度检索 Retrieval
向量数据库 Vector Database
匹配的知识库信息
生成最终提示词 Prompt
大模型 LLM 生成
最终响应
2.1 数据加载
2.1.1 数据解析
原始文件:乱七八糟、各种格式、机器看不懂;标准内容:纯文本、结构化、大模型能读懂
数据解析就是把「乱七八糟、各种格式、机器看不懂」的原始文件,转换成「纯文本、结构化、大模型能读懂}的标准内容。
python
from llama_index.core import SimpleDirectoryReader
from pathlib import Path
base_path = Path(__file__).parent
# 定义方法:加载并解析一个目录下面的所有文件
def pare_all_formats(input_dir):
reader = SimpleDirectoryReader(
input_dir = input_dir
)
documents = reader.load_data()
for i, doc in enumerate(documents):
file_name = doc.metadata.get("file_path")
print(f"\n 第 {i+1} 个对象 | 文件路径:{file_name}")
print(doc.text[:200])
return documents
if __name__ == "__main__":
pare_all_formats(base_path/"文档")
reader.load_data() 的核心特点是:先读取原始数据,再进行统一封装,而不是直接进行文本切分。
主要有以下几个特点:
-
自动解析
Reader 会根据数据类型进行解析,例如 PDF 提取文本、Word 提取正文、Markdown 读取内容等。
-
结构化处理
读取到的内容不会只是简单的字符串,而是会封装成
Document,同时可以保存文件名、路径、页码等metadata。 -
保留原始信息
在解析文本的同时,通常会尽可能保留数据的来源信息,方便后续 RAG 检索时追踪原文出处。
-
支持批量处理
一个 Reader 可以一次加载多个文件,最终返回多个
Document,一个文件如果有多面,则一面对应一个document对象。 -
不负责文本切分
文本切分通常由后面的 Node Parser / Text Splitter 完成。
安装读取文件的依赖,可以支持解析更多格式的文件
pip install llama-index-readers-file python-pptx python-docx openpyxl
2.1.2 数据清洗
- 过滤:筛选有效内容,去掉无关信息
- 剔除:删除广告、页眉页脚、重复内容、乱码等垃圾数据
- 修正:纠正错别字、OCR 错误、异常标点和断行
- 标准化:统一编码、空格、换行、标点和文本格式
- 去重:删除重复或高度相似的文本
- 结构化:整理标题、段落、章节等文档结构
最终效果:得到干净、准确、统一、结构清晰的文本 → 便于后续分块、向量化、入库,提高 RAG 检索质量。
配置文件 config.py
python
from pathlib import Path
base_path = Path(__file__).parent
工具文件 util.py
python
from llama_index.core import SimpleDirectoryReader
# 解析input_dir目录下所有的文件
def pare_all_formats(input_dir):
reader = SimpleDirectoryReader(
input_dir=input_dir
)
return reader.load_data()
markdown、txt、pdf这些属于通用文本,可以使用一个函数统一进行处理,所有文件都先做通用清洗,PPT 则需要再额外做一次 PPT 专用清洗,因为PPT包含有很多版式和演示相关的信息:
python
# 正则表达式库,用于匹配并替换各类文本噪声
import re
# Unicode标准化库,统一全角/半角、特殊字符格式
import unicodedata
# 路径处理工具,安全获取文件后缀、拼接路径
from pathlib import Path
# LlamaIndex 文档核心对象,承载单份文件文本+元数据
from llama_index.core import Document
# 项目全局配置,包含基础根目录路径
from config import base_path
# 自定义工具函数:自动解析目录下所有格式文件,输出Document列表
from util import pare_all_formats
# ---------------------- 预编译正则:PPT解析产生的结构噪声匹配规则 ----------------
# 匹配PPT解析生成的标题行:Title: xxx
_PPTX_TITLE_LINE = re.compile(r"^Title:\s*.+\s*$", re.MULTILINE)
# 匹配PPT内容分割线:连续三个及以上短横线
_PPTX_SEPARATOR = re.compile(r"^-{3,}\s*$", re.MULTILINE)
# 匹配PPT备注前缀:[Speaker Notes]:
_PPTX_SPEAKER_NOTES = re.compile(r"^\[Speaker Notes\]:\s*", re.MULTILINE)
# ---------------------- 预编译正则:Markdown标记清理规则 ----------------------
# 匹配加粗一级标题 # **标题**
_MD_HEADING_BOLD = re.compile(r"^#\s*\*\*(.+?)\*\*\s*$", re.MULTILINE)
# 匹配行内加粗标记 **内容**
_MD_BOLD = re.compile(r"\*\*(.+?)\*\*")
# 匹配行首标题符号 #
_LEADING_HASH = re.compile(r"^#\s+", re.MULTILINE)
# 匹配首尾包裹#的文本 # 内容 #
_INLINE_HASH_WRAP = re.compile(r"#\s*(.+?)\s*#")
# 匹配行尾多余#符号
_TRAILING_HASH = re.compile(r"#\s*$")
# ---------------------- 预编译正则:通用脏字符匹配规则 ----------------------
# 匹配不可见ASCII控制字符(换行、制表符除外)
_CONTROL_CHARS = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
# 匹配零宽空白、字节序标记等肉眼不可见隐形字符
_ZERO_WIDTH = re.compile(r"[\ufeff\u200b\u200c\u200d\ufeff]")
# 匹配连续3行及以上空行,统一压缩为两段换行
_MULTI_BLANK_LINES = re.compile(r"\n{3,}")
# 对单端文本执行清洗逻辑
def clean_text(text, source_suffix):
text = _ZERO_WIDTH.sub("", text)
text = text.replace("\ufeff", "")
text = unicodedata.normalize("NFKC", text)
text = _CONTROL_CHARS.sub("", text)
text = text.replace("\r\n", "\n").replace("\r", "\n")
if source_suffix.lower() in {".pptx", "ppt", "pptm"}:
text = clean_ppt(text)
lines = [line.strip() for line in text.split("\n") if line.strip()]
text = _MULTI_BLANK_LINES.sub("\n\n", "\n".join(lines))
return text.strip();
# 单独处理ppt文件
def clean_ppt(text):
text = _PPTX_TITLE_LINE.sub("", text)
text = _PPTX_SEPARATOR.sub("", text)
text = _PPTX_SPEAKER_NOTES.sub("", text)
text = _MD_HEADING_BOLD.sub(r"\1", text)
text = _MD_BOLD.sub(r"\1", text)
while True:
cleand = _INLINE_HASH_WRAP.sub(r"\1", text)
if cleand == text:
break;
text = cleand
text = _LEADING_HASH.sub("", text)
text = _TRAILING_HASH.sub("", text)
text = re.sub(r"\s+#\s+", "", text)
return text.replace("#", "")
# 构建清洗后新的Document对象
def clean_doc(doc):
suffix = Path(doc.metadata.get("file_path", "")).suffix
return Document(text=clean_text(doc.text, suffix), metadata=doc.metadata)
# 构建清洗后的Document对象
def clean_all_formats(input_dir):
# 原始解析出来的对象
docs = parse_all_formats(input_dir)
# 清洗之后的对象数组
cleaned_docs = []
for i, doc in enumerate(docs):
cleand_doc = clean_doc(doc)
cleaned_docs.append(cleand_doc)
file_path = cleand_doc.metadata.get("file_path")
print(f"\n第{i+1} 个对象|文件路径:{file_path}")
print(cleand_doc.text)
return cleaned_docs
if __name__ == "__main__":
clean_all_formats(base_path/"文档")
2.2 文本分块
文本分块的作用如下:
- 解决大模型「上下文长度有限」的硬限制
大模型有最大上下文窗口(比如 4k、8k、32k token)。你的本地文档、手册、资料可能几万、几十万字,全文直接塞给模型,装不下、直接报错 / 截断。- 分块:把长文档切成小片段,只有相关的小块才发给大模型。
- 让向量检索「精准匹配」
向量数据库只能存短文本向量:整篇文档转向量,内容太杂,一句话问题匹配到整篇无关内容;- 分块之后,每一块只讲一个小知识点,用户问啥只召回最相关的那一小段,匹配精度更高。
- 防止语义割裂、回答错乱
如果不分块 / 乱硬切:一句话劈成两半、上下文拆开,AI 只看碎片文字。- 合理分块(句子 / 段落分块):保证每一块语义完整、逻辑独立,AI 看得懂、不乱编。
- 降低成本 + 提升速度
发给大模型的文本越短,token 消耗越少、花钱越少;小块向量检索、计算、对比速度远快于超长文本,省钱、响应更快。
2.2.1 固定大小分块
固定大小分块(Fixed-size Chunking),将文本按固定长度(如字符数或token数)切分,每个块大小一致,可能通过重叠保留上下文连贯性。
先按照固定长度切分文本,例如每 256 个字符一个 Chunk。
text
Chunk1:1~256
Chunk2:257~512
Chunk3:513~768
重叠(overlap):让相邻 Chunk 保留一部分相同内容。
例如 chunk_overlap=20:Chunk1:1~256,Chunk2:237~492
这样可以避免关键信息刚好被切在两个 Chunk 之间,减少上下文丢失。
python
from util import parse_all_documents, clean_text,clean_all_formats
from llama_index.core import Document
from typing import List
# 按照固定大小分块
def fixed_size_chunk_documents(
input_dir: str,
chunk_size: int = 500,
chunk_overlap: int = 50,
) -> List[Document]:
chunks = []
step = chunk_size - chunk_overlap
# 获取清洗后的文档
documents = clean_all_formats(input_dir)
for d in documents:
text = d.text
path = d.metadata.get("file_path")
start = 0 #分块在原文中的起始位置
idx = 0 # 当前文档的分块序号
while start < len(text):
end = min(start + chunk_size, len(text)) #当前分块结束位置
# 复制原始文档
m = dict(d.metadata)
m.update({
"source_file_path":path,
"chunk_index":idx,
"chunk_start": start,
"chunk_end": end,
})
chunks.append(Document(text=text[start:end], metadata=m))
if end >= len(text):
break
start = start + step
idx = idx + 1
for i, d in enumerate(chunks):
print(f"第{i+1}个分块")
print(d.metadata)
print(d.text)
return chunks
if __name__ == "__main__":
fixed_size_chunk_documents(config.base_path/"文档")
2.2.2 句子分块
RAG 中的句子分块(Sentence Splitting) ,就是把长文本按照句子边界切成一个个较小的文本片段,而不是简单按照固定字符数切割。
主要操作是:识别句子边界 → 将连续句子组合成合适大小的 Chunk → 保留必要的上下文 → 生成多个文本块,之后这些 Chunk 再进行向量化和入库。
例如:一段 1000 字的文章 → 按句子切分 → 每 5~10 个句子组成一个 Chunk → 最终得到多个语义较完整的文本块。
python
import re
import util
from typing import List
import config
from llama_index.core import Document
import llama_index.core
# 实现按照句子去分块的功能
def sentence_chunk_documents(
input_str : str,
max_sentences: int = 5, # 按照5个句子为一个分块
) -> List[Document]:
chunks = []
documents = util.clean_all_formats(input_str)
for d in documents:
text = d.text
if not text:
continue
path = d.metadata.get("file_path")
sents = [
s.strip()
for s in re.split(r"(?<=[。!?.!?])", text)
if s.strip()
]
# 每次遍历max_sentences这么多的句子,合并一个块
for i in range(0, len(sents), max_sentences):
part = " ".join(sents[i : i+max_sentences]).strip()
if not part:
continue
m = dict(d.metadata)
m.update({
"source_file_path": path,
"chunk_index": i // max_sentences,
"chunk_start": i,
"chunk_end": min(i+max_sentences, len(sents)),
})
chunks.append(Document(text=part, metadata=m))
for i, chunk in enumerate(chunks):
print(f"第{i+1}个分块")
print(chunk.metadata)
print(chunk.text)
return chunks
if __name__ == "__main__":
# 示例用法
input_dir = config.base_path / "Docs"
sentence_chunk_documents(input_dir, max_sentences=5)
2.2.3 语义分块
RAG 中的语义分块(Semantic Chunking) ,不是按照固定字数或句子数量切分,而是根据文本内容的语义是否连续来决定分块边界。
主要操作是:先把文本拆成句子 → 对句子进行向量化 → 计算相邻句子的语义相似度 → 相似度明显下降的位置作为分块点 → 将语义相关的句子组成一个 Chunk。
例如一篇文章从"介绍 Redis"突然转到"介绍 MySQL",两部分语义差异较大,就会在这个位置进行切分。这样得到的 Chunk 通常具有更完整的语义,便于后续向量检索。
ollama本地部署嵌入模型:
qwen3-embedding:latest 是 Qwen3 系列专门用于文本向量化(Embedding)的模型 ,它把一段文本转换成一个高维向量,RAG 中主要用于把文档 Chunk 和用户问题向量化,再通过向量相似度进行检索 。Ollama 当前的 latest 对应 Qwen3-Embedding-8B 的 Q4_K_M 量化版本,约 4.7GB,支持最长 40K 上下文 。(Ollama)
Ollama 拉取:
bash
ollama pull qwen3-embedding
官方 Ollama 页面也是这个命令。(Ollama)
拉取完成后可以查看:
bash
ollama list
然后在 Python 中使用:
python
import ollama
response = ollama.embed(
model="qwen3-embedding",
input="这是一个测试文本"
)
print(response["embeddings"])
Qwen3 Embedding 系列还提供 0.6B / 4B / 8B 三种规模;8B 的原始模型输出维度最高支持 4096。(qwenlm.github.io)
这里建议使用llamaindex中的ollama包:
bash
pip install llama-index-embeddings-ollama
python
from llama_index.embeddings.ollama import OllamaEmbedding
from typing import List
from llama_index.core.schema import Document
import util
from llama_index.core.node_parser import SemanticSplitterNodeParser
import config
# 测试嵌入模型的方法
def embedding_similarity(
sentence1: str,
sentence2: str,
) -> float:
# 1. 加载 Ollama 中的 Qwen3-Embedding
embed_model = OllamaEmbedding(
model_name="qwen3-embedding:latest",
base_url="http://localhost:11434"
)
# 2. 计算文本的向量
vec1 = embed_model.get_text_embedding(sentence1)
vec2 = embed_model.get_text_embedding(sentence2)
# 3. 根据向量求相似度
similarity = embed_model.similarity(vec1, vec2)
# 4. 打印
print(f"\n句子1: {sentence1}")
print(f"向量1: (维度 {len(vec1)}): {vec1}")
print(f"\n句子2: {sentence2}")
print(f"向量2: (维度 {len(vec2)}): {vec2}")
print(f"\n句子相似度: {similarity:.4f}")
return similarity
# 通过嵌入模型实现语义分块
def semantic_chunk_documents(
input_str: str,
) -> List[Document]:
# 1. 获取清洗之后的所有文档
documents = util.clean_all_formats(input_str)
# 2. 加载 Ollama 中的 Qwen3-Embedding
embed_model = OllamaEmbedding(
model_name="qwen3-embedding:latest",
base_url="http://localhost:11434"
)
# 3. 创建语义分块器
splitter = SemanticSplitterNodeParser(
embed_model=embed_model
)
# 4. 执行语义分块
chunks = []
for d in documents:
text = d.text
if not text:
continue
path = d.metadata.get("file_path")
# 对单篇文档执行语义切分
nodes = splitter.get_nodes_from_documents(
[
Document(
text=text,
metadata=d.metadata
)
]
)
# 5. 将 Node 转换成 Document
for i, n in enumerate(nodes):
m = dict(n.metadata)
m.update({
"source_file_path": path,
"chunk_index": i,
})
chunks.append(
Document(
text=n.text,
metadata=m
)
)
# 6. 打印结果
for i, chunk in enumerate(chunks):
print(f"第{i + 1}个分块")
print(chunk.metadata)
print(chunk.text)
print("-" * 50)
return chunks
if __name__ == "__main__":
# 示例用法
input_dir = config.base_path / "Docs"
semantic_chunk_documents(input_dir)
2.2.4 递归分块
递归分块就是先按照段落 -> 换行 -> 中英文句末 -> 分号 -> 逗号 -> 空格 -> 单字符等文档结构 进行初步切分,如果某个分块超过设定的大小限制,就继续按照更细的层级进行划分,直到每个分块都满足大小要求。相比固定大小分块,它更倾向于在自然的语义边界进行切分,因此能够尽量保持内容的完整性和上下文的连贯性。
python
from typing import List
from llama_index.core.schema import Document
from llama_index.core.node_parser import TokenTextSplitter
import util
import config
# 递归分块
def recursive_chunk_documents(
intput_str :str
) -> List[Document]:
# 1. 获取清洗后的数据
documents = util.clean_all_formats(intput_str)
# 2. 根据符号去划分文本
# 分隔符优先级列表:从前到后依次尝试,越靠前越优先(语义边界越完整)
# 段落 -> 换行 -> 中英文句末 -> 分号 -> 逗号 -> 空格 -> 单字符
separators = [
"\n\n", "\n",
".", "?", "!",
";", ",",
" ", "",
]
# 3. 创建递归分块器
spliter = TokenTextSplitter(
separator=separators[0], #首选段落标识进行分块
backup_separators=separators[1:], #备用分隔符
chunk_size=512,# 每个分块的最大token数,默认是1024
)
# 4. 使用分块器来处理清洗好的文档
chunks = []
for d in documents:
text = d.text
if not text:
continue
path = d.metadata.get("file_path")
# 进行递归划分
nodes = spliter.get_nodes_from_documents(
[Document(text=text, metadata=d.metadata)]
)
for i, n in enumerate(nodes):
m = dict(n.metadata)
m.update(
{
"source_file_path" : path,
"chunk_index": i,
}
)
chunks.append(Document(text=n.text, metadata=m))
for i, chunk in enumerate(chunks):
print(f"第{i+1}个分块")
print(chunk.metadata)
print(chunk.text)
return chunks
if __name__ == "__main__":
input_dir = config.base_path / "Docs"
recursive_chunk_documents(input_dir)
2.2.5 基于文档结构的分块
基于文档结构的分块,是利用文档自身的层级和组织方式 进行切分,例如 Markdown 中的一级、二级标题,HTML 中的标签,Word/PDF 中的章节、段落、表格等,将内容划分为具有明确主题和逻辑关系的独立单元。它的重点不是强制让每个 Chunk 达到某个固定长度,而是尽量让一个 Chunk 对应一个完整的语义和结构单元,例如一个章节、一节内容、一个问题及其答案,或者一张完整表格。这样在进行向量化和 RAG 检索时,检索结果通常具有更完整的上下文,也更容易根据标题、章节等元数据追溯原文。
python
import re
import csv
from typing import List
from llama_index.core.schema import Document
from pathlib import Path
import util
import config
from Src.config import base_path
max_chunk_chars = 1500 # 设置最大的分块长度为1500个字符
# 从ppt的解析结果提取文本
def _extract_section_text(section: dict) -> str|None:
return section.get("content")
# 处理PPT类型文档分块
def _chunk_ppt_document(
d: Document
) -> List[Document] | None:
sections = d.metadata.get("text_sections") # 获取PPT的文本结构
slide_title = d.metadata.get("title") # 获取PPT的标题
# ppt清洗之后的结构为:
# [
# {
# "title": "Slide 1 Title",
# "content": "Slide 1 Content"
# },
# {
# "title": "Slide 2 Title",
# "content": "Slide 2 Content"
# },
# ...
# ]
texts = [_extract_section_text(s) for s in sections]
texts = [t for t in texts if t] # 过滤掉空文本
full_text = "\n".join(texts)
if len(full_text) <= max_chunk_chars:
meta = {
"chunk_index": 0
}
return [Document(text=full_text, metadata=meta)]
else:
# 当前页超过了最大长度,需要进行分块
chunks = []
for i, group in enumerate(full_text):
chunk_text = group
if slide_title and slide_title not in chunk_text: # 确保每一个 PPT 分块里都带有当前页面的标题,避免分块以后丢失上下文
chunk_text = f"{slide_title}\n{chunk_text}"
meta = {
"section_index": i
}
chunks.append(Document(text=chunk_text, metadata=meta))
return chunks
# 按照文档结构对pdf类型文档进行分块
def _chunk_pdf_document(
d: Document
) -> List[Document]:
text = d.text
if not text:
return []
page_label = d.metadata.get("page_label") # 提取当前页的页码信息
base_meta = {
"page_label": page_label
}
if len(text) <= max_chunk_chars:
meta = dict(base_meta)
meta["chunk_index"] = 0
return [Document(text=text, metadata=meta)]
else:
# 当前页超过了最大长度,需要进行分块,先按照换行后的编号 `1.`、`2.`、`3.`,
# 以及 `●`、`。`、`-` 这些结构标记进行切分。
parts = [p.strip() for p in r"(?=\n(?:\d+\. |●|。|\-)\s*)".split(text) if p.strip()]
if len(parts) <= 1: # 如果没有找到结构标记,则按照空行进行切分
parts = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
# 转换成分块
chunks = []
for i, part in enumerate(parts):
meta = dict(base_meta)
meta.update(
{
"chunk_index": i
}
)
chunks.append(Document(text=part, metadata=meta))
return chunks
# 获取csv文件的头
def _read_csv_header(path : str) -> str:
with open(path, newline="", encoding="utf‑8") as f:
row = next(csv.reader(f), None) # 读取第一行获取表头
return ",".join(row)
# 处理csv类型文件
def _chunk_csv_document(
d: Document
)->List[Document]:
text = d.text
if not text:
return []
lines = [ line.strip() for line in text.splitlines() if line.strip()] # 按照行分割并去掉空行
path = d.metadata.get("file_path")
header = _read_csv_header(path)
data_lines = lines[1:] # 去掉表头行
chunks = []
for i, line in enumerate(data_lines):
chunk_text = f"{header}\n{line}" # 每个分块都包含表头和当前行数据,避免丢失上下文
meta = {
"chunk_index": i
}
chunks.append(Document(text=chunk_text, metadata=meta))
return chunks
# 单个文档分块
def _chunk_single_document(
d: Document
)->List[Document]:
suffix = Path(d.metadata.get("file_path", "")).suffix.lower()
# 1. 处理ppt类型的文档
if suffix == ".ppt" or suffix == ".pptx" or suffix == ".pptm":
return _chunk_ppt_document(d)
# 2. 处理pdf类型的文档
if suffix == ".pdf":
return _chunk_pdf_document(d)
# 3. 处理csv类型的文档
if suffix == ".csv":
return _chunk_csv_document(d)
# 4. txt类型文档,由于txt文档没有结构信息,不适合进行结构分块,因此直接返回原文档,建议使用固定大小分块或句子分块方法
return [d]
# 基于文档结构的分块
def structure_chunk_documents(
input_str: str,
) -> List[Document]:
# 1. 获取清洗之后的数据
documents = util.clean_all_formats(input_str)
# 2. 逐一分块
chunks : List[Document] = []
for d in documents:
doc_chunks = _chunk_single_document(d)
chunks.extend(doc_chunks)
# 3. 测试输出
for i, chunk in enumerate(chunks):
print(f"第{i+1}个分块")
print(chunk.metadata)
print(chunk.text)
return chunks
if __name__ == "__main__":
structure_chunk_documents(
base_path/"Docs"
)
2.2.6 基于LLM的分块
**利用大语言模型来智能地确定文本切分边界的方法。**它不依赖固定分隔符、字符数或预定义规则,而是通过向LLM 提供提示词,让模型根据语义理解、主题连贯性、逻辑结构等因素,自主决定在哪里切分以及如何合并句子。
2.2.6.1 接入大模型
这里使用deepseek进行模型调用,参考:deepseek
引入依赖
bash
pip install llama-index
pip install llama-index-llms-deepseek
调用示例如下:
python
from llama_index.llms.deepseek import DeepSeek
def create_deepseek_llm(
temperature: float = 0.5,
max_tokens: int = 1024,
) -> DeepSeek:
return DeepSeek(
model="deepseek-v4-flash",
api_key=config.deepseek_api_key,
temperature=temperature,
max_tokens=max_tokens,
)
llm = create_deepseek_llm()
2.2.6.2 大模型功能封装
注意要单独创建一个.env文件并配置好api_key:
python
# 加载项目根目录下的 .env 文件
load_dotenv(base_path / ".env")
python
# 模型调用
def create_deepseek_llm(
temperature: float = 0.5, # 模型回答的随机性,0.0表示最确定,1.0表示最随机
max_tokens: int = 1024, # 模型回答的最大长度,单位为token
) -> DeepSeek:
return DeepSeek(
model="deepseek-v4-flash",
api_key=os.getenv("DEEPSEEK_API_KEY"),
temperature=temperature,
max_tokens=max_tokens,
)
llm = create_deepseek_llm()
# 单轮对话
def complete(
prompt: str
) -> str:
return llm.complete(prompt=prompt).text
# 单独对话(流式返回)
def stream_complete(
prompt: str
) -> Generator[str, None, None]: # Generator[产生的数据类型, send()传入的数据类型, 最终额外return的数据类型]
for chunk in llm.stream_complete(prompt=prompt):
if chunk.delta:
yield chunk.delta # yield关键字用于生成器函数中,表示生成一个值并暂停函数的执行,等待下一次迭代请求。
# 多轮对话
def chat(
user_prompt: str,
sys_prompt: str,
) -> str|None:
messages = [
ChatMessage(role = MessageRole.SYSTEM, content=sys_prompt),
ChatMessage(role=MessageRole.USER, content=user_prompt),
]
return llm.chat(messages).message.content
# 多轮对话(流式)
def stream_chat(
user_prompt: str,
sys_prompt: str,
) -> Generator[str, None, None]:
messages = [
ChatMessage(role=MessageRole.USER, content=user_prompt),
ChatMessage(role=MessageRole.SYSTEM, content=sys_prompt),
]
for chunk in llm.stream_chat(messages):
if chunk.delta:
yield chunk.delta
2.2.6.3 大模型分块代码
python
import re
from typing import List
from llama_index.core.schema import Document
import config
import util
# 因为LLM返回的是字符串类型,我们需要特殊标识
CHUNK_DELIMITER = "===CHUNK==="
# 系统提示词
SYSTEM_PROMPT = f"""你是RAG知识库文档分块助手。 请把用户提供的文档切分为多个语义完整、适合向量检索的分块。
要求:
1. 每个分块围绕一个独立的主题
2. 尽量不要拆分段落,不要拆分句子
3. 保持原始文本的语义不变
4. 各个分块之间用{CHUNK_DELIMITER}分隔开
5. 只输出分块的正文,不要额外的信息
"""
# 解析LLM返回的文本
def _parse_llm_chunks(response :str) -> List[str]:
parts = re.split(fr"\n?{CHUNK_DELIMITER}\n?", response.strip())
chunks = [part.strip() for part in parts]
if not chunks:
chunks=[response.strip()]
return chunks
# 调用LLM
def _llm_split_text(text: str, max_chunk_chars:int) -> List[str]:
USER_PROMPT = (
f"请将以下文档分块,每个块不超过{max_chunk_chars}字符:\n\n"
f"{text}"
)
all_chunks: List[str] =[]
result = util.chat(USER_PROMPT, SYSTEM_PROMPT)
all_chunks.extend(_parse_llm_chunks(result))
return all_chunks
# LLM分块
def llm_chunk_documents(
input_str:str,
max_chunk_chars: int=500
) -> List[Document]:
# 1. 获取清洗后的数据
documents = util.clean_all_formats(input_str)
# 2. 逐一执行LLM分块操作
chunks: List[Document] = []
for d in documents:
text = d.text
# 3. 调用LLM来处理分块
text_chunks = _llm_split_text(text, max_chunk_chars)
# 4. 修改元数据
for idx, chunk_text in enumerate(text_chunks):
m={
"chunk_index":idx
}
chunks.append(Document(text=chunk_text, metadata=m))
# 测试打印输出
for i, chunk in enumerate(chunks):
print(f"第{i+1}个分块")
print(chunk.metadata)
print(chunk.text)
return chunks
if __name__ == "__main__":
llm_chunk_documents(config.structure_path)
2.3 向量嵌入
向量嵌入(Embedding) :将文本、图像、音频等非结构化数据转换为高维向量 ,使语义越相似的数据在向量空间中距离越近,从而便于计算相似度和进行检索。
2.3.1 余弦相似度
余弦相似度是计算两个向量之间夹角的余弦值。余弦距离(Cosine distance)就是用1减去这个获得的余弦相似度。取值范围−1,1
- 计算公式:
cos ( θ ) = a ⋅ b ∣ ∣ a ∣ ∣ × ∣ ∣ b ∣ ∣ \cos(\theta)=\frac{a \cdot b}{||a|| \times ||b||} cos(θ)=∣∣a∣∣×∣∣b∣∣a⋅b - 逻辑:只看向量方向夹角,忽略向量长度(模长)。
- 适用场景:
- 绝大多数文本 Embedding(OpenAI、BGE、Sentence-BERT 等);
- 文本语义匹配,只关心语义方向,不关心向量长度;
- 取值范围:-1, 1 ,越接近 1 代表语义越相似,数值越大越相似。
- llamaindex默认采用这个来进行RAG检索。
计算相似度的代码已经包含在语义分块中了,这里省略,注意ollama部署步骤。
2.3.2 欧氏距离
欧式距离(也叫欧几里得相似度),就是把 "文档向量" 和 "查询向量" 当成高维空间里的两个点,算它们之间的直线距离;距离越小,说明文本越相似。取值范围[0,+∞)
- 计算公式:
d i s t = ∑ i = 1 n ( a i − b i ) 2 dist = \sqrt{\sum_{i=1}^{n} (a_i - b_i)^2} dist=i=1∑n(ai−bi)2 - 逻辑:计算向量空间两点的直线距离,数值越小越相似(和余弦 / 点积相反)。
- 适用场景:
- 图像、数值特征向量;
- 很少用于文本 Embedding,文本向量高维下欧氏距离区分度很差;
- 向量库内部会自动转换距离为相似度打分,业务层不用手动换算。
2.3.3 点积距离
点积距离(Dot Product Distance),用两个向量的点积来衡量相似度,值越大越相似。
计算公式:
a ⋅ b ˙ = ∑ i = 1 n a i b i \dot{a \cdot b}=\sum_{i=1}^{n} a_{i} b_{i} a⋅b˙=i=1∑naibi
逻辑:同时兼顾向量方向 + 向量长度。
- 适用场景:
- 做过归一化(L2 Norm) 的向量:归一化后点积 = 余弦相似度,归一化不是必须的,具体由 Embedding 模型决定
- 模型输出向量自带长度表征重要性的场景(部分自研 Embedding);
- 注意:未归一化向量不能随便用,长向量会天然得分更高,干扰语义匹配,归一化之后排除了长度的影响,这样才时候进行RAG检索。
python
from llama_index.core.base.embeddings.base import SimilarityMode
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.embeddings.ollama import OllamaEmbedding
import config
def embedding_similarity(
sentence1: str,
sentence2: str,
mode: SimilarityMode = SimilarityMode.DEFAULT # 相似度计算模式,默认使用余弦相似度
) -> float:
# 1. 加载 Ollama 中的 Qwen3-Embedding
embed_model = OllamaEmbedding(
model_name="qwen3-embedding:latest",
base_url="http://localhost:11434"
)
# 2. 计算文本的向量
vec1 = embed_model.get_text_embedding(sentence1)
vec2 = embed_model.get_text_embedding(sentence2)
# 3. 根据向量求相似度
similarity = embed_model.similarity(vec1, vec2, mode=mode)
# 4. 打印
print(f"\n句子1: {sentence1}")
print(f"向量1: (维度 {len(vec1)}): {vec1}")
print(f"\n句子2: {sentence2}")
print(f"向量2: (维度 {len(vec2)}): {vec2}")
print(f"\n句子相似度: {similarity:.4f}")
return similarity
if __name__ == "__main__":
sentence1="爸爸的妈妈叫奶奶"
sentence2="妈妈的爸爸叫外公"
print(embedding_similarity(sentence1, sentence2, mode=SimilarityMode.DEFAULT)) # 余弦相似度
print(embedding_similarity(sentence1, sentence2, mode=SimilarityMode.DOT_PRODUCT)) # 点积相似度
# LlamaIndex中为了把数值排序规则统一起来,改成了结果取负数,这样越相似得分越大,越不相似得分越小
print(embedding_similarity(sentence1, sentence2, mode=SimilarityMode.EUCLIDEAN)) # 欧几里得相似度
2.4 向量数据库
2.4.1 什么是向量数据库?
向量数据库是专门用于存储、管理和检索高维 Embedding 向量的数据库,核心优势是利用向量索引快速进行相似度搜索,从而高效找到与查询语义最相关的数据。
常见的向量数据库有如下种类:
- HNSW:工业最主流,速度、精度均衡(Milvus、Chroma、Pinecone)
- FAISS IVF:Facebook 开源,适合批量大规模数据
- Annoy、DiskANN:适合磁盘大容量存储
向量数据库专门优化了高维向量的相似度检索,比关系型数据库更适合 RAG 的语义搜索。
2.4.2 Milvus与MySQL核心概念对比
| Milvus | MySQL |
|---|---|
| Collection 集合 | Table 数据表 |
| Entity 实体 | Row 行记录 |
| Field 字段 | Column 列 |
| Partition 分区 | 分表 / 分区表 |
| Segment 磁盘数据文件 | |
| Shard 分片 | 分库分表 |
| Proxy | MySQL Proxy 网关 |
| QueryNode | 查询执行器 |
| etcd 元数据 | 系统库 information_schema |
| 对象存储 | 磁盘数据目录 |
2.4.3 Milvus与MySQL的区别
| 对比项 | 传统关系型数据库 | 向量数据库 |
|---|---|---|
| 存储对象 | 字符串、数字、表格 | 高维浮点向量 |
| 查询方式 | 精确匹配、模糊关键词 | 近似相似度匹配 |
| 适用场景 | 订单、用户、业务台账 | RAG 知识库、图文检索、推荐 |
| 高维性能 | 上万维检索极慢,全量遍历 | 内置索引算法,亿级向量秒查 |
2.4.4 部署Milvus 向量数据库
bash
pip install milvus-lite pymilvus
Milvus-Lite 是 Milvus 向量数据库的轻量级嵌入式版本,专为本地开发和测试场景设计。
#mermaid-svg-ba5yD5lEFqkpmWd6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ba5yD5lEFqkpmWd6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ba5yD5lEFqkpmWd6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ba5yD5lEFqkpmWd6 .error-icon{fill:#552222;}#mermaid-svg-ba5yD5lEFqkpmWd6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ba5yD5lEFqkpmWd6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ba5yD5lEFqkpmWd6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ba5yD5lEFqkpmWd6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ba5yD5lEFqkpmWd6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ba5yD5lEFqkpmWd6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ba5yD5lEFqkpmWd6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ba5yD5lEFqkpmWd6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ba5yD5lEFqkpmWd6 .marker.cross{stroke:#333333;}#mermaid-svg-ba5yD5lEFqkpmWd6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ba5yD5lEFqkpmWd6 p{margin:0;}#mermaid-svg-ba5yD5lEFqkpmWd6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ba5yD5lEFqkpmWd6 .cluster-label text{fill:#333;}#mermaid-svg-ba5yD5lEFqkpmWd6 .cluster-label span{color:#333;}#mermaid-svg-ba5yD5lEFqkpmWd6 .cluster-label span p{background-color:transparent;}#mermaid-svg-ba5yD5lEFqkpmWd6 .label text,#mermaid-svg-ba5yD5lEFqkpmWd6 span{fill:#333;color:#333;}#mermaid-svg-ba5yD5lEFqkpmWd6 .node rect,#mermaid-svg-ba5yD5lEFqkpmWd6 .node circle,#mermaid-svg-ba5yD5lEFqkpmWd6 .node ellipse,#mermaid-svg-ba5yD5lEFqkpmWd6 .node polygon,#mermaid-svg-ba5yD5lEFqkpmWd6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ba5yD5lEFqkpmWd6 .rough-node .label text,#mermaid-svg-ba5yD5lEFqkpmWd6 .node .label text,#mermaid-svg-ba5yD5lEFqkpmWd6 .image-shape .label,#mermaid-svg-ba5yD5lEFqkpmWd6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-ba5yD5lEFqkpmWd6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ba5yD5lEFqkpmWd6 .rough-node .label,#mermaid-svg-ba5yD5lEFqkpmWd6 .node .label,#mermaid-svg-ba5yD5lEFqkpmWd6 .image-shape .label,#mermaid-svg-ba5yD5lEFqkpmWd6 .icon-shape .label{text-align:center;}#mermaid-svg-ba5yD5lEFqkpmWd6 .node.clickable{cursor:pointer;}#mermaid-svg-ba5yD5lEFqkpmWd6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ba5yD5lEFqkpmWd6 .arrowheadPath{fill:#333333;}#mermaid-svg-ba5yD5lEFqkpmWd6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ba5yD5lEFqkpmWd6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ba5yD5lEFqkpmWd6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ba5yD5lEFqkpmWd6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ba5yD5lEFqkpmWd6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ba5yD5lEFqkpmWd6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ba5yD5lEFqkpmWd6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ba5yD5lEFqkpmWd6 .cluster text{fill:#333;}#mermaid-svg-ba5yD5lEFqkpmWd6 .cluster span{color:#333;}#mermaid-svg-ba5yD5lEFqkpmWd6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ba5yD5lEFqkpmWd6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ba5yD5lEFqkpmWd6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-ba5yD5lEFqkpmWd6 .icon-shape,#mermaid-svg-ba5yD5lEFqkpmWd6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ba5yD5lEFqkpmWd6 .icon-shape p,#mermaid-svg-ba5yD5lEFqkpmWd6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ba5yD5lEFqkpmWd6 .icon-shape .label rect,#mermaid-svg-ba5yD5lEFqkpmWd6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ba5yD5lEFqkpmWd6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ba5yD5lEFqkpmWd6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ba5yD5lEFqkpmWd6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 标准 Milvus 部署
gRPC
PyMilvus
Python端
Milvus 服务
独立进程/容器
Milvus-Lite 部署
PyMilvus
Python SDK
Milvus 引擎
C++ 核心
2.4.5 向量数据库常见操作
2.4.5.1 核心概念
| 概念 | 类比 | 示例中的体现 |
|---|---|---|
| Database(库) | 一个 SQLite 文件 / 目录 | ./milvus_demo.db |
| Collection(集合) | 一张表 | my_collection |
| Schema(模式) | 表结构 | 主键 id 、向量 vector 、标量 title/content/category |
| Vector(向量) | 语义坐标 | N 维浮点数组 |
| Metric(距离度量) | 相似度算法 | COSINE (余弦相似度) |
| Insert | 写入行 | insert_data() |
| Search | 按向量找最近邻 | search() |
| Query | 按条件过滤 | query_by_filter() |
| Delete | 按条件删除 | delete_by_filter() |
2.4.5.2 连接数据库
python
# 1. 连接milvus
def connect(dbpath : str) -> MilvusClient:
global client
client = MilvusClient(str(dbpath))
return client
要点
Milvus Lite 的"库"是目录,不是单个 .sqlite 文件
开发阶段用「删目录」比 drop_collection 更稳,尤其在 Windows 上
2.4.5.3 获取client操作db
python
# 2. 获取client
def get_client() -> MilvusClient:
return client
# 3. 创建db,类比mysql建库,其实就是创建一个文件夹,里面存放collection的文件夹
def create_db(dbname : str) -> MilvusClient:
return connect(base_path / f"{dbname}.db")
# 4. 切换db use db
def use_db(dbname : str) -> MilvusClient:
return connect(config.base_path / f"{dbname}.db")
# 5. 列出所有的db
def list_dbs() -> List[str]:
return [p.stem for p in config.base_path.glob("*.db")]
2.4.5.4 操作collection
python
# 6. 创建collection,类比mysql建表,collection是milvus中存储向量的最小单位,需要指定向量的维度dimension
def create_collection(name: str, dimension: int) -> None:
client = get_client()
if client.has_collection(name):
client.drop_collection(collection_name=name)
client.create_collection(collection_name=name, dimension=dimension)
# 7. 删除collection
def drop_collection(name:str) -> None:
get_client().drop_collection(collection_name=name)
2.4.5.5 数据常见操作
python
# 8. 插入数据,类比mysql的insert into table values(...),rows是一个字典列表,每个字典表示一行数据,键是字段名,值是字段值
# 具体的字段名和字段值需要根据实际情况来定,返回值是一个字典,包含插入的结果信息
def insert(collection_name: str, rows: List[Dict[str, Any]]) -> Dict:
return get_client().insert(collection_name=collection_name, data=rows)
# 9. 查询数据,ids为需要查询的向量id列表,fields为需要返回的字段列表,如果为空则只返回id和vector字段
def get_by_ids(collection_name: str, ids: List[int], fields: Optional[List[str]] = None) -> List[Dict]:
return get_client().get(collection_name=collection_name, ids=ids, output_fields=fields)
# 工具:获得一段文本的向量
def get_text_vector(text: str) -> List[float]:
embed_model = OllamaEmbedding(
model_name="qwen3-embedding:latest",
base_url="http://localhost:11434"
)
vector = embed_model.get_text_embedding(text)
return vector

这是插入后的目录结构:
plain
test_milvus.db/
├── .database-staging/ # 数据库内部临时目录
│
├── collections/ # Collection 相关数据
│ └── test_collection/ # 你的 Collection
│ └── partitions/ # 分区数据
│ └── _default/ # 默认分区
│ └── data/ # 实际数据
│ └── *.parquet
│
├── wal/ # WAL,记录数据变更操作
│
├── manifest.json # 数据库元数据/当前状态信息
├── manifest.json.prev # 上一个 manifest
├── schema.json # Collection 的 Schema 定义
├── databases/ # 数据库相关信息
└── LOCK # 防止多个进程同时操作数据库
2.4.5.6 向量相似度检索
python
# 10. 向量查询
# Collection、查询文本、返回数量和返回字段,
# 先将文本转为 Embedding 向量,再在 Milvus 中进行相似度检索并返回 Top-K 结果。
def search_by_text(
collection_name:str,
limit: int=5, # 默认返回五条最相似的结果
text: str="",
fields: Optional[List[str]] = None,
) -> List[List[dict]]:
vectors = [embed_model.get_text_embedding(text)]
return get_client().search(
collection_name=collection_name,
data=vectors,
limit=limit,
output_fields=fields,
)
3. 数据检索
RAG 的核心思想是:先从私有知识库或文档库中检索与用户问题最相关的真实资料,再将这些资料作为上下文交给大模型,让模型基于检索结果进行回答,从而减少脱离真实数据的凭空编造。
简单来说就是:先检索资料,再结合资料生成答案。
3.1.1 用户查询
用户查询(User Query)是用户向检索系统或对话系统输入的原始问题、指令或关键词,是 RAG 检索与生成流程的起点。简单来说,就是用户向 AI 提出的具体问题,例如:"公司新发布的休假政策是什么?"
3.1.2 查询向量化
用户输入问题后,系统使用与索引阶段相同的嵌入模型,将查询文本转换为向量,使其与知识库中的文档向量处于同一语义空间,从而为后续的向量检索提供基础。
3.1.3 向量检索
将查询转换为向量后,系统在向量数据库中检索与其最相似的文本块。本质上是计算查询向量与知识库中各向量之间的相似度或距离,距离越近(或相似度越高),表示语义越相关。
3.1.4 代码实现
python
# 向量检索
import shutil
import time
import config
import milvus
from sentenceChunk import sentence_chunk_documents
# 1. 构建向量数据库
DB_NAME = "vectorSearch"
COLLECTION = "vectorSearch"
DIMENSION = 4096
def init():
# 1. 判断db是否存在
db_path = config.base_path / f"{DB_NAME}.db"
if db_path.exists():
shutil.rmtree(db_path) # 如果存在就删除,重新创建
time.sleep(0.5)
# 2. 创建db
milvus.create_db(DB_NAME)
milvus.create_collection(COLLECTION, DIMENSION)
# 2. 文本分块,分块内容逐步向量化入库(文本分块的方案->句子分块)
def index_to_milvus() -> int:
chunks = sentence_chunk_documents(config.base_path / "Docs", 1)
if not chunks:
raise ValueError("分块结果为空,请检测数据集内容")
rows = []
# 把分好块的内容向量化,拼装成要入库的数据
for i, chunk in enumerate(chunks):
vec = milvus.embed_model.get_text_embedding(chunk.text)
rows.append(
{
"id": i+1,
"vector": vec,
"text": chunk.text
}
)
milvus.insert(COLLECTION, rows)
return len(rows)
# 3. 文本检索
def search_similar(query:str, limit: int = 5):
milvus.get_client().load_collection(collection_name=COLLECTION)
# 进行 Search 或 Query 前,Collection 必须处于 Loaded 状态,加载进内存中
results = milvus.search_by_text(
COLLECTION,
text=query,
limit=limit,
fields=["id", "text"],
)
hits = results[0] if results else [] # results是一个二维列表,每个元素是一个查询结果列表,这里取第一个查询结果列表
if not hits:
print("未检索到相关结果")
return
print(f"\n用户查询:{query}")
print(f"共{len(hits)} 条相似的结果")
# hit = {
# "id": 3, # 这条数据的 ID
# "distance": 0.1234, # 与查询向量的距离/相似度指标
# "entity": { # 这条数据本身的字段
# "id": 3,
# "text": "Redis 是一种内存数据库..."
# }
# }
for rank, hit in enumerate(hits, start=1): # rank从1开始
entity = hit.get("entity") # 拿出实际数据字段
print(f"\n[{rank}] 距离 distance={hit.get('distance'):.4f}")
print(f"id={entity.get('id', hit.get('id'))}")
print(entity.get("text"))
# 4. 循环输入用户查询,来获取知识库内容
def search():
print("\n向量检索就绪,请输入用户查询问题")
while True:
query = input("请输入查询问题:").strip()
if query.lower() == ("quit"):
break
if not query:
continue
search_similar(query)
if __name__ == "__main__":
init()
index_to_milvus()
search()
4. 提示词增强
RAG 中的提示词增强(Prompt Enhancement)是将用户问题、检索文档和指令模板 组合成完整提示词,引导大模型基于检索到的外部资料回答,从而减少幻觉、提升准确性,并控制输出格式与引用。
4.1 提示词模板化
可以整理成下面这样,更适合作为 RAG 笔记:
RAG 中的提示词模板化
RAG 中的提示词模板化,是将角色、任务、上下文、用户问题和输出规则 预先设计成固定模板,再通过占位符动态填入检索结果和用户问题,使每次发送给大模型的指令统一、可控、可复用,从而减少幻觉并提升回答质量。
一个典型的 RAG 提示词模板如下:
plain
【角色】你是专业客服/技术顾问,只基于提供的文档回答。
【任务】根据提供的文档回答用户问题,禁止编造信息。
【上下文】
{context}
# 动态填入检索到的文档片段
【用户问题】
{question}
# 动态填入用户当前问题
【输出要求】
1. 准确、简洁,分点说明;
2. 文档中没有相关信息时,明确说明;
3. 不使用文档之外的信息。
4.2 上下文压缩与过滤
4.2.1 背景
RAG 检索后通常会得到多个相关文档片段,但存在以下问题:
- 片段过长,包含大量冗余内容;
- 存在与用户问题无关的垃圾片段;
- 存在重复或语义相近的内容;
- 全部输入大模型会导致 Token 消耗大、成本高、回答容易跑偏甚至超时。
因此,需要对检索结果进行 上下文压缩 + 过滤 ,将其转化为精简、相关、低 Token 的有效上下文。
4.2.2 什么是上下文压缩
RAG 上下文压缩,是在保留文档有效信息和原意的基础上,对检索到的长文本进行精简和提炼,降低上下文长度,而不是直接删除整篇文档。
常见方式:
- 抽取式压缩:从长文本中提取关键句、关键段落,删除冗余内容。
- 生成式压缩:利用模型对长文本进行重写、总结和提炼,用更少的文字表达核心信息。
- 切块精简:将较大的文本块进一步拆分,只保留与用户问题高度相关的局部内容。
4.2.3 什么是过滤
RAG 过滤,是对检索得到的多个文档片段进行筛选,删除无用、垃圾和不相关内容,只保留真正有价值的片段提供给大模型。
常见方式:
-
相似度阈值过滤(最常用)
向量检索得到的每个片段都有相似度分数,设置阈值(如 0.75),低于阈值的直接过滤,去除语义不相关的片段。
-
重复过滤
对内容高度重复或语义相近的片段去重,只保留一份,避免浪费 Token 和干扰模型。
-
规则黑名单过滤
根据规则过滤乱码、无意义字符、广告、页眉页脚、目录,以及过短或过长的无效片段。
-
业务权限过滤
在多租户、企业知识库等场景中,对用户无权访问的文档进行过滤,防止敏感数据泄露。
-
语义相关性过滤
先通过粗召回获取大量候选片段,再使用重排模型(Reranker)重新评分,只保留 Top-N 最相关的结果,其余过滤掉。
4.3 角色设定
在 System Prompt 中明确模型的角色、能力边界和输出规范,对模型进行约束,使其优先基于检索内容回答,从而减少幻觉并提升回答的可控性。
-
身份定位(我是谁)
明确模型的专业角色和负责范围。
例:你是金融领域资深分析师,只回答金融产品与市场相关问题。
-
能力边界(能做 / 不能做)
- 只能基于提供的参考文档回答,不得编造文档之外的信息;
- 信息不足时,应明确说明:"根据现有资料,无法完整回答"。
-
输出规范(格式 / 语气)
- 按要求标注引用来源,如:
[来源:文档 X 第 Y 页]; - 保持专业、简洁的表达;
- 关键数据必须与原文保持一致。
- 按要求标注引用来源,如:
4.4 代码实现
python
import shutil
import time
from typing import List
import config
import milvus
import sentenceChunk
# 提示词的模版
PROMPT_TEMPLATE = """【角色】你是一个专业的客服/技术顾问,只基于文档回答问题。
【任务】根据文档回答用户的问题,禁止瞎编乱造
【上下文】
{context}
【用户的问题】
{question}
【输出要求】
1. 准确、简洁,分条回答;
2. 当用户查询无关信息的时候,直接说"文档中不存在";
3. 不要用文档以外的知识去回答。
"""
DB_NAME = "ragPromptEnhancer"
COLLECTION = "ragPromptEnhancer"
DIMENSION = 4096
# 1. 构建向量数据库
def init() -> None:
# 1. 判断db是否存在
db_path = config.base_path / f"{DB_NAME}.db"
if db_path.exists():
shutil.rmtree(db_path)
time.sleep(0.5)
# 2. 创建db
milvus.create_db(DB_NAME)
milvus.create_collection(COLLECTION, DIMENSION)
# 2. 把知识库分块,入库
def index_to_milvus(doc_dir: str) -> int:
# 1. 句子分块
chunks = sentenceChunk.sentence_chunk_documents(doc_dir, max_sentences=1)
if not chunks:
raise ValueError(f"句子分块结果为空,请重新检查目录:{doc_dir}")
# 2. 构造插入向量数据库的数据集
rows = []
for i, chunk in enumerate(chunks):
# 3. 计算向量
vec = milvus.embed_model.get_text_embedding(chunk.text)
rows.append({"id": i+1, "vector":vec, "text": chunk.text})
# 4. 向量入库
milvus.insert(COLLECTION, rows)
print(f"已经完成入库:{len(rows)}, 向量的维度{DIMENSION}")
return len(rows)
# 3. 实现向量检索
def retrieve(question:str, top_k: int = 5)-> List[dict]:
milvus.get_client().load_collection(collection_name=COLLECTION) # 进行查询前,一定要记住加载collection到内存中
results = milvus.search_by_text(
collection_name=COLLECTION,
text=question,
limit=top_k,
fields=["id", "text"]
)
return results[0] if results else []
# 4. 实现上下文的压缩
def compress_context(texts: List[str], max_chars: int=500) -> str:
seen = set() # 用来记录已经出现过的文本,避免重复
parts = []
total = 0 # 用来记录当前已经有了多少个字符
for text in texts:
seen.add(text)
sep_len = 2 if parts else 0 # 计算分隔符的长度,如果是第一个文本,就不需要分隔符,否则需要加上两个换行符
# 需要对已有长度进行判断
if total + sep_len + len(text) > max_chars:
# 记录当前的字符串容量
remain = max_chars - total - sep_len
if remain > 20: # 如果剩余的容量大于20个字符,就截取一部分文本,并加上省略号,否则没有截取的意义
parts.append(text[:remain] + "...")
break
# 剩余的文本直接舍弃,不再加入
else:
parts.append(text)
total += sep_len + len(text)
return "\n\n".join(parts)
# 5. 实现上下文的过滤
def filter_chunks(hits: List[dict], min_similarity: float = 0.5) -> List[str]:
texts = []
for hit in hits:
if hit.get("distance", 0.0) < min_similarity: # COSINE 指标越大越相似
continue
text = hit.get("entity", {}).get("text","").strip()
if len(text) >= 10: # 加入在数据库中查询出来的内容太短,直接舍弃
texts.append(text)
return texts
# 6. 构造提示词
def build_prompt(context: str,question: str,) -> str:
if not context:
context = "(无相关的文档片段)"
return PROMPT_TEMPLATE.format(context=context, question=question)
# 7. 根据原始文档和用户问题,生成提示词
def get_prompt(
question: str,
top_k: int=5,
min_similarity: float=0.5,
max_chars: int =500,
) -> str:
# 提示词增强过程为:检索->过滤->压缩->构造提示词
hits = retrieve(question, top_k=top_k)
texts = filter_chunks(hits, min_similarity=min_similarity)
context = compress_context(texts, max_chars=max_chars)
prompt = build_prompt(context, question)
return prompt
# 8. 处理方法(循环生成增强后的提示词)
def interactive():
while True:
question = input("请输入用户查询的问题: ").strip()
if question.lower() in ("quit"):
break
if not question:
continue
prompt = get_prompt(question)
print(prompt)
if __name__ == "__main__":
init()
index_to_milvus(config.base_path / "Docs")
interactive()
5. LLM 生成
5.1 LLM 概念
就是上面把检索到的参考文档 + 用户问题(增强后的提示词),一起喂给大模型 LLM,让它基于这些真实资料,整理、总结、组织语言,输出最终答案。
5.2 代码实现
python
import util
import time
from typing import List
from llama_index.core import Document
from config import base_path
import fixedSizedChunk
import llmChunk
import ragPromptEnhancer
import recursiveChunk
import semanticChunk
import sentenceChunk
import structureChunk
if __name__ == "__main__":
# 1. 生成数据库
ragPromptEnhancer.init()
# 2. 数据清洗
chunks: List[Document] = []
# chunks = fixedSizedChunk.fixed_sized_chunk_documents(base_path / "Docs")
chunks = sentenceChunk.sentence_chunk_documents(base_path / "Docs")
# chunks = semanticChunk.semantic_chunk_documents(base_path / "Docs")
# chunks = recursiveChunk.recursive_chunk_documents(base_path / "Docs")
# chunks = structureChunk.structure_chunk_documents(base_path / "Docs")
# chunks = llmChunk.llm_chunk_documents(base_path / "Docs")
# 3. 数据入库
ragPromptEnhancer.index_to_milvus_by_chunks(chunks)
# 4. 提示词增强
while True:
question = input("请输入用户查询的问题: ").strip()
if question.lower() in ("quit"):
print("已经退出循环")
break
if not question:
continue
prompt = ragPromptEnhancer.get_prompt(question)
print(prompt)
print("======================================")
for chunk in util.stream_complete(prompt):
print(chunk, end="")
print("\n======================================")
6. Chatgpt风格医疗助手
6.1 数据获取
Hugging Face是AI 时代的 GitHub,全球最大开源 AI 模型社区 + 全套 AI 开发工具链,主打让普通人低成本使用、微调、部署各类大模型、AI 生成模型。
Hugging Face 官方现在推荐使用 hf CLI 下载数据集。(Hugging Face)
在 RAG 环境里:
bash
conda activate RAG
pip install -U huggingface_hub
下载整个数据集:
bash
hf download InfiniFlow/medical_QA \
--repo-type dataset \
--local-dir ./medical_QA
下载完成后,当前目录会有:
text
medical_QA/
├── ...
└── Internal medicine_QA_all.csv
--local-dir 可以指定数据实际保存的位置。
也可以只下载它:
bash
hf download InfiniFlow/medical_QA \
"Internal medicine_QA_all.csv" \
--repo-type dataset \
--local-dir ./medical_QA
6.2 数据简化
完整的csv数据集过大,这里随机抽取千分之一的数据,构建成简化版本测试集test_QA用于测试,经过验证效果之后再来用完整数据集进行测试。
python
import csv
import random
from pathlib import Path
from Src.config import data_path, dataless_path
# 用于精简测试集的函数,从原始数据集中随机采样部分数据行,并将其保存到目标目录中。
def sample_csv_files():
source_dir = Path(data_path)
target_dir = Path(dataless_path)
target_dir.mkdir(parents=True, exist_ok=True)
for csv_file in source_dir.glob("*.csv"):
with open(csv_file, "r", encoding="utf-8-sig", newline="") as src:
rows = list(csv.reader(src))
if rows:
sample_size = max(1, len(rows) // 1000)
sampled_rows = random.sample(rows, min(sample_size, len(rows)))
else:
sampled_rows = []
with open(target_dir / csv_file.name, "w", encoding="utf-8", newline="") as dst:
print(f"已经采样 {len(sampled_rows)} 行数据到 {target_dir / csv_file.name}")
csv.writer(dst).writerows(sampled_rows)
if __name__ == "__main__":
sample_csv_files()
6.3 数据分块
6.3.1 数据分块
python
# 1. 数据的分块
def load_medical_qa(data_dir: str) -> List[Dict[str, str]]:
# 1. 读取目录下面所有的csv文件,每行是一条回答
data_dir = Path(data_dir)
csv_files = sorted(data_dir.glob("*.csv")) # 收集csv文件
if not csv_files:
raise FileNotFoundError(f"当前目录{data_dir} 里面没有csv文件")
# 2. 存放解析后的问答列表
qa_list: List[Dict[str, str]] = []
encodings = ["utf-8-sig", "utf-8", "gb18030", "gbk"]
# 3. 循环处理csv文件
for csv_file in csv_files:
df = None
for enc in encodings:
try:
df = pd.read_csv(
csv_file,
None,
"question", "answer"],
encoding=enc,
)
break
except UnicodeError:
continue
if df is None: # 所有的编码格式去打开csv文件都是失败的
df = pd.read_csv(csv_file, header=None, names=["question", "answer"])
df = df.fillna("")
for _, row in df.iterrows():
question=str(row["question"]).strip()
answer = str(row["answer"]).strip()
if question:
qa_list.append(
{
"question":question,
"answer":answer,
}
)
if not qa_list:
raise ValueError(f"目录 {data_dir} 中间没有有效的医疗问答数据")
return qa_list
6.4 数据入库
6.4.1 初始化数据库
python
# 2.创建向量数据库
def init_medical_db() -> None:
client = milvus.connect(Project1_path / "medical.db")
if not client.has_collection(COLLECTION):
milvus.create_collection(COLLECTION, DIMENSION)
6.4.2 数据向量化
python
# 3. 数据向量化
def build_milvus_rows(
qa_list: List[Dict[str, str]],
embed_bath_size: int=EMBED_BATCH_SIZE,
) -> List[Dict[str, Any]]:
# 1. 只转换question
questions = [qa["question"] for qa in qa_list]
# 2. 记录所有的向量数组
vectors: List[List[float]] = []
# 3. 按照批处理来向量化
for start in range(0, len(questions), embed_bath_size):
batch_texts = questions[start: start+embed_bath_size]
batch_vectors = milvus.embed_model.get_text_embedding_batch(texts=batch_texts)
vectors.extend(batch_vectors)
# 看向量化的进度
print(f"已经执行完向量化 {min(start + embed_bath_size, len(questions))} / {len(questions)}条")
# 4. 处理向量化结果
if len(vectors) != len(qa_list):
raise ValueError(
f"向量数 {len(vectors)} 与问答数 {len(qa_list)} 数目不一致"
)
# 5. 构建待插入的数据
rows: List[Dict[str, Any]] = []
for i, (qa, vector) in enumerate(zip(qa_list, vectors), start=1):
text = f"问:{qa["question"]}\n答:{qa["answer"]}"
rows.append(
{
"id":i,
"vector":vector,
"text": text,
"question":qa["question"],
"answer":qa["answer"],
}
)
print(f"构造待插入数据 {len(rows)}条")
return rows
6.4.3 向量数据入库
python
# 4. 向量化的数据插入向量数据库
def insert_data(
rows: List[Dict[str, Any]],
batch_size: int = INSERT_BATCH_SIZE,
) -> int:
total = len(rows)
inserted = 0
for batch_idx, start in enumerate(range(0, total, batch_size)) :
batch = rows[start: start + batch_size]
milvus.insert(COLLECTION, batch)
inserted += len(batch)
print(f"已经入库 {inserted}/{total} 条")
if start + batch_size < total:
time.sleep(0.5)
print(f"已经完成入库:{inserted} 条, 向量维度 {DIMENSION}")
return inserted
6.5 数据检索
python
# 5. 数据检索
def search_medical(question: str, top_k: int = 5) -> List[dict]:
# 1. 连接数据库
connect_db()
# 2. 查询操作
results = milvus.search_by_text(
collection_name=COLLECTION,
text=question,
limit=top_k,
fields=["id", "text", "question", "answer"],
)
# 3. 结果返回
return results[0] if results else []
6.6 生成提示词
直接复用前面写好的生成提示词的功能。向量检索、数据过滤、上下文压缩、模版生成提示词一起实现。
python
# 6. 提示词增强
def build_medical_prompt(question: str, top_k: int = 5, max_chars: int = 4000) -> str:
hits = search_medical(question, top_k=top_k)
context_parts = []
for hit in hits:
entity = hit.get("entity", {})
question_text = str(entity.get("question", "")).strip()
answer_text = str(entity.get("answer", "")).strip()
text = str(entity.get("text", "")).strip()
context_parts.append(text or f"问:{question_text}\n答:{answer_text}")
context = "\n\n".join(context_parts)[:max_chars]
if not context:
context = "(无相关的文档片段)"
return (
"【角色】你是一个专业的医疗客服,只基于文档回答问题。\n"
"【任务】根据文档回答用户的问题,禁止瞎编乱造。\n"
f"【上下文】\n{context}\n"
f"【用户的问题】\n{question}\n"
"【输出要求】\n"
"1. 准确、简洁,分条回答;\n"
"2. 当用户查询无关信息时,直接说"文档中不存在";\n"
"3. 不要使用文档以外的知识回答。"
)
6.7 LLM生成
python
# 7. LLM生成
def main(message: str) -> Generator[str, None, None]:
message = message.strip()
if not message:
raise ValueError("message cannot be empty")
init_medical_db()
prompt = build_medical_prompt(message)
yield from stream_complete(prompt)
6.8 web实现
1. FastAPI 是什么?
FastAPI 是一个基于 Python 的现代 Web 框架,主要用于开发:
- REST API
- AI 模型服务
- 数据查询接口
- 流式输出接口
- 前后端分离应用
它具有以下特点:
- 使用 Python 类型注解自动校验请求参数
- 自动生成 OpenAPI 接口文档
- 支持异步和流式响应
- 性能较高
- 适合部署机器学习、RAG 和大模型服务
2. 本项目 FastAPI 设计
整体调用流程如下:
text
前端输入问题
↓
POST /api/chat
↓
main(message)
↓
Milvus 向量检索
↓
拼接医疗知识上下文
↓
生成增强提示词
↓
调用 DeepSeek
↓
SSE 流式返回
↓
前端逐步显示回答
请求格式
json
{
"message": "高血压患者平时应该注意什么?"
}
请求模型由 Pydantic 定义:
python
class ChatRequest(BaseModel):
message: str
FastAPI 会自动检查请求中是否包含 message 字段。
流式返回
后端使用:
python
StreamingResponse(...)
并采用 SSE 格式返回:
text
data: {"delta": "高血压患者"}
data: {"delta": "需要注意"}
data: [DONE]
这样前端不需要等待大模型完整回答,而是可以边生成边显示。
对应代码位于:
3. 当前项目中的模块职责
| 模块 | 作用 |
|---|---|
frontend |
展示聊天界面、发送问题、接收流式回答 |
main.py |
FastAPI 接口、请求处理和 RAG 流程组织 |
milvus.py |
连接 Milvus、生成向量、执行相似度检索 |
util.py |
调用 DeepSeek,并提供流式生成 |
medical_QA |
医疗问答原始数据 |
medical.db |
医疗问答向量数据库 |
核心接口位于:
python
def main(message: str) -> Generator[str, None, None]:
...
这个函数依次完成:
- 检查用户消息是否为空;
- 初始化 Milvus 连接;
- 检索医疗问答;
- 构造增强提示词;
- 调用大模型并逐段返回结果。
此项目的前端基于Codex帮助完成,项目仓库地址为:https://github.com/Dairenwen/RAG/tree/main/Project1,有兴趣的读者可以前往,支持docker一键部署。