【打造能干活的Agent】管理Agent上下文避免任务遗漏

本文聚焦于AI智能体(Agent)开发中如何通过结构化任务状态避免任务遗漏。提出以LangGraph为框架,将目标、约束、证据、进度和验收项等关键信息存入明确字段的上下文,替代依赖冗长聊天记录的方式。结合SQLite检查点实现状态持久化与可追溯性,并强调上下文边界设计、压缩策略及需求变更处理机制,确保任务执行可验证、可复现、可审计。

管理Agent上下文避免任务遗漏:用结构化任务状态守住目标与待办

课程归属:打造能干活的Agent · 第 19 节

资料分析 Agent 往往要连续读取文件、提取要点、形成报告并检查结果。随着步骤增加,单靠一长串聊天历史容易出现目标被稀释、待办遗漏、证据与结论混淆等问题。本文用 LangGraph 状态保存一份结构化的“任务上下文”,把目标、约束、证据、已完成步骤和待办清单放在明确字段中,并在交付前逐项验收。最终生成 report.md、summary.json 和 run.log,且只有产物检查通过才将任务标记为完成。

一、为什么上下文不能只靠聊天记录

上下文管理不是把所有历史消息无限堆进提示词。对一个要完成实际工作的 Agent 来说,关键是每次执行都能回答五个问题:

  1. 目标是什么:用户最终需要什么交付物?
  2. 约束是什么:允许读取哪些资料,输出格式是什么,哪些操作不能执行?
  3. 证据是什么:每条结论来自哪段输入,而不是模型凭空补充?
  4. 进度是什么:哪些步骤已完成,哪些仍待处理?
  5. 怎样算完成:报告、摘要和日志是否真的写出,必需字段是否齐全?

聊天历史适合保存对话过程,结构化任务状态适合驱动执行。两者可以同时存在,但不能指望模型每次都从长历史里准确找回所有约束。

图 1:把对话要求整理成可执行上下文

是

否

用户任务描述

提取目标与约束

结构化任务上下文

读取资料并提取证据

更新已完成步骤与待办

生成交付物

验收规则全部满足

completed

保留未完成项并返回修正

观察重点是上下文会随着任务推进而更新,而不是在开头生成一次后就不再维护。每个节点完成后都要更新进度;最终验收依据文件与字段,而不是 Agent 自己说“已经完成”。

表 1:哪些信息应该放进上下文

信息类别示例用途不能替代什么
目标分析门店反馈并给出建议约束任务方向不能替代证据
约束仅使用 data/input/brief.txt限定资料与操作边界不能替代权限控制
证据原文反馈及其来源行支持结论追溯不能自动证明推论正确
进度已读取、待写报告防止跳步和遗漏不能证明文件已生成
验收项报告非空、摘要字段齐全判定是否交付不能替代业务审核

二、案例设计与运行环境

案例延续 agent_lab/ 资料分析任务助手。输入是一份教学模拟的门店反馈文本,任务是提炼主要问题、正向反馈和建议。为了让上下文逻辑可复现,示例不调用在线模型;实际项目可以把固定规则替换为模型或工具调用,但目标、证据、进度和验收字段仍应保留。

表 2:本节交付文件

文件作用验收方式
data/input/brief.txt教学模拟输入文件存在且非空
data/agent.db任务表、步骤表与图检查点能查询任务状态
output/<task_id>/report.md可阅读报告存在且包含必需章节
output/<task_id>/summary.json结构化摘要与上下文快照JSON 可解析、字段齐全
output/<task_id>/run.log执行记录包含任务 ID 与完成状态

主线命令在 WSL2 Ubuntu 执行,Python 3.11。版本基线采用 LangGraph 1.2.16 与 langgraph-checkpoint-sqlite 3.1.2;官方发行记录可在文末链接核对。本文未在当前回复环境安装并运行这组依赖,因此安装结果与运行输出均应由读者环境实际确认,不把预期输出描述成实测通过。

mkdir -p agent_lab/data/input agent_lab/output
cd agent_lab
python3.11 -m venv .venv
source .venv/bin/activate

创建 requirements.txt:

