RAGas 上下文相关性 Context Relevance

Peng Xia

原理:
Context Relevance 指标用于评估一个回答的句子是否受到提供的上下文支持,以及上下文是否受到回答的支持。其核心思想是:

  1. 拆分回答:将回答分解为独立的句子。
  2. 判定支持度:对于每个句子,判断它是否可以在上下文中找到支持。
  3. 计算支持度精度:通过统计支持的句子占比,衡量回答对上下文的依赖性。

目的:

  • 确保模型的回答是基于给定的上下文,而非凭空生成。
  • 评估回答对上下文的利用率,提高答案的可靠性。
  • 检测回答中的不可信或无依据的内容。
1
2
3
4
5
6
7
8
query = "美国最高法院关于堕胎的裁决对全球有什么影响?"
answer = "美国最高法院关于堕胎的裁决具有重要的全球影响。该裁决导致在堕胎访问受到限制的州,三分之一的生育年龄女性和女孩无法获得堕胎服务。那些州的母婴健康支持也较弱,母亲死亡率较高,儿童贫困率也较高。此外,裁决的影响超出了国界,由于美国在全球的地缘政治和文化影响力,这一裁决也产生了跨国影响。全球的组织和活动家担心这一裁决可能会激励其他国家出台反堕胎的立法和政策。裁决还妨碍了某些非洲国家的进步法律改革和堕胎指南的实施。此外,该裁决在国际政策领域造成了寒蝉效应,使得反堕胎的力量能够削弱人权保护。"
docs = [
"- 2022年,美国最高法院作出裁决,推翻了50年的判例法,取消了宪法堕胎权。\n- 这一裁决产生了巨大影响:三分之一的生育年龄女性和女孩现在生活在堕胎服务几乎完全无法获得的州。\n- 这些堕胎法律最为严格的州,母婴健康支持最为薄弱,母亲死亡率较高,儿童贫困率较高。\n- 美国最高法院的裁决还通过美国在全球的地缘政治和文化影响力,超越国界产生了影响。\n- 全球的SRR组织和活动家对这一裁决可能为其他国家的反堕胎立法和政策攻击铺路表示担忧。\n- 观察者还注意到该裁决对某些非洲国家的进步法律改革产生了影响,导致堕胎指导方针的 adoption 和执行停滞不前。\n- 该裁决在国际政策领域产生了寒蝉效应,助长了反堕胎的国家和非国家行为体破坏人权保护的势头。",
"美国最高法院的堕胎裁决不仅在国内引发了激烈的辩论和讨论,也在全球范围内引发了广泛关注。许多国家将美国视为法律和社会问题的领导者,因此这一裁决可能会影响其他国家对堕胎的政策和态度。",
"这一裁决还可能对国际组织和非政府组织(NGO)产生影响,尤其是那些致力于生育权和妇女健康问题的团体。根据裁决的结果,可能会出现资金、倡导工作和与美国同行的合作发生变化,进而在全球范围内引发生育正义斗争的连锁反应。"
]

代码实现

实现流程:

  1. 输入: 回答 answer,以及文档 document_list
  2. 支持度判定: 使用 Sentence_Support_Verdication 模型逐句分析回答的句子是否在上下文中得到支持。
  3. 计算精度: 计算被支持的句子占比,以 answer_supported_precisioncontext_supported_precision 进行衡量。

从文本A中抽取句子,再在文本B中验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import asyncio
import numpy as np
from typing import List
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

'''
输出格式形如:
{
"sentence_and_statements":
}
'''

class Sentence_Support_Verdication(BaseModel):
sentence: str = Field(description="回答的某个句子的原始内容。")
sentence_supported_reasoning: str = Field(description='分析该句子是否能得到上下文中任意一句话的支持。')
sentence_supported_verdication: int = Field(description='如果该句子能得到事实陈述中任意一句话的支持,为1,否则为0。')

