关于 RAGAS 的基础资料,可以在这篇文档中查阅
高级RAG(四):RAGAs评估-CSDN博客
文档写的很详细,基本上可以拿来作为入门内容,非常推荐各位仔细阅读一下。
按照个人的习惯,本文依旧只介绍碰到的坑和对应的解决方案。
前言
GPT 测试已经持续做了接近一年了,中间接触了很多内容,也推动了公司的一些规范的建立,但是也越发感觉目前按照直觉建立的测试体系越来越难以应对复杂的 GPT 应用的测试了。
通过阅读大量的资料,我们其实比较早期的时候就知道 Benchmark 的重要性,当然 Benchmark 的含义比较大,缩略到测试层面的话,我简单理解成固定的测试集,用于衡量 GPT 优化的效果,防止出现优化了 A 问题又导致了 B 问题的出现。
Benchmark不是万能的,但是没有 Benchmark 也是万万不能的。
那如何开始呢?我们找到了网上特别火的 RAGAS,准备用它来开始。GitHub地址在这里
问题一、ragas.exceptions.OpenAIKeyNotFound: OpenAI API key not found! Seems like your trying to use Ragas metrics with OpenAI endpoints. Please set 'OPENAI_API_KEY' environment variable
这个问题在初始化 evaluate
时出现。
我司,包括目前绝大部分公司,因为 OpenAI 锁区的原因,服务器都是架设在海外的,通过VPN进行访问,所以我们并没有传统意义上的 OpenAI_API_KEY
,只有一个内网访问的服务器地址,这个地址一般通过 openai.api_base=XXXX
或者 openai.base_url=XXXX
来设置,而对应的 KEY 一般是空的。
初始化 evaluate
时,并没有相应的入参,所以我按照往常的设置方法,去设置内网访问地址,并将 KEY 设置为空,但是最终发现并没有效果,并且报了上述的错。
这个问题通过 Debug ,发现在对应的 Metric 类中报的
# ragas/metrics/_answer_relevance.py
@dataclass
class AnswerRelevancy(MetricWithLLM):
....
def _score_batch
....
results = self.llm.generate(
prompts,
n=self.strictness,
callbacks=batch_group,
)
找到关键的 llm 属性,发现 base_url
的属性并没有修改为我们自己的地址,还是 OpenAI原生的,这里肯定有问题。
翻阅资料之后,发现 Metric 的方法是可以自定义 llm 的,源码如下
# ragas/metrics/base.py
@dataclass
class MetricWithLLM(Metric):
llm: RagasLLM = field(default_factory=llm_factory)
比较关键的是,它这边定义的 llm 必须是 RagasLLM类型的,那继续查找源码,在 RagasLLM
的定义文件中找到了一段注释
# ragas/llms/base.py
class RagasLLM(ABC):
"""
BaseLLM is the base class for all LLMs. It provides a consistent interface for other
classes that interact with LLMs like Langchains, LlamaIndex, LiteLLM etc. Handles
multiple_completions even if not supported by the LLM.
It currently takes in ChatPromptTemplates and returns LLMResults which are Langchain
primitives.
"""
在这里看到一个非常熟悉的名词 Langchains
。在该文件的同级目录下,也找到了 langchain.py
这个文件,阅读源码后,我抱着试一试的心态更新了如下代码
from langchain.chat_models.openai import ChatOpenAI
from ragas.llms.langchain import LangchainLLM
from ragas.metrics import context_recall, context_precision, faithfulness, answer_relevancy
os.environ["OPENAI_API_BASE"] = "公司提供的GPT访问地址"
llm = ChatOpenAI(model="gpt-3.5-turbo-1106")
rag_llm = LangchainLLM(llm=llm)
faithfulness.llm = rag_llm
context_recall.llm = rag_llm
context_precision.llm = rag_llm
answer_relevancy.llm = rag_llm
然后顺利的过了这个问题。
但是,过了这个坎,一刻都还没高兴呢,运行报了另一个错。
先总结一下这个问题吧。
刚开始接触 RAGAS,对于整个的运作逻辑不是很清晰,所以折腾了比较久才找到解决方案,源码都翻烂了,谷歌也找不到原因。
后面随着源码的深入阅读,逐渐理解了整个 RAGAS 的核心其实是 Metric
,evaluate
就负责计算,但计算所需的数据都是 Metirc
里提供的,这让我想到了深度定制化,不出意外的话这里应该可以自定义 Metric ,提供你的价值评判维度(猜的,还没验证)。
问题二、openai.APIConnectionError: Connection error.
这是一个漫长的问题,它有可能是 Mac 专属的问题。
事情的起因是因为这个问题:
langchain openai.lib._old_api.APIRemovedInV1:
这个问题 OpenAI 已经提供了详细的原因了
You tried to access openai.Completion, but this is no longer supported in openai>=1.0.0 - see the README at [GitHub - openai/openai-python: The official Python library for the OpenAI API 261](https://github.com/openai/openai-python) for the API.
You can run `openai migrate` to automatically upgrade your codebase to use the 1.0.0 interface.
Alternatively, you can pin your installation to the old version, e.g. `pip install openai==0.28`
A detailed migration guide is available here: [v1.0.0 Migration Guide · openai/openai-python · Discussion #742 · GitHub 332](https://github.com/openai/openai-python/discussions/742)
在使用 RAGAS 之前,我一直用的 openai=0.28.1
版本,而 RAGAS 要求 1.0 以上的,更新之后就出现这个错误。
于是我把请求方法从 openai.ChatCompletion.create
修改为了 openai.chat.completions.create
解决了这个问题。
可是我再次运行后,却发现事情远没有这么简单,对,你没猜错,又报了一个新的错,也就是这个问题二。
我非常纳闷,在没有更新新版的 OpenAI 之前,我一直都可以正确的去请求的,但是更新了之后为什么就报连接失败呢?
首先,肯定不是我的电脑配置问题。发现问题后,我回退到 0.28.1 版本后依旧可以正确的请求。
其次,肯定不是 RAGAS 的问题。2800 星的项目,肯定不会有这种入门的问题。
我只能想到是请求方法这里做了改动,于是又开始了漫长的代码翻阅工作,说实话,很累,尤其是对这个库没有任何基础的情况下,很多有的没得的方法,我都一一进行了阅读,毕竟,万一问题就在这里呢。
很幸运,这方面 谷歌 还能找到一些资料,让我直接定位到了 1.10 版本 OpenAI 使用的是 httpx
这个库进行网络请求。
于是我立刻拿完全相同的参数,分别使用 requests
和 httpx
分别进行请求,结果果然如我所想,requests
的请求依旧正常,而 httpx
则直接报错
Traceback (most recent call last):
File "/Users/ning/zhikan/GPT+/gpt_case/model/test11.py", line 52, in <module>
result = client.send(req, stream=True)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/ning/zhikan/GPT+/gpt_case/venv/lib/python3.11/site-packages/httpx/_client.py", line 915, in send
response = self._send_handling_auth(
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/ning/zhikan/GPT+/gpt_case/venv/lib/python3.11/site-packages/httpx/_client.py", line 943, in _send_handling_auth
response = self._send_handling_redirects(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/ning/zhikan/GPT+/gpt_case/venv/lib/python3.11/site-packages/httpx/_client.py", line 980, in _send_handling_redirects
response = self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/ning/zhikan/GPT+/gpt_case/venv/lib/python3.11/site-packages/httpx/_client.py", line 1016, in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/ning/zhikan/GPT+/gpt_case/venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 230, in handle_request
with map_httpcore_exceptions():
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/contextlib.py", line 155, in __exit__
self.gen.throw(typ, value, traceback)
File "/Users/ning/zhikan/GPT+/gpt_case/venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 84, in map_httpcore_exceptions
raise mapped_exc(message) from exc
httpx.RemoteProtocolError: Server disconnected without sending a response.
应该就是这个问题影响了整个请求,但是为什么呢?经过大量的谷歌,发现这个问题居然完全找不到和我一模一样的情况。只能继续硬着头皮看源码了。
从上述的报错信息中,我定位到了 response = transport.handle_request(request)
这一段。切到对应的源码处,我终于找到了一段像珍宝一样的注释(找到这段注释时,笔者已经渡过了漫长疲惫的三个小时)。
# httpx/_transports/base.py
def handle_request(self, request: Request) -> Response:
"""
Send a single HTTP request and return a response.
Developers shouldn't typically ever need to call into this API directly,
since the Client class provides all the higher level user-facing API
niceties.
In order to properly release any network resources, the response
stream should *either* be consumed immediately, with a call to
`response.stream.read()`, or else the `handle_request` call should
be followed with a try/finally block to ensuring the stream is
always closed.
Example usage:
with httpx.HTTPTransport() as transport:
req = httpx.Request(
method=b"GET",
url=(b"https", b"www.example.com", 443, b"/"),
headers=[(b"Host", b"www.example.com")],
)
resp = transport.handle_request(req)
body = resp.stream.read()
print(resp.status_code, resp.headers, body)
Takes a `Request` instance as the only argument.
Returns a `Response` instance.
"""
这里透露了一个非常重要的信息,就是 transport.handle_request
这个方法可以提取出来自己调试。这个时候我其实还不知道,这里能找到正确答案,但是漫长的3个小时没有结果的等待、搜索、翻阅,看到这段代码时,我依旧兴奋了起来。
with httpx.HTTPTransport() as transport:
req = httpx.Request(
method="POST",
url=url,
json=json_data
)
resp = transport.handle_request(req)
body = resp.read()
print(json.loads(body))
>>>{'id': 'chatcmpl-8mfo8psxLwl2upMU5R6iunFmgfX47', 'object': 'chat.completion', 'created': 1706611144, 'model': 'gpt-3.5-turbo-1106', 'choices': [{'index': 0, 'message': {'role': 'assistant', 'content': "J'aime la programmation."}, 'logprobs': None, 'finish_reason': 'stop'}], 'usage': {'prompt_tokens': 19, 'completion_tokens': 7, 'total_tokens': 26}, 'system_fingerprint': 'fp_aaa20cc2ba'}
OK,居然成功的得到了结果,我瞬间有种柳暗花明的感觉,连忙查看为什么自己手写的可以成功,而通过 RAGAS 走到这里的代码却报错了,经过 Debug ,发现了如下图的问题。
可以看到两个 _pool 的类型是不同的,这也是导致一个成功一个失败的关键点。
最终,我使用自定义的 transport 去替代 RAGAS 自带的,就可以成功跑通了。具体代码如下:
from langchain.chat_models.openai import ChatOpenAI
from ragas.llms.langchain import LangchainLLM
from ragas.metrics import context_recall, context_precision, faithfulness, answer_relevancy
import httpx
transport = httpx.HTTPTransport()
client = httpx.Client(verify=False, transport=transport, timeout=30)
llm = ChatOpenAI(model="gpt-3.5-turbo-1106", http_client=client)
rag_llm = LangchainLLM(llm=llm)
faithfulness.llm = rag_llm
context_recall.llm = rag_llm
context_precision.llm = rag_llm
answer_relevancy.llm = rag_llm
OK,到了这里,我也是如释重负,点击了启动按钮。
等待了大概30S左右后,一个令我意想不到的报错又弹了出来
openai.APIConnectionError: Connection error.
诶!诶?啊?
我直接一个三连!对,你没看错,就是跟问题二一模一样的一个问题,WTF!经过了三个小时的折磨,又经过20分钟的修改和调试,我在每一个步骤都确认了,这个解决方案是有效的,甚至上述几个 Metric
的结果都已经拿到了,为什么还报这个错?
仔细阅读了报错信息,发现虽然错误是一样的,但是报错发生的地点不一样,这次是发生在 openai/resources/embeddings.py
这个文件内,经过这么久的打磨,我看到这个问题的第一时间就发现了问题的关键——肯定是没有使用相同的 httpx 实例去请求的,应该还是走的默认生成的。
一番调试后确认了我的猜想,接下来就是怎么自定义一个新的 Embeddings 并传入了。
在问题一的时候,经过大量的调试,其实我已经明白 Metric
是负责数据生成的,很自然的我就想到应该在这里传入新的 Embeddings,查看源码后,也顺利在基类 MetricWithLLM
中找到了 embeddings
属性。最终代码如下
from langchain.chat_models.openai import ChatOpenAI
from ragas.llms.langchain import LangchainLLM
from ragas.metrics import context_recall, context_precision, faithfulness, answer_relevancy
import openai
from openai.resources.embeddings import Embeddings
import httpx
transport = httpx.HTTPTransport()
client = httpx.Client(verify=False, transport=transport, timeout=30)
llm = ChatOpenAI(model="gpt-3.5-turbo-1106", http_client=client)
this_open_ai = openai.OpenAI(base_url="公司地址", http_client=client)
embedding = Embeddings(this_open_ai)
rag_llm = LangchainLLM(llm=llm)
faithfulness.llm = rag_llm
faithfulness.embeddings = embedding
context_recall.llm = rag_llm
context_recall.embeddings = embedding
context_precision.llm = rag_llm
context_precision.embeddings = embedding
answer_relevancy.llm = rag_llm
answer_relevancy.embeddings = embedding
至此,踩坑完毕。
后记
啊!资料太少了,基本上碰到一个问题就要读源码,自己深入理解,再去解决,花了特别多时间。不过也算是对 RAGAS 有了一个特别深入的了解吧,收之桑榆了。