实战主线:从 demo 到系统
本页对应课程实战代码(`ai58/0902-bigmodel/code/3-rag-embeddings` 与 `ai58/0916-rag/13.向量工程和RAG系统`,两目录课上代码一致,后者多预习讲义/GraphRAG 文档/PPT)。知识链路: ```text 认知(LLM局限、RAG vs 微调) → 文档工程(PDF解析、数据转文本、滑窗分块+overlap) → 关键词检索(NLTK/jieba 预处理 → ES 倒排索引 → 局限演示) → 向量工程(Embedding 原理、API/本地BGE 双路线、相似度、Chroma、ANN选型) → 检索进阶(两阶段召回 + CrossEncoder 重排、混合检索) → 编排(模板注入、RAG_Bot、调试与批处理) → 评估与优化(对比测试、MTEB、三维度优化) → 扩展(多模态/增量/多语言、GraphRAG) ``` **通用原则:任何数据进 RAG 前先转文本**——图片→OCR/多模态描述;表格→文字描述或导入数据库走 nl2sql;目前纯文本搜索质量最好。
RAG_Bot(依赖注入设计:检索函数/LLM/模板皆可替换): ```python class RAG_Bot: def __init__(self, search_func, llm_func, prompt_template, n_results=2): self.search = search_func; self.llm = llm_func self.template = prompt_template; self.n_results = n_results def chat(self, user_query): results = self.search(user_query, self.n_results) if not results: return "抱歉,没有找到相关信息来回答您的问题。" prompt = build_prompt(self.template, info=results, query=user_query) return self.llm(prompt) ``` 把 `search_func` 换成 ES 关键词检索或 Chroma 向量检索,Bot 一行不改——**换引擎只换检索函数**。
升级版 AdvancedRAG_Bot: - 检索源换成向量库(Chroma 封装类) - 增加 `batch_chat` 批量问答(跑测试集) - 增加 `debug_search`:先看检索回了什么,再谈生成——**RAG 排障第一步永远是看召回** VectorDatabase 教学封装: ```python import chromadb class VectorDatabase: def __init__(self, name, embedding_fn): self.client = chromadb.Client(Settings(allow_reset=True)) self.collection = self.client.get_or_create_collection(name=name) self.embedding_fn = embedding_fn def add_documents(self, documents): self.collection.add( embeddings=self.embedding_fn(documents), documents=documents, ids=[f"doc_{i}" for i in range(len(documents))]) def search(self, query, top_k=3): res = self.collection.query(query_embeddings=self.embedding_fn([query]), n_results=top_k) return res['documents'][0] ```
文档工程
pdfminer 提取 + 按空行重组段落(`min_line_length` 过滤页眉/目录噪声,连字符修复跨行单词): ```python from pdfminer.high_level import extract_pages from pdfminer.layout import LTTextContainer def extract_text_from_pdf(filename, page_numbers=None, min_line_length=1): paragraphs, buffer, full_text = [], '', '' for i, page_layout in enumerate(extract_pages(filename)): if page_numbers is not None and i not in page_numbers: continue for element in page_layout: if isinstance(element, LTTextContainer): full_text += element.get_text() + '\n' for text in full_text.split('\n'): if len(text) >= min_line_length: buffer += (' ' + text) if not text.endswith('-') else text.strip('-') elif buffer: paragraphs.append(buffer); buffer = '' if buffer: paragraphs.append(buffer) return paragraphs paragraphs = extract_text_from_pdf("llama2.pdf", min_line_length=10) ```
**句子级滑窗分块(chunk + overlap)**——比固定字符切割更保语义: ```python from nltk.tokenize import sent_tokenize def split_text(paragraphs, chunk_size=300, overlap_size=100, min_line_length=10): sentences = [s.strip() for p in paragraphs for s in sent_tokenize(p)] chunks, i = [], 0 while i < len(sentences): chunk = sentences[i]; overlap = '' prev = i - 1 while prev >= 0 and len(sentences[prev]) + len(overlap) <= overlap_size: overlap = sentences[prev] + ' ' + overlap # 向前取重叠句 prev -= 1 chunk = overlap + chunk nxt = i + 1 while nxt < len(sentences) and len(sentences[nxt]) + len(chunk) <= chunk_size: chunk = chunk + ' ' + sentences[nxt] # 向后填充 nxt += 1 chunks.append(chunk); i = nxt return chunks ``` 要点:overlap 是"句级"而非字符级,保证块边界都落在完整句子处。
- **图片**:pdfminer 遍历 `LTFigure/LTImage` 递归抽取,PIL 解析失败则回退存原始字节(.bin 兜底) - **表格**:用文本元素 bbox 坐标聚类启发式检测(Y 容差 10 分行、X 容差 30 判列对齐),导出 CSV/Excel - 落库时带 metadata 溯源:`metadatas=[{"source": "llama2.pdf"} for _ in chunks]`
向量工程
路线一:API(火山方舟示例,OpenAI 兼容): ```python client = OpenAI(api_key=api_key, base_url="https://ark.cn-beijing.volces.com/api/v3") def get_embeddings(texts, model="doubao-embedding-large-text-250515", dimensions=None): if model == "doubao-embedding-large-text-250515": dimensions = None # 坑:该模型不支持 dimensions 参数 if dimensions: data = client.embeddings.create(input=texts, model=model, dimensions=dimensions).data else: data = client.embeddings.create(input=texts, model=model).data return [x.embedding for x in data] ``` 路线二:本地开源(BGE 中文,免 API 依赖): ```python from sentence_transformers import SentenceTransformer model = SentenceTransformer('BAAI/bge-large-zh-v1.5') vecs = model.encode(["文本1", "文本2"], normalize_embeddings=True) ```
本地中文模型谱系(embedding 演进 + 加载要点): | 模型 | 定位 | 关键用法 | |------|------|---------| | bert-chinese / roberta-wwm | 基础编码 | `last_hidden_state[:,0,:]` 取 [CLS] 句向量 | | text2vec-base-chinese | 句向量 | AutoModel + 平均池化 | | bge-base/large-zh-v1.5 | 检索专用 | `encode_queries()` 带检索指令;cls pooling + F.normalize | | bge-reranker-large | 重排 | `compute_score(['query','passage'])`,分数无固定范围 | bge 自定义组装: ```python from sentence_transformers import SentenceTransformer, models transformer = models.Transformer("/wks/models/bge-base-zh-v1.5") pooling = models.Pooling(word_embedding_dimension=transformer.get_word_embedding_dimension(), pooling_mode_cls_token=True) model = SentenceTransformer(modules=[transformer, pooling]) ``` 环境管理:`HF_HOME / TRANSFORMERS_CACHE / HUGGINGFACE_HUB_CACHE / MODELSCOPE_CACHE` 指到大磁盘;模型目录标准件 config.json / pytorch_model.bin / vocab.txt / tokenizer.json。
相似度与跨语言实测: ```python from numpy import dot from numpy.linalg import norm def cos_sim(a, b): return dot(a, b) / (norm(a) * norm(b)) def euclidean(a, b): from numpy.linalg import norm as N return N(a - b) ``` 实验:英文 query "global conflicts" 对 5 条中文新闻算余弦相似度排序——语义排序正确,证明中文 embedding 模型具备跨语言语义能力(跨语言检索可行但仍是常见弱点,需实测验证)。
系统化与踩坑
"向量工程"五组件(预习讲义框架):文档处理 / 向量化 / 存储 / 检索 / 生成——每层独立可替换: - 存储:Chroma(百万级) / Weaviate / Milvus(亿级) / Qdrant / 商业 Pinecone - 索引:IVF / HNSW / LSH - 框架:LangChain / LlamaIndex / Haystack(手写版便于理解,框架版便于复用) - 部署:Docker / K8s / FastAPI / Streamlit 演示层
优化三维度清单: | 维度 | 手段 | |------|------| | 检索质量 | 分块粒度调优 / 混合检索 / 重排序 / 查询改写 | | 生成质量 | 提示词 / 上下文管理 / 输出格式约束 | | 系统性能 | 缓存 / 并发 / 监控 |
课程实测踩坑(specstory 调试记录留档): 1. ES 服务不可用时 `search` 未定义 → `if 'search' not in globals()` 守卫 + 依赖检查 + 降级抛错 2. doubao-embedding-large-text 不支持 dimensions 参数,需强制 None 3. `chroma_client.reset()` 仅演示用,生产勿重置 4. key 与服务地址不硬编码进 notebook(做占位符) 5. 检索无结果 → 直接"无法回答",防幻觉约束同时写进提示词
参考
     MTEB 
     Sentence-Transformers 
     ElasticSearch