class Sentence_Support_Verdication_List(BaseModel):
verdications: List[Sentence_Support_Verdication] = Field(description="一组句子和对应的是否被上下文支持的判断")

verdication_parser = PydanticOutputParser(pydantic_object=Sentence_Support_Verdication_List)

llm = ChatOpenAI(
base_url='http://localhost:5551/v1',
api_key='EMPTY',
model_name='Qwen2.5-14B-Instruct',
temperature=0.5,
)

anwer_supported_verdict_prompt = (
"给定一段上下文和一个回答,分析回答的每个语句,并判断该语句是否得到上下文的支持,若得到支持则判断为1,若没有得到支持则判断为0。\n"
"输出格式:\n{output_format_instructions}\n\n"
"[上下文-开始]\n{context}\n[上下文-结束]\n\n"
"[回答-开始]\n{answer}\n[回答-结束]\n\n"
)
anwer_supported_verdict_prompt = PromptTemplate(
template=anwer_supported_verdict_prompt,
partial_variables={
"output_format_instructions":verdication_parser.get_format_instructions()
}
)

anwer_supported_verdict_chain = anwer_supported_verdict_prompt | llm | verdication_parser

anwer_supported_verdict_prompt.pretty_print()
给定一段上下文和一个回答,分析回答的每个语句,并判断该语句是否得到上下文的支持,若得到支持则判断为1,若没有得到支持则判断为0。
输出格式:
The output should be formatted as a JSON instance that conforms to the JSON schema below.

As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.

