GraphRag实战篇:RAG+知识图谱,提升模型推理能力的大杀器,值得收藏!

前言

经典RAG应用的范式与架构已经非常流行,我们可以在很短的时间内借助成熟框架开发一个简单能用的RAG应用。

但是传统的RAG系统有个非常大的局限,即不善于处理复杂关系推理、总结性问题和多跳问题。因为传统RAG需要对文本进行分块,然后进行向量化存储,这种处理模式就会天然导致RAG在全局查询或总结上的表现不会太好。比如面对这样的问题,“《跨越鸿沟》这本书整体上讲了什么?请撰写一份2000字的总结”,传统RAG大概率会表现不及格。

同样的,在对文本分块之后,可能会丢失一些信息之间的关系和因果性,因此在处理多跳问题时,会出现逻辑关系断裂的问题。比如面对这样的问题,“在华东区所有的门店中,哪个导购的消费者客单价最高”,传统RAG系统可能会给出错误的答复。

这正是知识图谱和GraphRAG可以发挥作用的场景。

1. 知识图谱与GraphRAG

在这里插入图片描述

知识图谱是一种将将信息表示为实体(节点)和关系(边)的网络,模仿了人类结构知识的组成方式。知识图谱不仅能捕获原始信息,还能捕获跨越多个文档的高阶关系,并具备强大的推理能力。

知识图谱的信息存储可以使用图数据库,图数据库是一种专门用于存储和操作图结构数据的数据库管理系统,如下图所示。与关系型数据库不同,图数据库使用节点、边和属性来表示和存储数据,非常适合处理高度连接的数据,提供高性能的复杂查询能力,用来遍历与发现有洞察力的数据关系。其最大特点是:

  • 灵活的模型:可以方便地表示复杂的关系。

  • 高效的查询:特别是多跳关系的查询,比关系数据库更高效。

  • 可扩展性:能够处理大量节点和边。

目前流行的图数据库包括Neo4j,OrientDB、TigerGraph等等。

我们接着来说GraphRag,它是一种将知识图谱与Rag相结合的技术范式。传统Rag是对向量数据库进行检索,而GraphRag则对存储在图数据库中的知识图谱(而非存储在向量数据库中的知识向量)进行检索,获得关联知识并实现增强生成的。

GraphRAG在整体架构与传统RAG非常相似,也是需要先建立索引,然后再通过检索召回,生成最终输出。区别在于,GraphRAG采用了图数据库进行索引构建和检索召回。

在这里插入图片描述

基于图数据库进行知识图谱检索的时候,有两种常见方式:

第一种是借助Text-to-GraphQL,即将自然语言输入转换为图数据库的查询语言,比如Neo4j的Cypher语言,再使用图数据库的查询语言从知识图谱中检索出需要的知识,这是一种精确的检索方式。

第二种是借助Vector索引,即在构建的Graph基础上,对其中的节点与关系创建向量索引,并通过向量相似性来检索出相关的节点和关系信息,还可以结合传统的关键词做混合检索,这种检索方式的精确度不如上一种。

2. GraphRAG适合的应用场景

下面我们来分析一下GraphRAG适合的场景,通常来说,具备如下特征的数据和场景更适合使用GraphRAG。

第一类是有较多相互关联实体与复杂关系,且结构较明确的数据。比如:

  • 人物关系网络数据:社交网络中的用户关系、历史人物关系、家族图谱等。

  • 企业级关系数据:公司结构、供应链、客户等之间的关系。

  • 医学类数据:疾病、症状、治疗、药物、传播、病例等之间复杂关系。

  • 法律法规数据:法律条款之间的引用关系、解释、判例与适用法律条款的关系。

  • 推荐系统数据:产品、用户、浏览内容、产品之间的关联、用户之间的关系等。

第二类是涉及复杂关系、语义推理和多步逻辑关联的查询,比如:

  • 多跳关系查询:在华东区所有的门店中,哪个导购的消费者客单价最高?

  • 知识推理查询:根据患者的症状和病史,推断可能的疾病并提供治疗方案。

  • 聚合统计查询:在《三国演义》中,出场次数最多的人是谁?

  • 时序关联查询:过去一年都有哪些AI大模型的投资与并购事件?

  • 跨多文档查询:在《三体3》中,有哪些人物在《三体1》中出现?

