基于Agent的金融問答系統(tǒng):代碼重構(gòu) 原創(chuàng)
前言
在上一章??【項(xiàng)目實(shí)戰(zhàn)】基于Agent的金融問答系統(tǒng):前后端流程打通??,我們已經(jīng)完成了金融問答系統(tǒng)的前后端搭建,形成了可用的Demo。本章,我們將介紹代碼重構(gòu)的過程,并介紹一些優(yōu)化點(diǎn)。
代碼重構(gòu)簡(jiǎn)介
在開啟本章介紹之前,請(qǐng)?jiān)试S我花點(diǎn)時(shí)間啰嗦兩句,聊一聊代碼重構(gòu)的哪些事兒。
在過去經(jīng)歷的項(xiàng)目中,代碼重構(gòu)很少被人重視。看著像??一樣的代碼(抱歉爆粗口,我所經(jīng)歷的一些項(xiàng)目包括我自己曾經(jīng)寫的代碼,回看確實(shí)像??一樣),它們并沒有被好好清理,然后我們?cè)??上面不斷加需求,導(dǎo)致需求迭代越來越難,Bug越來越多...
這種事情現(xiàn)在每天還在不斷地發(fā)生著,所以我決定有必要聊一聊代碼重構(gòu)。
什么是代碼重構(gòu)
代碼重構(gòu)是指對(duì)現(xiàn)有代碼進(jìn)行修改,以改善其結(jié)構(gòu)、可讀性和可維護(hù)性,而不改變其外部行為。重構(gòu)的主要目的是提高代碼質(zhì)量,使其更易于理解和擴(kuò)展。
代碼重構(gòu)的目的
? 提高可讀性:使代碼更易于理解,便于團(tuán)隊(duì)成員快速上手。
? 增強(qiáng)可維護(hù)性:降低后續(xù)修改和擴(kuò)展的難度,減少潛在的錯(cuò)誤。
? 優(yōu)化性能:在不改變功能的情況下,提升代碼的執(zhí)行效率。
? 消除重復(fù)代碼:通過抽象和重用,減少冗余,提高代碼的整潔性。
代碼重構(gòu)的重要性
據(jù)統(tǒng)計(jì),不好的代碼會(huì)占用更多開發(fā)的時(shí)間。
代碼重構(gòu)的難點(diǎn)
通過代碼重構(gòu)提升代碼質(zhì)量既然如此重要,那么為什么很少有項(xiàng)目開展呢?
究其原因,可能有三點(diǎn):
? 第一種:沒有精力重構(gòu)。開發(fā)工程師經(jīng)常性被老板或者產(chǎn)品牽著鼻子走,完成一個(gè)需求接著一個(gè)新的需求,所以很少開展重構(gòu)工作。這種情況在技術(shù)性為導(dǎo)向的項(xiàng)目還好,在以產(chǎn)品或市場(chǎng)為導(dǎo)向的項(xiàng)目中,尤其嚴(yán)重。
? 第二種:沒有重構(gòu)的思維。很多的開發(fā)工程師沒有重構(gòu)的思維甚至想法,他們以完成需求交付為目的,需求交付了也就代表他的工作結(jié)束了。
我曾經(jīng)與谷歌回來的一位朋友有次交流,我們探討的內(nèi)容是:為什么國(guó)內(nèi)的研發(fā)人員代碼質(zhì)量意識(shí)薄弱?他說其中一個(gè)很重要的原因是:硅谷的很多從業(yè)者,是因?yàn)闊釔?,熱愛編程、熱愛技術(shù),所以視自己寫的代碼為一件藝術(shù)品,力求精益求精;而國(guó)內(nèi)有很多從業(yè)者,是因?yàn)樯?,是因?yàn)樽鲩_發(fā)給錢多,是一份養(yǎng)家糊口的一份工作而已,因?yàn)槿鄙贌釔郏越徊盍耸录纯?。?duì)此,我深以為然。 ? 第三種:沒有重構(gòu)的方法論。雖然我們很像做重構(gòu),但是重構(gòu)工作就像修復(fù)一輛越開越慢的車子,如果沒有科學(xué)的方法,有可能出現(xiàn)拆了重裝之后,反而多了幾個(gè)螺絲的問題,這會(huì)讓老板更加恐怖。
本章,我將試圖以這個(gè)金融問答系統(tǒng)為例,簡(jiǎn)單介紹一些代碼重構(gòu)的原則、方法。
代碼重構(gòu)的過程
1、搭建測(cè)試框架以及用例集
在開展代碼重構(gòu)前,我們要搭建好一個(gè)便于回歸測(cè)試的測(cè)試框架,通過邊重構(gòu)邊回歸的方式,可以快速定位問題所在,以此降低問題排查的成本。
我們?cè)赼pp目錄下,已經(jīng)創(chuàng)建了一個(gè)test_framework.py中,繼續(xù)補(bǔ)充測(cè)試用例集,例如:
在大廠中,回歸測(cè)試一般會(huì)使用單元測(cè)試框架(如pytest)來進(jìn)行執(zhí)行,由于本例中我們的方法較為簡(jiǎn)單,所以就沒有使用pytest。
2、消滅代碼中的壞味道
2.1、統(tǒng)一管理配置相關(guān)內(nèi)容
在之前實(shí)現(xiàn)的RAG管理模塊中,有很多的配置是硬編碼寫在代碼初始化中的,例如:
# 原始的rag.py
class RagManager:
def __init__(self,
chroma_server_type="http",
host="localhost", port=8000,
persist_path="chroma_db",
llm=None, embed=None):
self.llm = llm
self.embed = embed
chrom_db = ChromaDB(chroma_server_type=chroma_server_type,
host=host, port=port,
persist_path=persist_path,
embed=embed)
self.store = chrom_db.get_store()
我們可以將所有的配置相關(guān)抽取到一個(gè)settings.py中,然后在使用的代碼中通過引用settings.py來進(jìn)行配置。
# setttings.py
"""
Chroma向量數(shù)據(jù)庫使用時(shí)的相關(guān)的配置
"""
# 默認(rèn)的ChromaDB的服務(wù)器類別
CHROMA_SERVER_TYPE ="http"
# 默認(rèn)本地?cái)?shù)據(jù)庫的持久化目錄
CHROMA_PERSIST_DB_PATH ="chroma_db"
CHROMA_HOST = os.getenv("CHROMA_HOST","localhost")
CHROMA_PORT =int(os.getenv("CHROMA_PORT",8000))
CHROMA_COLLECTION_NAME ="langchain"
說明:
- 為了有別于變量的命名,對(duì)于配置我們使用大寫的變量名,例如:CHROMA_HOST、CHROMA_PORT等。
# 重構(gòu)的rag.py
import settings
classRagManager:
def__init__(self,
vector_db_class=ChromaDB, # 默認(rèn)使用 ChromaDB
db_config=None, # 數(shù)據(jù)庫配置參數(shù)
llm=None, embed=None,
retriever_cls=SimpleRetrieverWrapper, **retriever_kwargs):
self.llm = llm
self.embed = embed
logger.info(f'初始化llm大模型:{self.llm}')
logger.info(f'初始化embed模型:{self.embed}')
# 如果沒有提供 db_config,使用默認(rèn)配置
if db_config isNone:
db_config ={
"chroma_server_type": settings.CHROMA_SERVER_TYPE,
"host": settings.CHROMA_HOST,
"port": settings.CHROMA_PORT,
"persist_path": settings.CHROMA_PERSIST_DB_PATH,
"collection_name": settings.CHROMA_COLLECTION_NAME
}
logger.info(f'初始化向量數(shù)據(jù)庫配置:{db_config}')
# 創(chuàng)建向量數(shù)據(jù)庫實(shí)例
self.vector_db = vector_db_class(**db_config, embed=self.embed)
self.store = self.vector_db.get_store()
說明:
? 上述代碼中通過import settings,在使用配置時(shí)通過settings.CHROMA_SERVER_TYPE、settings.CHROMA_HOST等來引用。
2.2、處理參數(shù)過長(zhǎng)的問題
在原始代碼中,隨著我們的需求迭代,在創(chuàng)建RAG時(shí)需要傳入多個(gè)的參數(shù),例如:
? chroma_server_type
? host
? port
? persist_path
? collection_name
如果按照原來的方法寫函數(shù),那么函數(shù)的參數(shù)列表就會(huì)非常長(zhǎng),如下:
RagManager(chroma_server_type="http", host="localhost", port=8000, persist_path="chroma_db", collection_name="langchain",llm , embed)
對(duì)于這種參數(shù)的問題,我們可以通過使用字典來處理,如下:
db_config = {
"chroma_server_type": settings.CHROMA_SERVER_TYPE,
"host": settings.CHROMA_HOST,
"port": settings.CHROMA_PORT,
"persist_path": settings.CHROMA_PERSIST_DB_PATH,
"collection_name": settings.CHROMA_COLLECTION_NAME,
}
RagManager(vector_db_class=ChromaDB, db_config=db_config, llm=self.llm, embed=self.embed)
說明:
? db_config是一個(gè)字典,可以包含多個(gè)配置參數(shù),例如:chroma_server_type、host、port、persist_path、collection_name等。
? db_config中的參數(shù)可以通過**關(guān)鍵字來解包,從而傳入到函數(shù)中。
? RagManager 的初始化函數(shù)中,通過**關(guān)鍵字來解包db_config,從而傳入到ChromaDB的初始化函數(shù)中。
2.3、減少重復(fù)代碼
在【項(xiàng)目實(shí)戰(zhàn)】基于Agent的金融問答系統(tǒng):RAG檢索模塊初建成中,我們?cè)鴮?shí)現(xiàn)了一個(gè)pdf_processor.py, 該函數(shù)主要的工作是:
def process_pdfs(self)# 處理pdf文件
defprocess_pdfs_group(self, pdf_files_group)# 分組處理pdf文件
defload_pdf_files(self)# 加載pdf文件
defload_pdf_content(self, pdf_path)# 讀取pdf文件內(nèi)容
defsplit_text(self, documents)# 分割讀取到的文本
definsert_docs_chromadb(self, docs, batch_size) # 向向量數(shù)據(jù)庫中插入數(shù)據(jù)
如果我們要將PDF文件給ElasticSearch服務(wù)里,那么這個(gè)過程大部分實(shí)現(xiàn)邏輯都是一樣的,只是插入的對(duì)象不同,一個(gè)是向向量數(shù)據(jù)庫中插入,一個(gè)是向elasticsearch中插入。
這種情況下,
? 不好的做法:復(fù)制上述代碼到一個(gè)新的函數(shù)中,然后將最后一步insert_docs_chromadb()改為insert_docs_elasticsearch(),這樣會(huì)導(dǎo)致代碼重復(fù)。
? 較好的做法:對(duì)上述的插入過程進(jìn)行重構(gòu),將插入函數(shù)通過函數(shù)類來調(diào)用,通過一個(gè)參數(shù)vector_db_class來決定插入向量數(shù)據(jù)庫還是ElasticSearch。
重構(gòu)后的pdf_processor.py
import os
import logging
import time
from tqdm import tqdm
from langchain_community.document_loaders importPyMuPDFLoader
from langchain_text_splitters importRecursiveCharacterTextSplitter
from rag.vector_db importVectorDB
from rag.elasticsearch_db importTraditionDB
from utils.logger_config importLoggerManager
logger =LoggerManager().logger
classPDFProcessor:
def__init__(self, directory, db_type='vector', **kwargs):
"""
初始化 PDF 處理器
:param directory: PDF 文件所在目錄
:param db_type: 數(shù)據(jù)庫類型 ('vector' 或 'es')
:param kwargs: 其他參數(shù)
"""
self.directory = directory # PDF 文件所在目錄
self.db_type = db_type # 數(shù)據(jù)庫類型
self.file_group_num = kwargs.get('file_group_num',20)# 每組處理的文件數(shù)
self.batch_num = kwargs.get('batch_num',6)# 每次插入的批次數(shù)量
self.chunksize = kwargs.get('chunksize',500)# 切分文本的大小
self.overlap = kwargs.get('overlap',100)# 切分文本的重疊大小
logger.info(f"""
初始化PDF文件導(dǎo)入器:
配置參數(shù):
- 導(dǎo)入的文件路徑:{self.directory}
- 每次處理文件數(shù):{self.file_group_num}
- 每批次處理樣本數(shù):{self.batch_num}
- 切分文本的大?。簕self.chunksize}
- 切分文本重疊大?。簕self.overlap}
""")
根據(jù)數(shù)據(jù)庫類型初始化相應(yīng)的客戶端
if db_type =='vector':
self.vector_db = kwargs.get('vector_db')# 向量數(shù)據(jù)庫實(shí)例
self.es_client =None
logger.info(f'導(dǎo)入的目標(biāo)數(shù)據(jù)庫為:向量數(shù)據(jù)庫')elif db_type =='es':
self.vector_db =None
self.es_client = kwargs.get('es_client')# Elasticsearch 客戶端
logger.info(f'導(dǎo)入的目標(biāo)數(shù)據(jù)庫為:ES數(shù)據(jù)庫')else:
raiseValueError("db_type must be either 'vector' or 'es'.")
defload_pdf_files(self):
這部分代碼未做修改,具體內(nèi)容省略
defload_pdf_content(self, pdf_path):
這部分代碼未做修改,具體內(nèi)容省略
defsplit_text(self, documents):
這部分代碼未做修改,具體內(nèi)容省略
defprocess_pdfs(self):
這部分代碼未做修改,具體內(nèi)容省略
definsert_docs(self, docs, insert_function, batch_size=None):
"""
將文檔插入到指定的數(shù)據(jù)庫,并顯示進(jìn)度
:param docs: 要插入的文檔列表
:param insert_function: 插入函數(shù)
:param batch_size: 批次大小
"""
if batch_size isNone:
batch_size = self.batch_num
logging.info(f"Inserting {len(docs)} documents.")
start_time = time.time()
total_docs_inserted =0
total_batches =(len(docs)+ batch_size -1)// batch_sizewith tqdm(total=total_batches, desc="Inserting batches", unit="batch")as pbar:
for i inrange(0,len(docs), batch_size):
batch = docs[i:i + batch_size]
insert_function(batch)# 調(diào)用傳入的插入函數(shù)
total_docs_inserted +=len(batch)計(jì)算并顯示當(dāng)前的TPM
elapsed_time = time.time()- start_timeif elapsed_time >0:
tpm =(total_docs_inserted / elapsed_time)*60
pbar.set_postfix({"TPM":f"{tpm:.2f}"})
pbar.update(1)definsert_to_vector_db(self, docs):
"""
將文檔插入到 VectorDB
"""
self.vector_db.add_with_langchain(docs)
definsert_to_elasticsearch(self, docs):
"""
將文檔插入到 Elasticsearch
"""
self.es_client.add_documents(docs)
defprocess_pdfs_group(self, pdf_files_group):
讀取PDF文件內(nèi)容
pdf_contents =[]for pdf_path in pdf_files_group:
讀取PDF文件內(nèi)容
documents = self.load_pdf_content(pdf_path)將documents 逐一添加到pdf_contents
pdf_contents.extend(documents)將文本切分成小段
docs = self.split_text(pdf_contents)if self.db_type =='vector':
將文檔插入到 VectorDB
self.insert_docs(docs, self.insert_to_vector_db)elif self.db_type =='es':
將文檔插入到 Elasticsearch
self.insert_docs(docs, self.insert_to_elasticsearch)else:
raiseValueError("db_type must be either 'vector' or 'es'.")
說明:
? 在類的初始化函數(shù)中,我們通過一個(gè)參數(shù)vector_db來連接對(duì)應(yīng)的數(shù)據(jù)庫實(shí)例,同時(shí)傳入db_type告知PDF處理器需要操作的數(shù)據(jù)庫類型。
? 在處理PDF文件時(shí),我們通過參數(shù)db_type來決定插入向量數(shù)據(jù)庫還是ElasticSearch。
? 在插入文檔 insert_docs 中,根據(jù)上一步驟傳入的 insert_function 來調(diào)用具體的插入函數(shù):如果是插入向量數(shù)據(jù)庫,則傳入的函數(shù)為self.insert_to_vector_db,那么調(diào)用時(shí)也會(huì)調(diào)用 insert_to_vector_db ;如果是插入ElasticSearch,則傳入的函數(shù)為self.insert_to_elasticsearch,那么調(diào)用時(shí)會(huì)調(diào)用 insert_to_elasticsearch 。
2.4、使用靜態(tài)掃描工具優(yōu)化代碼風(fēng)格
我們可以使用靜態(tài)掃描工具對(duì)代碼進(jìn)行風(fēng)格優(yōu)化,如Pylint、Flake8等,一般情況下PyCharm中會(huì)自帶這些工具。
具體方法:
- 啟動(dòng)PyCharm
- 打開工程時(shí),選擇app目錄
- 打開任意.py文件后,右上角會(huì)有靜態(tài)掃描問題提示(如下圖)
- 根據(jù)靜態(tài)掃描的問題,進(jìn)行代碼風(fēng)格修正(常見代碼風(fēng)格問題請(qǐng)見附錄部分)
3、回歸測(cè)試
在進(jìn)行上面每一步重構(gòu)時(shí),都需要使用test_framework.py進(jìn)行回歸測(cè)試,確保重構(gòu)后的代碼沒有引入新的錯(cuò)誤。
由于本項(xiàng)目重構(gòu)細(xì)節(jié)的內(nèi)容非常多,不能一一列舉,重構(gòu)后的內(nèi)容請(qǐng)查看Gitee或者Github倉庫的代碼。
?
本文轉(zhuǎn)載自公眾號(hào)一起AI技術(shù) 作者:Dongming
