Java 程序员第 46 阶段07:大模型调用链路追踪,SkyWalking 排查线上性能,异步线程池链路连续性修复与 CompletableFuture 断点实战
大模型应用里几乎离不开异步提示词组装、向量检索、推理调用、结果后处理常常并行执行用 CompletableFuture、线程池、Async 把任务丢到不同线程。但 SkyWalking 的链路上下文是保存在线程局部变量ThreadLocal里的一旦跨线程上下文就「带不过去」下游的 Span 会丢失父节点链路在异步边界处断开。本文用一个真实场景讲清楚为什么会断以及如何修复。异步为什么会导致链路断裂SkyWalking 线程上下文传播机制实战一RunnableWrapper 与 CallableWrapper实战二CompletableFuture 断点修复线程池与常见框架的踩坑清单1. 异步为什么会导致链路断裂SkyWalking 探针在拦截到一次请求时会把当前的 ContextSnapshot包含 TraceId、SegmentId、父 SpanId 等放进一个 ThreadLocal。后续在当前线程内创建的 Span 都从这个 ThreadLocal 里读取父上下文从而串成一条链路。问题就出在「ThreadLocal 不跨线程」。当你用 CompletableFuture.supplyAsync(() - {...}) 把一段逻辑提交到线程池时Lambda 在 worker 线程里执行而 worker 线程的 ThreadLocal 是空的。它不知道自己属于哪条链路于是新建一个 Segment并生成新的 TraceId。从监控上看这条异步任务「凭空出现」一条独立链路和主线程那条对不上这就是典型的链路断点。在大模型推理场景这种断点尤其隐蔽业务线程发起异步推理调用推理本身的耗时全部落在一条「孤儿链路」里主链路看起来很快但你永远看不到推理到底花了多久更无法把推理慢调用和上游请求关联起来。2. SkyWalking 线程上下文传播机制SkyWalking 提供了两类工具来解决跨线程传播**ContextSnapshot快照**在源线程调用 ContextManager.capture() 抓取当前上下文快照在目标线程调用 ContextManager.continueContext(snapshot) 把快照「续」上执行完再 stopSpan()。**Wrapper包装器**apm-toolkit-trace 提供的 RunnableWrapper、CallableWrapper以及注解 TraceCrossThread在提交任务时自动完成快照的抓取与续接对业务代码侵入最小。ContextSnapshot 的本质是一份只读的上下文拷贝它保住了「我是谁的孩子」。只要 worker 线程先 continueContext后续创建的所有 Span 都会正确挂到主链路下断点就被焊上了。需要引入的工具包依赖如下dependencygroupIdorg.apache.skywalking/groupIdartifactIdapm-toolkit-trace/artifactIdversion9.3.0/version/dependency注意 apm-toolkit-trace 必须和探针版本对齐否则可能出现 NoSuchMethodError 或快照字段不一致导致链路错乱。3. 实战一RunnableWrapper 与 CallableWrapper最简洁的修复方式是用 Wrapper 把任务包一层。下面这段异步日志与回调原始写法会断链// 断链写法worker 线程拿不到上下文executor.submit(() - {doInference(prompt); // 这条 Span 会变成孤儿链路});用 CallableWrapper / RunnableWrapper 改写后上下文自动透传import org.apache.skywalking.apm.toolkit.trace.CallableWrapper;import org.apache.skywalking.apm.toolkit.trace.RunnableWrapper;public void asyncWithWrapper(String prompt) {// Callable 任务executor.submit(CallableWrapper.of(() - {return doInference(prompt); // 自动延续主链路}));// Runnable 任务executor.execute(RunnableWrapper.of(() - {writeAuditLog(prompt); // 自动延续主链路}));}如果你希望某个类的 run() / call() 方法总是自动跨线程传播可以直接在该方法上加 TraceCrossThread 注解配合 Wrapper 使用即可无需手动 captureimport org.apache.skywalking.apm.toolkit.trace.TraceCrossThread;public class InferenceTask implements Runnable {TraceCrossThreadOverridepublic void run() {doInference(); // 跨线程后链路仍然连续}}// 提交时仍要用 Wrapper 包裹executor.execute(RunnableWrapper.of(new InferenceTask()));三种方式的对比如下表方式侵入度适用场景注意点------------ContextSnapshot 手动 capture/continue高需要精细控制 Span 生命周期必须配对 stopSpan易漏写CallableWrapper / RunnableWrapper低普通线程池提交任务提交处包一层即可TraceCrossThread 注解最低固定 Runnable/Callable 类仍需配合 Wrapper 提交4. 实战二CompletableFuture 断点修复CompletableFuture 是异步编排的主力但它的 supplyAsync 接收的是 Supplier而早期版本的 toolkit 没有 SupplierWrapper所以最稳妥的方案是「手动快照」。下面是一段推理服务的异步编排修复前链路会断// 断链写法public CompletableFutureString brokenInference(String prompt) {return CompletableFuture.supplyAsync(() - {return callInferenceService(prompt); // 孤儿链路}, inferenceExecutor);}修复后在调用线程捕获快照在 worker 线程续接import org.apache.skywalking.apm.toolkit.trace.ContextManager;import org.apache.skywalking.apm.toolkit.trace.ContextSnapshot;Tracepublic CompletableFutureString fixedInference(String prompt) {// 1. 在调用线程捕获上下文快照ContextSnapshot snapshot ContextManager.capture();return CompletableFuture.supplyAsync(() - {// 2. 在 worker 线程续接上下文ContextManager.continueContext(snapshot);try {return callInferenceService(prompt);} finally {// 3. 本线程 Span 结束清理上下文ContextManager.stopSpan();}}, inferenceExecutor);}如果是 thenApply / thenCompose 这类链式回调它们运行在哪个线程取决于前置阶段是否已完成若已完成则复用当前线程上下文还在若未完成则在回调注册的线程/默认 ForkJoinPool 执行上下文丢失。稳妥做法是给每个需要跨线程的环节都显式续接。下面展示 thenCompose 的连续修复public CompletableFutureString pipeline(String prompt) {ContextSnapshot s1 ContextManager.capture();return CompletableFuture.supplyAsync(() - {ContextManager.continueContext(s1);try { return retrieveContext(prompt); }finally { ContextManager.stopSpan(); }}, retrieveExecutor).thenCompose(ctx - {ContextSnapshot s2 ContextManager.capture();return CompletableFuture.supplyAsync(() - {ContextManager.continueContext(s2);try { return callInferenceService(ctx); }finally { ContextManager.stopSpan(); }}, inferenceExecutor);});}请记住一个铁律**每一次跨线程都必须重新 capture 一次快照并 continueContext**。因为快照是一次性的上下文快照跨多个异步阶段时要各自捕获不能复用同一个快照对象。5. 线程池与常见框架的踩坑清单异步链路修复做不对监控就会持续「失真」。下面是高频踩坑点现象根因修复方式---------Async 方法内链路断Spring 默认的 Async 走代理但子线程无上下文自定义 AsyncConfigurer 用 RunnableWrapper 包装任务CompletableFuture 偶发断链thenApply 落到默认 ForkJoinPool 公共线程显式传自定义 Executor 并续接上下文链路出现但 TraceId 不一致复用同一个 ContextSnapshot 给多个线程每个线程独立 capture修复后报错 ContextNotFoundcontinueContext 后漏写 stopSpan用 try/finally 保证配对监控显示多次 stopSpan手写 Span 与自动插件 Span 重复异步内部不要再手动 createSpan对于 Spring 的 Async推荐自定义执行器统一包裹ConfigurationEnableAsyncpublic class AsyncConfig implements AsyncConfigurer {Overridepublic Executor getAsyncExecutor() {ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor();executor.setCorePoolSize(8);executor.setMaxPoolSize(32);executor.setQueueCapacity(200);executor.setThreadNamePrefix(llm-async-);executor.initialize();return executor;}}然后在提交 Async 方法的具体实现里通过 RunnableWrapper.of 包裹你的任务逻辑或者更彻底地封装一个「链路感知」的线程池装饰器对所有 execute / submit 自动包 Wrapper这样业务代码零侵入。6. WebFlux 响应式上下文传播大模型网关和推理前置常使用 Spring WebFlux 响应式编程链路断点同样会发生但修复思路和线程池略有不同。响应式流的运算符operator可能在不同的调度器Scheduler线程上执行上下文随数据流而非线程传递。原则不变在「异步边界」之前 capture在边界之后 continueContext。public MonoString reactiveInference(String prompt) {// 在组装阶段当前线程捕获快照ContextSnapshot snapshot ContextManager.capture();return Mono.fromCallable(() - {// 在真正执行处续接上下文ContextManager.continueContext(snapshot);try {return doInference(prompt);} finally {ContextManager.stopSpan();}}).subscribeOn(Schedulers.boundedElastic());}需要提醒的是响应式场景下如果运算符链很长、且中间切换了多个 publishOn/subscribeOn 调度器最稳妥的做法是在每一个会切换线程的运算符内部都续接一次上下文否则仍可能在某些分支断链。SkyWalking 的 apm-spring-webflux-* 插件已经覆盖了 WebClient 调用与 Controller 入口业务内部的自定义 Mono/Flux 编排仍需按上面的方式手动处理。7. 线程池装饰器零侵入统一包裹如果每个提交任务的地方都手写 RunnableWrapper.of既啰嗦又容易漏。更彻底的做法是封装一个「链路感知」的线程池装饰器对所有任务自动包一层 Wrapper业务代码完全无感public class TracingExecutor implements Executor {private final Executor delegate;public TracingExecutor(Executor delegate) { this.delegate delegate; }Overridepublic void execute(Runnable command) {// 统一包裹业务侧无需关心链路delegate.execute(RunnableWrapper.of(command));}}使用时把原有线程池包一层即可ExecutorService raw Executors.newFixedThreadPool(16);Executor tracingExecutor new TracingExecutor(raw);// 之后所有 submit/execute 都自动续接链路如果要用在 ExecutorService需要 submit 返回 Future可以让装饰器同时实现 ExecutorService 并把每个 submit 的 Runnable/Callable 用对应的 Wrapper 包裹再委托给原始服务。这样整个项目只要「换掉线程池的获取入口」就能一次性修复所有异步断点是大型大模型服务最推荐的做法。8. Java 21 虚拟线程的注意事项当你的 JDK 升级到 21 并开始使用虚拟线程Thread.ofVirtual().start(...) 或 Executors.newVirtualThreadPerTaskExecutor()时链路传播要格外小心。虚拟线程基于载体线程carrier thread调度一个虚拟线程可能在执行过程中被挂载到不同的载体线程上而 SkyWalking 的上下文保存在 ThreadLocal 中单纯依赖线程局部变量在虚拟线程上可能出现「上下文跟着载体线程漂移」的错乱。实践建议优先升级到与新 JDK 匹配的 SkyWalking 探针版本9.x 后续版本对虚拟线程做了适配不要混用旧探针。即便使用虚拟线程仍坚持 capture continueContext 的显式快照方式把快照作为数据显式传入任务闭包而不是依赖 ThreadLocal 隐式传递。避免在虚拟线程里复用同一个 ContextSnapshot 对象给多个并发任务快照是一次性的必须各自捕获。9. 完整验证确认异步链路已修复修复后务必验证别凭感觉。三步走**第一步日志比对**。在主线程和 worker 线程里分别打印 TraceContext.traceId()确认两者一致log.info(main traceId{}, TraceContext.traceId());executor.submit(RunnableWrapper.of(() - {log.info(worker traceId{}, TraceContext.traceId()); // 应与上面相同}));**第二步UI 确认**。在 SkyWalking 的 Trace 详情里异步任务产生的 Span 应该作为主链路的一个子 Span 出现而不是单独成一条新 Trace。如果还看到「孤儿链路」说明仍有某个异步边界没修复。**第三步压测验证**。用压测工具打一轮流量观察调用线程池的 Span 是否稳定挂在主链路下、traceId 是否一致避免低并发时碰巧同线程没真正跨线程掩盖了问题。低并发下任务可能复用同一线程上下文「看似连续」其实没经过跨线程只有压测才能暴露真实断点。10. 与日志框架 MDC 联动把 traceId 带进每一行日志链路修好了但排障时你还是会翻日志。如果日志里没有 traceId你拿着 SkyWalking 里的 traceId 去日志系统 grep 不到东西链路和日志就「两张皮」。SkyWalking 工具包提供了 %tid 占位符让主流日志框架Logback / Log4j2自动把当前 traceId 打印到每一行日志appender nameCONSOLE classch.qos.logback.core.ConsoleAppenderencoderpattern%d{yyyy-MM-dd HH:mm:ss} [%thread] [%tid] %-5level %logger - %m%n/pattern/encoder/appender%tid 由 SkyWalking 的日志插件在运行时填充当请求处于链路中时它输出真实 traceId如 TID:7c8e1a2b...不在链路中如本地调试时输出 TID: N/A不会报错。加上它之后你在 SkyWalking 里看到一条慢 Trace复制 traceId 到 Elasticsearch / Loki 里一搜就能拿到这次请求在所有线程、所有服务里的完整日志异步分支也不再是盲区。如果使用的日志框架不支持 %tid也可以手动把 traceId 放进 MDCimport org.slf4j.MDC;import org.apache.skywalking.apm.toolkit.trace.TraceContext;public void withLog() {MDC.put(traceId, TraceContext.traceId());try {doWork();} finally {MDC.remove(traceId);}}11. 常见反模式清单异步链路修复里以下反模式最常见遇到断链先对照排查反模式后果正确做法---------异步里手动 createSpan 却不续接父上下文生成孤儿链路用 continueContext 续接多个线程复用同一个 ContextSnapshot上下文串号、链路错乱每个线程独立 capture忘记写 stopSpanContextNotFound 异常try/finally 保证配对用 ThreadLocal 传业务字段跨线程下游取不到值为空用 correlation / 显式传参只在低并发自测同线程假象掩盖断链压测验证跨线程Async 不包 Wrapper异步方法内链路断自定义执行器统一包裹把这六条记在心里配合第 9 节的验证手段异步链路基本能做到「零断点」。12. 一行命令快速自检修复上线后用几条命令做快速自检确认探针确实生效、链路确实连续避免「以为好了其实没好」jcmd pid VM.command_line | grep javaagentcurl http://oap:12800/graphql -d {query:query { services { name } }}第一行确认目标 Java 进程确实挂载了 -javaagent很多「没数据」的工单根因就是探针根本没挂上第二行直接问 OAP「你都收到了哪些服务」如果你的服务名出现在返回里说明上报链路是通的。再配合第 9 节的日志比对主线程与 worker 线程 traceId 一致三步即可确认异步链路修复成功。最后给一个上线检查清单照着勾探针 -javaagent 路径正确且与 OAP 大版本匹配所需插件webflux / httpclient / feign 等已放入 agent/plugins异步提交处统一用 RunnableWrapper / CallableWrapper 或装饰器包裹CompletableFuture 每个跨线程阶段都 capture continueContext stopSpan日志框架接入 %tidtraceId 可搜压测验证跨线程链路不断。把这六步做成上线卡点异步断链问题基本能从源头消失。总结异步本身没错错的是上下文没跟着线程走。掌握 ContextSnapshot 与 Wrapper 两种武器再牢记「一次跨线程、一次快照」你就能让大模型应用的每一条异步分支都严丝合缝地挂回主链路。下一篇我们谈高并发下如何通过采样降低开销与成本。

