堆栈分析

教 Agent 从栈底到栈顶识别第一帧业务代码、包装异常与异步边界;处理混淆、内联与 source map 缺失场景。

SKILL 应说明:根因常在最早抛出点或 Caused by 链最内层,而非日志里最后一行框架代码。要求展开 suppressed 与 aggregate 异常。

对 JS、JVM、Go、.NET 等栈格式分别给出字段含义;生产环境需配合 commit SHA、构建号与 source map 才能映射到源码行。

异步与线程池会打乱栈:须结合 trace_id、线程名或 reactive 上下文还原调用链;Agent 勿仅凭栈顶就改 unrelated 模块。

  • 先确认栈是否来自当前版本部署,避免分析过期二进制。
  • 区分 OOM 等无栈或截断栈,转向系统日志与指标。
  • 输出应包含:可疑帧、需打开的源文件、建议加的日志点。

栈帧:顺序、因果与噪音

栈顶通常是最后进入的调用(或异步回调入口),向下追溯才接近最初触发点;JVM / .NET 的打印顺序与 JS 控制台可能相反,SKILL 里要写清「哪一端代表更深调用」。

异步代码调用栈差异——Promise chain vs async/await:

// ===== Promise chain 的栈(噪音多,难追因果)=====
Error: Payment failed
    at chargeCard (/app/src/payment.js:45:13)      // ← 实际抛出点
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    // 注意:Promise chain 丢失了 handleCheckout 的栈帧
    // 因为 .then() 是异步调度的,调用者已出栈

// ===== async/await 的栈(V8 async stack traces)=====
Error: Payment failed
    at chargeCard (/app/src/payment.js:45:13)      // ← 实际抛出点
    at async processPayment (/app/src/payment.js:28:5)
    at async handleCheckout (/app/src/checkout.js:15:3)
    at async Router.post (/app/src/routes.js:42:5)
    // async/await 保留完整调用链(Node.js --async-context-frame-depth)

// 堆栈噪音过滤策略(框架层过滤)
function isBusinessFrame(frame) {
  // 过滤 Node.js internals
  if (frame.includes('node:internal') || frame.includes('node_modules')) {
    // 例外:保留业务相关的 node_modules(如 ORM)
    return frame.includes('@company/') || frame.includes('sequelize');
  }
  // 保留业务代码目录
  return frame.includes('/src/') || frame.includes('/app/');
}

// Sentry 等工具的 inAppInclude 配置等价逻辑
const businessFrames = stackTrace.split('\n')
  .filter(line => line.trim().startsWith('at '))
  .filter(isBusinessFrame);
  • 因果帧:包名 / 路径落在业务仓库、与变更或 feature flag 相关的帧;优先对照版本与分支。
  • 噪音帧:反射、AOP、测试框架、Promise 微任务、webpack / vite runtime;可折叠但勿在未读业务帧前据此改库代码。
  • 包装异常:外层 RuntimeExceptionaggregateError 可能掩盖内层;必须展开 Caused byerrors[]suppressed

常见异常包装解包方法(Java getCause / Python __cause__):

// ===== Java:递归展开 Caused by 链 =====
// 原始日志中的异常链:
// java.lang.RuntimeException: Payment processing failed
//     at com.acme.PaymentService.process(PaymentService.java:89)
// Caused by: java.sql.SQLTimeoutException: Query timed out after 30000ms
//     at com.zaxxer.hikari.pool.ProxyStatement.executeQuery(ProxyStatement.java:113)
// Caused by: java.net.SocketTimeoutException: Read timed out
//     at java.net.SocketInputStream.read(SocketInputStream.java:189)
//
// 根因在最内层:SocketTimeoutException(数据库连接超时)

// 编程方式展开 cause 链
public static Throwable getRootCause(Throwable throwable) {
    Throwable cause = throwable.getCause();
    while (cause != null && cause.getCause() != null) {
        cause = cause.getCause();
    }
    return cause != null ? cause : throwable;
}
// 分析:根因是网络超时,不是"支付逻辑"问题

// ===== Python:__cause__ 与 __context__ =====
# Python 的异常链
try:
    result = db.query("SELECT ...")
except DatabaseError as e:
    raise PaymentError("Payment failed") from e  # 显式链:__cause__

# 展开 cause 链
def get_root_cause(exc):
    while exc.__cause__ is not None:
        exc = exc.__cause__
    return exc

# 打印完整 cause 链
import traceback
traceback.print_exc()  # 自动展示 "The above exception was the direct cause of..."

# ===== JavaScript:AggregateError 解包(Promise.any / Promise.allSettled)=====
try {
  await Promise.any([fetchA(), fetchB(), fetchC()]);
} catch (err) {
  if (err instanceof AggregateError) {
    // 展开所有失败原因
    err.errors.forEach((e, i) => {
      console.error(`Promise ${i} failed:`, e.message, e.stack);
    });
  }
}

Source map 与生产符号化