langgraph==1.2.16
langgraph-checkpoint-sqlite==3.1.2

安装依赖:

python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip show langgraph langgraph-checkpoint-sqlite

创建 data/input/brief.txt:

资料名称:门店服务反馈(教学模拟)
反馈一:顾客希望缩短高峰时段排队时间。
反馈二:部分顾客认为商品说明不够清楚。
反馈三:顾客认可员工主动介绍替代商品。
分析要求:提炼主要问题、正向反馈和一条可执行建议。

三、设计结构化上下文,而不是无限追加消息

本例把任务上下文作为 LangGraph 状态的一部分。状态中既有业务输入,也有执行元数据。LangGraph 的 checkpointer 会在图的执行过程中保存状态快照;thread_id 用于标识同一条任务线程。官方文档将 checkpoint 描述为图状态在某个时间点的快照,并支持在后续运行中读取。

表 3:状态字段契约

字段类型示例更新时机
goal字符串分析门店反馈并输出报告初始化时确定
constraints字符串数组仅使用教学输入;不编造事实初始化时确定
evidence对象数组{text, source, line}提取原文证据后
completed_steps字符串数组read_source、extract_points每个节点成功后
pending_steps字符串数组write_report、validate节点成功或失败时
acceptance字符串数组报告存在、JSON 可解析初始化后保持稳定
warnings字符串数组结论基于教学模拟资料发现限制时

图 2:一次节点执行如何更新上下文

"SQLite Checkpointer" "任务状态" "当前节点" "LangGraph" "SQLite Checkpointer" "任务状态" "当前节点" "LangGraph" 读取目标、约束与待办 传入当前状态 执行读取、提取或写入 返回状态增量 合并节点结果 保存新的状态检查点 调度下一节点

这不是把所有历史消息压缩成一段摘要,而是把执行所需信息放在有明确含义的字段中。对于需要长期保留的对话偏好或跨任务知识,通常还需要单独的长期存储;本节只处理单个任务内的工作上下文。

四、完整最小案例

下面的 main.py 是完整示例。它会创建课程约定的 tasks 和 task_steps 表,编译带 SQLite 检查点的图,读取资料并保留证据,生成报告与 JSON 摘要,最后逐项检查交付物。公共函数 run_task(task_input, task_id=None) 返回固定的 task_id、status、report_path、summary 和 warnings 字段。

from __future__ import annotations

import json
import sqlite3
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import TypedDict

from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import END, START, StateGraph

ROOT = Path(__file__).resolve().parent
DB_PATH = ROOT / "data" / "agent.db"
INPUT_PATH = ROOT / "data" / "input" / "brief.txt"
OUTPUT_ROOT = ROOT / "output"


def utc_now() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")


def connect_db() -> sqlite3.Connection:
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    return conn


def init_db() -> None:
    with connect_db() as conn:
        conn.executescript("""
        CREATE TABLE IF NOT EXISTS tasks (
            task_id TEXT PRIMARY KEY,
            task_input TEXT NOT NULL,
            status TEXT NOT NULL,
            current_step INTEGER NOT NULL DEFAULT 0,
            max_steps INTEGER NOT NULL DEFAULT 8,
            result_json TEXT,
            error TEXT,
            created_at TEXT NOT NULL,
            updated_at TEXT NOT NULL
        );
        CREATE TABLE IF NOT EXISTS task_steps (
            step_id INTEGER PRIMARY KEY AUTOINCREMENT,
            task_id TEXT NOT NULL REFERENCES tasks(task_id),
            step_no INTEGER NOT NULL,
            tool_name TEXT NOT NULL,
            arguments_json TEXT NOT NULL,
            status TEXT NOT NULL,
            result_json TEXT,
            error TEXT,
            idempotency_key TEXT UNIQUE,
            started_at TEXT,
            finished_at TEXT
        );
        """)


def set_task(task_id: str, status: str, step: int,
             result: dict | None = None, error: str | None = None) -> None:
    with connect_db() as conn:
        conn.execute(
            """UPDATE tasks
               SET status=?, current_step=?, result_json=COALESCE(?, result_json),
                   error=?, updated_at=?
               WHERE task_id=?""",
            (
                status, step,
                json.dumps(result, ensure_ascii=False) if result is not None else None,
                error, utc_now(), task_id,
            ),
        )