相关新闻

【信息科学与工程学】计算机科学与自动化——第一百五十五篇 缓存系统设计01 基础知识

【信息科学与工程学】计算机科学与自动化——第一百五十五篇 缓存系统设计01 基础知识

硬件层面 CPU:拥有 L1/L2/L3 多级缓存,用于暂存即将执行的指令和频繁访问的数据。 GPU:包含显存、纹理缓存、常量缓存、指令缓存等,加速图形渲染和通用计算。 RAID 卡 / 存储控制器:自带缓存(通常带电池保护),用于合并写操作、加速读写。 打印机:内置内存缓存,用于…

2026/8/18 20:20:00 阅读更多 →
提示词工程实战指南:从基础结构到工作流构建

提示词工程实战指南:从基础结构到工作流构建

1. 先搞清楚提示词工程到底解决什么问题 如果你刚开始接触大语言模型,比如 ChatGPT、Claude 或者国内的文心一言、通义千问,最常遇到的困惑可能就是:为什么同一个模型,别人问出来的答案又准又好,自己问出来的却总是跑偏…

2026/8/18 20:18:59 阅读更多 →
AI驱动仿真:COMSOL与Python结合加速电机NVH设计与优化

AI驱动仿真:COMSOL与Python结合加速电机NVH设计与优化

