Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,14 @@
- 路径:`skill_output/troubleshoot/<YYYYMMDD_HHMMSS>/troubleshoot_report_<timestamp>.md`
- 主报告包含各维度总结 + 可视化图表(sparkline/柱状图/时间线等)
- 详情拆分到 `details/` 子目录:
- `details/health_events.md` — Worker 逐分钟健康事件
- `details/trace_<ID>.md` — 请求追踪事件链
- `detail/health_events.md` — Worker 逐分钟健康事件 + 健康诊断
- `detail/load_select_release.md` — 负载诊断 + select/release 明细
- `detail/load_diagnoses.md` — load 诊断列表
- `detail/load_counter_state.md` — request/token counter 末状态
- `detail/latency_diagnoses.md` — 延迟诊断详情
- `detail/cache_diagnosis.md` — cache 六维诊断详情(session 粘性/非最优/驱逐/Fallback/冷启动/交叉诊断)
- `detail/cache_session_stickiness.md` / `detail/cache_suboptimal.md` / `detail/cache_eviction.md` / `detail/cache_fallback.md` / `detail/cache_cross.md` — cache 分职责拆分明细
- `detail/trace_<ID>.md` — 请求追踪事件链

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ def analyze_cache(log_file, tail=None, eviction_duration_mins=30, hit_ratio_weig
"cold_starts": cold_starts,
"hitratio_stats": hitratio_stats,
"tokenizer_degraded_count": tokenizer_degraded_count,
"cross_diagnosis": _analyze_cross_diagnosis(
session_stickiness=session_stickiness,
hitratio_stats=hitratio_stats,
strategy_dist=strategy_dist,
eviction_impact=eviction_impact,
),
"diagnoses": diagnoses,
"summary": f"{total} 策略决策, cache_aware {cache_aware_count}, fallback {fallback_count}, "
f"冷启动 {cold_starts}",
Expand Down Expand Up @@ -339,6 +345,45 @@ def _diagnose(
return diagnoses


def _analyze_cross_diagnosis(session_stickiness, hitratio_stats, strategy_dist, eviction_impact):
"""交叉诊断:基于粘性/命中率/fallback/驱逐给出简表。"""
if not session_stickiness:
return []
avg_stickiness = sum(v["stickiness_pct"] for v in session_stickiness.values()) / max(len(session_stickiness), 1)
mean_hr = hitratio_stats.get("mean", 0)
fallback_pct = 0
for s in strategy_dist:
if s.get("value") == "process_tokens":
fallback_pct = s.get("pct", 0)
break
evicted_cnt = sum(1 for e in eviction_impact if e.get("evicted"))

diagnosis = "运行良好"
action = "-"
if avg_stickiness >= 70 and mean_hr >= 40 and fallback_pct < 10:
diagnosis = "运行良好"
elif avg_stickiness >= 70 and mean_hr < 20 and evicted_cnt > 0:
diagnosis = "疑似驱逐导致命中率低"
action = "考虑增大 eviction-duration-mins"
elif avg_stickiness < 40 and fallback_pct >= 20:
diagnosis = "低粘性 + 高 fallback"
action = "检查负载阈值与 cache-aware 参数"
elif avg_stickiness < 40 and mean_hr < 20:
diagnosis = "低粘性 + 低命中"
action = "检查缓存预热与 prompt 稳定性"

return [
{
"avg_stickiness_pct": round(avg_stickiness, 1),
"mean_hitRatio_pct": round(mean_hr, 1),
"fallback_pct": round(fallback_pct, 1),
"evicted_after_timeout": evicted_cnt,
"diagnosis": diagnosis,
"action": action,
}
]


# ════════════════════════════════════════════════════════════════
# 报告格式化
# ════════════════════════════════════════════════════════════════
Expand All @@ -349,13 +394,18 @@ def format_cache_report(result):
sections = ["## Cache 调度诊断", ""]
sections.append(f' {result["summary"]}')
sections.append("")
detail_sections = ["# Cache 调度详情", "", f'总结: {result["summary"]}', ""]

if result["diagnoses"]:
sections.append("### 诊断")
sections.append("")
for d in result["diagnoses"]:
sections.append(f' [{d["severity"]}] [{d["source_layer"]}] {d["message"]}')
sections.append(" 诊断见详情: [detail/cache_diagnosis.md](detail/cache_diagnosis.md)")
sections.append("")
detail_sections.append("## 诊断")
detail_sections.append("")
for d in result["diagnoses"]:
detail_sections.append(f'[{d["severity"]}] [{d["source_layer"]}] {d["message"]}')
detail_sections.append("")

# 策略分布
if result["strategy_dist"]:
Expand All @@ -364,6 +414,10 @@ def format_cache_report(result):
bar_data = [{"label": s["value"], "value": s["pct"], "count": s["count"]} for s in result["strategy_dist"]]
sections.append(render_bar(bar_data, show_count=True))
sections.append("")
detail_sections.append("## 策略分布")
detail_sections.append("")
detail_sections.append(render_bar(bar_data, show_count=True))
detail_sections.append("")

# hitRatio 统计
hs = result.get("hitratio_stats", {})
Expand All @@ -383,6 +437,10 @@ def format_cache_report(result):
bar_data = [{"label": f["value"], "value": f["pct"], "count": f["count"]} for f in result["fallback_reasons"]]
sections.append(render_bar(bar_data, show_count=True))
sections.append("")
detail_sections.append("## Fallback 原因分布")
detail_sections.append("")
detail_sections.append(render_bar(bar_data, show_count=True))
detail_sections.append("")

# Tokenizer 退化
if result.get("tokenizer_degraded_count", 0) > 0:
Expand All @@ -394,6 +452,8 @@ def format_cache_report(result):
if stickiness:
sections.append("### Session 粘性")
sections.append("")
sections.append(" Session 粘性详情见: [detail/cache_diagnosis.md](detail/cache_diagnosis.md)")
sections.append("")
table_data = [
{
"Session": sid[:16],
Expand All @@ -403,40 +463,99 @@ def format_cache_report(result):
}
for sid, s in sorted(stickiness.items(), key=lambda x: x[1]["stickiness_pct"])
]
sections.append(
detail_sections.append("## Session 粘性")
detail_sections.append("")
detail_sections.append(
render_table(
table_data[:10],
table_data,
columns=["Session", "请求数", "粘性率", "切换次数"],
right_align={"请求数", "粘性率", "切换次数"},
)
)
sections.append("")
detail_sections.append("")

# 非最优选择
if result.get("suboptimal_selections"):
subs = result["suboptimal_selections"]
sections.append(f"### 非最优选择 ({len(subs)} 次)")
sections.append("")
sections.append(" 详情见: [detail/cache_diagnosis.md](detail/cache_diagnosis.md)")
sections.append("")
reason_counts = defaultdict(int)
for s in subs:
reason_counts[s["reason"]] += 1
for reason, count in sorted(reason_counts.items(), key=lambda x: -x[1]):
sections.append(f" {reason}: {count} 次")
sections.append("")
detail_sections.append("## 非最优选择(Top 20)")
detail_sections.append("")
for s in subs[:20]:
detail_sections.append(
f'- [{s.get("ts","")}] selected={s.get("selected","")}({s.get("selected_hr",0)}), best={s.get("best_hr_worker","")}({s.get("best_hr",0)}), reason={s.get("reason","")}'
)
detail_sections.append("")

# 驱逐影响
if result.get("eviction_impact"):
evictions = result["eviction_impact"]
evicted = [e for e in evictions if e["evicted"]]
sections.append(f"### 驱逐影响 ({len(evictions)} 次超时, {len(evicted)} 次缓存失效)")
sections.append("")
sections.append(" 详情见: [detail/cache_diagnosis.md](detail/cache_diagnosis.md)")
sections.append("")
detail_sections.append("## 驱逐影响")
detail_sections.append("")
for e in evictions[:50]:
detail_sections.append(
f'- session={e.get("session_id","")[:24]} interval={e.get("interval_mins",0)}m hitRatio_after={e.get("hitRatio_after",0)} evicted={e.get("evicted",False)}'
)
detail_sections.append("")

# 冷启动
if result.get("cold_starts", 0) > 0:
sections.append(f' 冷启动: {result["cold_starts"]} 次(hitRatios=map[])')
sections.append("")
detail_sections.append("## 冷启动识别")
detail_sections.append("")
detail_sections.append(f'- 冷启动次数: {result["cold_starts"]}')
detail_sections.append("")

if result.get("cross_diagnosis"):
sections.append("### 交叉诊断")
sections.append("")
sections.append(" 详情见: [detail/cache_diagnosis.md](detail/cache_diagnosis.md)")
sections.append("")
detail_sections.append("## 交叉诊断")
detail_sections.append("")
detail_sections.append(
render_table(
result["cross_diagnosis"],
columns=["avg_stickiness_pct", "mean_hitRatio_pct", "fallback_pct", "evicted_after_timeout", "diagnosis", "action"],
right_align={"avg_stickiness_pct", "mean_hitRatio_pct", "fallback_pct", "evicted_after_timeout"},
)
)
detail_sections.append("")

if any(
[
result.get("session_stickiness"),
result.get("suboptimal_selections"),
result.get("eviction_impact"),
result.get("cross_diagnosis"),
result.get("diagnoses"),
]
):
sections.append(
"> 详细诊断: [detail/cache_diagnosis.md](detail/cache_diagnosis.md) | "
"[detail/cache_session_stickiness.md](detail/cache_session_stickiness.md) | "
"[detail/cache_suboptimal.md](detail/cache_suboptimal.md) | "
"[detail/cache_eviction.md](detail/cache_eviction.md) | "
"[detail/cache_fallback.md](detail/cache_fallback.md) | "
"[detail/cache_cross.md](detail/cache_cross.md)"
)
sections.append("")

return "\n".join(sections)
return "\n".join(sections), "\n".join(detail_sections)


# ════════════════════════════════════════════════════════════════
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@
("GetRemoteMetrics failed", "FD 后端"),
]

IMPACT_RULES = [
("Failed to select", "请求可能返回 502/503"),
("Failed to connect to backend", "后端不可达,请求失败"),
("Panic recovered", "Router 代码异常,可能影响稳定性"),
("scanner error", "流式响应中断"),
("copy error", "非流式响应中断"),
("Failed to read YAML file config/register.yaml", "可选配置未加载(若未启用可忽略)"),
]

# scanner error / copy error 特殊处理:context canceled → 客户端,其他 → FD 后端
SCANNER_COPY_PATTERNS = ("scanner error", "copy error")

Expand All @@ -76,6 +85,13 @@ def classify_source_layer(template, original=""):
return "未知"


def classify_impact(template):
for pattern, impact in IMPACT_RULES:
if pattern in template:
return impact
return "-"


# ════════════════════════════════════════════════════════════════
# 主分析函数
# ════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -183,7 +199,9 @@ def _compute_error_top_n(records, top_n):
"count": g["count"],
"pct": round(g["count"] / total * 100, 1) if total else 0,
"source_layer": source_layer,
"impact": classify_impact(g["template"]),
"level": g["level"],
"urls": _extract_urls(g["originals"]),
"sample_originals": g["originals"],
}
)
Expand All @@ -193,6 +211,16 @@ def _compute_error_top_n(records, top_n):
return result


