1. 项目背景
  • 在金融领域,开发一个能够仿效专家解读上市公司年报的智能对话系统,一直是人工智能技术进步的关键目标。尽管目前的人工智能系统在文本对话领域已展现出显著的进展,但在更为精细、更具挑战性的金融领域交互方面,其性能尚需进一步提升。因此,我们致力于在现有大型模型的基础上,通过精细化调整、大型与小型模型的协同工作以及利用向量数据库等尖端技术,旨在进一步增强人工智能模型的性能。

问题描述:

之前我们讲过我们半结构化数据对于传统 RAG 来说可能具有挑战性,文本拆分可能会分解表,从而损坏检索中的数据;嵌入表可能会给语义相似性搜索带来挑战。对于这个问题可以通过构建摘要索引解决这个问题:分别为每个文本和表格数据创建摘要,将其嵌入文档。

  • 首先用Unstructured 来提取文档 (PDF) 中的文本和表格,并进行分块

  • 然后用llm分别对每个文本和表格创建摘要,将其嵌入向量数据库

  • 最后通过摘要使用MultiVectorRetriever过滤出相关文档,喂给llm当作上下文

2. 环境配置
pip install langchain langchain-chroma "unstructured[all-docs]" pydantic lxml langchainhub pi_heif

Unstructured(非结构化数据) 使用的 PDF 分区将使用:

  • tesseract :用于光学字符识别 (OCR)

  • poppler :用于 PDF 渲染和处理

# mac下载方式
brew install tesseract
brew install poppler

Windows下载

  • poppler

  • Tesseract

    • 下载地址:https://digi.bib.uni-mannheim.de/tesseract/

    • 导入方式:下载后直接安装即可,安装完成后查看是否安装成功,若未识别,手动添加安装根目录至系统‘Path’变量

    • 验证方式:终端命令tesseract -v

3. 完整代码

from langchain_text_splitters import RecursiveCharacterTextSplitter
from unstructured.partition.pdf import partition_pdf
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
import uuid
from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain.storage import InMemoryStore
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.runnables import RunnablePassthrough
import os
from dotenv import load_dotenv

load_dotenv()

embedding_model_path = r'D:\LLM\Local_model\BAAI\bge-large-zh-v1___5'
# 初始化嵌入模型(用于文本向量化)
embeddings_model = HuggingFaceEmbeddings(
    model_name=embedding_model_path
)

# 定义文件路径(需根据自己的路径修改)准备科学上网
path = r"D:\python_project\AI_object\RAG备课\day03\2020-03-17__厦门灿坤实业股份有限公司__200512__闽灿坤__2019年__年度报告.pdf"

# ------------------------ 第一阶段:PDF解析处理 ------------------------
# 使用unstructured库解析PDF文档
raw_pdf_elements = partition_pdf(
    filename=path,
    extract_images_in_pdf=False,  # 不提取PDF中的图片
    infer_table_structure=True,  # 启用表格结构识别
    max_characters=4000,  # 每个文本块最大字符数
    new_after_n_chars=3800,  # 达到3800个字符后分新块
    combine_text_under_n_chars=2000,  # 合并小于2000个字符的碎片文本
    strategy='hi_res',
)

# 统计各类元素数量
category_counts = {}
for element in raw_pdf_elements:
    category = str(type(element))
    category_counts[category] = category_counts.get(category, 0) + 1

print("元素类型统计:", category_counts)


# ------------------------ 第二阶段:元素分类处理 ------------------------
# 分类处理PDF元素 把文本和表格元素分类存在列表
table_elements = []
text_elements = []
for element in raw_pdf_elements:
    if "unstructured.documents.elements.Table" in str(type(element)):
        table_elements.append(str(element))
    elif "unstructured.documents.elements.Text" in str(type(element)):
        text_elements.append(str(element))
    elif "unstructured.documents.elements.NarrativeText" in str(type(element)):
        text_elements.append(str(element))

# 手动将文本内容分块
chuck_text_elements = RecursiveCharacterTextSplitter(chunk_size=500,chunk_overlap=200).split_text(''.join(text_elements))
print(f'文本块内容:{chuck_text_elements}')
print(f"识别到表格数量: {len(table_elements)}, 文本块数量: {len(chuck_text_elements)}")
print("表格示例:", table_elements[0:10])


# ------------------------ 第三阶段:内容摘要生成 ------------------------
# 定义摘要生成提示模板
prompt_text = """您是一个专业的内容摘要助手,请对以下表格或文本块进行简洁的总结:
{element}"""
prompt = ChatPromptTemplate.from_template(prompt_text)

# 初始化大模型(此处使用阿里云通义千问)
model = ChatOpenAI(
    model="qwen-plus",
    api_key=os.getenv("api_key"),
    base_url=os.getenv("base_url")
)

