使用RAG技术构建企业级文档问答系统之使用TruLens进行评估
1 概述
RAG的评估,坦率地说,是个技术难点。目前业界的常见做法,大致有这么几种:

人工评估:用多人盲审投票,准确率通常最高,但成本太高、效率低到几乎没有可操作性。
BLEU、ROUGE这类老牌指标:需要参考答案,而且不太考虑语义,现在RAG评估里用得很少了。
RAGAS评估:来自论文《RAGAS: Automated Evaluation of Retrieval Augmented Generation》,是一种不需要参考答案的自动化评测方法,目前基本是主流。
用GPT-4打分:虽然争议不小,但实际中只要把Prompt调几轮,跟人工评估的一致性还挺高的,不过需要参考答案。
语义相似度+关键词加权打分:常用于竞赛,企业级开发中很少用——因为得有正确答案,还得有关键词列表。
普遍的认识是,自动化评估的精度并不算高。吴恩达DeepLearning.AI平台上那门《Building and Evaluating Advanced RAG》课程里提到,当前自动化评估与人工评估的一致性只有80%多。这个数字供参考,但实际场景里通常不会这么低——一来那门课是2023年出的,当时大模型整体还没现在这么强;二来课程里介绍的方法(也就是本文要讲的方法)没有参考答案,而实际工作中一般会准备参考答案,在有参考答案的情况下,自动化评估与人工评估的一致性还是容易达到较高水平的。
本文要介绍的方法,借助TruLens库评估三个指标,TruLens称之为RAG三元组(RAG triad),由上下文相关性(Context Relevance)、依据性(Groundedness)和答案相关性(Answer Relevance)组成,如下图所示。这三个指标可以看作是RAGAS的子集,后面会有专门文章讲RAGAS评估。
(参考官方文档:https://www.trulens.org/trulens_eval/getting_started/core_concepts/rag_triad/)
- :任何RAG应用的第一步就是信息检索。为了验证检索质量,需要确保每个上下文片段与输入查询高度相关。这一点至关重要,因为LLM将凭这些上下文来生成答案,不相关的信息会直接导致幻觉。TruLens通过序列化记录的结构来评估上下文的相关性。
上下文相关性(Context Relevance)
- :检索到上下文之后,LLM会用它来形成答案。但LLM经常偏离提供的事实,夸大或延展成看似合理的回答。要验证依据性,可以把回答拆成独立陈述,然后在检索到的上下文里逐一找证据支撑。
依据性(Groundedness)
- :最终回答还是得解决原始问题。通过评估最终答案与用户输入的相关性,就能验证这一点。
答案相关性(Answer Relevance)
TruLens默认使用OpenAI的模型作为打分模型,但本文提供了更多选择——可以使用OpenAI兼容的LLM(比如千问),也可以用Ollama提供的模型。
本文代码已开源,地址在:https://github.com/Steven-Luo/MasteringRAG/blob/main/evaluation/01_trulens_evaluation.ipynb
2 准备环境
2.1 Ollama
先访问Ollama官网下载对应操作系统的版本并安装,然后拉取模型,这个模型会用作RAG中基于知识片段回答问题的LLM:
ollama pull qwen2:7b-instruct
2.2 Python
所有示例在安装Anaconda环境后,再安装下面的Python库即可。代码已在Google Colab测试过:
pip install -U langchain langchain_community pypdf sentence_transformers chromadb trulens_eval langchain_openai
本文使用的版本如下:
| 库 | 版本 |
|---|---|
| langchain | 0.2.7 |
| langchain_community | 0.2.7 |
| langchain_openai | 0.1.7 |
| pypdf | 4.2.0 |
| sentence_transformers | 2.7.0 |
| chromadb | 0.5.3 |
| trulens_eval | 0.33.0 |
文中用到的问答对和原始数据,都可以从代码仓库里取用。
3 核心代码
import os import pandas as pd
EMBEDDING_MODEL_PATH = dt = version = output_dir = os.path.join(os.path.pardir, , ) os.environ[] = os.environ[] =
加载问答对:
qa_df = pd.read_excel(os.path.join(output_dir, ))
3.1 文档处理
PyPDFLoader loader = PyPDFLoader(os.path.join(os.path.pardir, , )) documents = loader.load()
uuid4 RecursiveCharacterTextSplitter Chroma
(documents, filepath, chunk_size=, chunk_overlap=, seperators=[, ], force_split=):
os.path.exists(filepath) force_split:
()
pickle.load((filepath, ))
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separators=seperators
)
split_docs = splitter.split_documents(documents)
chunk in split_docs:
chunk.metadata[] = (uuid4())
pickle.dump(split_docs, (filepath, ))
split_docs
splitted_docs = split_docs(documents, os.path.join(output_dir, ), chunk_size=, chunk_overlap=)
向量化:
HuggingFaceBgeEmbeddings
device = torch.cuda.is_a vailable() ()
embeddings = HuggingFaceBgeEmbeddings(
model_name=EMBEDDING_MODEL_PATH,
model_kwargs={: device},
encode_kwargs={: })
tqdm
(docs, store_path, force_rebuild=):
os.path.exists(store_path):
force_rebuild =
if force_rebuild:
vector_db = Chroma.from_documents(
docs,
embedding=embeddings,
persist_directory=store_path
)
else:
vector_db = Chroma(
persist_directory=store_path,
embedding_function=embeddings
)
vector_db
vector_db = get_vector_db(splitted_docs, store_path=os.path.join(os.path.pardir, output_dir, , ))
3.2 问答全流程Pipeline
Ollama StrOutputParser RunnablePassthrough PromptTemplate
(docs): .join(doc.page_content for doc in docs)
llm = Ollama(
model=,
base_url=)
prompt_tmpl =
prompt = PromptTemplate.from_template(prompt_tmpl)
retriever = vector_db.as_retriever(search_kwargs={: })
rag_chain = (
{: retriever | format_docs, : RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
3.3 评估
3.3.1 准备测试集
prediction_df = qa_df[qa_df[] == ][[, , , ]]
3.3.2 初始化Feedback函数
TruLens默认使用OpenAI的LLM,下面提供了自定义OpenAI兼容API以及Ollama的方式。
ChatOpenAI
llm_chain = ChatOpenAI(
api_key=os.environ[],
base_url=os.environ[],
model_name=)
也可以用Ollama提供的模型:
# from langchain.llms import Ollama # llm_chain = Ollama( # model='qwen2:7b-instruct', # base_url="http://192.168.31.92:11434" # )
使用方式:
llm_chain.invoke()
App Feedback OpenAI Langchain LangchainProvider
provider = LangchainProvider(chain=llm_chain)
context = App.select_context(rag_chain)
f_groundedness = (
Feedback(provider.groundedness_measure_with_cot_reasons, name = )
.on(context.collect())
.on_output()
)
f_answer_relevance = (
Feedback(provider.relevance_with_cot_reasons, name = )
.on_input_output()
)
f_context_relevance = (
Feedback(provider.context_relevance_with_cot_reasons, name = )
.on_input()
.on(context)
.aggregate(np.mean)
)
✅ In Groundedness, input source will be set to __record__.app.first.steps__.context.first.invoke.rets[:].page_content.collect() . ✅ In Groundedness, input statement will be set to __record__.main_output or `Select.RecordOutput` . ✅ In Answer Relevance, input prompt will be set to __record__.main_input or `Select.RecordInput` . ✅ In Answer Relevance, input response will be set to __record__.main_output or `Select.RecordOutput` . ✅ In Context Relevance, input question will be set to __record__.main_input or `Select.RecordInput` . ✅ In Context Relevance, input context will be set to __record__.app.first.steps__.context.first.invoke.rets[:].page_content .
3.3.3 创建TruLens记录器
TruChain, Tru tru = Tru() tru.reset_database()
注意:
tru.reset_database(),否则之前的评估记录会被清除。评估记录默认保存在一个名为default.sqlite的SQLite数据库中。
? Tru initialized with db url sqlite:///default.sqlite . ? Secret keys may be written to the database. See the `database_redact_keys` option of `Tru` to prevent this.
tru_recorder = TruChain(
rag_chain,
app_id=,
feedbacks=[f_answer_relevance, f_context_relevance, f_groundedness]
)
answer_dict = {}
for idx, row in tqdm(prediction_df.iterrows(), total=(prediction_df)):
with tru_recorder as recording:
uuid = row[]
question = row[]
answer = rag_chain.invoke(question)
answer_dict[question] = {: uuid, : row[], : answer }
3.4 检查结果
tru.get_leaderboard()
total_cost为0是因为所用的模型不是OpenAI的,关联不上价格。
tru.run_dashboard()
启动后会出现类似下面的信息:
Starting dashboard ... Config file already exists. Skipping writing process. Credentials file already exists. Skipping writing process. Dashboard already running at path: Network URL: http://192.168.31.92:48913 Popen: returncode: None args: ['streamlit', 'run', '--server.headless=True'...
用浏览器访问上面代码运行后出现的地址即可。
在Leaderboard里可以直观地看到RAG triad三个评估指标。从这个指标能明显看出当前整个流程的薄弱环节。点击Evaluations可以看到每条测试数据的三个评估指标,再点击测试数据还能查看整个Trace的详情。
-
- 关于宇宙的好的网名有哪些
- 角色扮演 | 1
- 网名