本章的目标主要是要将前5篇学到的所有知识------LCEL、Model I/O、状态管理、RAG技术------整合起来,然后构建一个智能知识库助手。
1. 项目概述
1.1 我们要做什么?
想象一下,你有很多内部文档,比如产品手册、技术规范、FAQ等等。你想让团队成员能够用自然语言来询问这些文档里的内容,而不需要手动去翻找。这就是我们要构建的智能知识库助手要解决的问题。
它主要有这几个功能:
- 文档理解:能够加载和理解各种格式的文档内容
- 智能问答:基于文档内容回答用户问题,而不是胡说八道
- 多轮对话:记住你们之前的对话,这样就能进行上下文相关的多轮问答
- 状态管理:支持多个人同时使用,每个人的对话历史都是独立的
- 性能监控:能看到每次查询花了多长时间,方便我们优化
1.2 用到的技术栈
做这个项目,我们会用到这些技术:
- LangChain:不用多说了吧,我们一直在学的框架
- OpenAI API:提供语言模型和嵌入模型,让机器能理解文字
- FAISS:Facebook开源的向量数据库,用来快速检索相关信息
- Python-dotenv:管理环境变量的小工具,保护我们的API密钥
1.3 整体架构是怎么设计的?
我采用了分层架构,这样代码更容易维护和扩展。
基础设施层
核心业务层
API接口层
main.py
应用入口
app.py
主应用类
config
配置管理
loader
文档加载模块
vector
向量存储
memory
状态管理
chain
RAG链构建
callback
回调监控
OpenAI API
语言模型服务
FAISS
向量数据库
文件系统
文档存储
简单解释一下这三层:
- API接口层:就是用户直接接触的部分,包括主入口和主要的应用类
- 核心业务层:这是我们项目的核心逻辑,每个模块都有明确的职责
- 基础设施层:底层的技术支撑,比如OpenAI的API、FAISS向量库等
1.4 我是怎么开发这个项目的?
我采用的是"自顶向下设计,自底向上实现"的方法:
- 先设计API接口层:我会先想想最终用户怎么使用这个系统,设计出app.py的基本框架
- 再开发核心模块:根据接口的需求,按依赖关系一个个开发核心模块
- 完善API接口层:有了核心模块,就可以完善app.py的具体实现了
- 写测试代码:开发完每个模块,都要写测试确保它们正常工作
- 最后做主入口:把所有东西整合起来,写main.py让程序跑起来
2. 项目初始化
2.1 创建项目目录结构
首先,创建一个新的项目文件夹,并使用conda创建虚拟环境:
bash
mkdir smart_knowledge_assistant
cd smart_knowledge_assistant
conda create -n smart_knowledge_assistant python=3.10 -y
conda activate smart_knowledge_assistant
然后安装必要的依赖:
bash
pip install langchain-openai faiss-cpu python-dotenv langchain-community
创建一个 requirements.txt 文件,记录依赖版本:
langchain-openai==1.1.2
faiss-cpu==1.13.1
python-dotenv==1.2.1
langchain-community==0.4.1
创建 .env 文件,添加你的API密钥:
OPENAI_API_KEY=your_openai_api_key_here
创建模块化项目结构:
smart_knowledge_assistant/
├── core/ # 核心业务逻辑
│ ├── __init__.py # 核心模块初始化
│ ├── config.py # 配置管理
│ ├── loaders.py # 文档加载
│ ├── vectorstore.py # 向量存储管理
│ ├── memory.py # 状态管理(RunnableWithMessageHistory)
│ ├── chain.py # RAG链构建
│ └── callbacks.py # 回调与监控
├── api/ # API接口层
│ ├── __init__.py # API模块初始化
│ └── app.py # 主应用类
├── tests/ # 测试目录
│ ├── __init__.py # 测试模块初始化
│ └── test_core.py # 核心功能测试
├── main.py # 主入口文件
├── requirements.txt # 依赖列表
└── .env # 环境变量配置
创建项目目录结构:
bash
mkdir core api tests
2.2 准备示例知识库
在项目目录创建 knowledge_base.txt 文件,这是我们的示例知识库:
LangChain是一个用于开发由语言模型驱动的应用程序的框架。
它允许开发者将LLM与外部数据源和计算连接起来。
主要组件包括:
- Model I/O: 与语言模型交互的接口
- Retrieval: 从外部数据源获取信息
- Chains: 组合多个组件的序列
- Agents: 让模型决定执行哪些工具
- Memory: 在对话间保持状态
LCEL(LangChain表达式语言)是v1版本的核心特性,使用|操作符组合组件。
3. API接口层 - 初版设计
3.1 类的设计
我们可以编写一个SmartKnowledgeAssistant类能够完成以下事情:
- 文档管理:用户要把自己的文档交给系统处理,系统得能加载这些文档,还要能把处理好的向量数据保存起来,下次直接加载使用
- 查询处理:这是核心功能,用户提问题,系统给答案,而且还要聪明一点,能记住之前的对话
- 状态管理:想象一下,如果多个人同时在用这个系统,张三问的问题不能影响到李四吧?所以我们需要为每个人维护独立的对话历史
- 交互界面:虽然最终可能要做成Web服务,但现在我们先做个简单的命令行界面来测试功能
3.2 先搭个骨架看看
下面就是我设计的初版框架,你可以看到里面很多方法都还是空的,只写了注释说明要做什么:
创建 api/app.py - 初版设计:
python
"""主应用模块 - 智能知识库助手(初版设计)"""
import os
import logging
from typing import Dict, Any, Optional
from dotenv import load_dotenv
# 配置日志
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger("SmartKnowledgeAssistant")
logger.setLevel(logging.INFO)
class SmartKnowledgeAssistant:
"""智能知识库助手(初版设计)"""
def __init__(self, config: Optional[Dict[str, Any]] = None):
"""
初始化智能知识库助手
Args:
config: 用户配置字典,如果为None则使用默认配置
"""
# 加载配置
self.config = config or {}
# 初始化向量存储管理器
self.vectorstore_manager = None
# 初始化状态管理器
self.memory_manager = None
# 初始化RAG链
self.chain = None
# 初始化环境
self._setup()
def _setup(self):
"""初始化所有组件"""
# 加载环境变量
load_dotenv()
# 检查API密钥
if not os.getenv("OPENAI_API_KEY"):
raise ValueError("未找到 OPENAI_API_KEY,请检查.env文件")
# TODO: 初始化各组件
logger.info("智能知识库助手初始化完成")
def load_documents(self, file_paths: list) -> list:
"""
加载多个文档文件
Args:
file_paths: 文档路径列表
Returns:
成功加载的文档路径列表
"""
# TODO: 实现文档加载逻辑
pass
def create_vector_store(self, documents: list, save_path: str = None) -> None:
"""
创建向量数据库
Args:
documents: 文档列表
save_path: 保存路径,如果为None则使用配置中的路径
"""
# TODO: 实现向量数据库创建逻辑
pass
def load_vector_store(self, save_path: str = None) -> None:
"""
加载已保存的向量数据库
Args:
save_path: 保存路径,如果为None则使用配置中的路径
"""
# TODO: 实现向量数据库加载逻辑
pass
def query(self, question: str, session_id: str = "default") -> str:
"""
执行查询
Args:
question: 用户问题
session_id: 会话ID,用于区分不同会话
Returns:
回答内容
"""
# TODO: 实现查询逻辑
pass
def clear_session(self, session_id: str = "default") -> None:
"""
清空指定会话的历史记录
Args:
session_id: 会话ID
"""
# TODO: 实现会话清空逻辑
pass
def interactive_chat(self, session_id: str = "default"):
"""
交互式对话界面
Args:
session_id: 会话ID,用于区分不同会话
"""
# TODO: 实现交互式对话逻辑
pass
创建 api/__init__.py:
python
"""智能知识库助手API模块"""
from .app import SmartKnowledgeAssistant
__all__ = ['SmartKnowledgeAssistant']
4. 核心模块开发
根据API接口层的设计,我们需要开发以下核心模块,按依赖关系顺序排列:
4.1 配置管理模块
配置管理是系统的基础,其他模块都依赖它。
创建 core/config.py:
python
"""配置管理模块"""
from typing import Dict, Any
def get_default_config() -> Dict[str, Any]:
"""获取默认配置"""
return {
"chunk_size": 500, # 文档分块大小
"chunk_overlap": 100, # 分块重叠大小
"search_k": 5, # 检索返回的相关文档数量
"memory_window": 10, # 记忆窗口大小
"temperature": 0.1, # 语言模型温度参数,控制生成文本的随机性,值越小输出越确定
"model": "gpt-3.5-turbo", # 使用的语言模型
"embedding_model": "text-embedding-3-small", # 文本嵌入模型,用于将文本转换为向量表示
"embedding_chunk_size": 200, # 嵌入处理分块大小
"embedding_timeout": 120, # 嵌入请求超时时间(秒)
"llm_timeout": 30, # 语言模型请求超时时间(秒)
"llm_max_retries": 3, # 语言模型最大重试次数
"vector_store_path": "./faiss_index" # 向量数据库存储路径
}
def merge_config(user_config: Dict[str, Any] = None) -> Dict[str, Any]:
"""合并用户配置和默认配置"""
default_config = get_default_config()
if user_config is None:
return default_config
return {**default_config, **user_config}
4.2 文档加载模块
文档加载是数据处理的第一步。
创建 core/loaders.py:
python
"""文档加载模块"""
import os
import logging
from typing import List
from langchain_community.document_loaders import TextLoader
from langchain_core.documents import Document
logger = logging.getLogger(__name__)
def load_documents(file_paths: List[str]) -> List[Document]:
"""
加载多个文档文件
Args:
file_paths: 文件路径列表
Returns:
文档列表
Raises:
ValueError: 如果没有成功加载任何文档
"""
all_documents = []
for file_path in file_paths:
if not os.path.exists(file_path):
logger.warning(f"文件不存在: {file_path}")
continue
try:
# 尝试多种编码方式
encodings = ['gbk', 'gb2312', 'ansi', 'utf-8']
documents = None
for encoding in encodings:
try:
loader = TextLoader(file_path, encoding=encoding)
documents = loader.load()
logger.info(f"成功使用 {encoding} 编码加载文件: {file_path}")
break
except UnicodeDecodeError:
continue
if documents is None:
raise ValueError(f"无法使用任何编码加载文件: {file_path}")
# 添加文件来源信息
for doc in documents:
doc.metadata["source"] = file_path
all_documents.extend(documents)
logger.info(f"成功加载文件: {file_path}, 文档数: {len(documents)}")
except Exception as e:
logger.error(f"加载文件失败 {file_path}: {e}")
continue
if not all_documents:
raise ValueError("没有成功加载任何文档")
return all_documents
4.3 向量存储模块
向量存储是RAG系统的核心,负责文档的向量化和检索。
创建 core/vectorstore.py:
python
"""向量存储模块"""
import os
import logging
from typing import List, Optional
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
logger = logging.getLogger(__name__)
class VectorStoreManager:
"""向量存储管理器"""
def __init__(self, config: dict):
"""
初始化向量存储管理器
Args:
config: 配置字典
"""
self.config = config
self.vectorstore: Optional[FAISS] = None
self.embeddings = OpenAIEmbeddings(
model=config.get("embedding_model", "text-embedding-3-small"),
chunk_size=config.get("embedding_chunk_size", 200),
timeout=config.get("embedding_timeout", 120)
)
def create_vector_store(self, documents: List[Document], save_path: str = None) -> None:
"""
创建向量数据库
Args:
documents: 文档列表
save_path: 保存路径,如果为None则使用配置中的路径
"""
save_path = save_path or self.config.get("vector_store_path", "./faiss_index")
# 文本分割
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=self.config.get("chunk_size", 500),
chunk_overlap=self.config.get("chunk_overlap", 100),
length_function=len,
separators=["\n\n", "\n", "。", "!", "?", ";", ",", "、", ""]
)
texts = text_splitter.split_documents(documents)
logger.info(f"文档分割完成,共 {len(texts)} 个文本块")
print(" 正在创建向量数据库,这可能需要几分钟时间...")
# 将文档转换为向量嵌入并创建FAISS向量数据库
self.vectorstore = FAISS.from_documents(texts, self.embeddings)
# 保存索引
self.vectorstore.save_local(save_path)
logger.info(f"向量数据库已保存到: {save_path}")
print(f" 向量数据库创建完成!")
def load_vector_store(self, load_path: str = None) -> None:
"""
加载已保存的向量数据库
Args:
load_path: 加载路径,如果为None则使用配置中的路径
Raises:
FileNotFoundError: 如果向量数据库不存在
"""
load_path = load_path or self.config.get("vector_store_path", "./faiss_index")
if not os.path.exists(f"{load_path}/index.faiss"):
raise FileNotFoundError(f"向量数据库不存在: {load_path}")
self.vectorstore = FAISS.load_local(
load_path,
self.embeddings,
allow_dangerous_deserialization=True
)
logger.info(f"已加载向量数据库: {load_path}")
def get_retriever(self):
"""
获取检索器
Returns:
检索器对象
Raises:
ValueError: 如果向量数据库未初始化
"""
if not self.vectorstore:
raise ValueError("向量数据库未初始化,请先加载或创建向量数据库")
return self.vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": self.config.get("search_k", 5)}
)
4.4 状态管理模块
状态管理模块负责管理多会话的对话历史。
创建 core/memory.py:
python
"""状态管理模块 - 使用RunnableWithMessageHistory实现LCEL链的状态管理"""
import logging
from typing import Dict
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.runnables import Runnable
logger = logging.getLogger(__name__)
class MemoryManager:
"""状态管理器 - 使用RunnableWithMessageHistory实现多会话支持"""
def __init__(self):
"""初始化状态管理器"""
# 存储不同会话的历史记录
self.store: Dict[str, ChatMessageHistory] = {}
def get_session_history(self, session_id: str) -> ChatMessageHistory:
"""
获取或创建会话历史记录
Args:
session_id: 会话ID
Returns:
ChatMessageHistory对象
"""
if session_id not in self.store:
self.store[session_id] = ChatMessageHistory()
logger.info(f"创建新会话: {session_id}")
return self.store[session_id]
def clear_session(self, session_id: str) -> None:
"""
清空指定会话的历史记录
Args:
session_id: 会话ID
"""
if session_id in self.store:
self.store[session_id].clear()
logger.info(f"已清空会话历史: {session_id}")
def wrap_chain_with_history(
self,
chain: Runnable,
input_messages_key: str = "input",
history_messages_key: str = "history"
) -> RunnableWithMessageHistory:
"""
使用RunnableWithMessageHistory包装链,使其具备自动管理历史的能力
Args:
chain: 要包装的链
input_messages_key: 输入消息的键名
history_messages_key: 历史消息的键名
Returns:
包装后的链
"""
return RunnableWithMessageHistory(
runnable=chain,
get_session_history=self.get_session_history,
input_messages_key=input_messages_key,
history_messages_key=history_messages_key,
)
def get_all_sessions(self) -> list:
"""
获取所有会话ID列表
Returns:
会话ID列表
"""
return list(self.store.keys())
4.5 RAG链构建模块
RAG链构建模块是系统的核心,负责将检索、提示和生成整合在一起。
创建 core/chain.py:
python
"""RAG链构建模块"""
import logging
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_openai import ChatOpenAI
from langchain_core.retrievers import BaseRetriever
logger = logging.getLogger(__name__)
def format_docs(docs) -> str:
"""
格式化检索结果
Args:
docs: 检索到的文档列表
Returns:
格式化后的文档字符串
"""
formatted = []
for i, doc in enumerate(docs, 1):
source = doc.metadata.get("source", "未知来源")
content = doc.page_content.strip()
formatted.append(f"[文档{i} - {source}]:\n{content}")
return "\n\n".join(formatted)
def build_prompt_template() -> ChatPromptTemplate:
"""
构建提示模板
注意:RunnableWithMessageHistory会自动将历史消息注入到history变量中
我们需要在prompt中同时包含context和history
Returns:
ChatPromptTemplate对象
"""
# 使用from_messages方式,这样可以更好地处理历史消息
# RunnableWithMessageHistory会自动将历史消息列表注入到history变量中
return ChatPromptTemplate.from_messages([
("system", """你是一个专业的知识库助手,基于提供的上下文和对话历史回答用户问题。
## 上下文信息:
{context}
## 回答要求:
1. 严格基于上下文信息回答,不要编造不知道的内容
2. 如果上下文信息不足,请明确告知并建议用户提供更多信息
3. 回答要专业、准确、简洁
4. 如果问题与知识库无关,请礼貌拒绝
5. 结合对话历史理解用户意图,保持对话连贯性
请开始回答:"""),
MessagesPlaceholder(variable_name="history"), # RunnableWithMessageHistory会自动填充
("human", "{input}")
])
def build_rag_chain(retriever: BaseRetriever, config: dict):
"""
构建RAG链(基础链,将被RunnableWithMessageHistory包装)
注意:RunnableWithMessageHistory期望链接受字典输入(包含input键),
然后它会自动将历史消息注入到字典的history键中。
我们需要在链内部处理context的获取。
Args:
retriever: 检索器
config: 配置字典
Returns:
构建好的RAG链
"""
# 构建提示模板
prompt = build_prompt_template()
# 初始化LLM
llm = ChatOpenAI(
model=config.get("model", "gpt-3.5-turbo"),
temperature=config.get("temperature", 0.1),
max_retries=config.get("llm_max_retries", 3),
timeout=config.get("llm_timeout", 30)
)
# 构建完整的LCEL链
# RunnableWithMessageHistory会将用户输入(字典格式{"input": "问题"})传入
# 同时自动将历史消息注入到字典的history键中
# 我们需要从input中提取问题,然后通过retriever获取context
def prepare_input(user_input_dict):
"""
准备输入:从用户输入字典中提取问题,并获取context
RunnableWithMessageHistory会传入字典,格式为:
{
"input": "用户问题",
"history": [...] # 历史消息列表(自动注入)
}
我们需要:
1. 提取问题(从input键)
2. 通过retriever获取context
3. 返回包含context、input和history的字典
"""
# 提取问题
if isinstance(user_input_dict, dict):
question = user_input_dict.get("input", "")
history = user_input_dict.get("history", []) # RunnableWithMessageHistory自动注入
else:
# 兼容性处理:如果不是字典,尝试转换
question = str(user_input_dict)
history = []
# 获取context
docs = retriever.invoke(question)
context = format_docs(docs)
return {
"context": context,
"input": question,
"history": history # 传递给prompt
}
# 构建链:接受字典输入 -> 准备context和input -> prompt -> llm -> 输出
rag_chain = (
RunnableLambda(prepare_input)
| prompt
| llm
| StrOutputParser()
)
logger.info("RAG链构建完成")
return rag_chain
4.6 回调与监控模块
回调模块用于性能监控和日志记录。
创建 core/callbacks.py:
python
"""回调与监控模块"""
import logging
from typing import Dict, Any
from datetime import datetime
from langchain_core.callbacks import BaseCallbackHandler
logger = logging.getLogger(__name__)
class PerformanceCallbackHandler(BaseCallbackHandler):
"""性能监控回调"""
def on_chain_start(self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any) -> None:
"""链开始执行时调用"""
self.start_time = datetime.now()
question = inputs.get('question') or inputs.get('input', 'Unknown')
logger.info(f"开始处理: {question}")
def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> None:
"""链执行结束时调用"""
duration = (datetime.now() - self.start_time).total_seconds()
logger.info(f"处理完成,耗时: {duration:.2f}秒")
def on_chain_error(self, error: Exception, **kwargs: Any) -> None:
"""链执行出错时调用"""
logger.error(f"链执行出错: {error}")
def get_callbacks(enable_performance: bool = True) -> list:
"""
获取回调列表
Args:
enable_performance: 是否启用性能监控
Returns:
回调列表
"""
callbacks = []
if enable_performance:
callbacks.append(PerformanceCallbackHandler())
return callbacks
4.7 核心模块初始化
创建 core/__init__.py:
python
"""智能知识库助手核心模块"""
from .config import get_default_config, merge_config
from .loaders import load_documents
from .vectorstore import VectorStoreManager
from .memory import MemoryManager
from .chain import build_rag_chain
from .callbacks import get_callbacks
__all__ = [
'get_default_config', 'merge_config', 'load_documents',
'VectorStoreManager', 'MemoryManager', 'build_rag_chain', 'get_callbacks'
]
5. 完善API接口层
现在我们已经完成了所有核心模块的开发,可以基于这些模块完善API接口层的实现。
更新 api/app.py - 完整实现:
python
"""主应用模块 - 智能知识库助手"""
import os
import logging
from typing import Dict, Any, Optional
from datetime import datetime
from dotenv import load_dotenv
from core.config import merge_config, get_default_config
from core.loaders import load_documents
from core.vectorstore import VectorStoreManager
from core.memory import MemoryManager
from core.chain import build_rag_chain
from core.callbacks import get_callbacks
# 配置日志
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger("SmartKnowledgeAssistant")
logger.setLevel(logging.INFO)
class SmartKnowledgeAssistant:
"""智能知识库助手"""
def __init__(self, config: Optional[Dict[str, Any]] = None):
"""
初始化智能知识库助手
Args:
config: 用户配置字典,如果为None则使用默认配置
"""
self.config = merge_config(config)
self.vectorstore_manager: Optional[VectorStoreManager] = None
self.memory_manager: Optional[MemoryManager] = None
self.chain = None
# 调用_setup方法完成初始化
self._setup()
def _setup(self):
"""初始化所有组件"""
# 加载环境变量
load_dotenv()
# 检查API密钥是否存在
if not os.getenv("OPENAI_API_KEY"):
raise ValueError("未找到 OPENAI_API_KEY,请检查.env文件")
# 初始化向量存储管理器
self.vectorstore_manager = VectorStoreManager(self.config)
# 初始化状态管理器
self.memory_manager = MemoryManager()
logger.info("智能知识库助手初始化完成")
def load_documents(self, file_paths: list) -> list:
"""
加载多个文档文件
Args:
file_paths: 文件路径列表
Returns:
文档列表
"""
# 直接调用核心模块的文档加载函数
return load_documents(file_paths)
def create_vector_store(self, documents: list, save_path: str = None) -> None:
"""
创建向量数据库
Args:
documents: 文档列表
save_path: 保存路径,如果为None则使用配置中的路径
"""
# 调用向量存储管理器的创建方法
self.vectorstore_manager.create_vector_store(documents, save_path)
def load_vector_store(self, save_path: str = None) -> None:
"""
加载已保存的向量数据库
Args:
save_path: 保存路径,如果为None则使用配置中的路径
"""
# 调用向量存储管理器的加载方法
self.vectorstore_manager.load_vector_store(save_path)
def _build_chain(self, session_id: str = "default"):
"""
构建RAG链(带状态管理)
Args:
session_id: 会话ID,用于区分不同会话
"""
# 检查向量数据库是否已经初始化
if not self.vectorstore_manager or not self.vectorstore_manager.vectorstore:
raise ValueError("向量数据库未初始化,请先加载或创建向量数据库")
# 获取检索器
retriever = self.vectorstore_manager.get_retriever()
# 构建基础RAG链
base_chain = build_rag_chain(
retriever=retriever,
config=self.config
)
# 使用RunnableWithMessageHistory包装链,实现多会话状态管理
self.chain = self.memory_manager.wrap_chain_with_history(
chain=base_chain,
input_messages_key="input",
history_messages_key="history"
)
return self.chain
def query(self, question: str, session_id: str = "default", max_retries: int = 3) -> str:
"""
执行查询(带重试机制)
Args:
question: 用户问题
session_id: 会话ID,用于区分不同会话
max_retries: 最大重试次数
Returns:
回答内容
"""
# 检查问题是否为空
if not question or not question.strip():
return "问题不能为空"
# 清理输入
question = question.strip()
# 检查向量数据库是否就绪
if not self.vectorstore_manager or not self.vectorstore_manager.vectorstore:
return "错误:知识库未加载,请先加载文档"
# 构建链(延迟构建,只有在第一次查询时才构建)
if not self.chain:
self._build_chain(session_id)
# 重试机制,防止网络波动等问题导致查询失败
for attempt in range(max_retries):
try:
# 使用RunnableWithMessageHistory调用,传入session_id
# RunnableWithMessageHistory期望字典输入,格式为 {"input": "用户问题"}
# 它会自动将历史消息注入到字典中
run_config = {"configurable": {"session_id": session_id}}
response = self.chain.invoke(
{"input": question}, # 传入字典格式
config=run_config
)
return response
except Exception as e:
logger.error(f"查询失败 (尝试 {attempt + 1}/{max_retries}): {e}")
if attempt == max_retries - 1:
return f"抱歉,查询过程中出现错误:{str(e)}"
def clear_session(self, session_id: str = "default") -> None:
"""
清空指定会话的历史记录
Args:
session_id: 会话ID
"""
# 调用内存管理器的清空方法
self.memory_manager.clear_session(session_id)
logger.info(f"已清空会话 {session_id} 的历史记录")
def interactive_chat(self, session_id: str = "default"):
"""
交互式对话界面
Args:
session_id: 会话ID,用于区分不同会话
"""
print("🚀 智能知识库助手已启动!")
print("📚 功能特性:")
print(" • 多轮对话记忆")
print(" • 智能检索优化")
print(" • 错误自动恢复")
print(" • 性能监控")
print(f" • 当前会话ID: {session_id}")
print("输入'退出'结束对话,'重置'清空记忆")
print("-" * 60)
while True:
try:
user_input = input("\n👤 你: ").strip()
if user_input.lower() in ['退出', 'quit', 'exit']:
print(" 感谢使用!")
break
elif user_input.lower() in ['重置', 'reset']:
# 重置对话记忆
self.clear_session(session_id)
print(" 对话记忆已重置")
continue
elif not user_input:
continue
print(" 助手: ", end="", flush=True)
# 添加性能监控
start_time = datetime.now()
response = self.query(user_input, session_id)
duration = (datetime.now() - start_time).total_seconds()
print(response)
print(f" 响应时间: {duration:.2f}秒")
except KeyboardInterrupt:
print("\n 对话已结束")
break
except Exception as e:
print(f"\n 系统错误: {e}")
logger.error(f"交互式聊天出错: {e}")
6. 核心模块测试
开发完成后,我们需要编写测试代码来验证各个模块的功能。
创建 tests/test_core.py:
python
"""核心功能测试模块"""
import os
import sys
import unittest
from unittest.mock import patch, MagicMock
# 添加项目根目录到路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from core.config import get_default_config, merge_config
from core.loaders import load_documents
from core.vectorstore import VectorStoreManager
from core.memory import MemoryManager
from core.chain import build_rag_chain
class TestCoreFunctions(unittest.TestCase):
"""核心功能测试类"""
def setUp(self):
"""测试前准备"""
self.config = get_default_config()
self.test_file = "test_document.txt"
# 创建一个简单的测试文档
with open(self.test_file, 'w', encoding='utf-8') as f:
f.write("LangChain是一个用于开发由语言模型驱动的应用程序的框架。\n")
f.write("它允许开发者将LLM与外部数据源和计算连接起来。\n")
def tearDown(self):
"""测试后清理"""
# 在每个测试方法执行后都会运行这个方法
# 清理测试过程中创建的文件
if os.path.exists(self.test_file):
os.remove(self.test_file)
def test_config_functions(self):
"""测试配置函数"""
# 首先测试默认配置是否正确加载
default_config = get_default_config()
self.assertIn("chunk_size", default_config)
self.assertIn("model", default_config)
# 然后测试配置合并功能
user_config = {"model": "gpt-4", "temperature": 0.5}
merged = merge_config(user_config)
# 检查用户配置是否正确覆盖默认配置
self.assertEqual(merged["model"], "gpt-4")
self.assertEqual(merged["temperature"], 0.5)
# 检查未指定的配置项是否保持默认值
self.assertEqual(merged["chunk_size"], default_config["chunk_size"])
def test_document_loading(self):
"""测试文档加载"""
# 测试能否正确加载我们创建的测试文档
documents = load_documents([self.test_file])
# 应该成功加载1个文档
self.assertEqual(len(documents), 1)
# 文档内容应该包含我们写入的文本
self.assertIn("LangChain", documents[0].page_content)
@patch('core.vectorstore.OpenAIEmbeddings')
def test_vectorstore_manager(self, mock_embeddings):
"""测试向量存储管理器"""
# 由于我们不想在测试中真的调用OpenAI API,所以使用mock来模拟嵌入模型
mock_embeddings.return_value = MagicMock()
# 创建向量存储管理器实例
manager = VectorStoreManager(self.config)
# 检查嵌入模型是否正确初始化
self.assertIsNotNone(manager.embeddings)
# 检查配置是否正确传递
self.assertEqual(manager.config, self.config)
def test_memory_manager(self):
"""测试状态管理器"""
# 创建状态管理器实例
memory_manager = MemoryManager()
# 测试会话创建功能
# 同一个会话ID应该返回相同的ChatMessageHistory对象
history1 = memory_manager.get_session_history("session1")
history2 = memory_manager.get_session_history("session1")
self.assertEqual(history1, history2) # 应该是同一个对象
# 不同的会话ID应该返回不同的ChatMessageHistory对象
history3 = memory_manager.get_session_history("session2")
self.assertNotEqual(history1, history3) # 应该是不同对象
# 测试获取所有会话ID的功能
sessions = memory_manager.get_all_sessions()
self.assertIn("session1", sessions)
self.assertIn("session2", sessions)
# 测试清空会话功能
memory_manager.clear_session("session1")
# 会话ID应该仍然存在,但历史记录已被清空
self.assertIn("session1", memory_manager.get_all_sessions())
# 这个条件确保只有直接运行此脚本时才会执行测试
if __name__ == "__main__":
unittest.main()
创建 tests/__init__.py:
python
"""测试模块初始化"""
这部分测试代码可能看起来有点复杂,但其实核心思想很简单:
-
setUp和tearDown:这两个方法分别在每个测试执行前后运行,用来准备测试环境和清理测试数据。
-
单元测试:每个test_开头的方法都是一个独立的测试,专门测试某个特定功能。
-
Mock技术:对于需要调用外部服务(如OpenAI API)的部分,我们使用mock来模拟,避免真实调用。
-
断言验证:使用assert语句来验证函数的输出是否符合预期。
运行这些测试可以帮助我们确认每个模块都能正常工作,为后续的集成和部署提供信心。
7. 主入口开发
最后,我们开发主入口文件,整合所有功能模块。
创建 main.py:
python
"""主入口文件 - 智能知识库助手"""
import os
import sys
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from api.app import SmartKnowledgeAssistant
from core.config import merge_config
def main():
"""主函数 - 演示完整工作流"""
try:
# 创建助手实例(使用自定义配置)
config = {
"chunk_size": 200,
"chunk_overlap": 50,
"search_k": 3,
"memory_window": 10,
"model": "gpt-3.5-turbo"
}
assistant = SmartKnowledgeAssistant(config)
# 检查是否已有向量数据库
vector_store_path = "./faiss_index"
if os.path.exists(f"{vector_store_path}/index.faiss"):
print("🔍 发现已存在的向量数据库,正在加载...")
assistant.load_vector_store(vector_store_path)
else:
print("📖 未找到现有向量数据库,正在创建新的...")
# 加载文档(支持多个文件)
documents = assistant.load_documents([
"knowledge_base.txt",
])
# 创建向量数据库
assistant.create_vector_store(documents, vector_store_path)
# 启动对话(使用默认会话ID)
assistant.interactive_chat()
except Exception as e:
print(f"❌ 错误: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()
8. 运行效果
运行 python main.py 后,你将看到以下交互效果:
� 发现已存在的向量数据库,正在加载...
🚀 智能知识库助手已启动!
📚 功能特性:
• 多轮对话记忆
• 智能检索优化
• 错误自动恢复
• 性能监控
• 当前会话ID: default
输入'退出'结束对话,'重置'清空记忆
------------------------------------------------------------
👤 你: LangChain是什么?
助手: LangChain是一个用于开发由语言模型驱动的应用程序的框架。它允许开发者将LLM与外部数据源和计算连接起来。
响应时间: 2.35秒
👤 你: 它有哪些主要组件?
助手: 根据上下文信息,LangChain的主要组件包括:
- Model I/O: 与语言模型交互的接口
- Retrieval: 从外部数据源获取信息
- Chains: 组合多个组件的序列
- Agents: 让模型决定执行哪些工具
- Memory: 在对话间保持状态
响应时间: 1.89秒
👤 你: LCEL是什么?
助手: LCEL(LangChain表达式语言)是v1版本的核心特性,使用|操作符组合组件。它提供了一种简洁的方式来组合不同的LangChain组件,构建复杂的应用程序。
响应时间: 1.76秒
👤 你: 刚才我们讨论了什么?
助手: 刚才我们讨论了LangChain的基本概念、主要组件以及LCEL的作用。具体包括:
1. LangChain是一个用于开发由语言模型驱动的应用程序的框架
2. LangChain的主要组件包括Model I/O、Retrieval、Chains、Agents和Memory
3. LCEL是LangChain表达式语言,使用|操作符组合组件
响应时间: 2.01秒
👤 你: 退出
感谢使用!
项目总结
通过分层架构设计,我们将LCEL、RAG和状态管理整合,构建了一个相对完整的智能知识库助手。这种架构有以下优势:
- 清晰的职责分离:core目录包含核心业务逻辑,api目录提供接口层,tests目录负责测试
- 更好的可维护性:各模块功能明确,便于独立开发和测试
- 易于扩展:新功能可以作为新的core模块添加,或者作为新的API端点
- 标准化测试:专门的测试目录和测试模块,确保代码质量
在下一章中,我们将学习LangChain中的Tool工具开发,了解如何扩展AI的能力边界,为后续Agent智能代理打下基础。