# 构建摘要生成链
summarize_chain = {"element": lambda x: x} | prompt | model | StrOutputParser()

# 批量生成表格摘要
table_summaries = summarize_chain.batch(table_elements, {"max_concurrency": 5})  # 并发处理
print("表格摘要示例:", table_summaries[0:10])

# 批量生成文本摘要
text_summaries = summarize_chain.batch(chuck_text_elements, {"max_concurrency": 5})
print("文本摘要示例:", text_summaries[0:10])

# ------------------------ 第四阶段:构建多向量检索器 ------------------------

# 创建向量数据库(用于存储摘要)
vectorstore = Chroma(
    collection_name="summaries",
    embedding_function=embeddings_model
)

# 创建内存存储(用于存储原始内容)
store = InMemoryStore()
id_key = "doc_id"  # 文档标识键

# 初始化多向量检索器
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    docstore=store,
    id_key=id_key,
)

# 添加文本数据到检索器
text_ids = [str(uuid.uuid4()) for _ in chuck_text_elements]
summary_texts = [
    Document(page_content=s, metadata={id_key: text_ids[i]})
    for i, s in enumerate(text_summaries)
]
retriever.vectorstore.add_documents(summary_texts)
retriever.docstore.mset(list(zip(text_ids, chuck_text_elements)))

# 添加表格数据到检索器
table_ids = [str(uuid.uuid4()) for _ in table_elements]
summary_tables = [
    Document(page_content=s, metadata={id_key: table_ids[i]})
    for i, s in enumerate(table_summaries)
]
retriever.vectorstore.add_documents(summary_tables)
retriever.docstore.mset(list(zip(table_ids, table_elements)))

# ------------------------ 第五阶段:构建问答链 ------------------------
# 定义问答提示模板
template = """请仅根据以下上下文(包含文本和表格)回答问题:
{context}
问题:{question}
"""
prompt = ChatPromptTemplate.from_template(template)

# 构建问答链
chain = (
        {"context": lambda x: retriever.invoke(input=x["question"]), "question": RunnablePassthrough()}
        | prompt
        | model
        | StrOutputParser()
)

# 示例问答测试
question = "营业收入构有哪些?可以往哪里发展?"
print("回答:", chain.invoke({"question": question}))
print("检索结果:", retriever.invoke(question))
4. PDF解析处理

可以使用 Unstructured chunking

  • 尝试识别文档部分(例如,Introduction等)

  • 然后,构建维护部分的文本块,同时也遵循用户定义的块大小

这里我们对某公司的年报进行分析,原文档有151页,考虑到大多数人的电脑性能,删减到47页

embedding_model_path = r'D:\LLM\Local_model\BAAI\bge-large-zh-v1___5'
# 初始化嵌入模型(用于文本向量化)
embeddings_model = HuggingFaceEmbeddings(
    model_name=embedding_model_path
)

# 定义文件路径(需根据自己的路径修改)准备科学上网
path = r"D:\python_project\AI_object\RAG备课\day03\2020-03-17__厦门灿坤实业股份有限公司__200512__闽灿坤__2019年__年度报告.pdf"

# ------------------------ 第一阶段:PDF解析处理 ------------------------
# 使用unstructured库解析PDF文档
raw_pdf_elements = partition_pdf(
    filename=path,
    extract_images_in_pdf=False,  # 不提取PDF中的图片
    infer_table_structure=True,  # 启用表格结构识别
    max_characters=4000,  # 每个文本块最大字符数
    new_after_n_chars=3800,  # 达到3800个字符后分新块
    combine_text_under_n_chars=2000,  # 合并小于2000个字符的碎片文本
    strategy='hi_res',
)

# 统计各类元素数量
category_counts = {}
for element in raw_pdf_elements:
    category = str(type(element))
    category_counts[category] = category_counts.get(category, 0) + 1

print("元素类型统计:", category_counts)

unstructured 文档元素类型全表:

