资讯动态

LangChain4j 可观测与透明化:traceId 串起全链路,SSE 直播思考过程

发布时间:2026/10/22 12:58:30 来源:尧图企业网站定制
LangChain4j 可观测与透明化traceId 串起全链路SSE 直播思考过程作者鱼宵 LangChain4j 实战精通营 · 第 12 篇上一课我们把记忆落了 Redis、向量落了 ES客服不怕重启了。可真上线第一天客服发来一句“刚才那次回答错了你帮我看看。”我盯着满屏System.out发呆——用户问了啥、后端是查了知识库、调了工具、还是直接编的日志散在好几个线程里我连哪几行是同一次请求都分不出来。说白了功能做出来只是能跑出问题能查才叫敢上线。这篇就把客服从黑盒刷成透明盒给每次请求发个快递单号 traceId把检索、工具、模型串起来再把它先翻知识库、再决定要不要调工具、临了才组织回答的全过程用 SSE 直播到前端。代码全在仓库的lesson-12/目录clone 下来重跑你也能在网页上看到一条回答怎么一步步冒出来。一、核心原理给每次请求贴个快递单号先认识三个词。MDC映射诊断上下文SLF4J/Logback 提供的线程级 Map。你往里面塞一个traceIdabc这个线程接下来打的每一行日志都能通过日志格式里的%X{traceId}取出来拼进去。快递单号类比一个包裹要经过分拣中心 → 运输 → 派送好几个站每个站扫码都贴同一个单号。你输一个单号所有站的记录全出来。traceId 就是这个单号——一次请求经过的每个组件打的日志都带同一个号。traceId一整串调用的总单号一次请求一个全程不变。spanId其中一小段的子单号检索 ES是一个 span调工具是另一个 span。光把号打进日志还不够我们还想在一个网页上看到这次请求花了 2 秒检索 50ms、调模型 1.9 秒——这就是调用链追踪。三个依赖各管一段依赖角色类比micrometer-tracing-bridge-brave把 Spring 观测体系接到 Brave 实现制定单号格式的机构zipkin-reporter-brave把产生的 span 用 HTTP 发给 Zipkin把各站扫码记录上传的网络brave-context-slf4j把 traceId/spanId 塞进 MDC让每个站小票上都印上单号Spring Boot 自动配置帮你把这三样粘好业务代码几乎零。Zipkin 就是收 span、提供网页 UI 看瀑布图的服务器——哪段慢、哪段失败一眼看出。SSE 命名事件是另一回事。上一课的/chat/stream只会推一种token帧。但前端要区分这是思考过程“这是工具调用”“这是正文”SSE 原生支持命名事件textevent:thinkingdata:{“type”:“retrieval”,“text”:“正在检索…”}event:tooldata:{“name”:“queryOrder”,“args”:“{“orderId”:“1001”}”,“durationMs”:2}浏览器端用 es.addEventListener(thinking, ...)、addEventListener(tool, ...) 分别收。一条流式连接就变成一套结构化协议——思考画灰框、工具画徽章、正文画气泡。 ## 二、动手复用 L11 容器只新加 Zipkin 关键**ES(9200)、Redis(6390) 复用 L11 的容器千万别停它们、别重起**。本课应用端口 **8088**。 **第 0 步确认中间件在跑。** powershell java -version # True [bool][Environment]::GetEnvironmentVariable(DEEPSEEK_API_KEY) # True docker ps --filter namees-lc-l11 --filter nameredis-lc-l11 # 必须都是 Up要是这俩没起先去lesson-11目录docker compose up -d拉起来再回来。第 1 步起 Zipkin收 span 的服务器。cd langchain4j-journey\lesson-12 docker compose up-d curl.exe-s http://localhost:9411/zipkin/# 看到 HTML 即就绪首次要拉镜像本机拉不到 Zipkin 镜像也别卡死进度它是旁路拉不下来只影响 Zipkin UI日志 traceId 和 SSE 事件这两个主验收项照常工作。第 2、3 步编译 启动。cd langchain4j-journey\lesson-12$env:JAVA_HOMEC:\Program Files\Java\jdk-17mvn clean install-DskipTests# BUILD SUCCESSmvn spring-boot:run# 在 lesson-12 目录启动要读 docs/company-faq.md第 4 步访问。浏览器开http://localhost:8088/。命令行测 SSE中文编码curl.exe-s-Nhttp://localhost:8088/chat/stream?sessionIdt1message$([uri]::EscapeDataString(订单1001什么状态))三、关键代码三件套配置 traceId 抓取 SSE 回调① application.yml让每行日志都带单号。spring:application:name:lc-l12-cs# 拼进日志前缀多应用排障一眼分清是谁打的zipkin:base-url:http://localhost:9411# span 上报到这里management:tracing:sampling:probability:1.0# 教学全采样生产一般 0.1否则量大了 Zipkin 扛不住logging:pattern:# %X{traceId:-} 从 MDC 取值取不到用 - 填充。这行让每行日志都带快递单号。level:%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]brave-context-slf4j会自动在请求开始时把 traceId/spanId 塞进 MDC结束时清掉——你一行MDC.put都不用写。② 手动加 span以检索为例。packagecom.langchain4j.lesson12.rag;importio.micrometer.tracing.Span;// ★是顶层类不是 Tracer.Span本机踩过这个坑importio.micrometer.tracing.Tracer;importorg.springframework.stereotype.Component;ComponentpublicclassKnowledgeBase{privatefinalTracertracer;// Spring Boot 自动装配注入即用publicKnowledgeBase(Tracertracer){this.tracertracer;}publicStringsearch(Stringquestion,inttopK){// 在当前 trace 下开一个子 span起名 rag.search、打个 topK 标签Spanspantracer.nextSpan().name(rag.search).tag(topK,String.valueOf(topK)).start();try(Tracer.SpanInScopewstracer.withSpan(span)){returndoSearch(question,topK);// 这段代码里再开的 span 都挂它下面}finally{span.end();// 必须 end不 end这个 span 永不结束Zipkin 里看不到}}}注意Span是顶层类io.micrometer.tracing.Span不是Tracer.Span——我第一次照旧教程写成Tracer.Span编译直接报找不到符号卡了半天才反应过来导错了类。③ SSE 透明化traceId 必须在请求线程抓回调再发。packagecom.langchain4j.lesson12.web;importdev.langchain4j.service.TokenStream;importorg.slf4j.MDC;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;importorg.springframework.web.servlet.mvc.method.annotation.SseEmitter;RestControllerpublicclassChatController{GetMapping(value/chat/stream,producesorg.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE)publicSseEmitterstream(RequestParam(defaultValuedefault)StringsessionId,RequestParamStringmessage){SseEmitteremitternewSseEmitter(120_000L);// ★关键技巧traceId 是 Brave 在【请求线程】上塞进 MDC 的。// 后面 onPartialResponse / onToolExecuted 跑在 HTTP 客户端的回调线程那时 MDC 已清。// 所以必须现在请求线程就把 traceId 抓下来后面 done 事件才能带上它。StringtraceIdMDC.get(traceId);TokenStreamtscustomerService.stream(sessionId,message,sink);// 工具调用完模型真调了 Tool 方法回调带工具名/参数/结果/耗时ts.onToolExecuted(te-sink.tool(te.request().name(),te.request().arguments(),te.result(),te.duration().toMillis()));ts.onPartialResponse(token-emitter.send(SseEmitter.event().name(token).data(token)))// 正文逐字.onCompleteResponse(complete-{emitter.send(SseEmitter.event().name(done).data(java.util.Map.of(traceId,traceId)));emitter.complete();}).onError(err-emitter.send(SseEmitter.event().name(error).data(err.getMessage()))).start();// 别忘了 start()否则流式根本不开始returnemitter;}}这串回调里有个本机真实踩过的坑onToolExecuted是 LangChain4j 1.21TokenStream的内置回调模型每真调一次工具就回调一次ToolExecution里带工具名/参数/结果/耗时。而onPartialThinking这个钩子本课留着却不触发——原因见第四节那个框架丢字段的故事。四、实测输出日志带单号事件全程直播以下是 2026-10-06 00:09~00:12 本机真实运行应用 8088复用 L11 的 ES 9200 / Redis 6390JDK17。① 日志从裸奔变成带单号。无请求时是空占位一发请求立刻带上真实单号无请求: INFO [lc-l12-cs,,] ... Tomcat started on port 8088 有请求: INFO [lc-l12-cs,6ac3cbd1bda8c58fd512db18a85069e7,d512db18a85069e7] ...ChatController : 收到流式问答请求...[lc-l12-cs, 6ac3cbd1..., d512db18...] 应用名, traceId, spanId。拿这个 traceId 去日志里 grep这次请求的所有日志就全捞出来了。② 一条工具题的 SSE 六类事件统计event:thinking ×3 event:tool ×1 event:token ×61 event:citations ×1 event:done ×1关键帧原文event:thinking data:{type:retrieval,text:正在知识库检索相关制度/产品资料…} event:thinking data:{type:retrieval,text:命中 3 条资料最高相似度 0.801} event:tool data:{name:queryOrder,args:{\orderId\: \1001\},result:订单号1001客户 张三...状态 已支付收货城市 郑州。,durationMs:2} event:token data:订单 ...共 61 个 token 帧... event:done data:{traceId:6ac3cb6456d0e0e307064406d7e4f121}思考面板收到 3 步检索开始→命中几条→开始生成模型真调了queryOrder正文 61 个 token 逐字推。六类事件齐全。③ traceId 前后端打通——这就是可观测的最后一公里前端收到: event:done data:{traceId:6ac3cbd1bda8c58fd512db18a85069e7} 日志里: INFO [lc-l12-cs,6ac3cbd1bda8c58fd512db18a85069e7,...] ...ChatController网页上看到一个 traceId拿它去日志里一搜这次请求的全过程就全出来了。④ Zipkin 上报这里要实话说本机openzipkin/zipkin镜像经代理多次拉取卡层UI 没在交付前跑通。但应用确实在向 Zipkin 发 span——容器没起时的 WARN 就是证据WARN [lc-l12-cs,,] ... z.r.i.AsyncReporter ResourceAccessException: I/O error on POST http://localhost:9411/api/v2/spans: Connection refused看出来了吗POST 地址/api/v2/spans已经对了只是接收端没起。镜像拉起来后跑几次问答再curl.exe -s http://localhost:9411/api/v2/traces?limit5就能看到 trace。span 发送失败只打 WARN不影响业务——监控是旁路不能因为监控挂了把主流程拖死。⑤ 复用 L11 容器但数据隔离$ docker exec redis-lc-l11 redis-cli --scan --pattern lc:l12:* lc:l12:chat:memory:mem1 ... $ curl.exe -s http://localhost:9200/lc-l12-faq/_count {count:12}Redis 用lc:l12:前缀、ES 用独立索引lc-l12-faq和 L11 完全不串。顺带说个框架丢字段的故事deepseek-reasoner 流式确实逐段返回delta.reasoning_content下划线但 LangChain4j 1.21.0 内部那个字段的 Jackson 注解没指定 name按驼峰reasoningContent反序列化和下划线对不上 → 恒为 null。实测onPartialThinking回调 0 次。结论API 支持、框架丢字段。所以本课走自己推 Agent 步骤的方案 B钩子留着等框架修映射。五、挑战题改个参数看看会怎样⭐玩采样率把management.tracing.sampling.probability从1.0改成0.5连问 4 次观察Zipkin 起了的话是不是大约只有一半请求有 trace。答案就在application.yml那行——跑过你就懂采样为什么是生产刚需。⭐⭐验证 traceId 跨线程会丢故意在onToolExecuted回调里打一行MDC.get(traceId)观察它在回调线程上是不是 null——然后想清楚为什么 Controller 非要在请求线程先抓 traceId。这是本机真实踩过的坑答案在第三节代码注释里。六、生产环境进阶三个加分项1. 采样率别学教学全量。本课probability1.0是为了每次都能看到 trace生产设 0.1采样 10%否则 QPS 上来 Zipkin 和网络都扛不住。2. 追踪是旁路故障别拖主流程。Zipkin 挂了应用照常跑只打 WARN。事件管道、指标上报这些监控组件出问题都只能打 warn不能反过来把业务打挂。3. 跨线程传 traceId 要手动。TokenStream 回调跑在模型 HTTP 客户端线程MDC 是空的。生产更优雅的做法是用ThreadPoolTaskExecutor的TaskDecorator统一装饰或上 Micrometer Context Propagation 库。七、面试回答模板面试官traceId / spanId / MDC 分别是什么怎么把一次请求的日志串起来一句话一次请求一个 traceId总单号每个处理环节一个 spanId子单号MDC 是线程级 Map%X{traceId}把它打进日志。展开Brave 在请求线程把 traceId 放进 MDC日志格式[应用名,traceId,spanId]每行都带出问题拿 traceId 一搜本次请求全链路日志就出来了。指向本课第一节 / lesson-12 的 application.yml追问Micrometer Tracing Brave Zipkin 三件套各管什么micrometer-tracing-bridge-brave桥接到 Brave 实现zipkin-reporter-brave用 HTTP 上报 spanbrave-context-slf4j把 traceId 塞进 MDC。Spring Boot 自动配置粘好业务几乎零代码手动加 span 用tracer.nextSpan().name(x).tag(k,v).start()finally{span.end()}注意Span是顶层类不是Tracer.Span。指向本课第三节追问SSE 怎么做命名事件怎么把工具调用实时推到前端emitter.name(tool).data(obj)发命名帧前端addEventListener(tool)接收一条连接混流 thinking/tool/token/citations/done/error 多种事件。LangChain4j 1.21 的TokenStream.onToolExecuted回调带工具名/参数/结果/耗时直接推前端画工具徽章。至于 deepseek-reasoner 的 reasoning_content 为什么业务代码拿不到——那是框架 Jackson 字段映射丢了靠抓包对比 snake_case 才定位API 有 ≠ 框架透传。指向本课第三、四节八、总结表坑现象解法日志没 traceId一次请求的日志散在各处串不起来logging.pattern.level加%X{traceId:-} 引brave-context-slf4jTracer.Span找不到符号编译报错找不到符号 Spanimport io.micrometer.tracing.Span;顶层类SSE 收不到 thinking/tool前端用 onmessage 啥也没有命名事件必须addEventListener(事件名)回调线程 MDC 丢 traceIddone 事件 traceId 永远 null请求线程先MDC.get(traceId)抓下来Zipkin 镜像拉不下来UI 打不开别卡死进度WARN 证明管道已接好业务不受影响ES/Redis 和 L11 串数据记忆/知识混在一起Redis key 前缀改lc:l12:、ES 索引改lc-l12-faq采样率设 1.0生产量大了 Zipkin 扛不住教学 1.0生产 0.1reasoning_content 不透传思考面板空走方案 B 自己推步骤钩子留着等框架修九、关于这个系列本文是「Java 后端实战精通营」系列第 12 篇原则实战驱动、由浅到深、面试向每篇文章的结论都可以亲手验证。LangChain4j 实战精通营https://gitee.com/j67mk2/langchain4j-journey本文对应源码位置lesson-12/在 L11 上升级核心看resources/application.yml的 tracing 配置、web/ChatController.java的 SSE 六类事件与 traceId 抓取、rag/KnowledgeBase.java手动 spanES/Redis 复用 L11 容器只新增 Zipkin 容器系列文章一览主线 10 篇 生产增强 3 篇按发布顺序篇主题1LangChain4j 第一个对话程序手写 LLM 调用链路看清模型怎么开口2LangChain4j 提示词工程模板 few-shot 温度提示词从写死到复用3LangChain4j 结构化输出让模型吐出 Java 对象JSON 不再手写解析4LangChain4j 对话记忆让模型记住聊过什么三种方式实测对比5LangChain4j 流式输出打字机效果 SSE回答不再干等三秒6LangChain4j 工具调用Function Calling 让模型动手查数据多工具连调实测7LangChain4j 向量检索文本怎么变成可搜索的坐标8LangChain4j RAG 流水线手写问答全流程给 AI 开卷考试9LangChain4j Agent多步推理自主调用放手让实习生自己安排工作10LangChain4j 企业智能客服RAG 工具 记忆 流式 兜底验收 5 项全过11LangChain4j 生产化改造记忆落 Redis、向量落 ES重启再也不丢12LangChain4j 可观测与透明化traceId 串起全链路SSE 直播思考过程下一篇预告《LangChain4j 企业级收官事件总线 Redis Pub/Sub 断线补拉客服终于敢上线》——这一课的 SSE 事件还是业务层直接拿 SseEmitter 发的单实例爽、多实例全废浏览器连 A、请求打 B事件根本推不回去。收官课用 Spring 事件总线 Redis Pub/Sub 重做事件管道加限流/重试/熔断/指标再支持断线重连补拉把客服真正推到敢上线。跑完有任何报错把终端输出发评论区一起排查。标签建议LangChain4j、链路追踪、SSE摘要建议≤256 字线上 AI 问答慢了错了怎么定位本文给每次请求发 traceIdMicrometer Tracing Brave Zipkin 三件套把检索/工具/模型串成调用链日志每行带 [应用,traceId,spanId]SSE 从 3 种事件扩到 6 种命名事件thinking/tool/token/citations/done/error把工具调用和思考过程实时直播到前端。还复盘了 deepseek-reasoner 的 reasoning_content 被框架丢字段的定位过程。附完整命令与面试模板源码在 gitee lesson-12 可 clone 重跑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