基于GraphRAG的特点,我们可以提炼出一些GraphRAG适合的应用场景

  • 智能客服:将产品手册或说明文档映射到知识图谱中,解答消费者各式各样的售后问题,不需要提前准备QA对,也不需要提前对问题进行扩写,GraphRAG可以在精确理解用户意图的基础上进行查找。

  • 智能检修:通常电气设备的产品说明短则几百页,长则数千页,即便是最资深的运维工程师也很难完全消化。因此,可以将设备的故障排除指南映射到知识图谱中,当设备出现故障时,方便工程师快速排查和解决问题。

  • 智能问诊:在医学领域,病人的症状和病因之间存在着非常复杂的因果关系,同时医学领域对精准度的要求也非常高,因此非常适合GraphRag的使用,比如智能问诊

  • **药物合成:**在医药领域,存在非常多的分子化合物,每种分子化合物的特性都相差迥异,不同的分子还能结合成新的分子化合物,其中的排列组合不胜枚举。因此,可以将各种已经发现的分子化合物的结构、特征映射到知识图谱中,帮助生物学家更高效地发现有益的大分子结构。

  • 报告总结:在金融领域,行业分析师每天都要阅读海量的市场信息或研究报告,过去都得完全依靠人工提炼和总结。而通过GraphRAG,构建智能投研、智能投顾等系统,帮助研究员快速提炼分析报告的核心内容。

GraphRAG虽然强大,但是相比传统RAG,其成本要高出不少。因此在以下常规场景下,我们还是尽量使用传统RAG。

首先,当大多数查询集中在单个实体上时,传统的 RAG 就够用了,因为相关的上下文很可能包含在以实体为中心的文档中,构建知识图谱会额外增加大量成本。其次,对于小型且垂直的语料库(几百个文档),仅对文档本身进行索引可能已经足够,即使存在一些文档间的关系。第三,如果主要目标是理解主题和叙事,比如社交媒体的负面评价分析,重点更多地放在语言上而不是关系上,使用传统Rag也绰绰有余。

下面,我们结合实际的源代码,介绍一下GraphRAG的实战技巧。风叔选了两个场景:一个是利用pdf、word等非结构化数据,实现内容的推理总结;另一个是利用MySQL结构化数据,实现精确查询。

3. GraphRAG实战之推理总结

构建一个非结构化数据的GraphRAG应用,首要任务是把非结构化数据转换成以图结构表示的知识图谱,并存储到GraphDB如Neo4j,用来提供后续检索与生成的基础。

从非结构化文本到知识图谱,借助LLM是一种常见且高效的方法。即利用LLM强大的语义理解与推理能力,从非结构化文本中抽取大量的类似实体-关系-实体的三元组,并借助必要的接口(如GraphDB支持的查询语言)导入到GraphDB中创建对应的实体、关系与属性,形成知识图谱。

第一步,将非结构化数据存储进GraphDB

这里的核心是基于LLM而实现的Extractor,即Graph结构的抽取组件。在不同的框架中有不同的组件实现,这里我们以LlamaIndex框架为例,其实现的核心组件为LLMPathExtractor,一般用最简单的SimpleLLMPathExtractor进行代码实现即可。

llm = OpenAI(model="gpt-4o")``embed_model=OpenAIEmbedding(model_name="text-embedding-3-small"),``   ``#加载数据``documents = SimpleDirectoryReader(input_files=['./dreamcity.txt']).load_data()``   ``#图数据库``from llama_index.graph_stores.neo4j import Neo4jPropertyGraphStore``from llama_index.core import PropertyGraphIndex``from llama_index.core.indices.property_graph import SimpleLLMPathExtractor``   ``#neo4j存储``graph_store = Neo4jPropertyGraphStore(`    `username="***",`    `password="***",`    `url="bolt://localhost:7687",``)``   ``#知识抽取的提示词``prompt = '''``下面提供了一些文本。根据文本,提取最多 {max_knowledge_triplets} 个知识三元组,形式为(实体,关系,实体或属性)。避免使用停用词。仅输出三元组,不要有多余解释和说明。``---------------------``示例:``文本:麦迪是姚明的队友。``三元组:麦迪,队友,姚明``文本:阿里巴巴是马云在1999年创立的公司``三元组:``阿里巴巴,是,公司``阿里巴巴,创立于,马云``阿里巴巴,创立于,1999年``--------------------``文本:{text}``三元组:``'''``   ``#抽取结果的解析``def parse_fn(response_str: str) -> List[Tuple[str, str, str]]:`    `lines = response_str.split("\n")`    `triples = [line.split(",") for line in lines]`    `return triples``   ``#定义知识抽取器``kg_extractor = SimpleLLMPathExtractor(`    `llm=llm,`    `extract_prompt=prompt,`    `max_paths_per_chunk=50,`    `parse_fn=parse_fn,``)``   ``#抽取创建图知识库索引(本地化做持久,避免反复抽取)``if not os.path.exists(f"./index_storage"):`    `index = PropertyGraphIndex.from_documents(`        `documents,`        `embed_model=embed_model,`        `kg_extractors=[kg_extractor],`        `property_graph_store=graph_store,`        `show_progress=True,`    `)`    `index.storage_context.persist("./index_storage")``else:``   `    `print('Loading index...\n')`    `storage_context = StorageContext.from_defaults(persist_dir="./index_storage",property_graph_store=graph_store)`    `index = load_index_from_storage(storage_context=storage_context)

