LLM中的应用程序中存在的问题
构建基于大型语言模型的应用程序,核心挑战之一,就是如何把LLM生成的通识性回答,和我们行业里那些具体的、专有的领域数据真正结合起来。这可不是一件简单的事。

一个常见的思路是微调LLM,让它更懂我们的领域。但即便经过微调,不准确和“一本正经地胡说八道”(也就是幻觉)的问题依然存在。所以,业界才催生了检索增强生成(RAG)技术。它的逻辑很清晰:让LLM的回答不再凭空想象,而是基于你提供的具体数据来生成,并且能指出信息来源。
RAG的工作原理
RAG的核心,是为你想使用的数据片段创建文本嵌入。这样一来,就能把源文本的一部分“塞进”LLM用来生成回答的语义空间里。同时,RAG系统还会把原文本一并返回,这意味着LLM的回答背后有真实的人类撰写的文本作为支撑,还能附带引用。
1、构建索引
首先,我们需要把数据准备好。通常是先将文本数据进行分块,然后把这些块构建成索引,以便后续能快速检索到相关的部分。
from sklearn.feature_extraction.text import TfidfVectorizer
def build_index(chunks):
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(chunks)
return vectorizer, X
2、检索相关块
有了索引,当用户提出一个查询时,系统就可以根据这个查询,在索引里快速找到最相关的几个文本块。
def retrieve_chunks(query, vectorizer, X, top_k=5):
query_vec = vectorizer.transform([query])
scores = (X * query_vec.T).toarray()
top_indices = scores.flatten().argsort()[-top_k:][::-1]
return top_indices
3、生成响应
最后一步,把检索到的相关文本块作为上下文,一并提供给LLM,让它基于这些新鲜的材料生成最终的回应。
def generate_response(query, chunks, vectorizer, X, model):
top_indices = retrieve_chunks(query, vectorizer, X)
relevant_chunks = [chunks[i] for i in top_indices]
context = .join(relevant_chunks)
response = model.generate_response(query, context)
return response
在整个RAG系统里,有一个细节特别值得留意:数据片段的大小。怎么划分数据,也就是所谓的“分块”,其实远比直接把整篇文档一股脑嵌入要复杂得多。
什么是分块?
分块,就是把长文档或者大数据集,切割成更小、更独立的单元。这么做是为了方便后续的处理、存储和检索。尤其是在处理大规模文本数据时,分块几乎是必须的,因为LLM对长文本的处理能力终究是有限的。
分块的优点
- • :处理小文本块总比啃完整篇长文档要省力得多。
减少计算资源消耗
- • :信息更集中的小块,能显著加快搜索和匹配的速度。
提高检索效率
- • :模型能聚焦于具体的内容块,从而生成更精确、更相关的回答。
提升生成质量
为什么分块很重要?
在构建LLM应用时,分块数据的大小,直接关系到搜索结果的准确性。当你把一段数据嵌入成向量,如果这个块包含的内容太多,这个向量就会变得“面目模糊”,难以精确描述其中的特定内容;反之,如果块切得太小,又会丢失关键的上下文信息。
Pinecone公司的Roie Schwaber-Cohen对此有个精辟的总结:“我开始思考如何将内容分成更小块的原因是,这样当我检索时,它实际上能够命中正确的内容。你将用户的查询嵌入,然后将其与内容的嵌入进行比较。如果你嵌入的内容大小与用户查询的大小差异很大,你就更可能得到较低的相似度得分。”
如何考虑大小
所以,不光要考虑查询和响应时用的文本块大小,还要想清楚最终返回给用户的响应块大小。举个例子,如果你嵌入的是整章的内容,而不是一页或一段,向量数据库或许能在查询和整章之间找到一些语义相似性。但问题是,整章都相关吗?很可能不是。更重要的是,LLM能从这个检索到的庞然大物和用户问题中,生成一个精准的回应吗?
最佳策略选择
说到底,分块不是一个能简单套公式的问题。行业内并没有一个放之四海而皆准的标准。最佳的分块策略,完全取决于你具体的应用场景。
不过也别太担心,你并非只能对着原始数据“撞大运”。你还有元数据这个利器。元数据就像是一个小标签,可以是指向原始块或更大文档的链接、类别、标签,甚至是任何文本。正如Schwaber-Cohen所说:“这有点像一个 JSON blob,你可以用它来过滤东西。如果你只是在寻找特定子集的数据,你可以大大减少搜索空间,并且可以使用元数据将你在响应中使用的内容链接回原始内容。”
总而言之,块的大小很重要。而选择合适的分块策略,加上对元数据的巧妙运用,能显著提升检索和响应的效率和准确性。
分块策略
实践中,主要有以下几种分块策略可供选择:
1、固定大小块分块策略
这是最直观的方法,就是把文本切成固定大小的块。如果数据集里的内容格式都比较统一,比如新闻文章或博客帖子,那这个方法就挺合适。它的优点是成本低、实现简单,但缺点也很明显——完全没考虑文本本身的上下文,在某些场景下可能会影响效果。
示例代码:
def chunk_text(text, chunk_size=500):
words = text.split()
chunks = []
current_chunk = []
current_length = 0
for word in words:
current_length += len(word) + 1
if current_length > chunk_size:
chunks.append(.join(current_chunk))
current_chunk = [word]
current_length = len(word) + 1
else:
current_chunk.append(word)
chunks.append(.join(current_chunk))
return chunks
2、随机块分块策略
如果你的数据集里混着好几种不同类型的文档,那试试随机大小的块或许是个办法。这种方法可能会无意中捕捉到更广泛的语义上下文。但风险也显而易见:块切得随机,很容易把一句话或一个概念打断,产生一些毫无意义的片段。
3、滑动窗口分块策略
滑动窗口是个很常见也很聪明的做法。它的关键是,让新的块和前一个块有一部分内容重叠。这样一来,能更好地保留每个块周围的上下文信息,提高语义相关性。但代价是需要更多存储空间,也可能带来冗余信息,让检索过程变慢,甚至让RAG系统在识别正确来源时犯迷糊。
4、上下文感知分块策略
这是目前比较精细的方法。它会根据标点符号、Markdown或HTML标签等语义标记来分割文本。可以递归地将文档分解成更小、更完整的片段,每个片段都尽量保持逻辑和上下文的完整性。效果通常很不错,但问题在于需要额外的预处理步骤,计算量也上去了,会拖慢分块的速度。
确定最佳方法
要找到最适合你业务场景的分块策略,免不了要花些功夫去测试和验证。你可以尝试不同的方法,然后通过人工审核或者LLM评估器来给它们打分。当你发现哪个方法表现更好时,还可以通过基于余弦相似度分数对结果做进一步过滤,让最终输出的质量再上一个台阶。
分块只是生成式AI技术拼图中的一块。完整的RAG系统还需要LLM、向量数据库和存储等组件的协同配合。但说到底,最重要的还是你得有一个明确的目标。目标清晰了,这个项目才能走得稳、走得远。