LangGraph实战:从零构建AI智能体工作流与状态管理

1. 项目概述:为什么是LangGraph?

最近和几个做AI应用的朋友聊天,发现大家讨论的焦点已经从“怎么调用大模型API”转向了“怎么让AI自己干活”。没错,就是 AI智能体 。但真上手去搭一个能自主决策、有记忆、能调用工具的智能体,你会发现这活儿比想象中复杂。状态管理、流程编排、错误处理……一堆琐事,代码写着写着就成了“意大利面条”。

这时候, LangGraph 走进了我的视野。它不是另一个大语言模型,而是一个专门用来构建 有状态、多步骤应用 的框架。你可以把它理解为一个专门为AI智能体设计的“流程图引擎”或“工作流编排器”。它的核心思想非常直观:把你的智能体逻辑画成一张 ,节点是执行步骤(比如调用LLM、执行工具),边是控制流(根据上一步的结果,决定下一步去哪)。

这听起来是不是比用一堆 if-else 和全局变量来管理状态要清爽得多?我最初也是抱着试试看的心态,但用LangGraph搭建了几个智能体原型后,发现它的设计确实切中了痛点。它让你能更专注于智能体的 业务逻辑 ,而不是在状态管理的泥潭里挣扎。今天,我就结合自己的实操经验,带你从零开始,用LangGraph搭建你的第一个智能体,并深入聊聊它和LangChain、CrewAI这些工具到底有什么区别,帮你做出最适合自己的技术选型。

2. 核心概念拆解:图、状态与智能体

在动手写代码之前,我们必须先吃透LangGraph的几个核心概念。这就像学开车先要明白方向盘、油门和刹车是干嘛的,不然直接上路肯定手忙脚乱。

2.1 图的思维:用节点和边构建工作流

LangGraph的基石是 。这里的图不是图表,而是计算机科学里的 有向图 。它由两部分组成:

  • 节点 :代表一个具体的执行单元或函数。比如“调用LLM分析用户意图”、“执行网络搜索工具”、“更新对话历史”。
  • :定义了节点之间的流转条件。通常基于上一个节点的输出结果来决定下一步走向哪个节点。

这种设计带来的最大好处是 可视化与可维护性 。你的智能体工作流不再是一行行难以追踪的代码,而是一张可以画出来的流程图。LangGraph Studio(一个Web界面)能直接渲染你定义的图,让你一眼看清整个智能体的决策路径,调试起来直观太多。

2.2 状态管理:共享的上下文信息池

智能体之所以“智能”,是因为它有记忆和上下文。在LangGraph中,所有节点共享一个中心化的 State 。这个 State 通常是一个Pydantic模型(一种Python数据验证和设置管理库),定义了智能体运行过程中需要跟踪的所有信息。

举个例子,一个客服智能体的State可能包含这些字段:

from typing import TypedDict, List, Annotated
from langgraph.graph.message import add_messages
import operator

class State(TypedDict):
    # 对话消息历史,LangGraph提供了专用类型来方便处理
    messages: Annotated[List, add_messages]
    # 用户当前明确的需求
    user_query: str
    # 从知识库检索到的相关文档片段
    retrieved_docs: List[str]
    # 智能体最终生成的回答
    final_answer: str
    # 控制流程的标志位,比如“是否需要进一步澄清”
    needs_clarification: bool

每个节点在执行时,都可以读取和修改这个共享的 State 。比如,“检索节点”会向 retrieved_docs 里填入内容,“LLM节点”会读取 messages retrieved_docs ,然后把生成的回复追加到 messages 中。这种设计保证了数据流清晰、一致。

2.3 智能体在其中的角色:图中的一个特殊节点

在LangGraph的语境下, 智能体 通常被实现为一个或多个特定的 节点 。最常见的模式是创建一个“代理节点”,这个节点内部封装了一个 LangChain Expression Language (LCEL) 创建的Runnable。这个Runnable通常由以下几部分组成:

  1. 提示词模板 :告诉LLM它现在是什么角色、有什么工具、上下文是什么。
  2. 大语言模型 :如OpenAI的GPT、Anthropic的Claude等。
  3. 工具绑定 :将外部功能(如搜索、计算、查数据库)暴露给LLM调用。
  4. 输出解析器 :将LLM非结构化的回复,解析成结构化的指令(如调用哪个工具、传入什么参数)。