def record_step(task_id: str, step_no: int, name: str,
                args: dict, result: dict) -> None:
    now = utc_now()
    key = f"{task_id}:step:{step_no}"
    with connect_db() as conn:
        conn.execute(
            """INSERT INTO task_steps
               (task_id, step_no, tool_name, arguments_json, status,
                result_json, idempotency_key, started_at, finished_at)
               VALUES (?, ?, ?, ?, 'completed', ?, ?, ?, ?)
               ON CONFLICT(idempotency_key) DO UPDATE SET
                 status='completed', result_json=excluded.result_json,
                 finished_at=excluded.finished_at""",
            (
                task_id, step_no, name,
                json.dumps(args, ensure_ascii=False),
                json.dumps(result, ensure_ascii=False),
                key, now, now,
            ),
        )


class TaskContext(TypedDict, total=False):
    task_id: str
    task_input: str
    goal: str
    constraints: list[str]
    acceptance: list[str]
    evidence: list[dict]
    key_points: list[str]
    completed_steps: list[str]
    pending_steps: list[str]
    warnings: list[str]
    report_path: str
    summary: dict


def init_context(state: TaskContext) -> dict:
    return {
        "goal": state["task_input"],
        "constraints": [
            "只使用 data/input/brief.txt 中的教学模拟资料",
            "结论必须能对应到原文证据",
            "不把工具成功等同于任务完成",
        ],
        "acceptance": [
            "report.md 存在且非空",
            "summary.json 可解析",
            "summary 包含 task_id、key_points、evidence",
            "所有必需步骤均已完成",
        ],
        "evidence": [],
        "key_points": [],
        "completed_steps": [],
        "pending_steps": ["read_source", "extract_points", "write_report", "validate"],
        "warnings": ["输入是教学模拟资料,不代表真实业务结论"],
    }


def read_source(state: TaskContext) -> dict:
    if not INPUT_PATH.is_file():
        raise FileNotFoundError(f"输入文件不存在:{INPUT_PATH}")
    lines = INPUT_PATH.read_text(encoding="utf-8").splitlines()
    evidence = [
        {"text": line.strip(), "source": "data/input/brief.txt", "line": i}
        for i, line in enumerate(lines, start=1)
        if line.startswith("反馈")
    ]
    if not evidence:
        raise ValueError("没有找到以“反馈”开头的资料行")
    record_step(
        state["task_id"], 1, "read_source",
        {"path": "data/input/brief.txt"},
        {"evidence_count": len(evidence)},
    )
    return {
        "evidence": evidence,
        "completed_steps": state["completed_steps"] + ["read_source"],
        "pending_steps": [x for x in state["pending_steps"] if x != "read_source"],
    }


def extract_points(state: TaskContext) -> dict:
    points = [item["text"] for item in state["evidence"]]
    record_step(
        state["task_id"], 2, "extract_points",
        {"evidence_count": len(state["evidence"])},
        {"point_count": len(points)},
    )
    return {
        "key_points": points,
        "completed_steps": state["completed_steps"] + ["extract_points"],
        "pending_steps": [x for x in state["pending_steps"] if x != "extract_points"],
    }