这里需要注意的是:

  • 抽取过程最重要的工具是LLM与对应的提示词,这里用了少量示例提示模式(few-shot prompt),大家可以根据需要做优化

  • 借助于PropertyGraphIndex组件,可以快速的基于文档与抽取器生成知识图谱并存储到图数据库中

  • 在生成知识图谱时,需要指定嵌入模型,这是为了在生成Graph的节点时,对节点的内容或者名称生成向量,用于后续的向量检索

运行这段代码后,原始的文本将被分割成多个chunk,并用来创建label为"chunk"的多个知识图谱节点,这个节点的text属性用来存放原始的文本,同时embedding属性用来存放生成向量。

第二步,基于知识图谱的检索与生成

这里先创建一个基于关键词的检索器,注意这里的prompt,是用来抽取输入中的关键词。检索器的参数通常包括使用的大模型、提取关键词的提示词、最大提取的关键词数量、检索的路径深度等。

def parse_fn(output: str) -> list[str]:`    `matches = output.strip().split("^")`    `keywords=[x.strip().capitalize() for x in matches if x.strip()]`    `print(keywords)`    `return keywords``   ``prompt = (`        `"给定一些初始查询,提取最多 {max_keywords} 个相关关键词,考虑大小写、复数形式、常见表达等。\n"`        `"用 '^' 符号分隔所有同义词/关键词:'关键词1^关键词2^...'\n"`        `"注意,结果应为一行,用 '^' 符号分隔。"`        `"----\n"`        `"查询: {query_str}\n"`        `"----\n"`        `"关键词: "`    `)``   ``synonym_retriever = LLMSynonymRetriever(`    `index.property_graph_store,`    `llm=llm,`    `include_text=False,`    `output_parsing_fn=parse_fn,`    `max_keywords=10,`    `synonym_prompt=prompt,`    `path_depth=1,``)

再创建一个向量的检索器,注意向量检索器需要指定的是嵌入模型而非大模型,因此也无需指定提示词参数:

vector_retriever = VectorContextRetriever(`    `index.property_graph_store,`    `embed_model=embed_model,`    `include_text=False,`    `similarity_top_k=2,`    `path_depth=1,``)

在LlamaIndex中,可以将创建的两种类型检索器作为子检索器进行融合检索,从而形成更加丰富的上下文。借助PGRetriever这个组件即可实现:

retriever = PGRetriever(sub_retrievers=[synonym_retriever,vector_retriever])``retrieve_nodes = retriever.retrieve("主人公在遇见谁之后,有了重大的人生目标改变?")``for node in retrieve_nodes:`    `print(node.text)

可以直接基于上述检索器创建查询引擎,即可用来生成查询响应:

...``query_engine = index.as_query_engine(`    `sub_retrievers=[synonym_retriever,vector_retriever]``)``   ``#查询``response = query_engine.query("主人公在遇见谁之后,有了重大的人生目标改变?")``print(response)

以上就是使用GraphRAG,对非结构化文本进行推理总结的过程

4. GraphRAG实战之精确查询

下面再介绍一个利用GraphRAG实现精确查询的例子。

第一步,原始MySQL数据准备

首先创建几个简单的表,可以借助工具生成必要的测试数据:

  • orders:订单表,包含客户id,产品id,数量,销售员,订单日期等。

  • custoemrs:客户信息表,包含客户id,姓名,电话,电子邮件,城市等。

  • products:产品信息表,包含产品ID,名称,单价等。

  • salespersons:销售员信息表,包含人员id,姓名,电话,所属部门等。

  • departs:部门信息表,包含部门id,部门名称等。

第二步,将MySQL数据映射到Neo4j图数据库

首先,我们将表数据读取到本地pandas的dataframe