这个“代理节点”的职责是:接收当前的 State (主要是对话历史和工具结果),让LLM进行思考,然后输出一个决定——是直接回复用户,还是调用某个工具。这个决定会驱动图沿着不同的边流向下一个节点(如“工具执行节点”或“结束节点”)。

3. 环境准备与工具选型

工欲善其事,必先利其器。搭建LangGraph智能体,你需要一个清晰的开发环境。下面是我经过多次项目实践后总结的一套稳定、高效的配置方案。

3.1 基础环境搭建

首先,我强烈建议使用 Python 3.10或更高版本 。LangGraph和一些底层依赖(如Pydantic)对新版本Python的支持更好,能避免很多兼容性坑。管理Python环境, Conda venv 任选其一,目的是隔离项目依赖。

我的常用命令如下:

# 使用conda创建环境
conda create -n langgraph-agent python=3.10
conda activate langgraph-agent

# 或者使用venv
python -m venv venv
# Windows: venv\Scripts\activate
# Mac/Linux: source venv/bin/activate

接下来是安装核心包。这里有个关键点:LangGraph和LangChain的版本需要匹配。直接安装 langgraph 通常会带上兼容版本的 langchain 。为了开发方便,我们一并安装常用的工具链。

pip install langgraph langchain langchain-openai langchain-community
  • langgraph : 核心框架。
  • langchain : 提供了构建智能体所需的链、工具、记忆等基础组件。
  • langchain-openai : 官方维护的OpenAI模型集成,比通用的 langchain 包里的更稳定。
  • langchain-community : 包含了大量第三方工具和集成,比如搜索引擎、维基百科等。

注意 :网络环境可能导致安装缓慢或失败。请确保你的pip源配置正确(例如使用清华、阿里等国内镜像源),并拥有稳定的网络连接来下载Python包。这是后续所有工作的基础,务必一次搞定。

3.2 模型API配置

目前,OpenAI的GPT系列模型在智能体开发中依然是主流选择,因其在函数调用(Tool Calling)方面的稳定性和性能表现。当然,你也可以选择Anthropic Claude、Google Gemini等,集成方式类似。

你需要准备一个有效的 OpenAI API Key 。获取后, 千万不要 把它硬编码在脚本里!最安全、最规范的做法是设置为环境变量。

# 在终端中临时设置(仅当前会话有效)
export OPENAI_API_KEY='你的-api-key-here'
# Windows (Cmd): set OPENAI_API_KEY=你的-api-key-here
# Windows (PowerShell): $env:OPENAI_API_KEY='你的-api-key-here'

在Python代码中,通过 os.environ 读取:

import os
from langchain_openai import ChatOpenAI

api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
    raise ValueError("请设置 OPENAI_API_KEY 环境变量")

llm = ChatOpenAI(model="gpt-4-turbo-preview", api_key=api_key, temperature=0)

这里我选择了 gpt-4-turbo-preview ,它在推理能力和成本间取得了较好的平衡。 temperature 设为0是为了让智能体的决策更稳定、可复现,这在调试阶段尤为重要。

3.3 辅助工具选择:为什么不用CrewAI?

在搜索“AI智能体框架”时,你肯定会看到 CrewAI OpenAI Assistants API 。这里我简单分析一下,为什么在这个项目中我们首选LangGraph。

  • vs CrewAI : CrewAI也是一个高阶框架,它抽象了“角色”、“任务”、“流程”的概念,开箱即用性很强。但它的 灵活性不足 。CrewAI预设了一套协作模式,如果你的智能体逻辑稍微特殊一点(比如需要复杂的自定义状态判断、非线性的循环流程),就会感到束手束脚。LangGraph则像一套乐高积木,给你最基础的“图”和“状态”原语,你可以搭建出任意复杂的结构,掌控每一个细节。 简单说,CrewAI是“封装好的产品”,LangGraph是“强大的开发框架”
  • vs OpenAI Assistants API : Assistants API提供了线程、代码解释器、文件检索等托管服务,非常方便。但它是一个 黑盒服务 ,运行在OpenAI的服务器上。你的数据要传出去,定制化能力有限(只能使用OpenAI提供的几种工具),且无法与本地系统深度集成。LangGraph运行在你自己的环境中,数据可控,可以无缝集成任何Python函数作为工具,更适合构建企业级、需要私有化部署的复杂应用。