类名(Class Name) 中文名称 说明 典型应用场景
CompositeElement 复合元素 包含混合内容(文本+简单表格/图片) 无法明确分类的混合内容区域
Table 表格 结构化表格数据,可能包含合并单元格 财务报表、数据报表
Title 标题 章节标题(通过字体大小和位置识别) 文档目录、章节标题
NarrativeText 叙述文本 段落正文(完整语义段落) 报告正文、论文段落
Image 图片 提取的图片元素(需启用 extract_images_in_pdf=True 扫描件、示意图
CheckBox 复选框 表单中的勾选框标记 调查问卷、申请表
PageBreak 分页符 分页符标记 跨页内容处理
Header 页眉 页面顶部重复出现的标题信息 合同、正式文件
Footer 页脚 页面底部信息(页码、版权声明) 技术手册、法律文件
Formula 公式 数学公式(LaTeX格式) 学术论文、数学教材
ListItem 列表项 带编号或符号的列表项 项目清单、步骤说明
CodeSnippet 代码片段 程序代码块(保留缩进和格式) 技术文档、API参考
EmailAddress 电子邮件地址 识别出的电子邮件地址 联系信息提取
PhoneNumber 电话号码 识别出的电话号码 客户信息提取
BulletedText 项目符号文本 带项目符号的无序列表 PPT内容、产品特性列表
FigureCaption 图注 图片/表格的说明文字(如 "图1-1") 学术论文、技术文档
Footnote 脚注 页面底部的注释引用 学术文献、法律条款
Quote 引用 引用文本(通常有缩进或特殊格式) 书籍、新闻报道
Metadata 元数据 文件元信息(作者、创建时间等) 文档属性分析
UncategorizedText 未分类文本 无法归类到任何已知类型的文本 边缘情况处理
5. 数据分类
  • 我们这里只处理了表格数据和文本数据

# ------------------------ 第二阶段:元素分类处理 ------------------------
# 分类处理PDF元素 把文本和表格元素分类存在列表
table_elements = []
text_elements = []
for element in raw_pdf_elements:
    if "unstructured.documents.elements.Table" in str(type(element)):
        table_elements.append(str(element))
    elif "unstructured.documents.elements.Text" in str(type(element)):
        text_elements.append(str(element))
    elif "unstructured.documents.elements.NarrativeText" in str(type(element)):
        text_elements.append(str(element))

# 手动将文本内容分块
chuck_text_elements = RecursiveCharacterTextSplitter(chunk_size=500,chunk_overlap=200).split_text(''.join(text_elements))
print(f'文本块内容:{chuck_text_elements}')
print(f"识别到表格数量: {len(table_elements)}, 文本块数量: {len(chuck_text_elements)}")
print("表格示例:", table_elements[0:10])
6. 生成摘要
  • 通过摘要索引进行查询优化

# ------------------------ 第三阶段:内容摘要生成 ------------------------
# 定义摘要生成提示模板
prompt_text = """您是一个专业的内容摘要助手,请对以下表格或文本块进行简洁的总结:
{element}"""
prompt = ChatPromptTemplate.from_template(prompt_text)

# 初始化大模型(此处使用阿里云通义千问)
model = ChatOpenAI(
    model="qwen-plus",
    api_key=os.getenv("api_key"),
    base_url=os.getenv("base_url")
)

# 构建摘要生成链
summarize_chain = {"element": lambda x: x} | prompt | model | StrOutputParser()

# 批量生成表格摘要
table_summaries = summarize_chain.batch(table_elements, {"max_concurrency": 5})  # 并发处理
print("表格摘要示例:", table_summaries[0:10])

# 批量生成文本摘要
text_summaries = summarize_chain.batch(chuck_text_elements, {"max_concurrency": 5})
print("文本摘要示例:", text_summaries[0:10])
7. 构建多向量检索器
# ------------------------ 第四阶段:构建多向量检索器 ------------------------

# 创建向量数据库(用于存储摘要)
vectorstore = Chroma(
    collection_name="summaries",
    embedding_function=embeddings_model
)

# 创建内存存储(用于存储原始内容)
store = InMemoryStore()
id_key = "doc_id"  # 文档标识键

# 初始化多向量检索器
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    docstore=store,
    id_key=id_key,
)

# 添加文本数据到检索器
text_ids = [str(uuid.uuid4()) for _ in chuck_text_elements]
summary_texts = [
    Document(page_content=s, metadata={id_key: text_ids[i]})
    for i, s in enumerate(text_summaries)
]
retriever.vectorstore.add_documents(summary_texts)
retriever.docstore.mset(list(zip(text_ids, chuck_text_elements)))

# 添加表格数据到检索器
table_ids = [str(uuid.uuid4()) for _ in table_elements]
summary_tables = [
    Document(page_content=s, metadata={id_key: table_ids[i]})
    for i, s in enumerate(table_summaries)
]
retriever.vectorstore.add_documents(summary_tables)
retriever.docstore.mset(list(zip(table_ids, table_elements)))
8. 构建问答链
  • 可以发现模型的回答和文档是一样的

# ------------------------ 第五阶段:构建问答链 ------------------------
# 定义问答提示模板
template = """请仅根据以下上下文(包含文本和表格)回答问题:
{context}
问题:{question}
"""
prompt = ChatPromptTemplate.from_template(template)

# 构建问答链
chain = (
        {"context": lambda x: retriever.invoke(input=x["question"]), "question": RunnablePassthrough()}
        | prompt
        | model
        | StrOutputParser()
)

# 示例问答测试
question = "营业收入构有哪些?可以往哪里发展?"
print("回答:", chain.invoke({"question": question}))
print("检索结果:", retriever.invoke(question))

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