def _extract_urls(originals):
import re

urls = set()
for line in originals:
for m in re.findall(r"https?://[A-Za-z0-9_.:-]+", line):
urls.add(m)
return sorted(urls)


def _grep_lines(log_file, pattern, tail=None):
"""用 grep 从日志文件提取匹配行。"""
try:
Expand Down Expand Up @@ -241,6 +269,9 @@ def format_errors_report(result):
f'请求总数: {result["total_requests"]} | '
f'错误率: {result["error_rate"]}%'
)
sections.append(" 指标口径: ERROR/WARN=日志级别计数;请求总数=HTTP 请求行数;错误率=非200请求数/请求总数×100%。")
if result["error_rate"] == 0 and (result["total_errors"] > 0 or result["total_warns"] > 0):
sections.append(" ℹ 错误率为 0.0% 仅表示 HTTP 状态码均为 200;并不代表没有 ERROR/WARN 日志。")
sections.append("")

# Panic
Expand Down Expand Up @@ -277,10 +308,16 @@ def format_errors_report(result):
"占比": f'{e["pct"]}%',
"级别": e["level"],
"来源层": e["source_layer"],
"影响": e.get("impact", "-"),
"URLs": ",".join(e.get("urls", [])[:2]) if e.get("urls") else "-",
}
)
sections.append(
render_table(table_data, columns=["模板", "数量", "占比", "级别", "来源层"], right_align={"数量", "占比"})
render_table(
table_data,
columns=["模板", "数量", "占比", "级别", "来源层", "影响", "URLs"],
right_align={"数量", "占比"},
)
)
sections.append("")
yaml_missing_count = sum(
Expand Down
Loading
Loading