因此,对于需要 深度定制、复杂逻辑、数据隐私和本地集成 的智能体项目,LangGraph是目前更优的选择。

4. 实战:构建一个研究助手智能体

理论说得再多,不如一行代码。现在,我们来构建一个实用的“研究助手智能体”。它的功能是:根据用户提出的复杂问题,自动进行网络搜索,汇总信息,并生成结构化的研究报告。

4.1 定义智能体的状态与工具

首先,我们定义智能体运行过程中需要维护的所有信息。

from typing import TypedDict, List, Annotated, Optional
from langgraph.graph.message import add_messages
import operator

class AgentState(TypedDict):
    """研究助手智能体的状态定义"""
    # 对话消息历史
    messages: Annotated[List, add_messages]
    # 用户的原始问题
    original_query: str
    # 网络搜索得到的结果列表
    search_results: List[str]
    # 经过LLM提炼后的关键信息点
    key_points: List[str]
    # 最终生成的研究报告
    final_report: Optional[str]
    # 控制流标志:是否需要更多搜索
    need_more_search: bool
    # 记录当前迭代次数,防止无限循环
    iteration_count: int

这里我们使用了 TypedDict Annotated add_messages 是一个特殊的缩减函数,它能智能地处理消息列表的合并(如将新的AI消息追加进去),非常方便。

接下来,为智能体装备两个核心工具: 网络搜索 总结提炼 。我们将使用 DuckDuckGo 进行搜索(无需API Key),并自己编写一个总结函数。

from langchain_community.tools import DuckDuckGoSearchRun
from langchain.tools import tool
from langchain_openai import ChatOpenAI

# 初始化工具和LLM
search = DuckDuckGoSearchRun()
llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0)

@tool
def summarize_content(content: str) -> str:
    """将大段文本内容总结成简洁的要点。"""
    prompt = f"""
    请将以下文本内容总结成3-5个最核心的要点。
    每个要点用‘-’开头,力求简洁清晰。
    文本内容:
    {content}
    """
    response = llm.invoke(prompt)
    return response.content

# 将工具打包成列表,供后续的智能体使用
tools = [search, summarize_content]

4.2 创建核心节点:代理、工具与判断

一个典型的LangGraph智能体包含三类节点: 代理节点 (负责思考决策)、 工具节点 (负责执行动作)、 路由节点 (负责判断下一步)。

首先,我们创建 代理节点 。它接收状态,让LLM决定该做什么。

from langchain_core.prompts import ChatPromptTemplate
from langgraph.prebuilt import create_react_agent

# 使用LangGraph内置的create_react_agent快速创建一个ReAct模式的代理
# ReAct模式是“推理(Reasoning)+ 行动(Acting)”的循环,非常适合工具调用。
agent_runnable = create_react_agent(llm, tools)

# 定义代理节点函数
def agent_node(state: AgentState):
    """代理节点:分析状态,决定行动。"""
    # 从状态中提取最新的用户消息(通常是用户问题或工具执行结果)
    latest_message = state["messages"][-1]
    
    # 调用代理,传入消息。代理会返回一个包含思考和行动的消息。
    agent_response = agent_runnable.invoke({"messages": [latest_message]})
    
    # 关键:我们需要将代理返回的新消息,追加到全局状态的消息历史中。
    # 这里返回的是一个字典,更新的是`messages`字段。
    return {"messages": agent_response["messages"]}

接着,创建 工具执行节点 。它负责运行代理节点选择要调用的工具。