import pandas as pd``import mysql.connector``from neo4j import GraphDatabase``   ``def fetch_table_data(table_name):`    `cnx = mysql.connector.connect(`        `host='******',`        `user='******',`        `password='******',`        `database='sales'`    `)`    `cursor = cnx.cursor()`    `query = f"SELECT * FROM {table_name}"`    `cursor.execute(query)`    `rows = cursor.fetchall()`    `column_names = [desc[0] for desc in cursor.description]`    `df = pd.DataFrame(rows, columns=column_names)`    `cursor.close()`    `cnx.close()`    `return df``   ``# 读取到dataframe``orders_df = fetch_table_data("orders")``customers_df = fetch_table_data("customers")``products_df = fetch_table_data("products")``salespersons_df = fetch_table_data("salespersons")``departs_df = fetch_table_data("departs")

然后,创建Graph的节点,可以使用Cypher的CREATE语句来批量创建节点

def create_unique_nodes_from_dataframe(df, label, unique_id_property):`    `# 连接到Neo4j数据库`    `driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "******"))`    `# 创建一个会话来执行Cypher查询`    `with driver.session() as session:`        `# 遍历DataFrame中的每一行`        `for _, row in df.iterrows():`            `# 从行中获取唯一id的值`            `unique_id_value = row[unique_id_property]`            `# 检查是否已经存在具有相同唯一id的节点`            `query = f"MATCH (n:{label} {{{unique_id_property}: '{unique_id_value}'}}) RETURN n"`            `result = session.run(query)`            `if result.single() is not None:`                `# 已经存在具有相同唯一id的节点,跳过创建新节点`                `continue`            `# 创建一个Cypher查询来创建具有给定标签和属性的节点`            `properties = ", ".join(f"{key}: '{value}'" for key, value in row.to_dict().items())`            `query = f"CREATE (n:{label} {{ {properties} }})"`            `# 执行Cypher查询`            `session.run(query)``   ``# 为订单创建节点``create_unique_nodes_from_dataframe(orders_df, "Order","order_id")``create_unique_nodes_from_dataframe(customers_df, "Customer","customer_id")``create_unique_nodes_from_dataframe(products_df, "Product","product_id")``create_unique_nodes_from_dataframe(salespersons_df, "Salesperson","salesperson_id")``create_unique_nodes_from_dataframe(departs_df, "Depart","depart_id")

最后,我们还要创建Graph的节点关系。这里的Cypher语句其实和SQL很类似。注意创建关系时我们使用的是MERGE,这是为了防止重复生成关系。

def create_relationships():`    `# 连接到Neo4j数据库`    `driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "******"))`    `# 创建一个会话来执行Cypher查询`    `with driver.session() as session:`        `# 在顾客和订单之间创建关系`        `query = "MATCH (c:Customer), (o:Order) WHERE c.customer_id = o.customer_id MERGE (c)-[:ORDERED]->(o)"`        `session.run(query)`               `# 在销售人员和订单之间创建关系`        `query = "MATCH (s:Salesperson), (o:Order) WHERE s.salesperson_id = o.salesperson_id MERGE (s)-[:CREATED]->(o)"`        `session.run(query)``   `        `# 在产品和订单之间创建关系`        `query = "MATCH (p:Product), (o:Order) WHERE p.product_id = o.product_id MERGE (p)-[:IS_ORDERED_IN]->(o)"`        `session.run(query)``   `        `# 在销售人员和部门之间创建关系`        `query = "MATCH (s:Salesperson), (d:Depart) WHERE s.depart_id = d.depart_id MERGE (s)-[:BELONGS]->(d)"`        `session.run(query)``   ``# 调用函数来创建关系``create_relationships()

运行以上代码,如果一切正常,将会在Neo4j数据库中看到我们创建的所有节点和关系信息。

第三步,实现GraphRag精确查询

我们可以借助LangChain中的GraphCypherQAChain组件来快速实现对Graph的检索与答案生成,这个组件的基本原理就是把输入的自然语言转换成Cypher语句,然后获得查询结果。

