ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

langchain4j-RAG企业真实项目实战- 可观测性

langchain4j-RAG企业真实项目实战- 可观测性 LangChain4j 实战系列番外篇。正篇三章把入库、检索生成两条主线讲完了这篇聊一个平时容易被忽视、但在 AI 应用上格外值钱的东西可观测性。前端 bug 看得见摸得着AI 应用的问题是全凭感觉——“感觉最近回答变蠢了”这句话你没法拿去排查。这一篇就讲我怎么把感觉变成数字。项目地址:github地址为什么 AI 应用尤其需要埋点传统后端服务的可观测性三板斧日志、指标、链路基本是标配。但 RAG 这种系统我把它的特殊性总结成三条不确定性同样的输入模型的回答可能每次都不同。出了问题你没法复现只能靠统计意义上的指标漂移来判断。链路长、依赖多一次问答要过大模型意图、ES两路检索、可能还有 Neo4j、SQL、本地重排模型、再生成。哪个环节变慢变坏用户体感都是回答慢了/差了但根因完全不同。按 token 烧钱调用量、限流、降级这些成本事件不埋点你压根不知道自己每天在哪个环节花了多少钱、谁在刷你的接口。所以这个项目从我写第一版起指标埋点就是和业务代码一起写的不是后补的。用到的东西很克制spring-boot-starter-actuatormicrometer-registry-prometheus resilience4j 的 micrometer 绑定没有自研任何采集组件。五个自定义业务指标一个类搞定全部业务埋点收敛在RagMetrics一个类里注入 Micrometer 的MeterRegistry五个方法五个指标。我逐个说它长什么样、在哪埋、以及它到底能替你看见什么。ComponentpublicclassRagMetrics{privatefinalMeterRegistryregistry;publicvoidrecordRetrieval(Stringsource,Durationlatency,inthits){Timer.builder(rag.retrieval).tag(source,source)// es / neo4j / sql.register(registry).record(latency);Counter.builder(rag.retrieval.hit).tag(source,source).tag(hit,hits0?true:false).register(registry).increment();}// recordFallback(source) / recordIntent(intent) / recordPipelineStage(stage, success) 同理}rag.retrievalTimer rag.retrieval.hitCounter埋点在GuardedContentRetriever.retrieve的finally块里——不管这路检索是成功、超时还是被熔断一定会经过这里所以数据是全的}finally{metrics.recordRetrieval(name,Duration.ofNanos(System.nanoTime()-startNanos),hits);}Timer 看各路检索源的延迟分布P95 是不是悄悄涨了hit Counter 看空召回率。这两个要配着用我一般是这么算的# 各检索源 5 分钟空手率 rate(rag_retrieval_hit_total{hitfalse}[5m]) / rate(rag_retrieval_hit_total[5m]) # 各检索源平均延迟 rate(rag_retrieval_seconds_sum[5m]) / rate(rag_retrieval_seconds_count[5m])空召回率是 RAG 系统最敏感的质量前兆指标。它突然涨通常不是代码 bug而是知识库刚更新把某类文档删了、改写 prompt 改了之后效果回退了、或者最近用户的问法变了新品类上来了。这东西没有埋点的话你只会听到业务方说最近搜不到东西了然后一头雾水。rag.retrieval.fallbackCounter埋在主备检索的切换点Neo4j 图检索失败、改走 ES 兜底的那一下FallbackContentRetriever里recordFallback(Neo4j)。这个指标的意义是让降级从隐身状态变成可见事件。降级设计得好用户是无感知的服务不报错、日志也只是 warn看都看不出来——但系统其实一直在带病运行。图上这个数持续大于 0说明 Neo4j 那条路一直是坏的你只是没被投诉而已。rag.chat.intentCountertag: intent每次意图识别的结果都记一笔。这是我最喜欢的一个业务体温计上线新 prompt 前后对比意图分布分类质量有没有回退一眼可见比如 GENERAL 占比突然从 8% 飙到 40%八成是某个新意图的关键词没写好全被兜到闲聊了看看价格咨询PRICE_INQUIRY占比走势这就是最真实的用户需求分布顺带能监控降级为 GENERAL的异常率识别抛异常时我也记tag 是降级后的值配合接口错误率看rag.pipeline.stageCountertag: stage/result这个埋点在文档入库的事件监听器里每个阶段处理完成功失败各记一次ragMetrics.recordPipelineStage(uploaded,true);// uploaded/converted/chunked/vector_stored入库是个异步流水线用户侧没有任何错误码可看这条链路的死活全靠在每个 handler 里记账。看板上我就放一行 PromQL# 最近一小时各阶段失败次数 increase(rag_pipeline_stage_total{resultfailure}[1h])哪个 stage 冒头就去捞那个 stage 的日志。FAILED 状态堆积同理knowledge_document_segment/ 文档表按 status 的 SQL 计数我也做成面板双保险。Resilience4j 白送的指标以及流式上报的大坑弹性层的指标基本不用自己写构造BailianResilience时把三个 Registry 绑到 MeterRegistry 上就够了TaggedCircuitBreakerMetrics.ofCircuitBreakerRegistry(circuitBreakerRegistry).bindTo(meterRegistry);TaggedRetryMetrics.ofRetryRegistry(retryRegistry).bindTo(meterRegistry);TaggedBulkheadMetrics.ofBulkheadRegistry(bulkheadRegistry).bindTo(meterRegistry);熔断器状态CLOSED/OPEN、重试次数、隔离舱剩余许可全都自动出现在/actuator/prometheus里。重点盯两个bailian-qwen-plus这个熔断器的 OPEN 次数和retrieval-es的失败率——模型出问题和中间件出问题命名空间是分开的检索熔断器用的是独立的 Registry慢调用阈值 10s比大模型的 60s 严格得多因为检索是延迟敏感操作。但有一类调用是现成装饰器搞不定的流式。这里踩了个挺隐蔽的坑值得单独讲。如果你用CircuitBreaker.decorateSupplier那套去包流式调用被装饰的方法在首 token 之前就返回了——它只证明了请求发出去了。真正的失败长什么样是流吐到一半断流、是整段流慢得要死。这些统统不会进熔断器的统计窗口等于你的熔断形同虚设。所以ResilientStreamingChatModel里我是手动管断路器额度的if(!circuitBreaker.tryAcquirePermission()){handler.onError(CallNotPermittedException.createCallNotPermittedException(circuitBreaker));return;}longstartNanosSystem.nanoTime();AtomicBooleanreportednewAtomicBoolean(false);// 成功失败只记一次// onCompleteResponse 时: circuitBreaker.onSuccess(整段流的实际耗时)// onError 时: circuitBreaker.onError(耗时, error)三个讲究额度自己tryAcquirePermission拿结果在整段流结束的回调里上报耗时按全流实际用时算慢调用阈值才有意义AtomicBoolean保证成功和失败不会双计污染窗口。限流可观测的下游战友埋点看成本限流保成本。ChatRateLimiter基于 Redisson 的RRateLimiter默认每桶 10 秒 10 次超了抛RateLimitExceededException日志会 warn 出是哪个桶触发的——排查谁在刷我就靠这一行。分桶键的优先级设计我觉得值得抄// 登录用户 u:{loginId} → 直连 IP → 会话 conv:{id} → shared飞书机器人那种在虚拟线程里处理、取不到请求上下文的入口就自然落到会话桶不同会话互不影响。两个刻意的不不读 X-Forwarded-For。这个头客户端想怎么写就怎么写拿它做限流键等于送给刷量的人一个免费的换 IP 按钮。我只取getRemoteAddr()真挂在反向代理后面请配server.forward-headers-strategy让容器统一解析而不是应用自己猜。不用trySetRate一把梭。Redisson 的trySetRate只在键不存在时写入——你改了配置重启服务老的速率桶永远沿旧值新配置不生效这坑我第一次就踩了。现在的applyRate会先读getConfig()比对不一致就用setRate强制覆盖并打一行配置变更日志。暴露与抓取从端点到看板yml 里就这一段配置management:endpoints:web:exposure:include:health,prometheus,metricsendpoint:health:show-details:when-authorizedPrometheus 侧加个 scrape job 就完事-job_name:rag-langchain4jmetrics_path:/actuator/prometheusstatic_configs:-targets:[localhost:8080]Grafana 面板我一共就四排按用户体感 → 检索质量 → 链路健康 → 成本排问答大盘QPS、SSE 平均时长、限流触发次数、意图分布饼图检索质量各源空召回率、P95 延迟、fallback 计数入库流水线各阶段成败计数、FAILED 堆积量这个配 MySQL 面板模型成本bailian-* 的调用量、重试率、熔断 OPEN 时长顺带一提 Druidstat-view-servlet开了之后/druid/*有个现成的监控页慢 SQL 阈值我在connection-properties里设了slowSqlMillis5000。它和 Micrometer 是两套体系但排查某条业务 SQL 变慢这种具体问题Druid 页面上直接看到语句比翻 Prometheus 快。说下还没做的诚实起见边界也讲清楚分布式链路追踪没上。目前一次问答内部靠conversationId/messageId串日志意图、检索、生成各自打了带上下文的 info/warn但没有 OpenTelemetry 那种 traceId 贯穿全链路的 Span 树。单体现状够用真要拆微服务了这块必须补。回答质量没有自动评估。检索指标能看查没查到但答得好不好目前只能靠意图分布和人工抽看。RAGAS 那类评估闭环在待办清单上。LLM 调用没有按用户维度记 token 成本账。rag_chat_message里存了 token 数的粗估中文按字、英文按词但那是给记忆窗口用的不是财务精度。小结这篇的核心就一句话在 AI 应用里降级和故障会因为设计得好而隐身只有埋点能让它们现形。五个业务指标分别盯着检索质量retrieval/hit、降级事件fallback、意图漂移intent、流水线死活pipeline.stageresilience4j 的指标绑一下就有但流式调用必须手动上报否则熔断统计全是虚的限流分桶不信任 X-Forwarded-ForRedisson 改速率要用 setRate 比对覆盖看板按体感→质量→链路→成本分层出问题顺着层级往下钻正篇三篇 这篇番外这个项目的分享就到这里完整了。后面如果我把链路追踪或者回答质量评估做出来了再来还这篇的债。老规矩评论区见。
返回列表