def write_report(state: TaskContext) -> dict:
    task_id = state["task_id"]
    out_dir = OUTPUT_ROOT / task_id
    out_dir.mkdir(parents=True, exist_ok=True)
    concerns = [p for p in state["key_points"] if "希望" in p or "认为" in p]
    positives = [p for p in state["key_points"] if "认可" in p]
    summary = {
        "task_id": task_id,
        "goal": state["goal"],
        "key_points": state["key_points"],
        "evidence": state["evidence"],
        "completed_steps": state["completed_steps"] + ["write_report"],
        "pending_steps": [x for x in state["pending_steps"] if x != "write_report"],
        "warnings": state["warnings"],
    }
    report = (
        "# 门店服务反馈分析(教学模拟)\\n\\n"
        f"- 任务 ID:`{task_id}`\\n"
        f"- 任务目标:{state['goal']}\\n\\n"
        "## 主要问题\\n"
        + "".join(f"- {p}\\n" for p in concerns)
        + "\\n## 正向反馈\\n"
        + "".join(f"- {p}\\n" for p in positives)
        + "\\n## 可执行建议\\n"
        "- 记录高峰时段的排队时长,按时段比较后再决定优化措施。\\n"
        "- 为商品说明补充简短、易理解的介绍。\\n\\n"
        "## 证据来源\\n"
        + "".join(
            f"- 第 {e['line']} 行:{e['text']}(来源:{e['source']})\\n"
            for e in state["evidence"]
        )
        + "\\n> 本报告基于固定规则和教学模拟数据。\\n"
    )
    (out_dir / "report.md").write_text(report, encoding="utf-8")
    (out_dir / "summary.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    with (out_dir / "run.log").open("a", encoding="utf-8") as f:
        f.write(f"{utc_now()} task={task_id} wrote_report_and_summary\\n")

    record_step(
        task_id, 3, "write_report",
        {"output_dir": str(out_dir.relative_to(ROOT))},
        {"report": "report.md", "summary": "summary.json"},
    )
    return {
        "report_path": str((out_dir / "report.md").relative_to(ROOT)),
        "summary": summary,
        "completed_steps": state["completed_steps"] + ["write_report"],
        "pending_steps": [x for x in state["pending_steps"] if x != "write_report"],
    }


def validate_output(state: TaskContext) -> dict:
    out_dir = OUTPUT_ROOT / state["task_id"]
    report_path = out_dir / "report.md"
    summary_path = out_dir / "summary.json"
    if not report_path.is_file() or report_path.stat().st_size == 0:
        raise RuntimeError("验收失败:report.md 不存在或为空")
    if not summary_path.is_file() or summary_path.stat().st_size == 0:
        raise RuntimeError("验收失败:summary.json 不存在或为空")

    summary = json.loads(summary_path.read_text(encoding="utf-8"))
    required = {"task_id", "key_points", "evidence"}
    if not required.issubset(summary):
        raise RuntimeError(f"验收失败:summary 缺少字段 {required - set(summary)}")
    remaining = [step for step in summary["pending_steps"] if step != "validate"]
    if remaining:
        raise RuntimeError(f"仍有未完成步骤:{remaining}")

    record_step(
        state["task_id"], 4, "validate_output",
        {"required_fields": sorted(required)},
        {"report_nonempty": True, "summary_valid": True},
    )
    final_summary = {
        **summary,
        "completed_steps": state["completed_steps"] + ["validate"],
        "pending_steps": [],
    }
    (out_dir / "summary.json").write_text(
        json.dumps(final_summary, ensure_ascii=False, indent=2), encoding="utf-8"
    )
    return {
        "summary": final_summary,
        "completed_steps": state["completed_steps"] + ["validate"],
        "pending_steps": [],
    }


def build_graph(checkpointer):
    graph = StateGraph(TaskContext)
    graph.add_node("init_context", init_context)
    graph.add_node("read_source", read_source)
    graph.add_node("extract_points", extract_points)
    graph.add_node("write_report", write_report)
    graph.add_node("validate_output", validate_output)
    graph.add_edge(START, "init_context")
    graph.add_edge("init_context", "read_source")
    graph.add_edge("read_source", "extract_points")
    graph.add_edge("extract_points", "write_report")
    graph.add_edge("write_report", "validate_output")
    graph.add_edge("validate_output", END)
    return graph.compile(checkpointer=checkpointer)


def run_task(task_input: str, task_id: str | None = None) -> dict:
    init_db()
    task_id = task_id or f"task-{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S')}"
    now = utc_now()
    with connect_db() as conn:
        conn.execute(
            """INSERT INTO tasks
               (task_id, task_input, status, current_step, max_steps, created_at, updated_at)
               VALUES (?, ?, 'running', 0, 8, ?, ?)""",
            (task_id, task_input, now, now),
        )

    try:
        with SqliteSaver.from_conn_string(str(DB_PATH)) as saver:
            graph = build_graph(saver)
            state = graph.invoke(
                {"task_id": task_id, "task_input": task_input},
                config={"configurable": {"thread_id": task_id}},
            )
        out_dir = OUTPUT_ROOT / task_id
        report_path = out_dir / "report.md"
        summary_path = out_dir / "summary.json"
        if not report_path.is_file() or not summary_path.is_file():
            raise RuntimeError("交付文件检查失败")
        summary = json.loads(summary_path.read_text(encoding="utf-8"))
        result = {
            "task_id": task_id,
            "status": "completed",
            "report_path": str(report_path.relative_to(ROOT)),
            "summary": summary,
            "warnings": state.get("warnings", []),
        }
        set_task(task_id, "completed", len(state.get("completed_steps", [])), result)
        return result
    except Exception as exc:
        set_task(task_id, "failed", 0, error=str(exc))
        return {
            "task_id": task_id,
            "status": "failed",
            "report_path": None,
            "summary": {},
            "warnings": [str(exc)],
        }


if __name__ == "__main__":
    task_input = "分析门店反馈,提炼问题、正向反馈和建议,并保留证据来源。"
    task_id = sys.argv[1] if len(sys.argv) > 1 else None
    print(json.dumps(run_task(task_input, task_id), ensure_ascii=False, indent=2))

代码如何防止任务遗漏

  • goal 和 constraints 明确保留任务目的与边界,不需要从历史消息里重新猜测。
  • evidence 记录原文、来源文件和行号。结论可以追溯,不把模型概括冒充原始资料。
  • completed_steps 与 pending_steps 在节点执行后更新。最终验收还会检查待办列表是否为空。
  • acceptance 将交付要求显式化。真实项目可以继续把每条验收条件实现成独立校验函数,而不是只把它们写进提示词。
  • summary.json 保存最终上下文快照,便于后续章节展示、排错和交接。

五、运行与验收

在 agent_lab/ 目录、已激活虚拟环境的 WSL2 终端执行:

python main.py context-demo-001

下面是预期输出形态,不是本回复环境实测生成的结果:

{
  "task_id": "context-demo-001",
  "status": "completed",
  "report_path": "output/context-demo-001/report.md",
  "summary": {
    "task_id": "context-demo-001",
    "key_points": ["反馈一:……", "反馈二:……", "反馈三:……"],
    "evidence": [
      {
        "text": "反馈一:顾客希望缩短高峰时段排队时间。",
        "source": "data/input/brief.txt",
        "line": 2
      }
    ],
    "pending_steps": []
  },
  "warnings": ["输入是教学模拟资料,不代表真实业务结论"]
}

注意:实际摘要会包含完整提取到的反馈与上下文字段,以上为便于阅读而缩短的示意结构。验收时应以真实生成文件为准。

表 4:可复现验收清单

用例操作成功判据
正常任务运行 python main.py context-demo-001返回 completed
报告交付查看 output/context-demo-001/report.md文件非空,包含问题、正向反馈、建议和来源
摘要结构解析 summary.json含 task_id、key_points、evidence
待办核对查看摘要的 pending_steps空数组
任务状态查询 tasks 表状态为 completed
边界用例临时把输入文件改名后运行新任务返回 failed,错误写入任务记录

查询业务状态与步骤:

python - <<'PY'
import sqlite3
con = sqlite3.connect("data/agent.db")
print("tasks:")
for row in con.execute(
    "SELECT task_id, status, current_step, error FROM tasks ORDER BY created_at"
):
    print(row)
print("task_steps:")
for row in con.execute(
    "SELECT task_id, step_no, tool_name, status FROM task_steps ORDER BY step_id"
):
    print(row)
con.close()
PY

如果缺少输入文件,程序应返回 failed,而不是伪造空报告并宣称完成。测试后恢复 brief.txt,避免影响下一次实验。

六、上下文管理常见误区

图 3:交付前的遗漏检测

否

是

是

否

准备交付

检查目标与约束

检查证据是否有来源

检查 completed_steps

pending_steps 是否为空

返回未完成清单

继续执行或说明阻塞原因

检查报告与 JSON 文件

文件和字段均有效

标记 completed

标记 failed 并记录错误

这条检查链把“计划完成”与“实际完成”分开。只要还有待办,或者交付文件缺失,就不能把任务标记为成功。

表 5:常见问题与处理方式

问题为什么会发生处理方式
任务目标越做越偏目标只存在于早期聊天消息把目标与约束放入结构化状态,并在关键节点读取
摘要很长但仍漏步骤摘要没有明确待办和验收字段使用 pending_steps 与 acceptance
结论无法追溯只保存总结,不保留原文出处在 evidence 保存文本、文件路径和行号
任务显示完成但没有文件仅凭节点返回或模型文字判断完成对实际文件、JSON 字段和待办进行程序化验收
多轮更新覆盖了旧进度状态更新规则不清楚明确每个节点负责更新哪些字段,并对列表合并策略作出约定

七、设计边界:什么该放上下文,什么不该放

结构化上下文也不是越大越好。任务目标、关键约束、待办清单、最近必要证据和交付状态通常应保留;大量原始文档、完整工具日志或大段中间结果可以保存在文件或数据库中,上下文只记录其路径、标识、摘要和可追溯引用。这样既避免提示上下文膨胀,也不至于丢失核查依据。

对于长任务,可以把每一步的输入、输出和状态写入 task_steps,把当前执行所需的精简状态放在图状态中。对话历史用于解释用户如何提出需求,任务状态用于驱动程序执行,检查点用于恢复图运行,最终报告用于交付——这几类数据应当各司其职。

本例使用 LangGraph 的 SQLite 检查点,适合本地学习和轻量单机任务。检查点保存的是图状态快照,并不自动保证外部副作用只发生一次;涉及发邮件、写业务系统或执行不可逆操作时,还需要幂等键、真实用户确认与失败后的对账机制。检查点可能包含输入和中间结果,应限制数据库文件访问,并按数据敏感程度设计保留与清理策略。

完成本节后,任务不再依赖模型“记得之前说过什么”,而是通过目标、约束、证据、步骤和验收规则形成可检查的执行上下文。后续章节可以在这套结构上继续加入长期记忆、上下文压缩、工具重试与日志追踪,但不应以长期记忆替代当前任务的明确状态。

什么时候应该压缩上下文

当资料量变大时,不建议把所有原文、每次工具返回和完整日志都复制到图状态或模型提示词中。更稳妥的做法是把原始资料保存到文件,把可追溯的 source、行号、记录 ID 放进 evidence,再为当前步骤生成短摘要。摘要必须保留影响决策的限制、未解决问题和来源引用;如果只留下结论而丢掉证据,后续节点就无法核对它是否有依据。

可以采用一个简单规则:执行状态保存“下一步必须用到的信息”,业务记录保存“排查和审计需要的信息”,原始文件保存“完整材料”。如果某项上下文被删除,系统仍应能通过文件路径或数据库记录重新取回原始依据。不要把“压缩”实现成不可逆地删除唯一证据。

处理用户需求变更

长任务中,用户可能在中途调整输出格式或新增限制。此时不要只把新要求追加到聊天历史,却继续沿用旧任务计划。应更新 goal 或 constraints,记录变更内容,并重新计算受影响的 pending_steps 与验收项。已经完成的步骤只有在新要求影响其结果时才需要重做;不受影响的步骤可以保留,但必须说明其结果仍符合当前约束。

对相互冲突的要求,Agent 不应自行挑选一个然后假装都满足。应将冲突写入 warnings,在确实无法安全继续时将任务置为 waiting_confirmation,由调用方澄清。这样比让模型在长上下文中自行“权衡”更容易追踪,也便于在后续章节接入人工确认机制。

参考资料

  1. LangGraph 官方文档:Persistence —— 图状态检查点与线程标识。
  2. LangGraph 官方文档:State —— 状态图与节点状态更新。
  3. LangGraph Checkpoint API Reference —— 检查点概念、pending writes 与序列化安全说明。
  4. LangGraph 官方发行记录 —— 核对核心库版本。
  5. LangGraph SQLite Checkpoint 官方 README —— SQLite 检查点使用说明。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值