在实际电机设计与NVH(Noise, Vibration, Harshness,噪声、振动与声振粗糙度)性能优化领域,工程师们长期面临一个核心矛盾:传统的仿真分析流程虽然精确,但往往耗时漫长、迭代成本高昂。一个典型的永磁同步电…

2026/8/18 20:18:59 阅读更多 →

最新新闻

6G原生推理智能体通信:从信念交换到分布式协同的未来网络范式

6G原生推理智能体通信:从信念交换到分布式协同的未来网络范式

1. 从“连接”到“认知”:为什么6G需要“原生推理”的智能体通信?如果你和我一样,在通信网络领域摸爬滚打了十几年,从3G的“能上网”到4G的“快上网”,再到5G的“万物互联”,我们似乎一直在沿着一条清晰的路…

2026/8/18 20:59:34 阅读更多 →
GLM-5.3本地部署与编程能力实战测试指南

GLM-5.3本地部署与编程能力实战测试指南

智谱AI最近发布了GLM-5.3,一个定位为“最强开源编程模型”的新版本。根据官方信息,它在编程相关的任务上,相比前代GLM-5.2有高达50%的性能提升。对于开发者、技术团队和任何需要将AI编程能力集成到本地环境或私有化部署的人来说,这…

2026/8/18 20:59:34 阅读更多 →
企业级灰度发布技术方案