def tools_node(state: AgentState):
    """工具执行节点:运行代理选择的工具,并将结果格式化为AI消息。"""
    # 获取代理的最后一条消息,其中应包含工具调用指令
    last_message = state["messages"][-1]
    
    # 检查消息中是否包含工具调用
    if not hasattr(last_message, 'tool_calls') or not last_message.tool_calls:
        # 如果没有工具调用,可能是LLM直接回复了,直接返回原状态
        return state
    
    outputs = []
    # 遍历所有被调用的工具
    for tool_call in last_message.tool_calls:
        tool_name = tool_call['name']
        tool_args = tool_call['args']
        
        # 根据工具名找到对应的工具函数
        tool_to_use = next((t for t in tools if t.name == tool_name), None)
        if tool_to_use:
            # 执行工具
            observation = tool_to_use.invoke(tool_args)
            outputs.append(f"工具调用 `{tool_name}` 返回结果:\n{observation}")
        else:
            outputs.append(f"错误:未知工具 `{tool_name}`")
    
    # 将工具执行结果构造为一个新的“AI”消息,这会被代理在下一次思考时看到。
    from langchain_core.messages import AIMessage
    tool_message = AIMessage(content="\n".join(outputs))
    
    # 更新状态,将工具结果消息追加到历史中
    return {"messages": [tool_message]}

最后,也是最体现LangGraph价值的部分: 路由逻辑 。我们需要判断,在代理行动之后,下一步应该去工具节点还是结束。

def should_continue(state: AgentState) -> str:
    """路由函数:根据最后一条消息决定下一步。
    返回下一个节点的名称。"""
    last_message = state["messages"][-1]
    
    # 如果最后一条消息是工具调用,那么下一步就是去执行工具
    if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
        return "use_tools"
    
    # 否则,说明代理已经给出了最终答案,流程结束
    return "__end__"

4.3 组装工作流图并运行

现在,我们把所有节点和边组装起来,形成完整的工作流。

from langgraph.graph import StateGraph, END

# 1. 创建一个图,并指定它操作的状态类型
workflow = StateGraph(AgentState)

# 2. 添加节点
workflow.add_node("agent", agent_node) # 思考决策节点
workflow.add_node("tools", tools_node) # 工具执行节点

# 3. 设置入口点
workflow.set_entry_point("agent")

# 4. 添加条件边
workflow.add_conditional_edges(
    "agent", # 从哪个节点出发
    should_continue, # 判断函数,返回下一个节点的名字
    {
        "use_tools": "tools", # 如果返回"use_tools",则前往"tools"节点
        "__end__": END # 如果返回"__end__",则结束图
    }
)

# 5. 添加固定边(从工具节点执行完后,无条件回到代理节点进行下一步思考)
workflow.add_edge("tools", "agent")

# 6. 编译图,得到一个可执行的对象
app = workflow.compile()

至此,你的第一个LangGraph智能体就构建完成了!你可以通过LangGraph Studio可视化它:

from langgraph.checkpoint.sqlite import SqliteSaver
import tempfile

# 创建一个临时数据库来存储检查点(用于可视化)
with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as tmp:
    db_path = tmp.name

memory = SqliteSaver.from_conn_string(f"sqlite:///{db_path}")
app_with_memory = workflow.compile(checkpointer=memory)

# 现在可以运行 app_with_memory,并在LangGraph Studio中通过链接查看图
# 通常Studio运行在本地 http://127.0.0.1:8123

让我们运行这个智能体,问它一个复杂问题:

from langchain_core.messages import HumanMessage

# 初始化状态
initial_state = AgentState(
    messages=[HumanMessage(content="请研究一下‘量子计算对现代密码学的主要挑战和潜在解决方案’,并给我一份简要报告。")],
    original_query="",
    search_results=[],
    key_points=[],
    final_report=None,
    need_more_search=False,
    iteration_count=0
)

# 运行图
final_state = app.invoke(initial_state)

# 查看最终结果
for msg in final_state["messages"]:
    if msg.type == "ai" and not hasattr(msg, 'tool_calls'):
        print("=== 智能体生成的研究报告 ===")
        print(msg.content)

运行后,你会看到智能体自动进行了多次“思考->搜索->再思考->总结”的循环,最终生成一份结构化的报告。整个过程完全自动化,无需人工干预。

5. 高级技巧与模式扩展

