尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

MTNode 1.1.25:长文本智能处理框架,构建小说知识图谱与AI应用开发实践

MTNode 1.1.25:长文本智能处理框架,构建小说知识图谱与AI应用开发实践 最近在 AI 应用开发圈里一个现象越来越明显很多开发者不再满足于调用现成的 API而是开始尝试构建能处理复杂、长文本任务的“智能体”。无论是分析一份几十页的合同还是理解一部百万字的小说传统的“一问一答”模式已经捉襟见肘。这时一个能帮你拆解、分析、重构长文本的底层工具价值就凸显出来了。今天要聊的MTNode正是这样一个工具。它最新的 1.1.25 版本核心更新就围绕“小说拆解”和“世界书提炼”这两个听起来就很有想象力的功能。但别被名字迷惑它绝不仅仅是一个“小说阅读器”。它的本质是一个面向开发者的、模块化的长文本处理与结构化分析框架。如果你正在尝试构建需要深度理解文档、自动生成摘要、提取知识图谱或者进行多轮复杂推理的 AI 应用那么 MTNode 提供的这套“拆解-分析-重构”的流水线很可能就是你缺失的那块拼图。它把处理一本小说的复杂过程抽象成了可配置、可插拔的标准化步骤。本文将带你深入拆解 MTNode 1.1.25 的这次更新。我们不止步于功能介绍而是要搞清楚它到底解决了什么工程难题为什么自己写代码处理长文本那么痛苦它的核心设计思想是什么“世界书”到底是什么数据结构作为一个开发者如何快速上手并集成到自己的项目中从环境搭建到跑通第一个示例在实际使用中有哪些“坑”和最佳实践如何避免内存溢出、处理解析错误我们直接进入正题。1. 从“文本切割”到“语义理解”MTNode 要解决的核心问题在深入代码之前我们必须先理解 MTNode 瞄准的痛点。处理长文本尤其是小说这类叙事性文本时开发者通常会遇到几个典型问题问题一简单的“切片”会破坏语义。用固定字符数比如每 2000 字切割小说很可能把一句话、一个场景甚至一段关键对话拦腰截断。后续的 AI 模型拿到的是支离破碎的片段根本无法进行有效的上下文理解。问题二缺乏结构化的分析维度。即使把整本小说喂给 AI你得到的回答也往往是笼统的。你想系统性地分析人物关系、情节脉络、世界观设定你需要自己设计复杂的提示词Prompt并手动组织多次问答过程繁琐且不易复用。问题三中间状态难以管理和复用。你写了一个脚本分析人物另一个脚本提取地点。这些分析结果是孤立的无法轻松地关联起来形成对作品的整体认知。下次换一本小说又得重头开始。MTNode 的解决方案是提供一个“处理管道Pipeline”。它不直接给你最终答案而是给你一套工具和标准流程让你可以像组装乐高一样构建自己的长文本分析应用。1.1.25 版本的“小说拆解”和“世界书提炼”就是这个管道中的两个关键环节拆解 智能地将长篇小说按章节、场景、甚至叙事单元进行分割保证切割点在语义边界上。提炼 将拆解后的文本块通过预定义的“技能Skill”提取出人物、地点、组织、事件、物品等实体及其关系最终组装成一个结构化的“世界书”可以理解为该小说的知识图谱或设定集。这样一来你的应用底层就拥有了一本可以被程序查询和推理的“结构化小说”。基于此你可以轻松实现角色关系查询、情节时间线梳理、自动生成内容摘要、甚至是基于原著的二次创作辅助。2. 核心概念解读节点、技能与世界书要使用 MTNode必须理解它的三个核心概念这决定了你如何设计处理流程。2.1 节点 (Node)在 MTNode 中一切皆“节点”。节点是数据处理的基本单元。一个节点可以代表一整本原始文本文件。被拆解后的一个章节。从一个章节中提取出的一个人物实体。一段总结性的摘要。节点之间通过“关系”连接形成一个有向无环图DAG。例如“小说节点”包含多个“章节节点”“章节节点”中提到了多个“人物节点”。这种设计让数据的流动和追溯变得非常清晰。2.2 技能 (Skill)技能是 MTNode 的灵魂它是一个个可执行的、具有特定功能的模块。每个技能接收一个或多个节点作为输入执行操作如调用 AI 模型进行分析、进行规则匹配、转换格式等并输出新的节点或丰富现有节点的属性。MTNode 1.1.25 在小说处理场景下内置或强化了多种技能拆分技能 将长文本节点拆分为章节节点、场景节点。实体提取技能 从文本节点中识别并提取人物、地点等实体生成相应的实体节点。关系提取技能 分析文本建立实体节点之间的关系如“A 是 B 的父亲”、“C 出生于地点 D”。摘要技能 为文本节点生成摘要。你可以像搭积木一样将这些技能组合成一个处理管道。2.3 世界书 (WorldBook)“世界书”是 MTNode 1.1.25 版本提出的一个核心输出概念。它不是一种新的文件格式而是一个逻辑上的集合包含了从一部作品中提炼出的所有结构化信息。你可以把它想象成一个项目的“工作空间”或“数据库”里面存储了原始文本节点及其拆解后的子节点。所有提取出的实体节点人物、地点、组织等。节点之间的丰富关系。由各种技能产生的分析结果和摘要节点。世界书是后续所有高级应用如问答、推理、生成的基石。它的数据通常以图数据库或结构化 JSON 的形式持久化。3. 环境准备与项目初始化MTNode 是一个 Python 库因此环境搭建相对简单。我们假设你已经在本地或开发服务器上准备好了 Python 环境。3.1 基础环境要求Python 版本: 推荐 Python 3.8 - 3.11。确保你的 pip 版本是最新的。包管理工具: 使用pip或poetry进行依赖管理。本文以pip为例。AI 模型 API: MTNode 的许多技能需要调用大语言模型LLM。你需要准备相应 API 的密钥。目前主流支持包括OpenAI GPT 系列智谱 AI (GLM)百度文心一言阿里通义千问月之暗面 (Kimi)深度求索 (DeepSeek)零一万物 (Yi)腾讯混元火山方舟minimaxOllama (本地模型)3.2 安装 MTNode通过 pip 安装最新版本1.1.25pip install mt-node安装完成后可以通过以下命令验证版本python -c import mtnode; print(mtnode.__version__)你应该能看到1.1.25或更高的版本号。3.3 配置 API 密钥MTNode 通过环境变量来管理模型 API 密钥。这是最安全、最灵活的方式。在你的项目根目录创建一个.env文件确保该文件被.gitignore忽略并填入你的密钥。例如如果你使用 OpenAI 和智谱 AI# .env 文件示例 OPENAI_API_KEYsk-your-openai-api-key-here ZHIPUAI_API_KEYyour-zhipuai-api-key-here # 其他模型的密钥按需添加然后在你的 Python 代码或脚本中使用python-dotenv加载这些变量pip install python-dotenv# config.py 或主程序开头 from dotenv import load_dotenv load_dotenv() # 加载 .env 文件中的环境变量 import os openai_api_key os.getenv(OPENAI_API_KEY) # 现在 MTNode 会自动读取这些环境变量4. 核心流程拆解四步构建你的小说分析管道MTNode 处理一本小说的典型流程可以概括为四个步骤我们用一个具体的例子来串联。目标分析金庸小说《射雕英雄传》的第一回提取主要人物和地点。4.1 第一步创建项目与加载文本首先我们需要创建一个 MTNode 项目并将原始文本加载进来形成一个“根节点”。# create_novel_project.py from mtnode import Project, Node # 1. 创建一个新项目命名为“射雕英雄传分析” project Project(name射雕英雄传分析) # 2. 读取小说文本文件假设我们有一个 txt 文件 with open(shediao_chapter1.txt, r, encodingutf-8) as f: novel_text f.read() # 3. 创建根节点代表整部小说或当前处理的章节 # 节点的 content 属性存储原始文本 # node_type 可以自定义这里用 ‘novel_text‘ 标识 root_node Node( contentnovel_text, node_typenovel_text, name《射雕英雄传》第一回 ) # 4. 将根节点添加到项目中 project.add_node(root_node) print(f项目 ‘{project.name}‘ 创建成功根节点 ID: {root_node.id})这一步之后你的项目里就有了一个包含原始文本的节点。这是所有后续处理的起点。4.2 第二步智能拆解文本接下来使用 MTNode 的拆分技能将长章节拆分成更小的、语义完整的片段比如按场景或自然段落拆分。这比粗暴的字符切割要有效得多。# split_novel.py from mtnode.skills import SplitSkill # 1. 初始化拆分技能。 # 这里使用基于 LLM 的智能拆分器它比基于标点的拆分更能理解叙事结构。 split_skill SplitSkill( splitter_typellm, # 使用大模型进行智能拆分 modelgpt-3.5-turbo, # 指定使用的模型也可以是 gln-4, qwen-max 等 chunk_size1000, # 指导性的大致块大小模型会尽量遵循 overlap50 # 块之间的重叠字符防止信息在边界丢失 ) # 2. 对根节点应用拆分技能。 # 技能会读取 root_node 的 content进行分析和拆分。 child_nodes split_skill.run(root_node) # 3. 拆分技能返回一个新的节点列表。 # 每个子节点都是原文本的一个片段并且自动建立了与父节点root_node的“子级”关系。 for i, node in enumerate(child_nodes): node.name f文本片段_{i1} project.add_node(node) print(f创建子节点: {node.name}, 长度: {len(node.content)} 字符) print(f共拆分为 {len(child_nodes)} 个片段。)关键点SplitSkill是核心。llm类型的拆分器虽然会产生一些 API 调用成本但对于小说这种结构复杂的文本它能更好地识别“场景结束”、“视角转换”等自然边界质量远高于正则表达式拆分。4.3 第三步提取实体与关系构建世界书这是最核心的一步。我们将对每个文本片段应用“实体提取”和“关系提取”技能从中抽取出人物、地点等信息并建立它们之间的联系。# extract_entities.py from mtnode.skills import EntityExtractionSkill, RelationExtractionSkill # 1. 初始化实体提取技能。 # 我们告诉技能需要提取哪些类型的实体。 entity_skill EntityExtractionSkill( entity_types[人物, 地点, 组织, 武功], modelgpt-4, # 实体提取对精度要求高建议使用能力更强的模型 description从武侠小说片段中提取实体 ) # 2. 初始化关系提取技能。 # 定义可能存在的核心关系类型。 relation_skill RelationExtractionSkill( relation_types[师徒, 父子, 夫妻, 结义, 出生于, 归属于], modelgpt-4 ) # 3. 遍历所有文本片段进行提取。 worldbook_nodes [] # 用于存放所有提取出的实体节点 for text_node in child_nodes: # child_nodes 是上一步拆分的片段 # 3.1 提取实体 entity_result entity_skill.run(text_node) # entity_result 可能包含 ‘entities‘ 字段里面是提取出的实体列表 for entity_info in entity_result.get(entities, []): # 为每个实体创建一个新节点 entity_node Node( contententity_info.get(description, ), # 实体的描述 node_typeentity_info[type], # 如 “人物” nameentity_info[name], # 如 “郭靖” propertiesentity_info # 将所有原始信息存入 properties 备用 ) # 建立关系文本片段“提及”了这个实体 project.add_relation(text_node, mentions, entity_node) worldbook_nodes.append(entity_node) project.add_node(entity_node) # 3.2 提取关系 (基于当前文本片段和已提取的实体) # 注意更复杂的实现可能会在所有实体提取完后进行全局关系推理。 # 这里演示基于单个片段的简单关系提取。 relation_result relation_skill.run(text_node) for rel_info in relation_result.get(relations, []): # rel_info 可能包含: source_entity, target_entity, relation_type # 在实际项目中你需要根据实体名称找到对应的 entity_node 对象然后建立关系 # project.add_relation(source_node, rel_info[type], target_node) pass # 简化处理实际逻辑需根据返回结构编写 print(f共提取出 {len(worldbook_nodes)} 个实体节点。)这一步结束后你的项目图数据库中就不再只有文本节点还有了大量的“人物”、“地点”等实体节点以及“提及”关系。这就是“世界书”的雏形。4.4 第四步持久化与查询处理完成后我们需要将结果世界书保存下来并能够方便地查询。# persist_and_query.py import json # 1. 持久化将整个项目所有节点和关系导出为 JSON。 def save_worldbook(project, filepath): worldbook_data { project_name: project.name, nodes: [], relations: [] } # 简化示例遍历项目中的节点和关系转换为可序列化字典 # MTNode 项目对象可能有自己的导出方法这里展示原理。 for node in project.nodes: worldbook_data[nodes].append({ id: node.id, name: node.name, type: node.node_type, content_preview: node.content[:100] if node.content else }) # ... 类似地处理 relations with open(filepath, w, encodingutf-8) as f: json.dump(worldbook_data, f, ensure_asciiFalse, indent2) print(f世界书已保存至 {filepath}) save_worldbook(project, worldbook_shediao_chapter1.json) # 2. 简单查询示例找出所有类型为“人物”的节点。 character_nodes [node for node in project.nodes if node.node_type 人物] print(提取到的人物) for char in character_nodes: print(f - {char.name}) # 3. 更高级的查询可以依赖于图数据库。 # 例如将节点和关系导入 Neo4j 或 Nebula Graph即可执行复杂的图查询。 # “找出所有与‘郭靖’有‘师徒’关系的人物”至此一个完整的、从原始小说文本到结构化世界书的流程就跑通了。你可以将这个流程封装成一个函数或类方便处理更多章节或书籍。5. 完整示例构建一个简易小说分析 CLI 工具让我们把上面的步骤整合起来创建一个简单的命令行工具输入小说文件输出人物列表和关系摘要。# novel_analyzer_cli.py import argparse import json from pathlib import Path from mtnode import Project, Node from mtnode.skills import SplitSkill, EntityExtractionSkill from dotenv import load_dotenv load_dotenv() # 加载 API 密钥 class NovelAnalyzer: def __init__(self, model_for_splitgpt-3.5-turbo, model_for_entitygpt-4): self.project Project(nameNovelAnalysis) self.split_skill SplitSkill(splitter_typellm, modelmodel_for_split, chunk_size1500) self.entity_skill EntityExtractionSkill( entity_types[人物, 地点, 组织, 事件], modelmodel_for_entity ) def analyze(self, novel_text_path, chapter_name第一章): 核心分析方法 # 1. 加载文本 with open(novel_text_path, r, encodingutf-8) as f: text f.read() root_node Node(contenttext, node_typenovel_chapter, namechapter_name) self.project.add_node(root_node) print(f已加载章节: {chapter_name}) # 2. 智能拆分 print(正在智能拆分文本...) text_chunks self.split_skill.run(root_node) for i, chunk in enumerate(text_chunks): chunk.name f{chapter_name}_片段{i1:03d} self.project.add_node(chunk) self.project.add_relation(root_node, contains, chunk) print(f拆分完成共 {len(text_chunks)} 个片段。) # 3. 提取实体 print(正在提取实体...) all_entities [] for chunk in text_chunks: try: result self.entity_skill.run(chunk) entities result.get(entities, []) for e in entities: e_node Node( namee[name], node_typee[type], contente.get(description, ), propertiese ) self.project.add_node(e_node) self.project.add_relation(chunk, mentions, e_node) all_entities.append(e) except Exception as e: print(f处理片段 {chunk.name} 时出错: {e}) continue # 4. 汇总结果 summary { chapter: chapter_name, total_chunks: len(text_chunks), entities_extracted: len(all_entities), entities_by_type: {} } for e in all_entities: e_type e[type] summary[entities_by_type].setdefault(e_type, []).append(e[name]) # 去重 for e_type in summary[entities_by_type]: summary[entities_by_type][e_type] list(set(summary[entities_by_type][e_type])) return summary def save_project(self, output_path): 保存项目数据到文件 # 这里简化处理实际应使用 MTNode 的持久化方法或自定义序列化 data { nodes: [{id: n.id, name: n.name, type: n.node_type} for n in self.project.nodes], relations: [] # 省略关系的序列化 } with open(output_path, w, encodingutf-8) as f: json.dump(data, f, indent2, ensure_asciiFalse) print(f项目数据已保存至: {output_path}) def main(): parser argparse.ArgumentParser(descriptionMTNode 小说分析命令行工具) parser.add_argument(input_file, help输入的小说文本文件路径) parser.add_argument(--chapter, default分析章节, help章节名称) parser.add_argument(--output, defaultanalysis_result.json, help结果输出文件路径) args parser.parse_args() if not Path(args.input_file).exists(): print(f错误文件 {args.input_file} 不存在。) return analyzer NovelAnalyzer() print(f开始分析文件: {args.input_file}) summary analyzer.analyze(args.input_file, args.chapter) # 打印摘要 print(\n *50) print(f分析摘要 - {summary[chapter]}) print(*50) print(f文本片段数: {summary[total_chunks]}) print(f提取实体总数: {summary[entities_extracted]}) for e_type, names in summary[entities_by_type].items(): print(f{e_type} ({len(names)} 个): {, .join(names[:5])}{... if len(names)5 else }) print(*50) # 保存详细数据 analyzer.save_project(args.output) if __name__ __main__: main()运行方式# 假设你的小说文本是 chapter1.txt python novel_analyzer_cli.py chapter1.txt --chapter 射雕英雄传 第一回 --output shediao_analysis.json这个工具展示了如何将 MTNode 的核心功能封装成一个可复用的模块你可以在此基础上增加关系提取、摘要生成、图形化展示等功能。6. 运行效果与验证运行上述 CLI 工具后你会在控制台看到类似如下的输出开始分析文件: chapter1.txt 已加载章节: 射雕英雄传 第一回 正在智能拆分文本... 拆分完成共 8 个片段。 正在提取实体... 项目数据已保存至: shediao_analysis.json 分析摘要 - 射雕英雄传 第一回 文本片段数: 8 提取实体总数: 23 人物 (12 个): 郭啸天, 杨铁心, 曲三, 丘处机, 包惜弱, 李萍, 段天德... 地点 (6 个): 牛家村, 临安府, 嘉兴, 醉仙楼, 杭州... 组织 (3 个): 全真教, 金国, 南宋... 事件 (2 个): 风雪惊变, 结识道长... 同时生成的shediao_analysis.json文件里包含了所有节点和关系的结构化数据可以作为其他应用的数据源。如何验证结果是否正确人工抽查随机打开原始文本核对提取出的实体如“曲三”是否在该片段真实出现类型是否准确。一致性检查同一个实体如“郭靖”在不同片段中是否被识别为同一个节点这需要你在代码中实现实体归一化MTNode 可能提供相关技能或需要自己根据名称相似度处理。关系验证检查提取出的“师徒”关系在原文中是否有明确描述。边界测试使用另一部风格迥异的小说如科幻、言情进行测试观察实体提取的准确率是否下降以此评估技能的泛化能力。7. 常见问题与排查思路在实际使用 MTNode 1.1.25 进行开发时你可能会遇到以下典型问题问题现象可能原因排查方式解决方案运行时报错ModuleNotFoundError: No module named mtnodeMTNode 未正确安装或不在当前 Python 环境。1. 在终端执行pip list | grep mt-node。2. 检查当前 Python 解释器路径是否与安装环境一致。1. 在正确的虚拟环境中重新安装pip install mt-node。2. 使用绝对路径调用解释器。调用技能时提示API key not found环境变量未正确设置或加载。1. 在代码中打印os.getenv(OPENAI_API_KEY)看是否为 None。2. 检查.env文件路径是否正确变量名是否拼写错误。1. 确保在导入mtnode之前调用load_dotenv()。2. 直接在运行脚本前设置环境变量export OPENAI_API_KEYsk-xxx。文本拆分结果不理想切割点在句子中间1.chunk_size设置过小。2. 使用的 LLM 拆分器 (llm) 对于该语言或体裁效果不佳。3. API 调用超时或失败回退到了基础拆分器。1. 检查拆分后片段的长度和内容。2. 查看技能运行的日志或返回信息确认是否使用了llm拆分器。3. 尝试增大chunk_size(如 2000)。1. 尝试使用splitter_typerecursive递归字符分割作为备选方案虽然智能性下降但更稳定。2. 更换更强大的模型如gpt-4进行拆分但成本会增加。3. 实现后处理手动合并过小的碎片。实体提取重复或不准1. 不同片段独立提取导致同一实体被多次创建。2. 模型对某些实体类型如“武功”、“法宝”理解有偏差。3. 提示词Skill 内部定义不适合当前文本类型。1. 检查输出中是否存在同名同类型实体。2. 分析提取错误的实体看是名称错误还是类型错误。1.实现实体归一化在存入项目前根据名称相似度如使用difflib合并相同实体节点。2.定制实体类型根据你的小说类型修改EntityExtractionSkill的entity_types列表。3.提供示例高级用法中可以向技能传入few_shot_examples来引导模型。处理长篇小说时内存占用高或速度慢1. 一次性加载了整个超长文本。2. 串行处理所有片段API 调用成为瓶颈。3. 节点和关系全部缓存在内存中。1. 监控程序内存使用情况。2. 观察任务队列是否是等待 API 响应耗时最长。1.分章处理不要一次性处理整本书按章节分别创建项目和运行管道。2.异步并发使用asyncio或线程池并发调用多个片段的提取技能注意 API 速率限制。3.及时持久化每处理完一个章节或一定数量的节点就将其保存到数据库或文件中释放内存。生成的“世界书”JSON 文件结构混乱难以使用直接简单序列化项目对象没有设计清晰的数据结构。审查输出的 JSON 文件看节点和关系是否包含了所有必要信息格式是否一致。设计输出 Schema不要直接 dump 对象。定义好世界书的输出格式例如jsonbr{br metadata: {...},br entities: [{id: ..., type: ..., name: ..., attrs: {...}}],br relations: [{source_id: ..., target_id: ..., type: ...}],br documents: [...]br}br8. 最佳实践与工程建议将 MTNode 集成到生产级项目中需要考虑更多工程化因素。技能组合与管道设计顺序很重要通常流程是加载 - 清洗 - 拆分 - (多轮提取) - 融合 - 持久化。关系提取最好在实体提取之后进行。错误处理与重试任何调用外部 API 的技能都必须包裹在try-except中并实现指数退避的重试机制以提高鲁棒性。技能定制MTNode 允许你继承BaseSkill创建自定义技能。如果你有特殊的文本处理逻辑如解析特定格式的对话这是最好的方式。性能与成本优化缓存对于相同的输入文本拆分结果和实体提取结果在一定时间内是稳定的。可以考虑将中间结果如拆分后的片段、提取的实体JSON缓存到本地文件或 Redis 中避免重复调用昂贵的 LLM API。模型选择拆分任务对智能要求相对较低可使用gpt-3.5-turbo以降低成本。实体和关系提取对精度要求高建议使用gpt-4或同等级别模型。可以先用小规模文本测试不同模型的效果和成本。批量处理如果 API 支持批量调用尽量将多个文本片段组合成一个批次请求可以显著减少网络延迟和开销。数据质量与后处理实体归一化这是构建高质量知识图谱的关键。除了名称字符串匹配还可以利用实体属性如别名、称号、描述进行融合。例如“郭靖”和“郭大侠”应合并为一个节点。关系验证从不同片段提取出的关系可能存在冲突。需要设计简单的冲突解决策略如“投票机制”或基于上下文可信度的加权。人工审核接口对于关键项目设计一个简单的 Web 界面允许人工审核和修正自动提取的实体和关系并将修正结果反馈回系统可以形成数据质量的闭环。生产环境部署配置中心将模型类型、API 密钥、超时时间、重试次数等配置项外置到配置中心如 Apollo, Nacos或环境变量中避免硬编码。任务队列处理长篇小说是一个耗时任务应该设计为异步任务。使用 Celery、RQ 或 Dramatiq 等队列将分析任务丢入队列通过 WebSocket 或轮询向前端反馈进度。监控与日志详细记录每个技能的输入、输出、耗时和错误信息。这不仅是排查问题的依据也是优化管道、分析成本的重要数据。MTNode 1.1.25 的更新为开发者处理复杂长文本提供了一个强大的框架式解决方案。它最大的价值不在于开箱即用的完美效果而在于将“从非结构化文本到结构化知识”这一复杂过程标准化、模块化了。你可以根据自己的需求选择合适的技能调整管道参数甚至编写自定义技能来构建专属的文本理解应用。从简单的实体提取到复杂的情节推演、人物关系图谱可视化再到基于“世界书”的自动问答和内容生成其可能性建立在你这套稳定、可扩展的数据处理流水线之上。建议从一个小而具体的场景开始比如分析一部短篇小说的所有人物跑通整个流程再逐步增加复杂度。在这个过程中你会更深刻地理解如何设计提示词、如何处理数据冲突、如何平衡效果与成本——这些经验远比单纯调用一个 API 更有价值。
返回列表