企业级灰度发布技术方案

企业级灰度发布技术方案 1. 方案目标 灰度发布不是简单地把新版本部署到几台服务器,而是让新版本先接收一小部分真实或仿真的请求,在可控范围内观察系统和业务指标,确认稳定后再逐步扩大流量。 对于国际支付系统,发布治理需要同…

2026/8/18 20:59:34 阅读更多 →
城市新能源汽车推广实战:从目标拆解到生态构建的完整路径

城市新能源汽车推广实战:从目标拆解到生态构建的完整路径

1. 项目背景与目标拆解“连云港市2018年推广应用新能源汽车2000辆”,这个标题看起来像是一份政府工作报告里的年度目标,或者某个专项工作的考核指标。对于不熟悉这个领域的朋友来说,可能觉得这就是一个冷冰冰的数字。但作为一个在新能源汽车推…

2026/8/18 20:59:34 阅读更多 →
哈希表优化两数之和算法:从暴力解法到高效实现

哈希表优化两数之和算法:从暴力解法到高效实现

1. 项目背景与题目解析 这道名为"两数之和"的CTF题目来自QSNCTF赛事,属于典型的算法类挑战题。这类题目在各大CTF比赛中非常常见,主要考察选手对基础算法的掌握程度和代码实现能力。题目要求看似简单:给定一个整数数组和一个目标值…

