ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Claude Code多线程实战:Agent View与Agent Teams并行编程指南

Claude Code多线程实战:Agent View与Agent Teams并行编程指南 Claude Code 这个工具从发布到现在我断断续续用了小半年。最开始只是拿它当个能读项目文件的命令行助手直到有一次要同时改三个模块的代码我盯着终端里那个单线程的对话窗口突然意识到——这玩意儿要是能并行跑效率至少翻三倍。后来翻文档才发现官方早就埋了 Agent View 和 Agent Teams 两套多线程机制只是藏得比较深中文资料也少得可怜。这篇就把我踩过的坑、试出来的配置、以及用 Polter 做实战验证的完整过程摊开讲清楚不管你是刚装完 Claude Code 的新手还是已经用它写过几个小项目的老用户看完都能直接上手多线程玩法。1. 先搞清楚 Claude Code 的线程到底指什么很多人一看到多线程三个字脑子里立刻蹦出 Java 的 Thread、Python 的 threading、或者 Kafka 消费端怎么保证顺序性这些概念。但 Claude Code 的多线程跟这些完全不是一回事理解错这一点后面所有配置都会走偏。1.1 它不是 CPU 线程而是并行对话上下文Claude Code 本质上是一个跑在终端里的 AI 编程助手它的线程指的是独立的对话上下文Context。每个线程有自己的消息历史、自己的文件读取状态、自己的工具调用记录。你可以把它想象成一个团队里坐着好几个工程师每个人手里都有一份自己的笔记本互不干扰地干活。这跟操作系统层面的线程调度没有半点关系。Claude Code 的并行是靠多个独立的 Agent 实例实现的每个实例背后是一次独立的模型调用。所以你在配置的时候不需要考虑 CPU 核心数、内存锁、线程池大小这些传统多线程参数你要考虑的是同时开几个 Agent、它们之间怎么通信、共享哪些文件。我一开始就是被这个误导了跑去查Claude Code 线程池配置结果文档里根本没这东西。后来才明白官方说的 Agent View 和 Agent Teams其实是两种不同的多 Agent 协作模式。1.2 Agent View 和 Agent Teams 的本质区别这两个概念是全文的核心必须先掰开揉碎。Agent View是一个主 Agent 视角下管理多个子任务。你还是在同一个对话窗口里操作但可以让主 Agent 把任务拆给多个后台 Agent 去执行执行结果汇总回主 Agent。它更像是一个项目经理带着几个实习生项目经理负责跟你沟通实习生负责干活。Agent Teams是多个对等 Agent 组成一个团队。每个 Agent 都是独立的有自己的角色定位它们之间可以互相发消息、共享工作区。这更像是一个扁平化的协作小组没有绝对的主从关系。用一张表对比更清楚维度Agent ViewAgent Teams交互入口单一主对话窗口多个独立窗口或统一面板Agent 关系主从主 Agent 调度子 Agent对等角色分工协作上下文共享子 Agent 共享主 Agent 的部分上下文通过消息传递按需共享适用场景单人多任务并行复杂项目多角色协作配置复杂度低开箱即用中高需要定义角色和通信规则典型用途同时改多个文件、跑多个测试前后端联调、代码审查流水线我个人的经验是如果你只是想让 Claude Code 同时干几件不相关的事用 Agent View 就够了如果你要模拟一个真实的开发团队流程比如一个人写代码、一个人审查、一个人写测试那才需要 Agent Teams。1.3 为什么官方要把这两个功能藏得这么深说实话我第一次找这两个功能的入口时翻了半天文档才找到。原因很简单多 Agent 并行会显著增加 token 消耗。每开一个 Agent就是一次独立的模型调用成本是线性增长的。官方不希望新手一上来就无脑开十个 Agent把额度烧光。另外多 Agent 协作的调试难度比单 Agent 高一个数量级。当三个 Agent 同时改同一个文件时冲突怎么解决消息乱序怎么办这些都是坑。官方把门槛设高一点其实是在保护用户。但对我们这些确实有并行需求的人来说这些功能一旦用起来效率提升是实打实的。接下来我就按实际操作的顺序把配置和使用过程完整走一遍。2. Agent View 的实操配置从单窗口到多任务并行Agent View 是我最常用的模式因为它改动最小几乎不需要额外配置就能跑起来。下面按步骤讲。2.1 环境准备确认你的 Claude Code 版本支持Agent View 不是所有版本都有。我踩过的第一个坑就是装了个老版本怎么找都找不到相关命令。确认版本的方法很简单在终端里跑claude --version如果版本号低于官方文档里标注的支持 Agent View 的最低版本就得先升级。升级命令根据你的安装方式不同而不同# 如果是 npm 全局安装 npm update -g anthropic-ai/claude-code # 如果是官方安装脚本 claude updateWindows 用户注意如果你是用 WSL 装的升级要在 WSL 里执行如果是桌面版直接在应用内检查更新。我有个朋友在 Windows 上折腾了半天结果发现他装的是两个版本终端里跑的是旧的桌面版是新的两边配置不互通白忙活一小时。提示升级前先备份你的配置文件通常在~/.claude/目录下。我有一次升级后配置被重置之前调好的模型参数全没了。2.2 启动 Agent View 模式的核心命令确认版本没问题后启动 Agent View 有两种方式。第一种是启动时直接指定claude --agent-view第二种是在对话中动态开启。你已经在跟 Claude Code 聊天了突然想开个子 Agent 去干别的可以直接在对话里说请用 Agent View 模式帮我同时处理以下三个任务 1. 重构 utils/date.js 里的时间格式化函数 2. 给 api/user.js 补充参数校验 3. 跑一遍现有的单元测试并汇总失败项Claude Code 会识别这个意图自动把任务拆给后台 Agent。实测下来第二种方式更灵活因为你可以根据当前对话的上下文临时决定要不要并行。但这里有个关键细节不是所有任务都适合拆给子 Agent。如果三个任务都要改同一个文件并行反而会冲突。我一般遵循一个原则——任务之间没有文件写冲突才用 Agent View 并行。读操作可以随便并行写操作要谨慎。2.3 子 Agent 的任务拆分逻辑与常见误区很多人以为 Agent View 是我说什么它就拆什么其实不是。Claude Code 在拆任务时有一套自己的判断逻辑我观察下来大致是这样的独立文件的操作直接拆并行执行有依赖关系的操作串行执行前一个的输出作为后一个的输入涉及同一文件的操作合并成一个任务避免冲突需要外部命令的操作根据命令是否幂等决定能否并行我踩过的一个坑是让 Agent View 同时跑两个npm install结果两个进程抢同一个node_modules目录直接报错。后来学乖了涉及包管理的操作一律串行。还有一个误区是以为子 Agent 能访问主 Agent 的全部上下文。实际上子 Agent 默认只拿到任务描述拿不到你之前跟主 Agent 聊的完整历史。如果你需要子 Agent 知道某些背景信息得在任务描述里显式带上。比如请用 Agent View 处理任务背景信息如下 - 项目使用 TypeScript 5.0 - 代码风格遵循 Airbnb 规范 - 测试框架是 Vitest 任务重构 src/utils/ 下的所有工具函数这样写子 Agent 才不会跑偏。2.4 查看和管理后台 Agent 的运行状态开了多个子 Agent 之后你得知道它们跑到哪了。Claude Code 提供了几个查看命令# 查看所有活跃 Agent claude agents list # 查看某个 Agent 的详细日志 claude agents logs agent-id # 终止某个 Agent claude agents kill agent-id我一般会在开完 Agent View 之后另开一个终端窗口跑claude agents list实时盯着状态。有一次一个子 Agent 卡在某个网络请求上主对话窗口一直没返回我还以为死机了结果一看日志是它在等一个超时。这种时候直接 kill 掉重来比干等强。注意子 Agent 的日志默认不显示在主对话窗口里。如果你不主动去看可能永远不知道它中间报了什么错。养成开完 Agent View 就瞄一眼日志的习惯。3. Agent Teams 的进阶玩法让多个 Agent 真正协作起来如果说 Agent View 是一个人带几个帮手那 Agent Teams 就是组一个真正的团队。这个模式的配置复杂度明显上一个台阶但能做的事情也多得多。3.1 定义团队角色配置文件怎么写Agent Teams 的核心是角色定义。你需要在一个配置文件里告诉 Claude Code这个团队有几个 Agent每个 Agent 负责什么它们之间怎么通信。配置文件通常放在项目根目录的.claude/teams.yaml具体路径以你的版本为准。一个典型的配置长这样team: name: fullstack-dev agents: - id: architect role: 负责整体架构设计和技术选型 model: claude-sonnet can_write: [docs/, *.md] - id: backend role: 负责后端 API 实现 model: claude-sonnet can_write: [src/api/, src/services/] - id: frontend role: 负责前端页面和组件 model: claude-sonnet can_write: [src/components/, src/pages/] - id: reviewer role: 负责代码审查不直接改代码 model: claude-opus can_write: [] communication: mode: message-passing channels: - general - review这个配置里几个关键点值得展开说。can_write字段是权限控制。我强烈建议给每个 Agent 限定可写目录否则多个 Agent 同时改代码冲突会让你怀疑人生。reviewer 角色我一般设成can_write: []只让它读和评论不让它动手。model字段可以给不同角色配不同模型。架构设计和代码审查这种需要深度思考的活用强一点的模型简单的代码搬运用轻量模型省钱。我实测下来一个四人团队如果全用最强模型token 消耗是单 Agent 的四到五倍成本压力不小。communication.mode决定消息传递方式。message-passing是最常用的Agent 之间通过消息队列通信。还有shared-memory模式多个 Agent 共享一块工作区但冲突处理更复杂新手不建议碰。3.2 消息传递机制Agent 之间怎么对话Agent Teams 最迷人的地方就是 Agent 之间能互相发消息。比如 backend 写完一个 API可以主动通知 frontend接口好了地址是/api/user/profile返回结构如下……消息传递的底层逻辑是基于文件的消息队列。每个 Agent 有一个收件箱文件发消息就是往对方收件箱里追加一条记录。这种设计的好处是简单可靠坏处是消息顺序不保证——如果两个 Agent 同时给第三个 Agent 发消息到达顺序可能是乱的。这就引出一个实战中的大坑不要让多个 Agent 同时给同一个 Agent 发关键指令。我有一次让 architect 和 reviewer 同时给 backend 发修改意见结果 backend 先处理了 reviewer 的意见把 architect 要求的架构改回去了来回折腾了三轮才对齐。解决办法是引入消息优先级或者串行化关键通信。在配置里可以给消息加priority字段或者在团队里指定一个协调者角色所有关键指令先发给协调者由协调者统一转发。3.3 共享工作区的冲突处理策略Agent Teams 里最头疼的问题就是多个 Agent 同时写同一个文件。虽然can_write能限制目录但同一个目录下的不同文件也可能有依赖关系。我总结了几条实战策略策略一按文件粒度划分职责。backend 只碰src/api/frontend 只碰src/components/井水不犯河水。这是最简单的但要求项目结构本身清晰。策略二引入文件锁。Claude Code 在写文件前会检查是否有其他 Agent 正在写同一个文件如果有就排队。但这个锁是软锁依赖 Agent 自觉遵守偶尔还是会出问题。策略三关键文件串行修改。像package.json、tsconfig.json这种全局配置文件指定只有一个 Agent 能改其他 Agent 需要改的时候发消息请求。策略四定期同步。我一般会让团队每完成一个阶段就做一次同步点所有 Agent 暂停主控 Agent 检查一遍文件状态确认没有冲突再继续。提示如果你的项目用 Git可以在每个阶段结束后让某个 Agent 跑一次git status看看有没有意外的文件改动。这个习惯帮我抓到过好几次 Agent 越权写文件的问题。3.4 一个四人团队的完整协作流程演示光说理论太干我拿一个真实的小项目走一遍。假设要做一个待办事项应用前后端分离。第一步architect 出方案。architect 读取需求输出一份技术方案文档包括 API 设计、数据结构、目录结构。这份文档写到docs/design.md。第二步backend 和 frontend 并行开发。backend 根据方案实现 APIfrontend 根据方案实现页面。两者通过约定的接口文档对接不需要实时通信。第三步backend 完成后通知 frontend。backend 往 general 频道发消息API 已就绪接口地址和返回结构见 docs/api.md。frontend 收到后开始联调。第四步reviewer 介入。reviewer 定期读取 backend 和 frontend 的代码把问题写到 review 频道。backend 和 frontend 根据 review 意见修改。第五步architect 做最终验收。所有 Agent 停止architect 通读全部代码和文档确认符合最初的设计。整个流程跑下来我最大的感受是Agent Teams 的价值不在于快而在于流程化。单 Agent 也能做完这些事但它是串行的而且容易漏步骤。多 Agent 团队强制你把流程拆清楚每个环节都有专人负责反而更不容易出错。4. Polter 实战用多 Agent 并行重构一个真实项目前面讲的都是机制这一节上真家伙。Polter 是我自己维护的一个小工具库主要做数据格式转换代码量不大但模块多正好拿来验证 Agent Teams 的效果。4.1 Polter 项目结构与重构目标Polter 的原始结构是这样的polter/ ├── src/ │ ├── parsers/ │ │ ├── json.js │ │ ├── xml.js │ │ └── csv.js │ ├── converters/ │ │ ├── json-to-xml.js │ │ ├── xml-to-json.js │ │ └── csv-to-json.js │ ├── utils/ │ │ ├── validate.js │ │ └── normalize.js │ └── index.js ├── tests/ └── package.json重构目标有三个把所有模块从 CommonJS 改成 ESM给每个 parser 和 converter 补充类型定义统一错误处理逻辑。这三个目标里改 ESM 和补类型定义可以并行因为改的是不同层面的东西统一错误处理需要等前两个做完因为它要读取所有模块的代码。4.2 用 Agent Teams 拆分重构任务我配了一个三人团队team: name: polter-refactor agents: - id: module-agent role: 负责把所有模块改成 ESM 语法 can_write: [src/] - id: type-agent role: 负责补充 JSDoc 类型定义 can_write: [src/, types/] - id: error-agent role: 负责统一错误处理需等待前两个 Agent 完成 can_write: [src/utils/error.js] communication: mode: message-passing channels: [general]这里有个关键设计module-agent 和 type-agent 都写src/目录理论上会冲突。但实际跑下来没冲突因为它们改的是文件的不同部分——module-agent 改import/export语句type-agent 加 JSDoc 注释。Claude Code 的文件写入是按行合并的只要不碰同一行就不会冲突。不过这个不冲突是运气好。如果两个 Agent 都要改同一个函数的签名那就麻烦了。所以我在配置里加了一条规则type-agent 只加注释不改代码逻辑。这条规则写在 role 描述里Claude Code 会遵守。4.3 并行执行中的三次意外与解决过程第一次意外module-agent 把测试文件也改了。我明明限定了can_write: [src/]但它还是动了tests/下的文件。查日志发现是它在改src/index.js的时候顺手把引用到的测试文件也优化了。解决办法是在 role 描述里明确写不要修改 tests 目录下的任何文件并且在配置里把tests/加进所有 Agent 的只读列表。第二次意外type-agent 和 module-agent 同时改 package.json。两个 Agent 都想往package.json里加字段——一个加type: module一个加types: ./types/index.d.ts。结果后写的覆盖了先写的。这个问题的根源是package.json不在任何 Agent 的can_write列表里但 Claude Code 默认允许写项目根目录的配置文件。解决办法是显式把package.json加入只读列表需要改的时候由主控 Agent 统一改。第三次意外error-agent 启动太早。我原本以为它会等前两个 Agent 完成结果它一上来就开始读代码读到的还是半成品生成的错误处理逻辑跟最终代码对不上。后来我在配置里加了depends_on字段- id: error-agent role: 负责统一错误处理 depends_on: [module-agent, type-agent] can_write: [src/utils/error.js]加上这个之后error-agent 会等前两个 Agent 都发完成信号才启动。4.4 重构结果对比单 Agent vs 多 Agent同一套重构任务我分别用单 Agent 和三人团队跑了一遍记录如下指标单 Agent三人团队总耗时约 18 分钟约 9 分钟Token 消耗约 45k约 120k人工干预次数5 次8 次最终代码质量良好良好遗漏项2 处0 处几个观察耗时减半但 token 消耗接近三倍。这是多 Agent 的必然代价每个 Agent 都要独立读取上下文。如果你的额度紧张得权衡一下。人工干预次数反而增加了。因为多 Agent 的协调问题更多我需要时不时去处理冲突、调整任务。这部分成本在表格里体现不出来但实际感受很明显。遗漏项少了。单 Agent 跑的时候有两个模块的类型定义漏了多 Agent 因为 type-agent 专门盯这件事一个没漏。这是分工带来的好处。代码质量差不多。说明多 Agent 不会让代码变得更好它只是让流程更可控。质量还是取决于你的 prompt 和配置。5. 多 Agent 并行的成本控制与性能调优聊到这里必须泼一盆冷水多 Agent 不是免费的。我见过有人一上来就开八个 Agent跑了一个下午额度直接见底。这一节讲讲怎么在效率和成本之间找平衡。5.1 Token 消耗的构成与预估方法多 Agent 的 token 消耗主要来自三块每个 Agent 的初始上下文包括系统提示、角色定义、项目背景。这部分是固定开销Agent 越多总开销越大。Agent 之间的消息传递每条消息都要被发送方生成、接收方读取双向消耗。任务执行本身的消耗读写文件、调用工具、生成代码这部分跟单 Agent 差不多。预估方法很简单单 Agent 的消耗乘以 Agent 数量再乘以 1.5 到 2 的协调系数。比如单 Agent 跑一个任务消耗 10k token三个 Agent 大概要 45k 到 60k。我一般会在跑之前先估算一下如果预估消耗超过我当天额度的三分之一就拆成两天跑或者减少 Agent 数量。5.2 哪些任务值得并行哪些纯属浪费不是所有任务都适合多 Agent。我总结了一个判断标准值得并行的任务多个独立模块的相似改造比如给十个文件加同一种注释读写分离的任务一个 Agent 读一个 Agent 写需要不同专业视角的任务架构、实现、审查不值得并行的任务单个文件的修改一个 Agent 就够了强依赖链的任务A 完成才能 BB 完成才能 C需要频繁通信的任务通信成本超过并行收益我踩过的最亏的一次是让两个 Agent 并行改同一个文件的不同函数。结果两个 Agent 互相等对方的文件锁实际耗时比单 Agent 还长。同一个文件永远只让一个 Agent 碰这是铁律。5.3 模型选择对成本和速度的影响不同角色的模型选择直接影响成本和速度。我的经验配置角色推荐模型理由架构设计强模型需要深度推理值得花钱代码实现中等模型量大用强模型太贵代码审查强模型需要发现细微问题文档生成轻量模型格式化工作不需要太聪明测试编写中等模型需要理解业务逻辑实测下来把代码实现角色从强模型换成中等模型成本能降 40% 左右质量下降不明显。但审查角色千万别省用弱模型审查等于没审查。5.4 避免 Agent 空转的几个配置技巧Agent 空转是指 Agent 启动了但没活干白白消耗上下文。常见原因有三个原因一依赖没满足就启动。用depends_on解决前面讲过。原因二任务描述太模糊Agent 不知道从哪下手。解决办法是把任务拆得足够细每个 Agent 的任务描述里包含明确的输入、输出、验收标准。原因三Agent 之间互相等待。比如 A 等 B 的消息B 等 A 的消息死锁。解决办法是设置超时超过一定时间没有进展就强制推进或终止。我在配置里一般会加一个全局超时team: timeout: 600 # 单位秒 on_timeout: terminate # 或 continueterminate是直接终止continue是让主控 Agent 接管。我一般用terminate因为超时往往意味着配置有问题硬跑下去也是浪费。6. 踩坑实录那些文档里不会写的细节这一节专门记录我在实际使用中遇到的各种奇葩问题都是文档里找不到的。6.1 配置文件路径在不同系统下的差异Claude Code 的配置文件路径在 Windows、macOS、Linux 上不一样而且项目级配置和用户级配置的优先级也容易搞混。用户级配置~/.claude/Windows 是%USERPROFILE%\.claude\项目级配置项目根目录下的.claude/项目级配置会覆盖用户级配置。我有个项目在.claude/teams.yaml里配了团队结果跑的时候一直用的是用户级的默认配置查了半天才发现是项目级配置的文件名写错了——我写成了team.yaml少了个 s。提示配置改完之后用claude config show确认一下实际生效的配置别凭记忆。6.2 Agent 之间消息乱序的真实案例前面提过消息乱序这里给个真实案例。我有一次让三个 Agent 协作改一个 APIarchitect 发消息说接口路径改成/v2/userreviewer 同时发消息说接口路径保持/v1/user不变。backend 收到两条消息先处理了 reviewer 的结果改回了 v1。architect 发现后重新发消息又改回 v2。来回三次。后来我的解决办法是给消息加时间戳和优先级接收方按优先级处理。在配置里可以这样写communication: mode: message-passing priority_rules: - from: architect priority: 10 - from: reviewer priority: 5这样 architect 的消息永远优先于 reviewer 的。但这也带来新问题如果 reviewer 发现了严重 bug优先级低反而被压后处理。所以优先级规则要按场景调整没有万能配置。6.3 子 Agent 越权修改文件的拦截方法子 Agent 越权修改文件是最常见的问题。除了前面说的can_write限制还有几个补充手段手段一Git 钩子。在项目里配一个 pre-commit 钩子检查改动的文件是否在允许列表里不在就拒绝提交。这个能兜底但只在提交时生效运行中的越权拦不住。手段二文件监控。跑一个后台脚本监控文件变化发现越权修改就报警。我用的是chokidar配置简单const chokidar require(chokidar); const allowedPaths [src/, docs/]; chokidar.watch(.).on(change, (path) { if (!allowedPaths.some(p path.startsWith(p))) { console.warn(越权修改: ${path}); } });手段三定期 diff。每个阶段结束后跑一次git diff --stat看看有没有意外的文件改动。这个最简单但需要人工检查。6.4 任务卡死时的排查链路Agent 卡死是家常便饭排查链路我总结成四步第一步看 Agent 状态。claude agents list看是哪个 Agent 卡住了状态是running还是waiting。第二步看日志。claude agents logs id看最后几条日志通常能看出卡在哪。常见的是卡在网络请求、卡在文件锁、卡在等消息。第三步看系统资源。如果是本地跑模型看 CPU 和内存如果是调云端 API看网络连接。第四步强制终止重来。如果前三步没找到原因直接claude agents kill id然后重新发起任务。别在卡死的 Agent 上浪费时间。我有一次卡了二十分钟最后发现是某个 Agent 在等一个永远不会来的消息——因为发消息的那个 Agent 已经崩了。这种死锁只能靠超时机制解决。7. 从单 Agent 到多 Agent 的迁移建议如果你现在还在用单 Agent想迁移到多 Agent我的建议是循序渐进别一步到位。7.1 先跑通 Agent View 再上 TeamsAgent View 的配置成本几乎为零先用它跑几个并行任务熟悉一下多 Agent 的感觉。等你对任务拆分、冲突处理有了直觉再上 Agent Teams。我见过有人直接上 Teams配了五个 Agent结果第一个任务就冲突了折腾一下午没跑通直接放弃。其实如果先用 Agent View 练手很多坑可以提前踩到。7.2 团队规模从两人开始Agent Teams 的第一个团队建议只配两个 Agent。一个负责写一个负责审查。这个最小团队能让你理解消息传递、角色分工、冲突处理的全流程而且成本可控。跑顺了之后再逐步加人。每加一个人都要重新评估任务拆分和通信规则。我现在的稳定配置是四人团队再多就管不过来了。7.3 建立自己的配置模板库多 Agent 配置有很多重复的部分建议建一个模板库按项目类型分类。比如templates/web-fullstack.yaml前后端分离项目templates/library-refactor.yaml库重构项目templates/data-pipeline.yaml数据处理项目每个模板里预置好角色、权限、通信规则新项目直接复制改改就能用。我现在有七八个模板开新项目的时候省了大量配置时间。7.4 记录每次并行的效果数据最后一条建议记录数据。每次跑多 Agent记下耗时、token 消耗、人工干预次数、最终质量。跑多了之后你就能总结出什么任务适合并行、什么配置最划算。我自己的记录表已经积累了三十多条回头看最有价值的不是那些成功的案例而是失败的案例——它们告诉我哪些任务不该并行哪些配置是坑。多 Agent 并行这件事说到底是个工程权衡。它不是银弹不会让所有任务都变快但在合适的场景下它确实能把效率提升一个档次。关键是搞清楚它的边界知道什么时候该用、什么时候不该用。我现在的习惯是拿到任务先判断能不能拆能拆就用 Agent View 快速并行需要流程化协作才上 Agent Teams。这套组合拳打下来日常开发效率比纯单 Agent 高了大概一倍成本控制在可接受范围内。
返回列表