祝你好运的技术博客

Published on

从零开始学RAG(三)

Authors
  • avatar
    Name
    祝你好运
    Twitter

前面我们已经做了V2的版本,已经可以实现多轮对话(比如询问有多长时间病假,就需要知道用户的国家,用户没说的话就得询问用户),同时之前也加了几个测试用例,但这些太少了不足以覆盖各个文档。

AI结果的验证

大模型的输出结果验证是一个很大的学习方向,我这里也只是粗略学一下。对比一下我们之前做单元测试,各个函数或者系统的结果基本都是确定性的,所以测试的时候各种断言也都比较好写。但大模型的输出结果是不确定的,尤其是他可以用自然语言的形式给出答案。目前我知道的几种验证方法:

关键词

比如用户问病假有几天,我们已经知道了对应答案(直接看对应的输入材料),比如西班牙的非工伤病假前3天没有工资,如果是中国这种没有特殊规定的国家用兜底政策的话至少有20天病假。那添加这些测试用例的时候,我们就可以用下面的条件作为校验条件:

  • 对应的国家名称。比如“西班牙”或者“中国”,当然我这里说的是测试直接用中文。
  • 包含3(如果是西班牙),或者包含20(如果是中国)。

当然我们也可以一眼看出来通过关键词并不是完全靠谱的,不然他换成日语的话,这些关键词就完全作废了。但他的优势也很明显,规则简单好实现,执行速度快。

结构化输出校验

大模型的默认输出是自然语言形式的,所以不好测试,那我们可以要求他按照我们要求的格式输出,这样我们就好测试了。比如我们问病假有几天,我要求他回答的时候有一个intent字段,这个字段的值是固定的,有"sick_leave", "annual_leave", "salary", "unknown"等等。然后我们校验的时候就直接精准匹配就好了。 当然这样也不是没有缺点的,我们得精准覆盖所有的intent选项,前期材料整理很费劲。

检索依据

前面几个校验都没有这个校验简单准确,因为回答问题的材料基本是固定的,就直接校验下面的条件就好了:

  • source精准匹配。也就是具体来自哪个文件。
  • chunk精准匹配。具体是哪个chunk。

语义相似度比较

自然语言输出的特点就是不好做比较,但我们可以再次把标准答案和待核验答案这些自然语言再做一次embedding,然后比较他们的相似度。当然这个肯定也不是完全靠谱,如果直接堆砌关键词,相似度达标,但意思肯定不对的。所以这个可以作为一个备用手段,辅助检查测试结果。

LLM-as-Judge

上面语义相似度比较容易被堆砌关键词这种情况绕过,但如果我们直接把这些句子交给大模型,让他帮我们做校验,不就好了?好是好,就是有点贵,而且慢,还有就是大模型也不是万能的,他会不稳定。这个也只能作为少部分case使用, 因为真的是又贵又慢,如果弄个几百条,一轮测试都得几十分钟,成本几十几百的话,一天跑那么多轮(CI/CD肯定要跑的),也会有压力。

综合打分机制

对于不涉及大模型调用的,通常都可以用精准匹配。对于涉及大模型的,我们可以组合各个验证方法,每种方法打分,然后我们按照总分评判是否算通过,这样算是最可靠的方法。当然具体细节要根据实际情况(比如多少问题,成本考量,问题类型等等因素)来针对性的调整。

我们项目中的情况

这一期我准备了大约292个retrieval相关的问题,12个e2e的测试。retrieval相关的问题都是精准匹配,e2e的目前是计划用关键词匹配,别管那么多,先干起来,不行再学再调整。当然了这么多问题,我肯定是借助AI生成的问题,然后我抽查一部分,然后再看测试的时候,不对的我再去仔细分析。

测试情况

可以看出第一轮的测试情况非常不理想,30%的用例都没通过,没通过的原因有下面几类:

  • 问题不合适。垃圾问题那只能用垃圾答案,挺多的,需要重新整理问题
  • 提问和答案语言不一样。我们的材料是英文的,我的提问和校验材料是中文的,这个也引出了另一个重要主题:query rewrite(后面再整理,目前unit test里面不适合引入,因为太慢)

然后经过题目优化,我们的回答错误率从41.4%降低到了19%,巨大进步:

再次优化

然后我们再次分析看还有什么可以优化的:

期望源选错(约 20+ 条)

也就说我们选答案的时候,选错了,选成了答案所在目录的入口页,入口页是一个薄页,内容很少,泛泛的概括了一下整个目录内的内容。比如sales/process/_index.md,只有 front matter,向量里几乎没有内容。

还有就是选了一个更宽泛的页面作为答案,而我们搜索到的是更具体的答案,也是参考答案错了,不是我们搜索回答错了。

search_query 被批量灌污染(约 15+ 条)

这个就得详细解释下我们这些问题的来源,最开始我们是通过AI生成的中文问题,这样不好,因为中文embedding成向量之后,跟原始文档英文embedding成向量之后,本身就是有差距的。所以在上一轮优化的时候,问题都用英文的了。又因为生成的比较粗糙,所以search_query跟问题都对不上,比如下面这两个:

    "id": "eng-001",
    "question": "代码评审 Code Review 怎么做?",
    "search_query": "Overview code review code review engineering workflow iteration"

所以这些问题我们得再完善一下search_query

其他

也是问题和答案构造的不够好(不是不够好,而是乱七八糟)。当然这里就不得不提到现实情况,现实情况里面肯定一开始也是乱七八糟的问题,用户也会有各种不满意,我们得记录线上跑的情况,然后多优化几轮。

结果

这次优化完就剩约5.5%的错误率了,进步巨大:

第3次优化

原因分析

  • 给出的答案仍然类似空壳页
  • Query太短,就三四个单词(query Rewrite后面再做)

优化后还有2个错误,然后再仔细看下对应的问题和答案,说实话剩余的这两个问题,太宽泛了,单个文档确实是不好回答,所以并不是我们chunk和Query有问题,就是问题出的不好,或者说现在的这种验证机制不好,回答这些问题就是需要多个文档参与,我们现在设计的校验,是单个文档中单个chunk作为标准答案的。

  {
    "id": "com-004",
    "topic": "communication",
    "question": "内部沟通指引?",
    "expected_source": "gitlab_handbook/communication/_index.md",
    "expected_section": "Effective & Responsible Communication Guidelines",
    "expected_country_code": null,
    "expected_entity": null,
    "search_query": "Internal communication handbook guidelines"
  },

还有下面这个问题:

  {
    "id": "sup-005",
    "topic": "support",
    "question": "Support 数据如何处理 data handling?",
    "search_query": "Handling data as a Support Engineer sensitive data best practices workflow",
    "expected_source": "gitlab_handbook/support/workflows/data_handling.md",
    "expected_section": "Overview",
    "expected_country_code": null,
    "expected_entity": null
  },

最后

到这里基本算是完成了这一轮添加unit test的基本任务,虽然要优化的点还有很多,但阶段性的任务已经完成了。后续要做的任务比如:

  • rerank。比如上面com-004的期望答案都不在top-5,肯定找不到这个答案的
  • query rewrite。这个放到unit test里面太慢太贵
  • 添加更多的测试用例。