2026/8/18 20:59:34 阅读更多 →
BetterJoy完整上手攻略:让Switch手柄在PC上从“插上没反应“到“玩出花样“

BetterJoy完整上手攻略:让Switch手柄在PC上从“插上没反应“到“玩出花样“

BetterJoy完整上手攻略:让Switch手柄在PC上从"插上没反应"到"玩出花样" 【免费下载链接】BetterJoy Allows the Nintendo Switch Pro Controller, Joycons and SNES controller to be used with CEMU, Citra, Dolphin, Yuzu and as generic XIn…

2026/8/18 20:58:33 阅读更多 →

日新闻

告别逐帧截图:用 extract-video-ppt 快速提取视频中的 PPT 并一键导出 PDF

告别逐帧截图:用 extract-video-ppt 快速提取视频中的 PPT 并一键导出 PDF

告别逐帧截图:用 extract-video-ppt 快速提取视频中的 PPT 并一键导出 PDF 【免费下载链接】extract-video-ppt extract the ppt in the video 项目地址: https://gitcode.com/gh_mirrors/ex/extract-video-ppt 如果你还停留在"看网课 不停暂停 截图 …

2026/8/18 0:00:57 阅读更多 →
思源宋体TTF一站式上手:7个字重免费商用,从下载到上线的完整走查

