堆栈分析
教 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;可折叠但勿在未读业务帧前据此改库代码。
-
包装异常:外层
RuntimeException或aggregateError可能掩盖内层;必须展开Caused by、errors[]、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 名 + 版本 ]
│ │
└────────┬────────┘
▼
[ 提出假设:输入、并发、配置、依赖版本;列出验证步骤与日志点 ]
帧行提取器(演示)
以下为本地演示用的轻量解析器:用正则从几类常见栈文本中抽出「路径 / 类 + 行号」,便于 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[]