from langchain.prompts import PromptTemplate``cypher_generation_template = """``任务:为Neo4j图数据库生成Cypher查询。``说明:仅使用提供的模式中的关系类型和属性,不要使用任何未提供的其他关系类型或属性。``模式:{schema}``注意:不要回答任何可能要求你构建除Cypher语句之外的任何文本的问题。``确保查询中的关系方向是正确的,确保正确地为实体和关系设置别名。``不要运行会向数据库添加或删除内容的任何查询。``问题是:{question}``"""``   ``cypher_generation_prompt = PromptTemplate(`    `input_variables=["schema", "question"], template=cypher_generation_template``)``   ``qa_generation_template = """``你是一个助手,根据Neo4j Cypher查询的结果生成人类可读的响应。``查询结果部分包含基于用户自然语言问题生成的Cypher查询的结果。``查询结果:{context}``问题:{question}``如果提供的信息为空,请说你不知道答案。``如果信息不为空,您必须使用结果提供答案。``"""``   ``qa_generation_prompt = PromptTemplate(`    `input_variables=["context", "question"], template=qa_generation_template``)``   ``from langchain_community.graphs import Neo4jGraph``from langchain.chains import GraphCypherQAChain``from langchain_openai import ChatOpenAI``   ``graph = Neo4jGraph(`    `url="bolt://localhost:7687",`    `username="******",`    `password="******",``)``   ``graph.refresh_schema()``   ``hospital_cypher_chain = GraphCypherQAChain.from_llm(`    `cypher_llm=ChatOpenAI(model='gpt-4o'),`    `qa_llm=ChatOpenAI(model='gpt-4o'),`    `graph=graph,`    `verbose=True,`    `qa_prompt=qa_generation_prompt,`    `cypher_prompt=cypher_generation_prompt,`    `validate_cypher=True,`    `top_k=100,``)``   ``response = hospital_cypher_chain.invoke("张三订购了哪些产品?总金额是多少")``print(response['result'])

在控制台运行可以观察到输出提示与结果,可以看到生成的完整Cypher语句和运行结果,以及最后LLM的生成答案。

通过这个例子,大家知晓了如何通过GraphRag提升精确查询能力,如前文所述,另一种提升精确查询能力的方案是text-to-SQL。根据实际测试情况发现,在关系比较简单的场景下,两者在功能上并没有太大的区别,大部分任务都可以完成。但是如果涉及较复杂的多跳查询,且在数据量较大(如100万以上)时,基于GraphRAG的检索性能会更好。

总结

整体来说,GraphRAG 擅长处理复杂、互联的数据集以及需要深度关系理解的查询。特别是在需要多层次分析和推理的情况下,GraphRAG能够显著提升信息检索的精度和深度。

然而,这种能力的提升也必然伴随着更高的系统复杂性和资源消耗。因此,在决定是否采用 GraphRAG 之前,必须仔细分析具体的应用场景、数据结构以及典型的查询模式。在简单和事实性查询、较小数据集以及简单应用场景下,传统 RAG 仍然是更理想的选择。

GraphRAG是一个非常热门的领域,很多公司都在研究如何利用图数据库,进一步提升RAG的推理和总结能力。今年,微软开源了自主研发的Microsoft GraghRAG,犹如一枚重磅炸弹,给市场带来了非常大的反响。

如何学习大模型 AI ?

由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。

但是具体到个人,只能说是:

“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。

这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。

我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

在这里插入图片描述

第一阶段(10天):初阶应用

该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。

  • 大模型 AI 能干什么?
  • 大模型是怎样获得「智能」的?
  • 用好 AI 的核心心法
  • 大模型应用业务架构
  • 大模型应用技术架构
  • 代码示例:向 GPT-3.5 灌入新知识
  • 提示工程的意义和核心思想
  • Prompt 典型构成
  • 指令调优方法论
  • 思维链和思维树
  • Prompt 攻击和防范

第二阶段(30天):高阶应用

该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。

  • 为什么要做 RAG
  • 搭建一个简单的 ChatPDF
  • 检索的基础概念
  • 什么是向量表示(Embeddings)
  • 向量数据库与向量检索
  • 基于向量检索的 RAG
  • 搭建 RAG 系统的扩展知识
  • 混合检索与 RAG-Fusion 简介
  • 向量模型本地部署

第三阶段(30天):模型训练

恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。

到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?

  • 为什么要做 RAG
  • 什么是模型
  • 什么是模型训练
  • 求解器 & 损失函数简介
  • 小实验2:手写一个简单的神经网络并训练它
  • 什么是训练/预训练/微调/轻量化微调
  • Transformer结构简介
  • 轻量化微调
  • 实验数据集的构建

第四阶段(20天):商业闭环

对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。

  • 硬件选型
  • 带你了解全球大模型
  • 使用国产大模型服务
  • 搭建 OpenAI 代理
  • 热身:基于阿里云 PAI 部署 Stable Diffusion
  • 在本地计算机运行大模型
  • 大模型的私有化部署
  • 基于 vLLM 部署大模型
  • 案例:如何优雅地在阿里云私有部署开源大模型
  • 部署一套开源 LLM 项目
  • 内容安全
  • 互联网信息服务算法备案

学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。

如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值