前言
这几年好久没写过博客了,最近沉迷AI coding, 工作之余,给大家分享一下我最近AI coding的成果,下面直接上干货
生产环境一崩,告警炸成雪花片------同一个 NPE 因为不同的参数、不同的 SQL 字面量,在告警群里被刷了几百遍。研发疲于奔命,真正要紧的那 1% 反而被淹没。
这是我开源的一个项目 StackWatch :AI 驱动的 Java 生产错误根因分析系统。它做了一件反直觉的事------虽然接入了 LLM,但只有约 1% 的异常真正调用大模型,其余 99% 在缓存层就免费归并掉了。
🔗 项目地址:https://github.com/AllenMuu/stackwatch (MIT 协议,欢迎 Star 🌟)
🔗 Gitee项目地址:https://gitee.com/AllenMoving/stackwatch
一、先说痛点:生产异常排查的"三宗罪"
做过线上保障的 Java 同学应该都经历过:
- 告警洪水:同一个根因,因为不同的入参字面量、不同的用户 ID,产生几百条语义相同、堆栈字面不同的异常。告警群瞬间被刷屏。
- 根因定位慢 :看到一个
NullPointerException,你得进日志、翻链路、看上下文,半个点过去了才知道是哪个对象为 null。MTTI(平均定位耗时)居高不下。 - 高频问题后知后觉:某个新上线的 bug 已经在线上炸了三天,直到有人手动统计才发现"原来这个异常这周出现了 800 次"------发现周期是"天"级,而不是"小时"级。
业界用 LLM 做 RCA(Root Cause Analysis)已经不是新鲜事,但直接把每条异常都丢给大模型有两个致命问题:
- 贵:生产异常量大,token 账单扛不住。
- 乱:异常 message 经常携带完整 SQL、响应体,甚至接了 SkyWalking/ARMS 后单条 trace 日志能到数万字符,上下文窗口直接爆满,幻觉风险飙升。
StackWatch 的解法就一句话:用"成本阶梯"把 LLM 调用压到约 1%,并在 LLM 前面加两道防线压住上下文。
二、项目简介
AI 驱动的 Java 生产错误根因分析 ------ 异常堆栈指纹归并 + LLM 根因定位 + 定时周报聚合。
把生产异常堆栈自动投喂 LLM 进行根因定位与分类归并,结合定时任务按周聚合高频错误并推送飞书周报。
两个量化目标:
- 生产故障平均定位耗时缩短约 40%
- 高频问题发现周期由 天级 缩短至 小时级
技术栈很新,全是 2025/2026 的版本:
| 领域 | 选型 |
|---|---|
| 框架 | Spring Boot 4.1 + Java 21 |
| LLM | Spring AI 2.0(OpenAI 兼容协议,DashScope/DeepSeek 可切换) |
| L1 缓存 | Caffeine(生产可换 Redis) |
| L2 向量 | PgVector(默认关闭,渐进式解锁) |
| 消息队列 | Kafka(采集层第三入口,默认关闭) |
| 弹性 | Resilience4j |
| 度量 | Micrometer + Prometheus |
三、架构总览:五层主链路 + 两层横切
#mermaid-svg-lcps82nB5oKYsPIl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lcps82nB5oKYsPIl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lcps82nB5oKYsPIl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lcps82nB5oKYsPIl .error-icon{fill:#552222;}#mermaid-svg-lcps82nB5oKYsPIl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lcps82nB5oKYsPIl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lcps82nB5oKYsPIl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lcps82nB5oKYsPIl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lcps82nB5oKYsPIl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lcps82nB5oKYsPIl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lcps82nB5oKYsPIl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lcps82nB5oKYsPIl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lcps82nB5oKYsPIl .marker.cross{stroke:#333333;}#mermaid-svg-lcps82nB5oKYsPIl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lcps82nB5oKYsPIl p{margin:0;}#mermaid-svg-lcps82nB5oKYsPIl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lcps82nB5oKYsPIl .cluster-label text{fill:#333;}#mermaid-svg-lcps82nB5oKYsPIl .cluster-label span{color:#333;}#mermaid-svg-lcps82nB5oKYsPIl .cluster-label span p{background-color:transparent;}#mermaid-svg-lcps82nB5oKYsPIl .label text,#mermaid-svg-lcps82nB5oKYsPIl span{fill:#333;color:#333;}#mermaid-svg-lcps82nB5oKYsPIl .node rect,#mermaid-svg-lcps82nB5oKYsPIl .node circle,#mermaid-svg-lcps82nB5oKYsPIl .node ellipse,#mermaid-svg-lcps82nB5oKYsPIl .node polygon,#mermaid-svg-lcps82nB5oKYsPIl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lcps82nB5oKYsPIl .rough-node .label text,#mermaid-svg-lcps82nB5oKYsPIl .node .label text,#mermaid-svg-lcps82nB5oKYsPIl .image-shape .label,#mermaid-svg-lcps82nB5oKYsPIl .icon-shape .label{text-anchor:middle;}#mermaid-svg-lcps82nB5oKYsPIl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lcps82nB5oKYsPIl .rough-node .label,#mermaid-svg-lcps82nB5oKYsPIl .node .label,#mermaid-svg-lcps82nB5oKYsPIl .image-shape .label,#mermaid-svg-lcps82nB5oKYsPIl .icon-shape .label{text-align:center;}#mermaid-svg-lcps82nB5oKYsPIl .node.clickable{cursor:pointer;}#mermaid-svg-lcps82nB5oKYsPIl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lcps82nB5oKYsPIl .arrowheadPath{fill:#333333;}#mermaid-svg-lcps82nB5oKYsPIl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lcps82nB5oKYsPIl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lcps82nB5oKYsPIl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lcps82nB5oKYsPIl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lcps82nB5oKYsPIl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lcps82nB5oKYsPIl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lcps82nB5oKYsPIl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lcps82nB5oKYsPIl .cluster text{fill:#333;}#mermaid-svg-lcps82nB5oKYsPIl .cluster span{color:#333;}#mermaid-svg-lcps82nB5oKYsPIl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lcps82nB5oKYsPIl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lcps82nB5oKYsPIl rect.text{fill:none;stroke-width:0;}#mermaid-svg-lcps82nB5oKYsPIl .icon-shape,#mermaid-svg-lcps82nB5oKYsPIl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lcps82nB5oKYsPIl .icon-shape p,#mermaid-svg-lcps82nB5oKYsPIl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lcps82nB5oKYsPIl .icon-shape .label rect,#mermaid-svg-lcps82nB5oKYsPIl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lcps82nB5oKYsPIl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lcps82nB5oKYsPIl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lcps82nB5oKYsPIl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 采集层 Collector
Logback / HTTP / Kafka
② 预处理层
指纹去重
③ 分析层 Analyzer
L1 / L2 / L3 三层级联
④ 聚合层
激增检测 + 按周聚合
⑤ 投递层
飞书告警 + 周报
横切A 度量层
耗时 · 准确率 · token 成本
横切B 反馈层
研发反馈飞轮
一条数据从左到右流过五层:
- 采集层 :三个入口------Logback Appender(无侵入接日志)/ HTTP
/collect(标准上报)/ Kafka(默认关,做异步削峰)。 - 预处理层:生成指纹(SHA-256 + 框架帧过滤 + 版本化),相同指纹天然去重。
- 分析层 :系统中枢,L1→L2→L3 三层级联,下文重点讲。
- 聚合层:实时激增检测 + 按周 Top-N 聚合。
- 投递层:飞书实时告警 + 飞书周报。
两个横切层不参与主链路,但决定项目能不能上生产:
- 横切 A 度量层 :Micrometer 打点,
path=cache_hit|vector_merged|llm_new这个 tag 是后面讲"95%/4%/1% 成本故事"的数据来源。 - 横切 B 反馈层:研发纠正结果回流,注入下一次 prompt,构成自学习闭环。
四、核心设计:三层级联归并(成本阶梯)
这是整个项目的灵魂。分析层 ErrorAnalyzer.analyze 是架构枢纽,绝大多数异常在 L1/L2 免费归并,仅约 1% 真正调用 LLM。
| 层级 | 机制 | token 成本 |
|---|---|---|
| L1 | 指纹精确命中缓存(Caffeine) | ≈ 0 |
| L2 | 向量近似归并(PgVector) | ≈ 0 |
| L3 | LLM 簇代表根因分析 | 真正调 LLM(约 1%) |
核心流程示意(伪代码,展示思路而非源码):
java
RootCauseAnalysis analyze(ErrorEvent event) {
String fingerprint = fingerprinter.generate(event); // 预处理:指纹
String hash = sha256(fingerprint);
// L1:指纹精确命中,零成本复用历史根因
if (cache.has(hash)) {
metrics.record(path = "cache_hit");
return cache.get(hash);
}
// L2:向量近似归并(embedding == null 时跳过,即 L2 关闭的信号)
if (event.embedding != null) {
var hit = clusterRepo.findSimilar(event.embedding, threshold = 0.92);
if (hit.isPresent()) {
metrics.record(path = "vector_merged");
cache.backfill(hash, hit.rootCause); // 反向填充 L1,下文细讲
return hit.rootCause;
}
}
// L3:新簇,调用 LLM(约 1% 走到这里)
var rootCause = callLlm(event);
metrics.record(path = "llm_new");
return rootCause;
}
为什么这样分层?
关键洞察是:对 RCA 来说,同一个根因会以大量"字面不同但语义相同"的堆栈实例反复出现。
比如同一个 NPE:
orderService.process里userId为 nullorderService.process里orderId为 nullorderService.process里tradeNo为 null
这三条堆栈在指纹层面可能不完全一致(行号、上下文变量不同),但 embedding 相似度极高。L2 用向量把它们归到同一个簇,复用第一次 L3 算出来的根因------后面两条零 token。
这就是"成本阶梯":第一次出现的异常付 LLM 的钱,之后所有语义近似的异常都搭便车。
五、两个让人眼前一亮的巧思
1. 语义缓存 + 反向填充 L1
L2 不只是"近似归并",本质是语义缓存 :新错误 embedding 与已有簇相似度 ≥ 0.92 时,直接返回该簇的根因,零 LLM 调用。
更妙的是命中后的处理:结果反向填充 L1。
新异常 → L1 未命中 → L2 命中(相似度 0.92+)→ 返回根因
↓
把 (指纹hash → 根因) 回填进 L1
↓
后续相同指纹 → L1 直接命中,连向量检索都省了
缓存梯度自洽:L2 命中越多,L1 命中率越高,整体越快越省。 用得越久越省钱,这是一个有"飞轮效应"的缓存设计。
2. LLM 前置两道防线,压住上下文窗口
直接喂异常给 LLM 会爆上下文------生产异常 message 可能携带完整 SQL / 响应体,接了 SkyWalking/ARMS 后 queryTraceContext 单条 trace 日志能到数万字符。
ContextOptimizer 在 Prompt 入参(exceptionMessage、mdc)和每个 @Tool 返回值进入 LLM 之前 做截断。阈值走 stackwatch.context-optimizer.* 配置。没有这道闸,上下文窗口易爆满、幻觉风险上升。
这道防线和 L1/L2 缓存是互补的:缓存解决"调几次",上下文优化解决"单次调多贵"。
六、不止省钱:三档复核门控 + 反馈飞轮
省钱只是基础,StackWatch 在 L3 路径上还套了一层三档复核门控(借鉴阿里云 PagePilot 的置信度分档思想):
| 置信度 / 信号 | 复核级别 | 动作 |
|---|---|---|
| ≥ 高阈值(0.9)且有证据 | AUTO_CONFIRMED |
自动归因,不打扰研发 |
| 兜底阈值(0.6)与高阈值之间且有证据 | NEEDS_CONFIRMATION |
输出根因,标记待确认(低介入) |
| < 兜底阈值 / 无证据 / LLM 失败 | NEEDS_HUMAN_REVIEW |
转人工排查 |
LLM 不是黑盒拍板,而是按置信度 + 证据双判分流------高置信度自动归因省人力,低置信度老实转人工,不拿不靠谱的结论糊弄研发。
正负双向反馈飞轮
门控之外,还有一条正负双向飞轮让系统越用越准:
研发通过 POST /feedback 确认或纠正根因:
- 正样本(few-shot,「该这么答」):研发确认了根因 → 作为示范注入下次 prompt。
- 负样本(anti-pattern,「别这么答」) :研发携带
wrongRootCause纠正 → 作为反例注入下次 prompt。
两者都注入下一次 L3 prompt------正样本引导、负样本警示。这是 PagePilot known-failures 思想在负样本侧的落地,与 few-shot 正样本侧对偶,构成完整的自学习闭环。
一句话:用得越多,知道"该怎么答"也知道"不该怎么答",准确率持续上升。
七、零基础设施启动:能跑起来再说
这点对想尝鲜的同学很友好------项目默认不依赖任何外部中间件就能启动:无 DB、无 Kafka、无向量库。
靠两个机制协同实现:
application.yml里的spring.autoconfigure.exclude列表(DataSourceAutoConfiguration/PgVectorStoreAutoConfiguration/KafkaAutoConfiguration)------总开关。- 每个 Repository / Channel 通过
@ConditionalOnProperty按需选择实现。
ClusterRepository 有两个互斥实现,由 stackwatch.l2.enabled 切换:
InMemoryClusterRepository(默认,matchIfMissing=true):findSimilar永远返回空,强制走 L3。PgVectorClusterRepository(havingValue=true):真正的向量归并。
想解锁 L2 或 Kafka,三处同步改(缺一即启动失败,Javadoc 里有明确说明):
pom.xml取消对应 starter 注释;- 从
spring.autoconfigure.exclude移除对应 AutoConfiguration; - 配
spring.datasource/spring.kafka.bootstrap-servers,置stackwatch.l2.enabled/stackwatch.collector.kafka.enabled = true。
这种"渐进式解锁"的设计哲学是:先让你 5 分钟跑通看到效果,再按需上重型组件。
八、指纹算法:为什么能稳定归并
Fingerprinter 的输入是 exceptionType + 归一化后的 top-N 应用帧。
关键在于框架帧过滤 :把 java. / javax. / org.springframework. / io.netty. 这类框架内部帧过滤掉。原因很实在------框架内部帧会随依赖版本抖动,而业务帧稳定。用业务帧算指纹,升级个 Spring 版本不会让历史归并关系断裂。
当应用帧不足 2 个时,回退到 top-N 原始帧,保证极端情况也有指纹。
另外指纹是版本化的 (FingerprintVersion)------算法升级后,老指纹的归并关系不会被破坏。buildEmbeddingText 喂给 L2 做 embedding,渲染策略由 EmbeddingRendering 记录(借鉴 PostHog 的做法,可追溯"这个 embedding 是怎么生成的")。
九、快速上手
bash
# 1. 配置 LLM API Key(走环境变量,禁止写入代码)
export DASHSCOPE_API_KEY=sk-...
# 2. 构建(需 JDK 21+,Spring Boot 4.1 + Spring AI 2.0 强制要求)
mvn clean package
# 3. 运行
mvn spring-boot:run
# 4. 试用:输入异常堆栈,获取 LLM 根因
curl -X POST http://localhost:8080/analyze \
-H "Content-Type: application/json" \
-d '{
"appName":"order-service",
"exceptionType":"NullPointerException",
"exceptionMessage":"Cannot invoke method on null",
"stackTrace":[
"com.foo.OrderService.process(OrderService.java:42)",
"com.foo.OrderController.handle(OrderController.java:17)"
]
}'
JDK 21 是硬要求(Spring Boot 4.1 + Spring AI 2.0 不支持 Java 8/11/17)。本地用 jenv 管理版本即可,无需手动 export JAVA_HOME。
十、当前状态 & 适合谁用
MVP 阶段,五层主链路 + 两层横切均已实现:
| 层 | 状态 |
|---|---|
| domain 数据结构(不可变 record) | ✅ |
| ② 预处理层 - 指纹生成(SHA-256 + 框架帧过滤 + 版本化) | ✅ |
| ③ 分析层 - L1 缓存 + L2 向量归并 + L3 LLM 根因(结构化输出 + Function Calling) | ✅ |
| 上下文优化 - ContextOptimizer | ✅ |
| ① 采集层 - Logback Appender + HTTP + Kafka | ✅ |
| ④ ⑤ 聚合层 / 投递层 - 实时激增检测 + 飞书周报 | ✅ |
| 横切 A/B - Micrometer 度量 + 正负双向反馈飞轮 | ✅ |
适合这几类同学关注:
- Java 后端 / SRE:被生产告警洪水折磨,想用 AI 提效但又怕 token 账单爆炸。
- AI 应用落地实践者:想看一个"LLM 不是无脑调用,而是按成本阶梯编排"的真实工程案例。
- Spring AI 学习者 :项目用了很新的 Spring AI 2.0 + Spring Boot 4.1 + JDK 21,结构化输出(
.entity())、Function Calling(.tools())、ChatClient 链式调用都有实战用法。 - 想做类似系统的同学:指纹算法、语义缓存反向填充、三档复核门控、正负反馈飞轮,这些设计模式都可以直接借鉴到自己的项目里。
十一、设计借鉴与致谢
项目站在巨人的肩膀上,借鉴了几个优秀开源项目的设计思想:
- PostHog error_tracking:指纹算法版本化、embedding rendering 元数据、周报结构
- Arvo-AI/aurora:RCA 后动作自动化、知识库积累思想
- salesforce/PyRCA:RCA 评估方法论
- PagePilot(支付宝商家中心测试团队):置信度分档门控 + known-failures 失败模式自学习库;本项目的 L3 三档复核 + 正负双向反馈飞轮即借鉴于此
写在最后
做这个项目,我最想表达的一个观点是:
在工程系统里接入 LLM,"调用大模型"应该是最后一步,而不是第一步。
先用指纹精确去重(L1),再用向量语义归并(L2),把重复的、相似的异常都挡在 LLM 之外;只有真正新出现的、需要推理的,才走到 L3。再用上下文优化压住单次成本,用三档门控保证结论可信,用反馈飞轮让系统越用越准。
这样一套下来,LLM 的"贵"和"乱"两个痛点都被结构性地解决了,而不是靠堆 prompt 技巧。
如果你也在做 AI 落地、或者被生产异常排查困扰,欢迎来看看、提 issue、提 PR。
🔗 GitHub:https://github.com/AllenMuu/stackwatch
📐 详细设计文档、🧭 选型分析、🚀 演进路线都在 repo 的
docs/目录下。如果对你有启发,给个 ⭐ Star 是对我最大的鼓励。
MIT 协议开源,欢迎交流。