Here is the output schema:
1
{"$defs": {"Sentence_Support_Verdication": {"properties": {"sentence": {"description": "回答的某个句子的原始内容。", "title": "Sentence", "type": "string"}, "sentence_supported_reasoning": {"description": "分析该句子是否能得到上下文中任意一句话的支持。", "title": "Sentence Supported Reasoning", "type": "string"}, "sentence_supported_verdication": {"description": "如果该句子能得到事实陈述中任意一句话的支持,为1,否则为0。", "title": "Sentence Supported Verdication", "type": "integer"}}, "required": ["sentence", "sentence_supported_reasoning", "sentence_supported_verdication"], "title": "Sentence_Support_Verdication", "type": "object"}}, "properties": {"verdications": {"description": "一组句子和对应的是否被上下文支持的判断", "items": {"$ref": "#/$defs/Sentence_Support_Verdication"}, "title": "Verdications", "type": "array"}}, "required": ["verdications"]}
[上下文-开始] [33;1m[1;3m{context}[0m [上下文-结束] [回答-开始] [33;1m[1;3m{answer}[0m [回答-结束]

回答对于上下文的相关度

1
2
3
4
5
6
result = anwer_supported_verdict_chain.invoke(
{
"context":'\n'.join(docs),
"answer":answer,
}
)

这是从回答中抽取的每个句子,如果句子有上下文(RAG文档)支持,那它与上下文关联度高/遵守上下文;否则它纯粹是大模型自己无根据生成的,与上下文关联度低。

好的上下文可以完全/较全地支持回答,那前者的出现率就应该很高。那我们可以视为分类问题,两者分别为T、F,从而得到 precision 等指标。

1
2
3
4
5
6
7
[Sentence_Support_Verdication(sentence='美国最高法院关于堕胎的裁决具有重要的全球影响。', sentence_supported_reasoning='上下文中的最后一段提到了裁决不仅在国内引发了激烈的辩论和讨论,也在全球范围内引发了广泛关注,说明了裁决的全球影响。', sentence_supported_verdication=1),
Sentence_Support_Verdication(sentence='该裁决导致在堕胎访问受到限制的州,三分之一的生育年龄女性和女孩无法获得堕胎服务。', sentence_supported_reasoning='上下文中的第二段明确提到三分之一的生育年龄女性和女孩现在生活在堕胎服务几乎完全无法获得的州。', sentence_supported_verdication=1),
Sentence_Support_Verdication(sentence='那些州的母婴健康支持也较弱,母亲死亡率较高,儿童贫困率也较高。', sentence_supported_reasoning='上下文中的第三段提到了这些堕胎法律最为严格的州,母婴健康支持最为薄弱,母亲死亡率较高,儿童贫困率较高。', sentence_supported_verdication=1),
Sentence_Support_Verdication(sentence='此外,裁决的影响超出了国界,由于美国在全球的地缘政治和文化影响力,这一裁决也产生了跨国影响。', sentence_supported_reasoning='上下文中的第四段提到了美国最高法院的裁决还通过美国在全球的地缘政治和文化影响力,超越国界产生了影响。', sentence_supported_verdication=1),
Sentence_Support_Verdication(sentence='全球的组织和活动家担心这一裁决可能会激励其他国家出台反堕胎的立法和政策。', sentence_supported_reasoning='上下文中的第五段提到全球的SRR组织和活动家对这一裁决可能为其他国家的反堕胎立法和政策攻击铺路表示担忧。', sentence_supported_verdication=1),
Sentence_Support_Verdication(sentence='裁决还妨碍了某些非洲国家的进步法律改革和堕胎指南的实施。', sentence_supported_reasoning='上下文中的第六段提到裁决对某些非洲国家的进步法律改革产生了影响,导致堕胎指导方针的adoption和执行停滞不前。', sentence_supported_verdication=1),
Sentence_Support_Verdication(sentence='此外,该裁决在国际政策领域造成了寒蝉效应,使得反堕胎的力量能够削弱人权保护。', sentence_supported_reasoning='上下文中的第七段提到裁决在国际政策领域产生了寒蝉效应,助长了反堕胎的国家和非国家行为体破坏人权保护的势头。', sentence_supported_verdication=1)]
1
2
3
4
5
6
7
8
9
10
verdications_true = []
verdications_false = []
for item in result.verdications:
if item.sentence_supported_verdication == 1:
verdications_true.append(item)
else:
verdications_false.append(item)

precision = len(verdications_true) / (len(verdications_true)+len(verdications_false))
print(precision)
1.0

上下文对于回答的相关度

反转上下文和回答的位置,现在从上下文中提取句子,并验证回答是否支持

1
2
3
4
5
6
result = anwer_supported_verdict_chain.invoke(
{
"context":answer,
"answer":'\n'.join(docs),
}
)
1
2
3
4
5
6
7
8
9
10
verdications_true = []
verdications_false = []
for item in result.verdications:
if item.sentence_supported_verdication == 1:
verdications_true.append(item)
else:
verdications_false.append(item)

precision = len(verdications_true) / (len(verdications_true)+len(verdications_false))
print(precision)
0.5454545454545454

反思

实际回答只用了上下文的小部分,所以上下文中部分内容不会在回答中体现/未被回答支持。
换句话说,RAG文档中有用的内容如果不多,那准确率就低/RAG效果差。

感觉上,直接把所有RAG文档拼成单一的上下文不大合理,每个文档的准确率都会不一样,但同样准确率的第3个文档和第10个文档是不一样的,随着排序顺序,准确率下降其实也合理,所以更合理的方式是:单独计算每个文档的准确率,然后基于顺序位置做加权平均,更后位置的文档的权重应该更低。

一个容易混淆的概念是把上下文和回答的内容识别为TP、FP、TN、FN。
假设上下文的内容都是P,而回答做分类。

  • 上下文中提取的是TP、FP,是上下文有的内容分别被回答体现和没体现。
  • 回答中提取的是TP、TN,是回答中的内容分别被上下文支持和没支持。

因为上下文和回答的内容数量肯定会不一样,整体数量上TP+FP != TP+TN,而且回答中的单个TP可能是多个TP的组合,所以我倾向于不用混淆矩阵。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import asyncio
import numpy as np
from typing import List, Dict
from langchain_openai import ChatOpenAI
from langchain.schema.runnable import Runnable
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

class Sentence_Support_Verdication(BaseModel):
"""
句子支持度判断结果。
"""
sentence: str = Field(description="回答的某个句子的原始内容。")
sentence_supported_reasoning: str = Field(description='分析该句子是否能得到上下文中任意一句话的支持。')
sentence_supported_verdication: int = Field(description='如果该句子能得到事实陈述中任意一句话的支持,为1,否则为0。')

class Sentence_Support_Verdication_List(BaseModel):
"""
包含多个句子支持度判断的列表。
"""
verdications: List[Sentence_Support_Verdication] = Field(description="一组句子和对应的是否被上下文支持的判断")

class Context_Relevance(Runnable):
"""
评估回答是否受到上下文支持。
"""

def __init__(self, llm: ChatOpenAI):
"""
初始化 Context_Relevance 类。

:param llm: ChatOpenAI 实例,用于 LLM 调用。
"""
self.llm = llm
self.verdication_parser = PydanticOutputParser(pydantic_object=Sentence_Support_Verdication_List)

answer_supported_verdict_prompt = (
"给定一段上下文和一个回答,分析回答的每个语句,并判断该语句是否得到上下文的支持,若得到支持则判断为1,若没有得到支持则判断为0。\n"
"输出格式:\n{output_format_instructions}\n\n"
"[上下文-开始]\n{context}\n[上下文-结束]\n\n"
"[回答-开始]\n{answer}\n[回答-结束]\n\n"
)

self.answer_supported_verdict_prompt = PromptTemplate(
template=answer_supported_verdict_prompt,
partial_variables={
"output_format_instructions": self.verdication_parser.get_format_instructions()
}
)

self.answer_supported_verdict_chain = self.answer_supported_verdict_prompt | self.llm | self.verdication_parser

def verdict_supported(self, context: str, answer: str) -> List[Sentence_Support_Verdication]:
"""
评估回答中的每个句子是否受到上下文的支持。

:param context: 提供的上下文内容。
:param answer: 回答内容。
:return: 句子的支持度判决列表。
"""
result = self.answer_supported_verdict_chain.invoke({
"context": context,
"answer": answer,
})
return result.verdications

def calculate_precision(self, verdications: List[Sentence_Support_Verdication]) -> float:
"""
计算支持度的精度。

:param verdications: 句子的支持度判决列表。
:return: 支持精度(支持的句子占比)。
"""
if not verdications:
return 0.0

verdications_true = sum(1 for item in verdications if item.sentence_supported_verdication == 1)
precision = verdications_true / len(verdications)
return precision

def invoke(self, inputs: Dict[str, List[str]]) -> Dict[str, float]:
"""
计算回答和上下文之间的相互支持度。

:param inputs: 包含 `answer`(回答)和 `document_list`(上下文列表)的输入字典。
:return: 一个字典,包含 answer_supported_precision 和 context_supported_precision。
"""
answer = inputs["answer"]
document_list = inputs["document_list"]
context = '\n'.join(document_list)

# 计算回答是否被上下文支持
answer_verdications = self.verdict_supported(context, answer)
answer_supported_precision = self.calculate_precision(answer_verdications)

# 计算上下文是否被回答支持(互相验证)
context_verdications = self.verdict_supported(answer, context)
context_supported_precision = self.calculate_precision(context_verdications)

return {
"answer_supported_precision": answer_supported_precision,
"context_supported_precision": context_supported_precision
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
llm = ChatOpenAI(
base_url='http://localhost:5551/v1',
api_key='EMPTY',
model_name='Qwen2.5-14B-Instruct',
temperature=0.5,
)

inputs = {
"answer":answer,
"document_list":docs
}

tool = Context_Relevance(llm)

tool.invoke(inputs)
{'answer_supported_precision': 1.0, 'context_supported_precision': 0.75}
Comments