基础的ReAct循环智能体已经能完成很多任务。但在实际项目中,我们往往需要更复杂的逻辑。下面分享两个我常用的高级模式。

5.1 实现带检查点的长对话记忆

上面的例子是单次执行。对于聊天机器人,我们需要它记住整个对话历史。LangGraph的 检查点 功能完美解决了这个问题。它能在每个步骤后自动保存完整状态,并可以随时回溯到任意历史步骤。

from langgraph.checkpoint.memory import MemorySaver

# 创建一个内存检查点管理器(生产环境可用数据库)
memory = MemorySaver()

# 在编译图时传入 checkpointer
app_with_memory = workflow.compile(checkpointer=memory)

# 使用线程ID来区分不同的对话会话
config = {"configurable": {"thread_id": "user_session_123"}}

# 第一次输入
initial_state = {"messages": [HumanMessage(content="你好,我是小明。")]}
result1 = app_with_memory.invoke(initial_state, config)
print(result1["messages"][-1].content) # AI回复

# 第二次输入,基于同一个thread_id,智能体会记住之前的对话
new_state = {"messages": [HumanMessage(content="我刚刚说我叫什么名字?")]}
result2 = app_with_memory.invoke(new_state, config)
# AI应该能回答“你叫小明”,因为它记住了历史。

5.2 构建多角色协作的智能体团队

LangGraph最强大的地方在于可以轻松构建多个智能体协作的流水线。例如,一个“写作团队”智能体:一个负责搜集资料,一个负责撰写草稿,一个负责润色修改。

# 定义团队状态
class TeamState(TypedDict):
    topic: str
    collected_data: List[str]
    draft: Optional[str]
    polished_content: Optional[str]

# 创建三个不同的节点函数,代表三个专家角色
def researcher_node(state: TeamState):
    """研究员节点:负责搜索和收集资料"""
    # ... 调用搜索工具,更新 collected_data
    return {"collected_data": [f"关于{state['topic']}的资料..."]}

def writer_node(state: TeamState):
    """写手节点:基于资料撰写草稿"""
    data = "\n".join(state["collected_data"])
    prompt = f"根据以下资料,撰写关于{state['topic']}的文章草稿:\n{data}"
    response = llm.invoke(prompt)
    return {"draft": response.content}

def editor_node(state: TeamState):
    """编辑节点:润色草稿"""
    prompt = f"请润色以下文章,使其更流畅专业:\n{state['draft']}"
    response = llm.invoke(prompt)
    return {"polished_content": response.content}

# 构建顺序工作流
team_workflow = StateGraph(TeamState)
team_workflow.add_node("researcher", researcher_node)
team_workflow.add_node("writer", writer_node)
team_workflow.add_node("editor", editor_node)

team_workflow.set_entry_point("researcher")
team_workflow.add_edge("researcher", "writer")
team_workflow.add_edge("writer", "editor")
team_workflow.add_edge("editor", END)

team_app = team_workflow.compile()

通过这种方式,你可以将复杂任务分解,让不同的“专家”智能体各司其职,形成高效的自动化流水线。

6. 常见问题、调试技巧与性能优化

在实际开发和部署中,你一定会遇到各种问题。下面是我踩过坑后总结的一些实战经验。

6.1 常见错误与排查表