思源宋体TTF一站式上手:7个字重免费商用,从下载到上线的完整走查

思源宋体TTF一站式上手:7个字重免费商用,从下载到上线的完整走查 【免费下载链接】source-han-serif-ttf Source Han Serif TTF 项目地址: https://gitcode.com/gh_mirrors/so/source-han-serif-ttf 你是不是也经历过这种时刻:设计稿里…

2026/8/18 0:00:58 阅读更多 →
华硕笔记本控制权回收指南:GHelper 如何用一个 10MB 文件替代 Armoury Crate

华硕笔记本控制权回收指南:GHelper 如何用一个 10MB 文件替代 Armoury Crate

华硕笔记本控制权回收指南:GHelper 如何用一个 10MB 文件替代 Armoury Crate 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, …

2026/8/18 0:00:59 阅读更多 →

周新闻

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

基于阿里云与通义千问(Qwen)构建AI应用:从模型调用到生产部署的完整实践指南

如果你是一名开发者,最近可能已经感受到了AI大模型正在从“玩具”变成“生产力工具”的强烈信号。从代码补全到智能Agent,从本地部署到云端API,我们正处在一个技术栈快速重构的节点。然而,面对层出不穷的模型、框架和工具&#xf…

2026/8/18 9:15:35 阅读更多 →
工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

工业通信系统底层逻辑:04 反射——高频能量撞墙之后会发生什么?

第四篇:反射——高频能量撞墙之后会发生什么? —— 你以为信号已经过去了,其实它正在回来打你 老Q的现场笔记 第五季,我们正式进入工业神经系统层。这里不再是单个设备的战斗,而是整个工厂“经脉”层面的秩序之战。从这一篇开始,你将第一次看清:看似简单的信号传播,背…

2026/8/18 9:06:28 阅读更多 →
【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

【文章复现】非线性值迭代自适应动态规划(ADP):离散时间非线性系统的策略迭代自适应动态规划算法研究附Matlab代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。🍎 往期回顾关注个人主页:Matlab科研工作室👇 关注我领取海量matlab电子书和…

2026/8/18 9:04:56 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/17 18:54:37 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/17 18:55:16 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/17 18:55:55 阅读更多 →