第一章:Go语言大模型服务可观测性体系概述
在构建面向生产环境的Go语言大模型服务(如LLM推理API、微调任务调度器或RAG服务网关)时,传统日志+指标+链路的“老三样”已不足以应对高并发、长生命周期、非确定性响应(如流式token生成)、GPU资源耦合等特有挑战。可观测性在此场景下,不再仅是问题排查手段,而是服务稳定性、成本治理与用户体验优化的核心基础设施。
核心观测维度重构
- 语义化追踪:需捕获Prompt输入哈希、模型版本、采样参数(temperature/top_p)、输出token数及首token延迟(TTFT),而非仅HTTP状态码;
- 资源感知指标:除CPU/Mem外,必须采集CUDA显存占用、GPU利用率、KV Cache命中率、批处理队列深度;
- 上下文感知日志:结构化日志中嵌入trace_id、request_id、model_name,并标记是否为重试请求或流式响应分片。
Go生态关键工具链
| 组件类型 | 推荐方案 | 适配说明 |
|---|---|---|
| 分布式追踪 | OpenTelemetry SDK + Jaeger exporter | 使用otelhttp.NewHandler自动注入LLM请求上下文,支持Span属性动态添加模型元数据 |
| 指标采集 | Prometheus + promauto.With(reg) |
自定义Gauge记录实时并发请求数,Counter统计各模型调用频次 |
| 日志结构化 | zerolog + With().Str("prompt_hash", hash).Int("output_tokens", n) |
避免JSON序列化开销,直接写入结构化字段 |
快速集成示例
// 初始化OTel Tracer(需在main入口调用)
func initTracer() {
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://jaeger:14268/api/traces")))
if err != nil {
log.Fatal(err)
}
tp := tracesdk.NewTracerProvider(
tracesdk.WithBatcher(exp),
tracesdk.WithResource(resource.NewSchemaless(
semconv.ServiceNameKey.String("llm-gateway"),
semconv.ServiceVersionKey.String("v1.2.0"),
)),
)
otel.SetTracerProvider(tp)
}
// 在HTTP Handler中注入模型上下文
func llmHandler(w http.ResponseWriter, r *http.Request) {
ctx, span := otel.Tracer("llm").Start(r.Context(), "inference")
defer span.End()
// 动态注入模型相关属性(实际业务中从配置或路由解析)
span.SetAttributes(
attribute.String("llm.model", "qwen2-7b"),
attribute.Int("llm.input_tokens", 512),
attribute.Int("llm.output_tokens", 256),
)
}
第二章:Metrics采集与建模:从基础指标到LLM性能画像
2.1 Go原生pprof与OpenTelemetry Metrics集成实践
Go 的 net/http/pprof 提供运行时性能剖析能力,而 OpenTelemetry Metrics 聚焦标准化指标采集。二者互补而非替代——pprof 擅长低开销、高密度的运行时诊断(如 goroutine 数、heap profile),OTel 则统一暴露 Prometheus 兼容的计量指标(如请求计数、延迟直方图)。
数据同步机制
需桥接两套系统:利用 runtime.ReadMemStats 等 API 主动拉取 pprof 关键统计量,并通过 OTel Meter 记录为 Int64Gauge:
// 将 pprof 内存统计同步为 OTel 指标
memGauge := meter.NewInt64Gauge("go.memstats.alloc.bytes",
metric.WithDescription("Bytes allocated and not yet freed"))
go func() {
var m runtime.MemStats
for range time.Tick(5 * time.Second) {
runtime.ReadMemStats(&m)
memGauge.Record(ctx, int64(m.Alloc))
}
}()
逻辑分析:
runtime.ReadMemStats是零分配快照读,避免 GC 干扰;5s采样间隔平衡精度与开销;Int64Gauge适配瞬时值语义,Alloc字段反映当前堆活跃字节数。
集成对比维度
| 维度 | pprof 原生支持 | OTel Metrics 集成点 |
|---|---|---|
| 数据类型 | Profile(CPU/heap/goroutine) | Metric(Counter/Gauge/Histogram) |
| 传输协议 | HTTP /debug/pprof/* |
OTLP/gRPC 或 Prometheus Exporter |
| 采集触发 | 手动抓取或持续采样 | 定时轮询 + 事件钩子(如 HTTP middleware) |
graph TD
A[Go Runtime] -->|runtime.ReadMemStats| B[pprof Stats]
B --> C[自定义采集器]
C --> D[OTel Meter.Record]
D --> E[OTLP Exporter]
E --> F[Prometheus / Tempo / Grafana]
2.2 LLM服务核心指标建模:token吞吐、首字延迟、KV缓存命中率
LLM推理服务的性能瓶颈常隐匿于三个正交但强耦合的维度:token吞吐(tokens/s) 衡量持续处理能力,首字延迟(Time to First Token, TTFT) 反映用户感知响应速度,KV缓存命中率 则直接决定计算与内存效率。
指标定义与联动关系
- TTFT 主要受prefill阶段计算开销与调度排队影响;
- 吞吐受限于decode阶段的batch size、序列长度及GPU显存带宽;
- KV缓存命中率下降10%,常导致decode延迟上升35%+(实测A10G@batch=8)。
实时采集示例(Prometheus Exporter)
# metrics_collector.py
from prometheus_client import Gauge
ttft_gauge = Gauge('llm_ttft_ms', 'Time to first token (ms)')
throughput_gauge = Gauge('llm_throughput_tps', 'Token throughput (tokens/sec)')
kv_hit_ratio = Gauge('llm_kv_cache_hit_ratio', 'KV cache hit ratio (0.0–1.0)')
# 注:需在generate()入口打点,ttft记录首次yield时间戳差;
# throughput按decode循环内累计token数/耗时;kv_hit_ratio由kvcache.stats()实时上报。
核心指标健康阈值参考
| 指标 | 健康区间 | 风险信号 |
|---|---|---|
| TTFT | > 1200 ms(提示prefill过载或冷启未预热) | |
| 吞吐 | ≥ 120 tps | |
| KV命中率 | ≥ 0.92 |
graph TD
A[请求到达] --> B{Prefill阶段}
B -->|计算KV Cache| C[首字输出 → TTFT]
B --> D[Cache写入]
C --> E[Decode循环]
E -->|查KV Cache| F{Hit?}
F -->|Yes| G[Fast attention]
F -->|No| H[Recompute KV]
G & H --> I[生成下一token]
I --> E
2.3 动态标签注入与多租户维度切分(model/version/tenant)
在模型服务网关层,请求上下文需实时注入 model、version、tenant 三重标签,支撑路由、限流与计费隔离。
标签注入时机
- 请求进入 API 网关时解析
X-Model-ID、X-Version、X-Tenant-ID头; - 缺失任一标签则拒绝(HTTP 400),避免下游维度污染。
路由匹配逻辑(Go 伪代码)
func buildRoutingKey(ctx context.Context) string {
model := getHeader(ctx, "X-Model-ID") // 如 "fraud-detect"
version := getHeader(ctx, "X-Version") // 如 "v2.1.0"
tenant := getHeader(ctx, "X-Tenant-ID") // 如 "acme-prod"
return fmt.Sprintf("%s:%s:%s", model, version, tenant)
}
该键用于查表路由至对应服务实例;
:分隔确保字符串唯一性与可解析性,避免租户间命名冲突。
多租户切分策略对比
| 维度 | 共享粒度 | 隔离强度 | 存储成本 |
|---|---|---|---|
| model | 跨租户 | 弱 | 低 |
| model+version | 按发布周期 | 中 | 中 |
| model+version+tenant | 完全独占 | 强 | 高 |
graph TD
A[HTTP Request] --> B{Has all 3 headers?}
B -->|Yes| C[Inject labels → context]
B -->|No| D[Reject 400]
C --> E[Route to tenant-scoped endpoint]
2.4 Prometheus+Grafana构建LLM SLO看板:P99生成延迟与错误率告警联动
数据同步机制
Prometheus 通过 http_sd_configs 动态拉取 LLM 推理服务的 /metrics 端点,注入 OpenTelemetry SDK 上报的 llm_request_duration_seconds 和 llm_request_errors_total 指标。
告警规则定义
# alert_rules.yml
- alert: LLMHighP99Latency
expr: histogram_quantile(0.99, sum(rate(llm_request_duration_seconds_bucket[1h])) by (le, model))
> 3.5 # 单位:秒
for: 5m
labels:
severity: critical
annotations:
summary: "P99 latency > 3.5s for {{ $labels.model }}"
该规则基于直方图桶(_bucket)计算 P99 延迟,rate(...[1h]) 抵消瞬时抖动,for: 5m 避免毛刺误触发。
联动告警逻辑
graph TD
A[Prometheus Alert] --> B{Error Rate > 1%?}
B -->|Yes| C[Grafana Dashboard高亮异常模型]
B -->|No| D[仅延迟告警]
关键指标映射表
| Prometheus 指标 | 含义 | SLO 关联 |
|---|---|---|
llm_request_duration_seconds{quantile="0.99"} |
模型级 P99 延迟 | 可用性 & 性能 |
llm_request_errors_total{code=~"5..|429"} |
服务端错误/限流 | 错误率 ≤ 0.5% |
2.5 指标采样策略优化:高频低开销采样 vs 关键路径全量埋点
在高吞吐服务中,盲目全量埋点会导致可观测性开销飙升。需按语义分层设计采样策略:
- 高频低开销采样:适用于基础指标(如 HTTP 状态码、响应延迟 P90),采用动态概率采样(如
sample_rate = min(1.0, 1000 / qps)) - 关键路径全量埋点:限定于支付确认、库存扣减等核心链路,通过注解或 AOP 自动注入
采样配置示例
# metrics-sampling.yaml
sampling_rules:
- endpoint: "/api/v1/order/submit"
mode: "full" # 全量采集
- endpoint: "/api/v1/health"
mode: "probabilistic"
rate: 0.01 # 1% 固定采样
逻辑说明:
rate: 0.01表示每 100 次请求保留 1 条原始指标,降低存储与传输压力;mode: "full"触发无损 trace 上下文透传。
策略效果对比
| 维度 | 高频低开销采样 | 关键路径全量埋点 |
|---|---|---|
| 数据完整性 | 有损 | 无损 |
| CPU 增益 | ≤ 0.3% | ≤ 1.2% |
graph TD
A[请求入口] --> B{是否命中关键路径?}
B -->|是| C[启用全量埋点 + traceID 注入]
B -->|否| D[执行动态采样决策]
D --> E[按 QPS 调整采样率]
第三章:分布式Trace链路追踪:穿透LLM推理全流程
3.1 OpenTelemetry Go SDK深度定制:自动注入prompt hash与response status
为增强LLM可观测性,需在Span生命周期中自动注入语义化属性。核心在于扩展SpanProcessor与SpanStartEvent钩子。
自定义SpanProcessor注入逻辑
type PromptAwareProcessor struct {
delegate sdktrace.SpanProcessor
}
func (p *PromptAwareProcessor) OnStart(ctx context.Context, span sdktrace.ReadWriteSpan) {
prompt := span.SpanContext().TraceID().String() // 实际应从context提取prompt
hash := fmt.Sprintf("%x", md5.Sum([]byte(prompt[:min(len(prompt), 256)])))
span.SetAttributes(attribute.String("llm.prompt.hash", hash))
// 响应状态暂设为待定,由后续Finish事件更新
span.SetAttributes(attribute.String("llm.response.status", "pending"))
}
该处理器在Span创建时计算prompt前256字节的MD5哈希,并标记初始响应状态。min()防越界,attribute.String确保类型安全。
属性注入时机对比
| 阶段 | 可访问数据 | 是否支持动态更新 |
|---|---|---|
OnStart |
prompt(需透传) | 否(只读span) |
OnEnd |
response、error | 是(通过SetAttributes) |
状态流转示意
graph TD
A[OnStart] -->|注入prompt.hash<br>status=“pending”| B[LLM调用]
B --> C{响应返回}
C -->|成功| D[OnEnd: status=“success”]
C -->|失败| E[OnEnd: status=“error”]
3.2 跨组件Trace透传:从HTTP网关→Router→Adapter→LLM Runtime→VectorDB
为保障全链路可观测性,各组件需透传唯一 trace_id 与 span_id,遵循 W3C Trace Context 规范。
HTTP头传播机制
服务间通过 traceparent(必需)与 tracestate(可选)传递上下文:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
逻辑分析:
00表示版本;4bf92f3577b34da6a3ce929d0e0e4736是全局 trace_id;00f067aa0ba902b7是当前 span_id;末尾01标识采样标志。所有中间件须解析并注入新 span。
组件间透传责任分工
| 组件 | 职责 |
|---|---|
| HTTP网关 | 从请求头提取/生成初始 traceparent |
| Router | 透传 header,不修改 span_id |
| Adapter | 创建子 span,关联父 span_id |
| LLM Runtime | 注入模型调用 span,标注 prompt tokens |
| VectorDB | 记录检索 span,携带 query_id 关联 |
graph TD
A[HTTP网关] -->|traceparent| B[Router]
B -->|traceparent + new span_id| C[Adapter]
C -->|child_of| D[LLM Runtime]
D -->|child_of| E[VectorDB]
3.3 Trace语义约定扩展:LLM-specific Span Attributes(input_tokens, output_tokens, stop_reason)
为精准刻画大语言模型调用行为,OpenTelemetry语义约定在llm.*命名空间下新增三项关键Span属性:
llm.input_tokens: 输入提示(prompt)经tokenizer后的token总数llm.output_tokens: 模型实际生成的token数量(不含padding或特殊控制符)llm.stop_reason: 终止原因枚举值(如"stop","length","tool_calls")
# 示例:向Span注入LLM专属属性
span.set_attribute("llm.input_tokens", 127)
span.set_attribute("llm.output_tokens", 42)
span.set_attribute("llm.stop_reason", "length")
该代码显式标注了本次推理的输入/输出规模及截断原因,使可观测性系统可区分“自然结束”与“max_tokens触发截断”,支撑准确的延迟-吞吐量归因分析。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
llm.input_tokens |
int | 是 | prompt编码后长度,含system/user/assistant角色标记 |
llm.output_tokens |
int | 是 | 仅统计模型generated_tokens,不含<eos>等后缀 |
llm.stop_reason |
string | 否 | 标准化枚举,支持监控告警策略配置 |
graph TD
A[LLM API调用] --> B[Tokenizer统计input_tokens]
A --> C[Streaming响应流中累加output_tokens]
C --> D{生成终止?}
D -->|max_tokens| E[set stop_reason = “length”]
D -->|EOS token| F[set stop_reason = “stop”]
第四章:Prompt级结构化日志:可审计、可回溯、可分析的LLM交互记录
4.1 Prompt模板渲染日志:保留变量绑定上下文与安全脱敏机制
在生产级大模型服务中,Prompt模板渲染需同时满足可追溯性与隐私合规性。
日志结构设计原则
- 保留原始变量名与作用域(如
user.profile.name@session_abc123) - 自动识别并脱敏敏感字段(PII、token、ID等)
- 记录渲染前/后快照及绑定时间戳
安全脱敏策略对照表
| 字段类型 | 脱敏方式 | 示例输入 | 日志输出 |
|---|---|---|---|
| 手机号 | 掩码替换 | 13812345678 |
138****5678 |
| API Key | 全量哈希截断 | sk-abc123... |
sk-...f8a9 |
| 邮箱 | 局部保留+域掩码 | alice@xx.com |
a***e@xx.com |
def render_with_context(template: str, context: dict) -> tuple[str, dict]:
# 使用 jinja2.SafeLoader + 自定义过滤器链
env = Environment(autoescape=True) # 防XSS基础防护
env.filters['sensitive'] = lambda v: mask_pii(v) # 注入脱敏过滤器
tmpl = env.from_string(template)
rendered = tmpl.render(**context)
return rendered, {
"bound_vars": list(context.keys()),
"render_time": time.time(),
"template_hash": hashlib.sha256(template.encode()).hexdigest()[:8]
}
该函数返回渲染结果与元数据上下文。mask_pii() 内部基于正则+词典双路识别,支持动态白名单配置;template_hash 用于快速定位模板变更影响面。
graph TD
A[原始模板+上下文] --> B{变量绑定解析}
B --> C[敏感字段检测]
C --> D[脱敏规则匹配]
D --> E[生成带上下文日志]
E --> F[写入审计流水表]
4.2 响应质量元数据日志:logprobs采样、repetition_penalty生效值、temperature实际应用值
响应质量元数据日志是模型推理可观察性的核心维度,精准记录生成过程中的关键调控参数真实取值。
logprobs采样机制
当启用 logprobs=True 时,API 返回每个 token 的 top-k 对数概率(如 logprobs: {"tokens": ["a", "the"], "token_logprobs": [-0.23, -1.87]}),用于置信度分析与错误归因。
参数实际生效值校验
以下代码片段演示如何从响应中提取并验证动态参数:
response = client.chat.completions.create(
model="qwen2.5-7b",
messages=[{"role": "user", "content": "Hello"}],
temperature=0.7,
repetition_penalty=1.1,
logprobs=True,
top_logprobs=5
)
# 实际生效值可能受服务端策略调整(如温度钳制)
actual_temp = response.usage.temperature_actual # 示例字段
actual_rep_penalty = response.usage.repetition_penalty_actual
逻辑说明:
temperature_actual和repetition_penalty_actual是服务端最终应用的浮点值,可能因安全策略微调(如将temperature=0.0自动提升至0.01防止退化)。
元数据一致性校验表
| 字段 | 语义 | 是否必现 | 示例值 |
|---|---|---|---|
logprobs.token_logprobs |
当前 token 的对数概率 | 条件返回 | [-0.15, -2.31] |
usage.temperature_actual |
温度实际执行值 | 是 | 0.698 |
usage.repetition_penalty_actual |
重复惩罚实际值 | 是 | 1.102 |
graph TD
A[请求参数] --> B{服务端校验}
B -->|截断/平滑| C[temperature_actual]
B -->|动态增强| D[repetition_penalty_actual]
C & D --> E[logprobs注入]
E --> F[结构化日志输出]
4.3 异步批处理日志管道:基于Zap+Loki+Promtail的高吞吐prompt日志流
为应对大模型服务中高频、短时爆发的 prompt 日志(含上下文、token数、响应延迟等),我们构建了零阻塞异步日志管道。
核心组件协同机制
- Zap(结构化日志)以
AddSync+BufferedWriteSyncer实现内存缓冲写入; - Promtail 通过
pipeline_stages提取prompt_id和latency_ms字段; - Loki 接收压缩后的
snappy编码日志流,按{app="llm-api", env="prod"}标签索引。
日志采样与批处理策略
| 场景 | 采样率 | 批大小 | 触发条件 |
|---|---|---|---|
| 正常推理请求 | 100% | 512 | 内存缓冲满或 200ms |
| Debug级详细日志 | 1% | 64 | level == "debug" |
# promtail-config.yaml 片段:提取 prompt 元数据
pipeline_stages:
- json:
expressions:
prompt_id: "meta.prompt_id"
latency_ms: "meta.latency_ms"
- labels:
prompt_id: ""
该配置将 JSON 日志中的
meta.prompt_id提升为 Loki 标签,支持毫秒级logql聚合查询(如rate({app="llm-api"} | json | __error__ == "" [5m]))。
graph TD
A[Zap Logger] -->|Async Buffered Write| B[Local Log File]
B --> C[Promtail Tail]
C -->|HTTP POST /loki/api/v1/push| D[Loki Distributor]
D --> E[Ingester → Chunk Storage]
4.4 Prompt版本溯源与A/B测试日志标记:关联git commit + experiment_id + variant
为保障Prompt迭代可追溯、实验结果可归因,需在日志中注入三重标识:代码版本(git commit)、实验上下文(experiment_id)和分支变体(variant)。
日志结构设计
import subprocess
def get_git_commit():
return subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"]
).decode().strip()
# 示例日志字段注入
log_entry = {
"prompt_id": "summarize_v2",
"git_commit": get_git_commit(), # 当前构建的commit短哈希
"experiment_id": "exp-2024-05-11-b", # 实验唯一ID(由调度系统生成)
"variant": "control" # 取值:control / treatment_a / treatment_b
}
该函数确保每次服务启动时捕获精确代码快照;experiment_id由实验平台统一分发,避免本地生成冲突;variant由AB分流中间件注入,保证请求级一致性。
标识关联性保障机制
| 维度 | 来源 | 不可变性保障 |
|---|---|---|
git_commit |
构建时执行git rev-parse |
构建镜像固化,不可运行时篡改 |
experiment_id |
API网关/实验SDK注入 | 全链路透传,拒绝空值或默认值 |
variant |
分流规则引擎实时计算 | 与用户ID+seed强绑定,可复现 |
数据同步机制
graph TD
A[LLM服务] -->|注入三元标识| B[结构化日志]
B --> C[Fluentd采集]
C --> D[ClickHouse表: ab_logs]
D --> E[BI看板按 experiment_id + variant 聚合]
第五章:可观测性体系演进与工程落地总结
从日志单点采集到全链路信号融合
某金融核心支付系统在2021年仍依赖ELK栈做日志聚合,告警平均响应时长达47分钟。2023年完成可观测性升级后,通过OpenTelemetry统一采集指标(Prometheus)、链路(Jaeger)、日志(Loki)三类信号,并在Grafana中构建「交易健康度看板」——实时展示成功率、P99延迟、DB连接池饱和度、Kafka积压量四维热力图。一次线上退款超时故障中,工程师5分钟内定位到是下游风控服务gRPC拦截器引入了未设超时的HTTP调用,该问题在旧体系中需跨3个独立平台人工关联分析。
自动化根因推荐引擎实践
我们基于PyTorch构建轻量级RCA模型,输入为过去15分钟内异常指标(如http_server_requests_seconds_count{status=~"5..", uri="/pay"}突增200%)及关联Span标签(service.name="payment-gateway"、error.type="TimeoutException")。模型输出Top3根因概率及证据路径,例如: |
排名 | 根因假设 | 置信度 | 关键证据 |
|---|---|---|---|---|
| 1 | Redis连接池耗尽 | 89% | redis_connection_pool_used{pool="payment-cache"}=98% |
|
| 2 | TLS握手失败率上升 | 63% | go_tls_handshake_errors_total{job="ingress"}+120% |
基于SLO的告警降噪机制
将传统阈值告警重构为SLO偏差检测:对/order/create接口定义季度错误预算(Error Budget)为0.1%,当当前周期错误率连续5分钟超过0.1% × (1 - 已消耗预算比例)时触发告警。2024年Q2该策略使无效告警下降76%,同时首次实现「预算耗尽预警」——当剩余预算低于5%时自动创建Jira工单并通知架构委员会。
混沌工程验证可观测性完备性
在生产环境定期执行Chaos实验:随机kill Envoy sidecar进程。可观测性平台必须在2分钟内自动识别出受影响服务(通过Service Graph节点突变)、定位异常Span(span.kind=client且status.code=UNAVAILABLE)、关联到具体Pod(通过k8s.pod.uid标签)。2024年共执行17次实验,发现3处埋点盲区:Istio mTLS证书过期未上报、gRPC流式响应未记录stream_status标签、数据库连接池关闭事件未触发connection_closed事件。
# otel-collector-config.yaml 片段:动态采样策略
processors:
probabilistic_sampler:
hash_seed: 42
sampling_percentage: 100 # 全量采样高危服务
override:
- service_name: "payment-gateway"
sampling_percentage: 100
- service_name: "risk-service"
sampling_percentage: 30
开发者自助诊断能力下沉
在GitLab MR界面集成可观测性插件:开发者提交代码后,插件自动解析@Trace注解方法,生成该MR影响范围内的监控视图链接(如/dashboard/db/payment-gateway-trace?var-service=risk-service&from=now-1h&to=now),并高亮对比MR合并前后P95延迟变化曲线。上线半年内,一线开发人员自主解决性能回归问题占比达68%。
成本与效能的持续平衡
通过eBPF技术替代部分应用层埋点,在支付网关集群节省23%CPU资源;采用WAL压缩算法将Loki日志存储成本降低41%;但保留关键业务字段(如trace_id、user_id、amount)的全文索引以保障审计合规性。当前全链路数据保留策略为:指标(90天)、链路(30天)、日志(180天),所有数据均通过Kyverno策略强制加密落盘。
graph LR
A[应用埋点] --> B[OTLP Exporter]
B --> C{Collector路由}
C --> D[Metrics→Prometheus]
C --> E[Traces→Jaeger]
C --> F[Logs→Loki]
D --> G[Grafana告警引擎]
E --> H[Jaeger UI + RCA模型]
F --> I[Loki日志搜索]
G --> J[PagerDuty/企微机器人]
H --> J
I --> J 