打包、压缩、tree-shaking 后,栈上多为 .min.js 或 chunk 哈希路径;没有 sourceMappingURL 或未上传 map 时,行号只对构建产物有效,不能直接对应 TypeScript / Kotlin 源文件。

Node.js 未捕获异常和 Promise rejection 的正确处理:

// Node.js 全局异常处理(在 app 入口注册)
// 捕获同步未处理异常
process.on('uncaughtException', (err, origin) => {
  // origin: 'uncaughtException' | 'unhandledRejection'
  logger.error('Uncaught exception', {
    error: {
      type: err.constructor.name,
      message: err.message,
      stack: err.stack,
    },
    origin,
    // 生产环境添加 trace context(若已初始化)
    trace_id: getCurrentTraceId(),
  });
  // 给进程 graceful shutdown 时间(例如关闭 DB 连接)
  gracefulShutdown().finally(() => process.exit(1));
});

// 捕获未处理的 Promise rejection
process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled promise rejection', {
    reason: reason instanceof Error ? {
      type: reason.constructor.name,
      message: reason.message,
      stack: reason.stack,
    } : reason,
    // 注意:promise 对象本身不要序列化(循环引用)
  });
  // 建议:在 Node.js 15+ 默认行为是 crash,与 uncaughtException 保持一致
});

// 更好的方案:使用 async_hooks 追踪异步上下文
import { AsyncLocalStorage } from 'async_hooks';
const requestContext = new AsyncLocalStorage();

// 在请求处理中设置上下文,确保异步任务能访问 trace_id
app.use((req, res, next) => {
  requestContext.run({ traceId: req.headers['x-trace-id'] }, next);
});
  • 在 SKILL 中要求:报错里附带 release / commit / build id,与 CI 产物或 Sentry 等平台的「符号文件」绑定。
  • 前端:区分「浏览器展示的映射后栈」与「服务端日志里的原始栈」;二者可能不一致。
  • 原生与后端:dSYM、ProGuard mapping、Go 无内联符号表等各自流程需在项目文档中写明,避免 Agent 假设总有 TS 行号。

分析主流程(skill-flow-block)

  [ 收集:完整栈 + Caused by / aggregate + 部署版本 / commit ]
                    │
                    ▼
         [ 识别格式:V8 / JVM / Go / .NET,确认栈增长方向 ]
                    │
                    ▼
    [ 剥离噪音:runtime、框架胶水、测试夹具、min 路径是否可映射 ]
                    │
                    ▼
         [ 锁定因果:第一帧业务代码 + 最内层根因异常 ]
                    │
           ┌────────┴────────┐
           ▼                 ▼
   [ 有 source map / 符号 ]   [ 无映射:仅产物行号 ]
           │                 │
           ▼                 ▼
  [ 映射到源文件:行:列 ]     [ 用构建产物或反查 chunk 名 + 版本 ]
           │                 │
           └────────┬────────┘
                    ▼
    [ 提出假设:输入、并发、配置、依赖版本;列出验证步骤与日志点 ]
Agent 输出应显式写出「当前栈指向的是产物还是源码」,避免把 .min.js 行号当成可直接修改的 .ts 行号。

帧行提取器(演示)

以下为本地演示用的轻量解析器:用正则从几类常见栈文本中抽出「路径 / 类 + 行号」,便于 SKILL 描述「Agent 可先结构化再推理」。非完整语言解析器。


              

支持常见 at … (file:line:col)at file:line:col(Foo.java:line) 形式;未匹配行会跳过。

小结

堆栈分析的关键是方向、因果链与版本映射:先确认栈序与运行时,再落到业务帧与最内层异常,最后用 source map / 符号表把行号还原到可编辑源码;演示区的提取器仅辅助结构化,不能替代平台符号化流水线。

---
name: stack-trace-analysis
description: 解读异常栈与 Caused by
model: claude-sonnet-4-5
---

# 步骤
steps:
  1. 收集完整栈(含 Caused by/aggregate/suppressed)+ 部署版本
  2. 识别栈格式(V8/JVM/Go/.NET)与增长方向
  3. 剥离噪音帧(runtime/框架/测试夹具/min路径)
  4. 展开包装异常(getCause/errors[]/suppressed递归)
  5. 锁定第一帧业务代码与最内层根因异常
  6. 确认 source map / 符号可用性
  7. 映射到源文件行号
  8. 提出可验证假设与日志点建议

# 异步栈注意事项
async_stack_notes:
  - async/await 保留完整调用链(优于 Promise chain)
  - Promise chain 可能丢失中间帧
  - Node.js: --async-context-frame-depth 可增加追踪深度
  - 线程池/Worker: 需结合 trace_id 关联上下文

# 栈帧分类规则
frame_classification:
  business: 包含 /src/ /app/ @company/ 路径的帧
  noise: node:internal/ node_modules/ .min.js webpack-runtime
  exception: 展开 Caused by / __cause__ / AggregateError.errors[]

返回技能库 更多技能入口