问题现象 可能原因 排查步骤与解决方案
KeyError 或状态字段不存在 1. 状态类 TypedDict 定义与节点中实际访问的字段名不一致。
2. 节点返回的更新字典键名拼写错误。
1. 仔细核对 State 定义和所有节点中读写字段的名称,确保完全一致。
2. 使用IDE的自动补全功能,避免手动输入。
智能体陷入无限循环 1. 路由逻辑 should_continue 判断条件有误,始终无法走向 __end__
2. LLM被提示词误导,反复调用同一个工具。
1. 在 should_continue 函数中添加打印语句,检查其返回值。
2. 在状态中添加 iteration_count 字段,在路由函数中判断如果超过阈值(如10次)则强制结束。
3. 优化提示词,明确告诉LLM在获得足够信息后应直接给出最终答案。
工具调用失败或参数错误 1. 工具函数的参数定义与LLM生成的参数不匹配。
2. 工具执行过程中抛出异常(如网络超时)。
1. 确保工具函数的参数有清晰的名称和类型提示,这能帮助LLM更好地生成参数。
2. 在 tools_node 中增加 try-except 块,捕获工具异常,并将错误信息作为观察结果返回给代理,让其有机会重试或调整。
LangGraph Studio 无法连接或白屏 1. 检查点数据库路径错误或权限问题。
2. 端口冲突。
1. 确保 SqliteSaver 使用的数据库文件路径可访问。
2. 尝试更换Studio的默认端口( 8123 ),可以通过环境变量 LANGRAPH_STUDIO_PORT 设置。
LLM响应慢或成本高 1. 使用了不必要的大模型(如用GPT-4处理简单任务)。
2. 提示词过于冗长,导致Tokens消耗大。
1. 任务分级 :简单的路由、判断使用 gpt-3.5-turbo ,复杂的推理、创作再用 GPT-4
2. 精简状态 :定期清理 messages 历史,只保留最近几轮对话,或使用摘要记忆法。
3. 设置超时 :在初始化 ChatOpenAI 时设置 timeout max_retries 参数。

6.2 调试技巧:可视化与日志

1. 善用LangGraph Studio: 这是最强大的调试工具。运行应用后,打开Studio网页,你可以:

  • 可视化执行流程 :清晰看到每个节点是否执行、输入输出状态。
  • 检查状态快照 :点击任意节点,查看当时完整的 State 内容。
  • 回溯执行历史 :对于检查点应用,可以回退到任何一步重新执行,这对复现问题至关重要。

2. 添加结构化日志: 在关键节点函数的开头和结尾添加日志,记录状态的变迁。

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def agent_node(state: AgentState):
    logger.info(f"进入agent_node,当前消息数:{len(state['messages'])}")
    # ... 业务逻辑
    logger.info(f"离开agent_node,新生成消息:{agent_response['messages'][-1].content[:100]}...")
    return {"messages": agent_response["messages"]}

3. 使用 stream 进行实时调试: app.invoke() 是阻塞执行。使用 app.stream() 可以实时看到每一步的输出,方便跟踪流程。

for event in app.stream(initial_state, stream_mode="values"):
    event_type = list(event.keys())[0]
    print(f"--- 事件类型: {event_type} ---")
    print(event[event_type])
    print("\n")

6.3 性能与成本优化实践

1. 状态设计优化:

  • 只保留必要数据 State 字段越多,每次序列化/反序列化的开销就越大。定期清理过期数据。
  • 使用 Annotated 和缩减器 :像 add_messages 这样的内置缩减器能高效处理列表合并,比自己手动操作更优。

2. 异步执行: 如果节点中的操作是IO密集型(如调用多个独立的API),可以将其定义为 async 函数,并使用 async 接口调用图,能显著提升吞吐量。

async def async_tool_node(state):
    # 使用await调用异步工具
    result = await async_tool.invoke(...)
    return {"result": result}

# 使用 async invoke
final_state = await app.ainvoke(initial_state)

3. 智能体流程优化:

  • 设置最大循环次数 :在状态中设置 max_iterations ,在路由逻辑中判断,避免个别问题导致无限循环消耗大量Token。
  • 缓存机制 :对于重复性的工具调用(如对相同查询的搜索),可以在状态或外部引入缓存,避免重复计算和网络请求。

4. 模型选择策略: 建立模型路由层。根据任务的复杂度和状态,动态选择性价比最高的模型。例如,在 agent_node 中:

def agent_node(state):
    if state.get("iteration_count", 0) > 2:
        # 如果已经思考多轮,问题可能较复杂,切换至更强模型
        llm = powerful_llm
    else:
        llm = fast_cheap_llm
    # ... 使用llm继续流程

从我的经验来看,LangGraph最大的优势在于它将复杂的智能体逻辑“可视化”和“模块化”了。调试一个基于图的系统,远比调试一堆相互回调的函数要简单。当你看到智能体在Studio里沿着你设计的路径一步步执行,那种掌控感是其他框架难以提供的。它可能不是最简单的入门选择,但绝对是构建复杂、可靠、可维护AI智能体应用时最强大的武器之一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值