技术栈
前端: Next.js + Ts
后端:Python(Flask)
模型相关:
- LLM 模型(Ollama,Lama3):实现核心问答和扩展功能。
- LangChain 工具链:处理 Prompt、上下文检索和输出解析。
- NLP 工具(Jieba、TF-IDF):用于中文语料分析和推荐。
- 文档加载与处理(PyPDFLoader):支持用户内容输入。
效果图
首页搜索图
热搜展开图
三大模块细节
文档的问答 + 相关问题生成
核心处理流程
-
流式响应处理
使用了
StreamingCallbackHandler
实现流式输出响应,可以在生成答案的过程中逐步将结果返回给客户端,提升用户体验。on_llm_new_token
: 在每次生成新 token 时,将其写入响应流。
-
Prompt 模板设计
定义了多个 Prompt 模板,用于生成答案和相关问题:
- 查询 Prompt (
QUERY_PROMPT
): 生成与用户问题相关的三个问题,以中文返回结果。 - 文档搜索 Prompt (
prompt
): 基于上下文和问题生成答案,要求答案清晰流畅,且全部用中文。 - 相关问题 Prompt (
relatePrompt
): 为查询问题生成从不同视角出发的相关问题,输出 JSON 格式。
- 查询 Prompt (
-
检索与上下文合并
- 向量数据库检索 :
- 使用
get_vector_db
获取向量数据库实例。 - 调用检索器方法
retriever.get_relevant_documents
获取与问题相关的文档。
- 使用
- 上下文合并: 将用户输入的上下文与检索到的文档内容结合,为后续模型生成提供更准确的上下文信息。
- 向量数据库检索 :
-
回答与相关问题生成
根据
isRelate
参数分支:- 回答内容生成(文档模式):使用 Prompt 结合检索到的上下文和用户问题,通过 LLM(如 Ollama 模型)生成答案。
- 相关问题生成:仅基于用户问题,生成多个从不同角度出发的相关问题。
-
容错与日志记录
- 捕获异常并记录日志,确保错误可追踪。
- 在异常情况下返回友好的错误信息。
主要功能模块与设计要点
-
流式输出 (
StreamingCallbackHandler
)- 提供了实时输出能力。
- 改善用户体验,特别是处理复杂问题时无需等待完整答案生成。
-
Prompt 模板与分支逻辑
- 灵活的 Prompt 模板设计支持多种任务(文档问答、相关问题生成)。
- 基于
isRelate
的条件判断,选择不同任务逻辑,提高代码复用性。
-
向量数据库检索
- 使用向量数据库(例如通过
get_vector_db
方法获取)提升了答案生成的准确性。 - 支持文档内容的语义检索,能够高效定位相关文档。
- 使用向量数据库(例如通过
-
模型调用与流式处理
- 使用
ChatOllama
实现语言模型的调用。 - 流式 token 输出,减少用户等待时间。
- 使用
-
上下文管理与合并
- 通过
combine_contexts
方法确保用户输入与检索内容的有机结合,为 LLM 提供更高质量的上下文。
- 通过
核心技术点总结
- LangChain 集成 : 使用了
langchain
提供的多个工具,包括 Prompt 模板、可运行模块(RunnableMap
)、向量数据库接口等。 - LLM 模型支持 : 支持
ChatOllama
等大语言模型的调用,能适配不同场景。 - 流式响应 : 基于 Flask 的
stream_with_context
和自定义回调实现实时响应。 - 日志记录: 详细的日志记录用于监控和调试整个系统的运行。
简要代码展示
python
def query(input_context, input, response_stream, isRelate):
# 初始化模型和 Prompt
handler = StreamingCallbackHandler(response_stream)
llm = ChatOllama(model='llama3', callbacks=[handler])
QUERY_PROMPT, prompt, relatePrompt = get_prompt()
db = get_vector_db() # 获取向量数据库
retriever = db.as_retriever() # 转换为检索器
try:
if isRelate == "false": # 文档问答模式
relevant_docs = retriever.get_relevant_documents(input)
response = prompt.invoke({"context": relevant_docs, "question": input})
for token in response:
yield token
else: # 相关问题模式
response = relatePrompt.invoke({"question": input})
for token in response:
yield token
except Exception as e:
logging.error(f"Error: {e}")
yield "Error in processing your query."
日常模式 + 文档检索模式
核心依据功能: 从 Google 搜索中获取与查询相关的有机搜索结果(即非广告和非赞助内容)。具体步骤如下:
- 输入查询 :函数接收一个查询参数(
query
),这是用户想要搜索的内容。 - 发送API请求 :它通过 POST 请求将查询发送到
https://google.serper.dev/search
,同时附带一些搜索参数(如gl
指定地区为中国,hl
设置语言为简体中文)和 API 密钥用于身份验证。 - 处理响应:API 返回的响应数据包括 Google 搜索的有机结果,函数提取并返回这些结果。
通过 Serper API 获取 Google 搜索的自然搜索结果,返回相关内容以供进一步处理。
- 日常模式: 只依据此搜索结果 + 模型自身能力 进行回答
- 文档模式: 按照矢量数据库进行检索, 依靠相关内容信息进行回答
文档上传 + 文本嵌入 + 基于RAG
文件上传与文本嵌入处理模块说明
本文档概述了代码中实现的文件上传、内容分块、文本嵌入的核心逻辑和重要设计点。
功能模块概述
代码实现了一个文件上传与处理系统,主要包括以下功能:
- 文件类型检查与安全性保证。
- PDF 文件解析和文本分块。
- 向量数据库存储。
- 临时文件的管理与清理。
核心处理逻辑
1. 文件类型检查
- 函数:
allowed_file(filename)
- 作用:判断用户上传的文件是否是支持的格式(PDF)。
- 设计细节 :
- 使用文件扩展名验证,确保仅处理 PDF 格式。
- 提高文件系统安全性,防止潜在的恶意文件。
2. 文件存储与安全
- 函数:
save_file(file)
- 作用:保存用户上传的文件到临时目录,并返回文件路径。
- 设计细节 :
- 使用
secure_filename
确保文件名安全性,防止路径注入等攻击。 - 通过时间戳生成唯一文件名,避免文件名冲突。
- 存储在一个指定的临时目录中,确保后续处理文件可追踪。
- 使用
3. PDF 文件加载与分块
- 函数:
load_and_split_data(file_path)
- 作用:将上传的 PDF 文件解析为文本,并拆分为小块。
- 设计细节 :
- 使用
PyPDFLoader
加载 PDF 内容。 - 借助
RecursiveCharacterTextSplitter
按指定的分隔符(如段落、标点符号)将文本分块。 - 块大小:每块约 500 个字符,重叠部分 200 字符,确保上下文连续性。
- 提供块起始索引(
add_start_index=True
),便于内容定位和后续查询。
- 使用
4. 文本嵌入与存储
- 函数:
embed(file)
- 作用:实现完整的文件处理流程,包括文件合法性验证、内容分块、向量嵌入、存储和清理。
- 处理流程 :
- 文件验证:确保文件存在且为合法类型。
- 内容加载 :通过上述
save_file
和load_and_split_data
获取文档块。 - 向量嵌入 :调用
get_vector_db()
获取向量数据库,将分块内容嵌入并持久化。 - 日志记录:在每一步(嵌入成功、错误处理、清理临时文件)记录详细日志。
- 临时文件清理:确保处理后删除临时文件,避免资源占用。
文件处理部分流程图
plaintext
用户上传文件
└── 检查文件合法性 (`allowed_file`)
└── 保存文件到临时目录 (`save_file`)
└── 加载文件内容 (`load_and_split_data`)
└── 分块并添加到向量数据库 (`db.add_documents`)
└── 记录日志和清理临时文件
基于所描述代码功能的简易代码片段,快速理解系统流程和关键点:
python
import os
from werkzeug.utils import secure_filename
from datetime import datetime
# 临时文件目录
TEMP_FOLDER = './_temp'
# 检查文件类型是否为 PDF
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() == 'pdf'
# 保存上传的文件到临时目录
def save_file(file):
timestamp = datetime.now().timestamp()
secure_name = secure_filename(file.filename)
file_path = os.path.join(TEMP_FOLDER, f"{timestamp}_{secure_name}")
file.save(file_path)
return file_path
# 处理上传的 PDF 文件
def process_file(file):
if allowed_file(file.filename):
try:
# 保存文件
file_path = save_file(file)
print(f"File saved at: {file_path}")
# 模拟加载与分块(伪代码展示)
chunks = ["Chunk 1", "Chunk 2", "Chunk 3"] # 示例
print(f"Processed {len(chunks)} chunks from the file.")
finally:
# 清理临时文件
if os.path.exists(file_path):
os.remove(file_path)
print(f"Temporary file {file_path} deleted.")
else:
print("Invalid file type. Only PDF files are allowed.")
# 示例:模拟文件上传
class MockFile:
filename = "example.pdf"
def save(self, path):
print(f"Mock save file to: {path}")
file = MockFile()
process_file(file)
搜索推荐机制
这段代码的run(query)
函数的处理思路和重点在于根据用户的查询,提供相关的内容推荐。核心流程可以概括为以下几个步骤:
1. 加载训练数据:
- 从
get_train_data()
函数加载一组已分类的文本数据(包括不同的主题如摄影、编程、健康等)。 - 数据被组织成
pandas
的DataFrame
格式,方便后续的处理和分析。
2. 构建One-Hot编码矩阵:
get_group_one_hot()
函数根据每个文本中的关键词,生成One-Hot编码矩阵。每个关键词是否出现在某篇文章中,用0和1表示。- 关键词通过
jieba
分词工具提取,extract_keywords()
函数从文本中提取重要的关键词,并形成一个包含关键词的集合。
3. 筛选最相关的组:
- 使用
get_top_similars()
函数,根据用户输入的查询,提取查询文本的关键词并生成One-Hot编码。 - 然后计算查询与每个组(分类)的相似度,使用余弦相似度(
cosine_similarity()
)衡量查询与各组之间的相似性。 - 获取与查询最相关的前2个组。
4. 在最相关的组中筛选具体内容:
- 通过
get_recommendations()
函数,计算用户查询与每个组内内容的相似度,挑选出最相关的Top N(默认5个)文本内容。 - 推荐的内容是根据相似度排序的,并且附带相似度得分。
5. 输出推荐结果:
- 输出包含推荐文本、类别和相似度分数的结果列表。
- 同时,推荐结果会通过日志记录输出,并格式化为保留两位小数的相似度得分。
核心:
- 关键词提取 :通过
jieba
分词和TF-IDF方法提取关键词,为后续的相似度计算提供基础。 - 相似度计算:通过余弦相似度计算查询与各组、组内内容的相关性。
- 推荐排序:通过相似度得分,返回最相关的推荐内容,并确保推荐结果的准确性。
- One-Hot编码:通过One-Hot编码矩阵计算查询与组的相似度,进一步提高推荐的准确性。
总结
作为前端AI项目还是很不错的,后续会继续完善,尽量训练自己模型进行使用。