大家好,我是你们的AI技术技术博主许泽宇。今天我们要聊一个让RAG(检索增强生成)系统“脱胎换骨”的秘密武器——语义切分(Semantic Chunking)。别急,听起来高大上,其实它的本质就是:如何把一大坨文本,切成AI最爱吃的“小块”,让它检索起来又快又准。
你是不是也遇到过这种尴尬:明明知识库里啥都有,AI一问三不知,检索出来的内容不是驴唇不对马嘴,就是答非所问?别怀疑,问题很可能就出在“切块”这一步!
今天,我就带你从头到尾,手把手撸一遍语义切分的全流程,让你的RAG系统从“东拼西凑”进化到“对答如流”!
一、为什么要语义切分?定长切分的“原罪”
传统的文本切分方法,最常见的就是定长切分:比如每隔500个字符、100个词,或者10句话就切一刀。听起来很科学,实际上很“机械”——
-
优点:实现简单,代码一行搞定。 -
缺点:完全不懂内容,可能把一个完整的知识点切成两半,或者把风马牛不相及的内容拼在一起。
举个栗子:
❝
“小明喜欢吃苹果。他每天早上都吃一个。苹果富含维生素C。深度学习是一种人工智能方法。”
你看,前面三句聊的是苹果,最后一句突然AI上身。定长切分可能把“苹果”和“深度学习”硬塞一块,AI检索时一脸懵逼:你到底问的是水果还是算法?
所以,定长切分的“原罪”就是——不懂内容!
二、语义切分:让AI“按内容分块”,检索更聪明
语义切分,顾名思义,就是根据内容的“相似度”来切块。它的核心思想是:
-
相似的句子放一起,不相似的分开 -
每个块尽量语义连贯,便于AI理解和检索
这样,AI在检索时,拿到的都是“主题明确”的内容块,回答自然更靠谱!
三、语义切分的三大“分刀法”
要实现语义切分,关键是找到“切点”。怎么找?主流有三种方法,都是基于“相邻句子的相似度”:
1. 百分位法(Percentile)
-
算法思路:统计所有相邻句子的相似度差,找到“掉得最狠”的那一批(比如前10%),在这些地方切块。 -
适用场景:文本风格多变、主题跳跃明显时,效果拔群。
2. 标准差法(Standard Deviation)
-
算法思路:计算相似度的均值和标准差,凡是低于“均值-若干个标准差”的地方,都是“语义断层”,果断切! -
适用场景:文本整体较为均匀,但偶有“断崖式”跳变。
3. 四分位法(IQR)
-
算法思路:用统计学里的IQR(Q3-Q1),找出那些“极端低”的相似度点,作为切分点。 -
适用场景:数据分布偏态、异常值多时,IQR法更稳健。
一句话总结:都是在找“语义跳变”的地方下刀!
四、撸代码:从PDF到语义切分,步步为营
1. PDF文本提取
首先,得有“原材料”。假设你有一本AI教材的PDF,咱用PyMuPDF一把梭:
import fitz
def extract_text_from_pdf(pdf_path):
mypdf = fitz.open(pdf_path)
all_text = ""
for page in mypdf:
all_text += page.get_text("text") + " "
return all_text.strip()
pdf_path = "data/AI_Information.pdf"
extracted_text = extract_text_from_pdf(pdf_path)
print(extracted_text[:500]) # 预览前500字符
2. 句子切分 & 句向量生成
有了文本,下一步就是“分句”,然后用大模型(比如OpenAI、BAAI/bge等)生成每句话的向量(embedding):
from openai import OpenAI
import numpy as np
import os
client = OpenAI(
base_url="https://api.studio.nebius.com/v1/",
api_key=os.getenv("OPENAI_API_KEY")
)
def get_embedding(text, model="BAAI/bge-en-icl"):
response = client.embeddings.create(model=model, input=text)
return np.array(response.data[0].embedding)
sentences = extracted_text.split(". ")
embeddings = [get_embedding(sentence) for sentence in sentences]
print(f"Generated {len(embeddings)} sentence embeddings.")
3. 计算相邻句子的相似度
用余弦相似度衡量每对相邻句子的“亲密度”:
def cosine_similarity(vec1, vec2):
return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
similarities = [cosine_similarity(embeddings[i], embeddings[i + 1]) for i in range(len(embeddings) - 1)]
4. 找“切点”:以百分位法为例
def compute_breakpoints(similarities, method="percentile", threshold=90):
if method == "percentile":
threshold_value = np.percentile(similarities, threshold)
# ... 省略其他方法
return [i for i, sim in enumerate(similarities) if sim < threshold_value]
breakpoints = compute_breakpoints(similarities, method="percentile", threshold=90)
5. 真正“切块”
def split_into_chunks(sentences, breakpoints):
chunks = []
start = 0
for bp in breakpoints:
chunks.append(". ".join(sentences[start:bp + 1]) + ".")
start = bp + 1
chunks.append(". ".join(sentences[start:]))
return chunks
text_chunks = split_into_chunks(sentences, breakpoints)
print(f"Number of semantic chunks: {len(text_chunks)}")
print("nFirst text chunk:n", text_chunks[0])
6. 块级向量 & 语义检索
每个块再生成一个embedding,检索时直接比对块向量:
def create_embeddings(text_chunks):
return [get_embedding(chunk) for chunk in text_chunks]
chunk_embeddings = create_embeddings(text_chunks)
def semantic_search(query, text_chunks, chunk_embeddings, k=5):
query_embedding = get_embedding(query)
similarities = [cosine_similarity(query_embedding, emb) for emb in chunk_embeddings]
top_indices = np.argsort(similarities)[-k:][::-1]
return [text_chunks[i] for i in top_indices]
五、实战演练:让AI“只答上下文有的内容”
假设你有如下问题:
❝
“什么是可解释AI(Explainable AI),为什么它很重要?”
用上面的方法,检索出来的块内容大致如下:
Context 1:
Explainable AI (XAI) aims to make AI systems more transparent and understandable. Research in XAI focuses on developing methods for explaining AI decisions, enhancing trust, and improving accountability.
========================================
Context 2:
Transparency and explainability are essential for building trust in AI systems. Explainable AI (XAI) techniques aim to make AI decisions more understandable, enabling users to assess their fairness and accuracy.
========================================
然后,用大模型生成答案,并且严格要求只根据检索到的内容作答:
system_prompt = "You are an AI assistant that strictly answers based on the given context. If the answer cannot be derived directly from the provided context, respond with: 'I do not have enough information to answer that.'"
user_prompt = "n".join([f"Context {i + 1}:n{chunk}n=====================================n" for i, chunk in enumerate(top_chunks)])
user_prompt = f"{user_prompt}nQuestion: {query}"
ai_response = generate_response(system_prompt, user_prompt)
print(ai_response.choices[0].message.content)
六、自动评测:让AI自己打分,闭环优化
最后,别忘了用AI自动评测答案质量,闭环优化:
evaluate_system_prompt = "You are an intelligent evaluation system tasked with assessing the AI assistant's responses. If the AI assistant's response is very close to the true response, assign a score of 1. If the response is incorrect or unsatisfactory in relation to the true response, assign a score of 0. If the response is partially aligned with the true response, assign a score of 0.5."
evaluation_prompt = f"User Query: {query}nAI Response:n{ai_response.choices[0].message.content}nTrue Response: {data[0]['ideal_answer']}n{evaluate_system_prompt}"
evaluation_response = generate_response(evaluate_system_prompt, evaluation_prompt)
print(evaluation_response.choices[0].message.content)
七、语义切分的“隐藏价值”:让RAG系统更像“懂王”
-
检索更精准:每个块语义连贯,AI不再“东拉西扯”。 -
上下文更相关:减少无关内容混入,提升答案质量。 -
可扩展性强:新文档、新领域,通用性极高。 -
自动化闭环:配合自动评测,持续优化切分策略。
八、实战Tips & 踩坑指南
-
句子切分要靠谱:英文可以用 .split(". ")
,中文建议用snownlp
、jieba
等分句工具,别让AI“断句失误”。 -
embedding模型选得好,检索效率高:推荐 bge
、text-embedding-ada-002
等主流模型。 -
切分阈值要调优:不同文档风格,阈值要动态调整,多试几次,效果肉眼可见。 -
块太大太小都不好:太大检索不准,太小上下文不全。建议每块200-500字,视场景微调。 -
自动评测不是万能:AI评测有时会“自嗨”,建议人工spot check辅助。
九、总结:让你的RAG系统“聪明又懂你”
语义切分,说白了就是让AI“按内容分块”,而不是“按长度分尸”。它是RAG系统“检索-生成”闭环的关键一环,直接决定了AI的“知识召回率”和“答题准确率”。
一句话,想让你的AI像“懂王”一样,问啥都能答到点子上?先把语义切分练到极致!
最后,欢迎大家留言讨论:你在RAG项目中遇到过哪些“切分翻车”的坑?你觉得还有哪些更骚的切分方法?评论区见!
如果觉得本文有用,记得点赞、转发、关注三连,咱们下期继续深扒AI技术内幕!
