1. 项目概述
最近我在尝试构建一个基于《三国演义》的智能问答系统,核心目标是能够准确回答关于这部古典名著的各类问题。这个项目主要使用了LangChain框架、ModelScope的bge-large-zh模型以及FAISS向量数据库。整个系统的工作流程可以概括为:文本加载→分割→嵌入→存储→检索,最终实现语义级别的问答功能。
选择这个技术栈有几个重要考虑:首先,LangChain提供了完整的LLM应用开发生态;其次,bge-large-zh是目前中文领域表现优秀的开源嵌入模型;最后,FAISS能够高效处理向量相似度搜索。这种组合既保证了效果,又完全避免了商业API的依赖。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心组件解析
2.1 LangChain框架
LangChain是一个专门为大语言模型应用开发设计的开源框架。它抽象了LLM应用的常见模式,提供了标准化的组件接口。在这个项目中,我们主要用到了以下几个核心模块:
-
模型I/O:包括提示词模板、模型包装器和输出解析器。虽然当前项目没有直接使用提示工程,但这个模块为后续扩展问答功能奠定了基础。
-
数据连接:这是本项目使用最深入的部分,包含文本加载器、文档转换器、文本嵌入接口和向量存储器。特别是它的文档转换器接口,让我们可以灵活地处理《三国演义》这样的长文本。
-
链(Chains):虽然当前示例没有展示,但LangChain的链式组合能力可以让我们将检索到的内容与LLM生成无缝衔接,未来可以轻松扩展出更智能的问答功能。
安装非常简单:
bash复制pip install langchain
2.2 文档分割器
处理《三国演义》这样的长文本时,直接输入大模型会面临上下文长度限制和注意力分散的问题。RecursiveCharacterTextSplitter是LangChain提供的一个智能文本分割工具,它的工作原理是:
- 优先按最大分隔符(如双换行)分割
- 如果分割后仍超过chunk_size,则尝试下一个分隔符(如单换行)
- 依次尝试空格、无分隔符直切,直到满足大小要求
在我们的配置中:
python复制text_splitter = RecursiveCharacterTextSplitter(
chunk_size=216, # 每个块约108个汉字
chunk_overlap=100, # 块间重叠约50个汉字
length_function=len,
is_separator_regex=False
)
这个配置的考量是:
- 中文平均每个token约2字符,216字符≈108字,适合大多数嵌入模型
- 较大重叠保证关键情节不被割裂(如对话跨越分割点)
- 实际测试表明这种配置对《三国演义》的叙事结构保留效果最佳
注意:chunk_size需要根据具体嵌入模型调整。例如bge-large-zh的理想输入是512token,但考虑到平均2:1的中英token比例,我们选择了这个折中值。
2.3 文本嵌入模型
我们选择了ModelScope上的bge-large-zh模型,这是目前中文领域表现最好的开源嵌入模型之一。其优势包括:
- 专门针对中文优化,对古典文学理解更好
- 768维的嵌入空间,平衡了效果和效率
- 支持长文本输入(最大512token)
安装ModelScope和模型:
bash复制pip install modelscope
模型初始化代码:
python复制self.embedding_pipeline = pipeline(
task=Tasks.sentence_embedding,
model="/path/to/bge-large-zh"
)
在实际使用中,我们发现需要特别处理不同的返回格式:
python复制# 处理不同的嵌入返回格式
if isinstance(results, np.ndarray):
embeddings = results
elif isinstance(results, dict):
embeddings = results['text_embedding']
else:
embeddings = [r['text_embedding'] for r in results]
2.4 FAISS向量数据库
FAISS是Meta开源的向量相似度搜索库,特别适合我们的应用场景:
- 支持高效的近似最近邻搜索
- 内存和磁盘存储可灵活配置
- 与LangChain深度集成
在我们的实现中,特别加入了元数据支持:
python复制vectorstore = FAISS.from_documents(
documents=docs_with_metadata,
embedding=embedding_model
)
vectorstore.save_local("faiss_sanguo")
3. 完整实现解析
3.1 数据准备与增强
我们从古登堡计划获取《三国演义》原始文本:
python复制text_url = "https://www.gutenberg.org/cache/epub/23950/pg23950.txt"
为了提高检索质量,我们为每个文本块添加了丰富的元数据:
python复制metadata = {
'chunk_id': i,
'source': '三国演义',
'text_length': len(text),
'position': f"块{i}",
'characters': self.extract_main_characters(text)
}
人物提取采用简单但有效的关键词匹配:
python复制main_chars = ['刘备', '关羽', '张飞', '曹操', '孙权', '诸葛亮', '周瑜', '吕布']
return [char for char in main_chars if char in text]
3.2 嵌入与存储流程
完整的处理流程封装在LangChainBuilder类中:
- 初始化嵌入管道
- 读取并分割文本
- 生成带元数据的文档
- 批量生成嵌入
- 存储到FAISS
关键实现细节:
python复制# 自定义嵌入类解决LangChain接口兼容问题
class FixedModelScopeEmbedding(Embeddings):
def embed_documents(self, texts):
return self.embeddings_array.tolist()
def embed_query(self, text):
result = self.pipeline([text])
# 处理多种返回格式...
3.3 检索实现
检索时支持多种查询方式:
python复制retriever = vectorstore.as_retriever(
search_type='similarity',
search_kwargs={'k': 5}
)
# 支持三种调用方式兼容不同版本
try:
results = retriever.invoke(query)
except:
results = vectorstore.similarity_search(query, k=5)
4. 效果验证与优化
4.1 查询示例测试
我们测试了"刘备三顾茅庐"相关查询:
python复制task = "刘备三顾茅庐,与诸葛亮在隆中对中聊了什么?"
results = langChainBuilder.load_model(langChainBuilder, task)
输出结果展示了:
- 相关文本片段
- 每个片段的元数据
- 涉及的主要人物
- 文本位置信息
4.2 性能优化点
在实际运行中,我们发现几个关键优化点:
-
批处理嵌入:ModelScope的pipeline支持批量输入,比单条处理快3-5倍
python复制# 批量处理文本列表 results = self.embedding_pipeline(texts) -
内存管理:大文本需要分批次处理,避免OOM
python复制for i in range(0, len(texts), batch_size): batch = texts[i:i+batch_size] embeddings.extend(self.embedding_pipeline(batch)) -
元数据索引:FAISS支持基于元数据的过滤搜索
python复制retriever = vectorstore.as_retriever( search_kwargs={'filter': {'characters': ['诸葛亮']}} )
5. 常见问题与解决方案
5.1 中文分割问题
问题:直接使用默认分隔符会导致中文成语、人名被割裂。
解决方案:
python复制text_splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "。", "!", "?", ";", "\n", ",", " "],
keep_separator=True
)
5.2 嵌入维度不匹配
问题:不同模型产生的嵌入维度不同,导致FAISS报错。
解决方案:
python复制# 显式指定维度并统一类型
embeddings = np.array(embeddings).astype('float32')
assert embeddings.shape[1] == 768 # bge-large-zh的维度
5.3 版本兼容问题
问题:LangChain不同版本的API变化较大。
兼容处理:
python复制# 检索API的多版本支持
if hasattr(retriever, 'invoke'):
results = retriever.invoke(query)
else:
results = vectorstore.similarity_search(query)
6. 扩展应用场景
这个框架可以轻松扩展到其他古典文学作品处理:
- 多作品联合检索:将《三国演义》《水浒传》等嵌入同一空间
- 时间线分析:利用元数据中的位置信息重建事件时序
- 人物关系挖掘:基于共现分析和嵌入相似度
例如处理《红楼梦》:
python复制text_splitter = RecursiveCharacterTextSplitter(
chunk_size=150, # 红楼梦对话更多,减小chunk_size
chunk_overlap=30,
separators=["\n\n", "。", "......", "\n", ","]
)
我在实际应用中发现,这套方案特别适合具有以下特点的文本:
- 篇幅较长且结构清晰
- 包含丰富的人物和事件
- 需要保持上下文连贯性的场景
