RAGAS 踩坑记

关于 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,
                    )
image.png

找到关键的 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 的核心其实是 Metricevaluate 就负责计算,但计算所需的数据都是 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 这个库进行网络请求。

于是我立刻拿完全相同的参数,分别使用 requestshttpx 分别进行请求,结果果然如我所想,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 ,发现了如下图的问题。

RAGAS自带的transport pool 类型
自定义transport的 pool 类型

可以看到两个 _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 有了一个特别深入的了解吧,收之桑榆了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容