微信扫码
添加专属顾问
我要投稿
探索微软Phi-4-mini如何在GraphRAG中展现卓越性能,以及优化知识图谱构建的重要性。 核心内容: 1. Phi-4-mini模型的参数规模及其在特定任务中的表现 2. GraphRAG环境下与其他模型的对比实验结果 3. LlamaIndex框架下的知识图谱提取和检索流程的定制化方案
微软近期开源发布 Phi-4-mini,这是一款拥有 38 亿参数(3.8×10⁹)的模型,专注于文本理解与生成。据报道,Phi-4-mini 在某些任务上,尤其是在数学和编程领域,其性能可与规模为其两倍的模型相媲美。为了进一步验证这一说法,我们在 GraphRAG 环境下进行了对比实验,评估了 Phi-4-mini 与包括 Phi-4 和 DeepSeek-R1 7B 在内的多款更大模型的生成性能 [1]。
在那项研究期间,我们观察到 Phi-4-mini 构建的知识图谱远比其他模型简单,而且这些图谱大多零散、不连贯。为了进一步提升该模型的实用价值,优化知识图谱的构建质量或许能为其生成任务提供更相关的上下文支持。当模型应用于某个特定领域,尤其是该领域的信息未被用于其训练时,这一点尤为关键。
在本文中,我们将采用 LlamaIndex 框架,探索如何在一个本地化的 GraphRAG 环境中,为选定的语言模型 Phi-4-mini 定制知识图谱的提取和检索流程。至于应用领域,我们将探讨其在生成关于某个网络性能监控 (NPM) 设备的 REST API 服务使用说明方面的可行性。为了在此设置中托管知识图谱,Neo4j 是一个理想的选择。它不仅是一个企业级的图数据库管理系统 (DBMS),还拥有直观的界面和强大的可视化能力,使我们能够轻松快捷地洞察数据背后的信息。
让我们先仔细看看所使用的技术栈。
本研究的实验环境为一台搭载 MacOS Sequioa 操作系统、配备 16 核 GPU 和 24 GB RAM 的 MacBook M4 Pro。使用的 Python 版本为 3.12.9,建议读者保持版本一致,以避免潜在的兼容性问题。
在存储和查询知识图谱方面,Neo4j 图数据库管理系统是自然之选。Neo4j 的优势在于生产级别的稳定性、卓越的易用性以及无与伦比的可视化能力,这能帮助我们更好地理解和展示知识图谱的构建效果,这对于本次研究至关重要。在[之前的一篇文章(https://ai.gopubby.com/local-llm-generated-knowledge-graphs-powered-by-local-neo4j-4111c5234993#b768)中,已详细描述了 Neo4j 的安装与设置步骤。本次研究沿用了相同的步骤来设置最新版本 Neo4j 2025.01.0。
作为最佳实践,我们将创建一个虚拟环境并激活它,如下所示:
python3.12 -m venv kg_qa
source kg_qa/bin/activate
与我们近期的其他工作类似,我们选择了 AI 框架 LlamaIndex
,因为它提供了涵盖数据获取、索引、检索和查询全流程的模块与工具。其中包含了 PropertyGraphIndex
模块,该模块简化了从非结构化文本构建知识图谱以及后续的检索过程。为了让 Neo4j 作为图存储后端,我们使用了 Neo4jPropertyGraphStore
模块以及名为 neo4j
的 Python 驱动程序。
我们将使用 llama-cpp-python
包来托管本地语言模型(LLM),该包提供了 llama.cpp
库的 Python 绑定。同时,我们选用了量化 8 位的 Phi-4-mini Instruct(https://huggingface.co/microsoft/Phi-4-mini-instruct) 版本作为语言模型。
相应地,以下是安装所需库的 pip install
命令:
pip install llama-index llama-index-readers-file llama-index-embeddings-huggingface
pip install neo4j llama-index-graph-stores-neo4j
CMAKE_ARGS="-DLLAMA_METAL=on" FORCE_CMAKE=1 pip install --upgrade --force-reinstall llama-index-llms-llama-cpp
在之前的工作(https://ai.gopubby.com/is-phi-4-mini-in-graphrag-the-out-right-king-for-resource-starved-setups-0622db9c9ba0#ff4a)中,我们展示了使用 PropertyGraphIndex.from_documents
配合默认的 KG 提取器和默认提取提示词的知识图谱构建流程。该默认设置对某些模型(如 DeepSeek-R1 7B)效果良好,但对 Phi-4 系列模型效果欠佳 [1]。具体到 Phi-4-mini,生成的图谱规模小且大多是零散的,难以连接成网。图 1 展示了 Phi-4-mini 与 DeepSeek-R1 在处理相同文档时构建的知识图谱对比。后者的知识图谱连接良好,甚至包含多跳深度路径。相比之下,Phi-4-mini 构建的知识图谱不仅层级浅,还包含了一些不相关的实体。
现在,让我们来看看如何为 Phi-4-mini 定制知识图谱的构建和检索流程,以提升其知识图谱构建质量和检索性能。
为了支持问答系统的工作流程,我们需要涉及包含 KG 提取器的知识获取流程、包含检索器的 KG 查询引擎,以及利用检索到的上下文进行 LLM 提示词工程。
首先,导入所有必要的库:
from llama_index.core import (
PropertyGraphIndex,
SimpleDirectoryReader,
StorageContext,
Settings,
)
from llama_index.llms.llama_cpp import LlamaCPP
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.graph_stores.neo4j import Neo4jPropertyGraphStore
from llama_index.core.indices.property_graph import (
SimpleLLMPathExtractor,
LLMSynonymRetriever,
VectorContextRetriever,
)
import re
接下来,实例化 LlamaCPP
对象,并指定本地模型的路径。同时,实例化 HuggingFaceEmbedding
嵌入模型。为了让 LlamaIndex 的各个模块能全局使用这个语言模型(LLM)和嵌入模型,我们将 Settings
类的 llm
属性设为该 LLM 实例,embed_model
属性设为 HuggingFaceEmbedding
对象。然后,通过实例化 Neo4jPropertyGraphStore
对象来设置一个由 Neo4j 数据库支持的图存储。以下代码展示了这些步骤:
# 加载本地模型
model_path='./models/Phi-4-mini-instruct.Q8_0.gguf'
llm = LlamaCPP(
model_path=model_path,
temperature=0.0,
max_new_tokens=2000,
context_window=5096,
model_kwargs={"n_gpu_layers": -1}, # 使用所有 GPU 层
verbose=False
)
embed_model = HuggingFaceEmbedding()
# 将 llm 和 嵌入模型对象关联到 Settings 的属性
Settings.llm = llm
Settings.embed_model = embed_model
Settings.chunk_size = 512# 设置文本块大小
# 初始化图数据库连接
url = "bolt://localhost:7687"
username = "neo4j"
password = "password"# 请替换为你的密码
database = "neo4j"
graph_store = Neo4jPropertyGraphStore(
username=username,
password=password,
url=url,
database=database,
)
在 LlamaIndex 中,KG 构建是通过在每个文本块 (chunk) 上应用一个或多个 kg_extractors
来实现的,并将提取出的实体和关系作为元数据附加到每个节点上。默认情况下会使用两个提取器,其中包括 SimpleLLMPathExtractor
模块 [2]。该提取器通过解析短句中的单跳路径,生成形如“主语、谓语、宾语”的三元组。 为了确保其最佳工作效果,我们需要根据 Phi-4-mini 的格式以及我们的应用领域来调整提示词。
由于不同 LLM 的特性不同,为了让 Phi-4-mini 更好地理解我们的意图,我们需要对提取提示词进行定制。 这是我们定制的提取提示词:
extract_prompt = (
"下面提供了一些文本。根据这些文本,提取最多 "
"{max_paths_per_chunk} "
"个“主语、谓语、宾语”形式的知识三元组。\n"
"请避免使用停用词。\n"
"---------------------\n"
"示例:\n"
"文本: Alice is Bob's mother.\n"
"三元组:\nAlice, is mother of, Bob\n"
"文本: API requests must include an Authorization header with an access token preceded by "
"word 'Bearer', which expires after 15 minutes.\n"
"三元组:\n"
"API request, requires, Authorization header\n"
"Authorization header, contains, access token\n"
"Access token, preceded by, word 'Bearer'\n"
"Access token, expires after, 15 minutes\n"
"---------------------\n"
"文本: {text}\n"
"三元组:\n"
)
# 包装成 Phi-4-mini 的提示格式
ep = f'<|system|>You are a helpful AI assistant.<|end|><|user|>{extract_prompt}<|end|><|assistant|>'
当这个提示词用于 Phi-4-mini 时,它通常会生成一个带编号的三元组列表。为了确保这些三元组符合 KG 构建所需的格式,我们定义一个 parse_fn
函数来辅助解析响应:
def parse_fn(response_str: str) -> list[tuple[str, str, str]]:
# 按行分割
lines = response_str.strip().split("\n")
# 移除行首的编号和点号
lines = list(set([re.sub(r'^\d*\.\s*','',line) for line in lines if line.strip()]))
# 过滤掉格式不正确的行,并分割成包含3个元素(首字母大写)的列表
triplets = []
for line in lines:
parts = [item.strip().capitalize() for item in line.split(',') if item.strip()]
if len(parts) == 3:
triplets.append(tuple(parts))
return triplets
例如,如果模型返回 ‘1. Alice, is mother of, Bob\n2. Alice, lives in, Wonderland’,这个函数会将其转换为 [('Alice', 'is mother of', 'Bob'), ('Alice', 'lives in', 'Wonderland')]
。 这个函数中的第一个列表推导式生成一个去除了行首编号和多余空格的字符串列表。Phi-4-mini 有时会生成大量重复的三元组,通过 set
转换再转回列表可以有效去重。之后,对于包含恰好两个逗号的字符串,将其按逗号分割,形成三元组。三元组的每个元素都被去除首尾空格并首字母大写。有了适配 Phi-4-mini 格式的提示词和解析函数,我们就可以完成 kg_extractor
的实例化了:
kg_extractor = SimpleLLMPathExtractor(
llm=llm, # 明确传递 LLM 实例
extract_prompt=ep,
parse_fn=parse_fn,
max_paths_per_chunk=10 # 每个 chunk 最多提取10个三元组
)
有了这个定制的提取器,知识获取流程可以定义如下:
# 假设 documents 是已加载的文档列表
# documents = SimpleDirectoryReader("./data").load_data() # 例如
storage_context = StorageContext.from_defaults(property_graph_store=graph_store)
kg_index = PropertyGraphIndex.from_documents(
documents,
storage_context=storage_context,
max_triplets_per_chunk=10, # 与kg_extractor中的设置保持一致
kg_extractors=[kg_extractor],
embed_model=embed_model, # 明确传递嵌入模型
# show_progress=True # 可选:显示处理进度
)
接下来,我们继续定制上下文检索和查询流程。
默认情况下,LlamaIndex 使用两个模块来辅助从 KG 中检索上下文,即 LLMSynonymRetriever
和 VectorContextRetriever
。前者基于 LLM 生成的关键词或同义词列表进行检索,这正是我们可以针对所选 LLM 进行优化的一个好地方。
下面是基于 LlamaIndex 默认检索提示词修改后,适配 Phi-4-mini 格式的检索提示词:
retrieve_prompt = (
"给定一个初始查询,请生成最多 {max_keywords} 个同义词或相关关键词,"
"考虑可能的大小写、复数形式、常用表达等。\n"
"请将所有同义词/关键词用 '^' 符号分隔:'keyword1^keyword2^...'\n"
"注意,结果应在单行内,并用 '^' 符号分隔。\n"
"----\n"
"查询: {query_str}\n"
"----\n"
"关键词: "
)
# 包装成 Phi-4-mini 的提示格式
rp = f'<|system|>You are a helpful AI assistant.<|end|><|user|>{retrieve_prompt}<|end|><|assistant|>'
这个提示词会生成一个由 '^' 分隔的单行同义词列表。如前所述,Phi-4-mini 有时会返回许多重复项。为了帮助解析响应,我们定义如下的解析函数:
def parse_fn_r(output: str) -> list[str]:
# 使用集合去重,并按 '^' 分割
matches = list(set(output.strip().split("^")))
# 去除空字符串,去除首尾空格,首字母大写
return [x.strip().capitalize() for x in matches if x.strip()]
该函数负责分割关键词、去重、去除空格,并统一将首字母大写,以便在 LLMSynonymRetriever
中用于 KG 检索,具体实例化如下:
synonym_retriever = LLMSynonymRetriever(
storage_context.property_graph_store, # 使用 storage_context 中的图存储
include_text=True, # 检索时包含关联的文本块
synonym_prompt=rp, # 使用定制的提示词
output_parsing_fn=parse_fn_r, # 使用定制的解析函数
max_keywords=10, # 最多生成10个同义词
# 检索到节点后,沿着关系路径探索的深度
path_depth=2,
)
由于 Neo4j 数据库也支持向量查询,我们可以使用 VectorContextRetriever
来基于向量相似性检索节点,然后获取与这些节点相连的路径,作为我们的第二个检索器:
vector_retriever = VectorContextRetriever(
storage_context.property_graph_store, # 使用 storage_context 中的图存储
include_text=True, # 检索时包含关联的文本块
similarity_top_k=2, # 检索最相似的2个节点
path_depth=1, # 探索1跳路径
embed_model=embed_model # 明确传递嵌入模型
)
检索器准备就绪后,我们定义包含它们的查询引擎。 此外,由于 Neo4j 数据库支持向量查询,我们还可以使用 VectorContextRetriever
基于向量相似性检索节点,并获取与这些节点相连的路径,作为第二个检索器。
kg_keyword_query_engine = kg_index.as_query_engine(
retriever_mode="keyword", # 指定使用关键词检索模式
sub_retrievers=[synonym_retriever, vector_retriever],
llm=llm # 明确传递 LLM 实例
)
查询引擎定义完毕,现在可以完成查询流程了:
query = '请说明处理使用 DH 密码套件的 TLS 流量解密时,应该使用哪个版本的 AppResponse REST API?'
# 包装成 Phi-4-mini 的提示格式
llm_prompt = f'<|system|>You are a helpful AI assistant.<|end|><|user|>{query}<|end|><|assistant|>'
response = kg_keyword_query_engine.query(llm_prompt)
print(f'查询: {query}\n响应: {response.response}')
现在我们准备好进行实验了。这样的定制化对构建的知识图谱有何影响?让我们一探究竟!
首先,确保本地 Neo4j 数据库管理系统已启动并监听连接。本次测试中,我们将采用几份来自 Riverbed Technology 的 AppResponse(一种基于数据包的网络和应用分析设备)的 API 文档。该设备从特定网络监控点捕获网络数据包,进行自动化分析并提供洞察。我们的第一份 API 文档(https://github.com/drskennedy/qa-kg-neo4j/blob/main/pdf/Auth_REST_api.pdf)描述了围绕身份验证的工作流程。第二份文档(https://github.com/drskennedy/qa-kg-neo4j/blob/main/pdf/KB_S34414_PFS_setup.pdf)讨论了如何使用某些 API 端点对特定类型的 TLS 流量进行解密。
为了展示针对 KG 提取器和检索组件所建议的定制化带来的影响,我们将比较 Phi-4-mini 在定制前后的结果。为便于参考和说明,我们从现在起将前者称为 Phi-4-mini-default,将后者(带有定制)称为 Phi-4-mini-custom。我们将有选择地利用 Neo4j 的高级可视化功能来突出关键差异。
在本测试中,Phi-4-mini-default 和 Phi-4-mini-custom 分别使用相同的文档重复进行了 5 次 KG 构建实验。它们生成的图谱特征在多次运行中表现出高度一致性。图 2 展示了使用 Neo4j Browser 可视化的构建结果。差异非常明显。定制后,知识图谱的 最长路径长度 显著增加。识别出的实体都与文档直接相关,不像使用默认设置提取时看到的一些无关实体。Phi-4-mini-custom 还避免了因大小写问题导致的重复实体,这得益于我们在 parse_fn
函数中将所有实体的首字母统一大写,避免了因大小写差异导致的重复实体。
在本测试中,我们针对 API 文档提出了三个相同的问题。为了体现定制化的影响,我们将首先展示使用默认 LlamaIndex 设置的 Phi-4-mini 的响应,然后是采用建议定制后的同一模型的响应。
让我们看第一个问题:
Q1: 请说明处理使用 DH 密码套件的 TLS 流量解密时,应该使用哪个版本的 AppResponse REST API?
Phi-4-mini-default 的响应如下:
可以看出,默认设置下的 Phi-4-mini 并没有准确理解问题的意图。 它回答了 1.0 版本,并给出了一个合理的解释,但对其响应的准确性表示怀疑。为了理解它为何得出此结论,我们来看看查询引擎检索到并用作上下文的三元组和文档块(使用以下代码查看):
for n,sn in enumerate(response.__dict__.get('source_nodes')):
print(f'{"/"*30} 上下文节点 #{n}: {"\\"*30}:\n{sn.get_text()}')
下面是检索流程提供给本地模型的唯一源节点的截图:
而 Phi-4-mini-custom 则正确地回答了预期的 1.1 版本。然而,它同样只有一个源节点作为上下文,如下所示:
请注意,检索到的两个三元组及相关的文档块精确地指向了正确的上下文,帮助模型确定 1.1 是正确的 API 版本。
接下来看第二个问题:
Q2: 我有 client_random 和 master_key 的十六进制值。我想通过 REST API 将它们安全地发送到 AppResponse 设备,以解密服务器 172.168.22.2 上端口 8443 的 TLS 会话。如何使用 curl 实现这一点?
Phi-4-mini-default 的响应如下:
红色高亮部分指出了不准确之处。这个响应中的 API 端点是错误的。模型还将端口 8443 误认为是 API 服务监听的端口,而实际上它应该是需要解密的 TLS 流量的端口。此外,此 API 调用中缺少认证头(Authorization header),POST 请求体中的 JSON 属性也缺失了。检索到的三元组和文档块与此问题无关,因此导致了模型的幻觉(hallucination)。
相反,Phi-4-mini-custom 的响应如下:
这里的 curl 命令选项几乎是完整的,JSON 请求体也完全正确。尽管被处理的文档中没有包含演示如何指定服务器端口的示例,但当提供了正确的上下文时,模型能够推断出来。然而,这里同样缺少认证头,并且 API 端点路径的最后一部分是错误的。对于这次生成,模型获得了四个源节点作为上下文,而 Phi-4-mini-default 只有一个。前者的第一个源节点包含了以下与问题直接相关的三元组和文本块:
结合其余的相关上下文,模型得以生成一个近乎完整的响应。
现在,让我们明确要求在进行任何进一步 API 调用之前先进行身份验证。这将是我们最后一个较长问题的一部分:
_Q3:_ 一个负载均衡器和一台 AppResponse 设备都能看到相同的、使用 DH 密码套件的 TLS 会话。对于其中一个 TLS 会话,我想将它的 client_random 值 3c555b83e8db003fc35a4a5394e566c3234e2d325213b3a3a82ab2d651c8a151 和 master_key 3142a6a50f8e1dc17f2b22644464820cf59edb7792f0e36bc3dca4aa3fb5b99dbed7e2d00a2924ab 从负载均衡器发送到 AppResponse,以便它解密此会话。首先,提供一个 curl 命令,使用账户用户名/密码向 IP 地址为 10.1.1.123 的 AppResponse 设备执行 API 调用身份验证以获取访问令牌。然后,提供第二个 curl 命令,将给定的密钥安全地以 JSON 格式发送到 AppResponse。
对于这个问题,Phi-4-mini-default 的响应如下:
它正确地生成了两条用于 IP 地址为 10.1.1.123 的设备的 curl 命令。但是,第一个命令的 API 端点是错误的,其用于身份验证的 JSON 对象结构也不正确。
现在看看 Phi-4-mini-custom 的响应:
这个响应几乎完全正确,并且编号清晰。身份验证请求使用了正确的 JSON 对象结构。发送密钥的第二个命令也完全正确。然而,值得注意的是,它选择了不安全的 HTTP 端点,而非 HTTPS,这与提示中要求的安全传输及其自身声明的安全传输目标存在矛盾。另一个小错误是身份验证 API 端点,应该是 /api/mgmt.aaa/1.0/token
才对。
为了理解 Phi-4-mini-custom 为何能基本正确地给出第一个 curl 命令的选项,让我们检查它的上下文。与 Phi-4-mini-default 的检索器只返回一个源节点不同,Phi-4-mini-custom 的检索器返回了 6 个源节点!并且第一个源节点就包含了预期的格式,如下所示:
第一个三元组 (Api calls, Require, Authentication) 与查询的第一部分匹配。此外,第二个和第三个三元组似乎是从实体 Api calls 出发的第二跳关系。如果你还记得之前的 实现 部分,LLMSynonymRetriever
的 path_depth
属性控制着这种行为。
为了帮助我们更好地理解第一个源节点的选择过程,让我们利用 Neo4j 的一些高级可视化功能。在 Neo4j Browser 中,如果我们使用 Cypher 查询 MATCH (a)-[r]-(b) WHERE a.name='Api calls' RETURN r,a,b
来查找从实体 Api calls 出发的三元组,结果图如图 3 所示。
图 3 中的红色节点代表提及这两个实体的文档块。选中这个红色节点后,该图右侧的 节点属性 面板显示了之前提供的源节点 0 中的部分内容。要获取从 Api calls 出发最多 2 跳的实体,我们可以使用 Cypher 查询 MATCH (a)-[r*1..2]-(b) WHERE a.name = 'Api calls' RETURN r,a,b
。结果图如图 4 所示。源节点 0 中剩余的两个三元组代表了该图底部附近的 1 跳路径。同样,所有这三个三元组都由同一个文本块引用,因此该文本块作为相关文本被包含在内。
相比之下,Phi-4-mini-default 再次只有一个源节点作为上下文,包含三元组 (Philz, Uses, Client handshake secret) 和 (Philz, Uses, Server handshake secret),其主语是无关的。宾语则在一个讨论用于发送 PFS 解密密钥的端点的文本块中被提及。因此,这导致模型生成了针对我们问题第二部分的正确命令。
从以上三个示例中可以清楚地看到高质量知识图谱的影响。通过从给定问题中选择同义词,Phi-4-mini-custom 的同义词检索器能够从这些知识图谱中找到相关的三元组以及关联的文档块,从而为语言模型提供正确的上下文以改进其响应。此外,由于图谱的连接性更好,检索器能够持续地为该模型找到更多相关的上下文,从而带来更好的生成结果。
微软发布的 Phi-4-mini 是一款面向资源受限环境的小型语言模型。对于一个小模型而言,它拥有不错的生成性能。要将像 Phi-4-mini 这样的公开模型应用于特定细分领域,一定程度的适配是必要的。GraphRAG 是一种流行的方法,它将文档内容提取并表示为知识图谱,后续通过检索这些图谱来为模型的生成提供上下文依据,同时减少幻觉现象。为了高效地处理这些图谱,我们采用了 Neo4j 图数据库管理系统。作为一个额外的好处,Neo4j 的 Web UI 支持丰富的查询语言和出色的可视化功能,使我们能够深入洞察数据。
在本文中,我们探讨了如何定制 LlamaIndex 的知识获取和检索流程以支持 Phi-4-mini。对于获取流程,我们调整了 SimpleLLMPathExtractor
模块的提取提示词和响应解析函数;对于检索流程,我们以类似的方式修改了 LLMSynonymRetriever
的相同方面。通过前者的改动,生成的知识图谱连接性大大增强,包含了更多与领域相关的有意义的实体和关系。有了这些更高质量的图谱,检索质量以及为支持生成而找到的相关源节点的数量都得到了持续改善。
通过这种 GraphRAG 设置,我们发现 Phi-4-mini 能够相当出色地生成用于访问 NPM 设备 REST API 服务的完整 curl 命令。虽然定制化显著降低了幻觉现象,但模型在某些信息推理上仍有改进空间。
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2025-04-15
知识库优化之路(三):嵌入模型的选择和使用方法
2025-04-14
MCP技术革命:元控制协议如何重构AI与数据库的交互范式
2025-04-13
用大模型构建企业级知识图谱真的太简单了!
2025-04-13
深入解析 GreptimeDB MCP:连接数据库与 LLM 的桥梁
2025-04-13
GraphRAG的100%!把Agent接入知识图谱,自主精准找到数据!与LangGraph融合最终形态!day1
2025-04-09
LLM知识图谱构建器:前端架构如何革新数据可视化?
2025-04-07
解决Dify与Milvus集成难题:从零到一的实战避坑指南
2025-04-07
别只看LLM!为什么知识图谱才是通往AGI的关键一步
2025-01-02
2024-07-17
2024-08-13
2025-01-03
2024-07-11
2024-08-27
2024-06-24
2024-07-13
2024-07-12
2024-06-10
2025-04-15
2025-04-09
2025-03-29
2025-02-13
2025-01-14
2025-01-10
2025-01-06
2025-01-02