ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LangChain4j —— 可观测性(Observability)

LangChain4j —— 可观测性(Observability)

1. 聊天模型可观测性(Chat Model Observability)

部分ChatModel/StreamingChatModel实现(见「Observability」列)支持配置ChatModelListener,监听三类事件:

事件回调方法时机
请求onRequest(ChatModelRequestContext)调用 LLM 提供商 API之前
响应onResponse(ChatModelResponseContext)成功收到响应后立即调用
错误onError(ChatModelErrorContext)出错时(仅一次)

事件属性遵循 OpenTelemetry Generative AI 语义约定。

事件包含的属性

请求(Request)

  • 消息(messages)
  • 模型(model)
  • 温度(Temperature)
  • Top P
  • 最大 tokens 数
  • 工具(Tools)
  • 响应格式(responseFormat)
  • 等等

响应(Response)

  • 助手消息(AiMessage)
  • ID
  • 模型
  • Token 使用情况(TokenUsage)
  • 结束原因(finishReason)
  • 等等

2. ChatModelListener 完整示例

ChatModelListenerlistener=newChatModelListener(){@OverridepublicvoidonRequest(ChatModelRequestContextrequestContext){ChatRequestchatRequest=requestContext.chatRequest();List<ChatMessage>messages=chatRequest.messages();System.out.println(messages);ChatRequestParametersparameters=chatRequest.parameters();System.out.println(parameters.modelName());System.out.println(parameters.temperature());System.out.println(parameters.topP());System.out.println(parameters.topK());System.out.println(parameters.frequencyPenalty());System.out.println(parameters.presencePenalty());System.out.println(parameters.maxOutputTokens());System.out.println(parameters.stopSequences());System.out.println(parameters.toolSpecifications());System.out.println(parameters.toolChoice());System.out.println(parameters.responseFormat());// 提供商特有参数(以 OpenAI 为例,需 instanceof 判断)if(parametersinstanceofOpenAiChatRequestParametersopenAiParameters){System.out.println(openAiParameters.maxCompletionTokens());System.out.println(openAiParameters.logitBias());System.out.println(openAiParameters.parallelToolCalls());System.out.println(openAiParameters.seed());System.out.println(openAiParameters.user());System.out.println(openAiParameters.store());System.out.println(openAiParameters.metadata());System.out.println(openAiParameters.serviceTier());System.out.println(openAiParameters.reasoningEffort());}System.out.println(requestContext.modelProvider());// 在 onRequest / onResponse / onError 之间传递数据Map<Object,Object>attributes=requestContext.attributes();attributes.put("my-attribute","my-value");}@OverridepublicvoidonResponse(ChatModelResponseContextresponseContext){ChatResponsechatResponse=responseContext.chatResponse();AiMessageaiMessage=chatResponse.aiMessage();System.out.println(aiMessage);ChatResponseMetadatametadata=chatResponse.metadata();System.out.println(metadata.id());System.out.println(metadata.modelName());System.out.println(metadata.finishReason());if(metadatainstanceofOpenAiChatResponseMetadataopenAiMetadata){System.out.println(openAiMetadata.created());System.out.println(openAiMetadata.serviceTier());System.out.println(openAiMetadata.systemFingerprint());}TokenUsagetokenUsage=metadata.tokenUsage();System.out.println(tokenUsage.inputTokenCount());System.out.println(tokenUsage.outputTokenCount());System.out.println(tokenUsage.totalTokenCount());if(tokenUsageinstanceofOpenAiTokenUsageopenAiTokenUsage){System.out.println(openAiTokenUsage.inputTokensDetails().cachedTokens());System.out.println(openAiTokenUsage.outputTokensDetails().reasoningTokens());}ChatRequestchatRequest=responseContext.chatRequest();System.out.println(chatRequest);System.out.println(responseContext.modelProvider());Map<Object,Object>attributes=responseContext.attributes();System.out.println(attributes.get("my-attribute"));}@OverridepublicvoidonError(ChatModelErrorContexterrorContext){Throwableerror=errorContext.error();error.printStackTrace();ChatRequestchatRequest=errorContext.chatRequest();System.out.println(chatRequest);System.out.println(errorContext.modelProvider());Map<Object,Object>attributes=errorContext.attributes();System.out.println(attributes.get("my-attribute"));}};ChatModelmodel=OpenAiChatModel.builder().apiKey(System.getenv("OPENAI_API_KEY")).modelName(GPT_4_O_MINI).listeners(List.of(listener)).build();model.chat("Tell me a joke about Java");

attributes 映射的作用

attributes(Map<Object, Object>)用于:

  • 在同一个ChatModelListener的onRequest→onResponse/onError之间传递信息(如请求开始时间、traceId)。
  • 在多个ChatModelListener之间传递信息。

3. 监听器的工作方式(重要规则)

调用顺序与线程

  • 监听器以List<ChatModelListener>形式配置,按迭代顺序依次调用。
  • 同步调用,运行在同一个线程(流式场景例外,见下)。
  • 第一个监听器返回之前,第二个不会被调用。

调用次数

  • onRequest():每次请求只调用一次;LLM API 出错重试时,不会为每次重试重复调用。
  • onResponse():只调用一次,成功收到响应后立即调用。
  • onError():只调用一次;重试时不会为每次重试重复调用。

异常处理

  • 监听器方法抛出的异常以WARN级别记录日志。
  • 后续监听器照常继续执行,不会中断。

请求内容是“最终请求”

  • 三个 Context 中拿到的ChatRequest是合并后的最终请求:
    ChatModel上配置的默认ChatRequestParameters+ 请求特定的ChatRequestParameters。

StreamingChatModel 的特殊点

  • onResponse()/onError()与onRequest()在不同线程中执行。
  • 线程上下文(如 MDC、ThreadLocal)不会自动传播→ 需要用attributes把数据从onRequest()传到onResponse()/onError()。
  • 时序:
    • onResponse()早于StreamingChatResponseHandler.onCompleteResponse()。
    • onError()早于StreamingChatResponseHandler.onError()。

4. 在 Spring Boot 应用中的可观测性

原文此处为小节标题,展开内容待补充。
实践要点(结合上文规则的落地建议):

  • 把ChatModelListener注册为 Spring Bean(如@Component或@Bean),在构建ChatModel时通过.listeners(...)注入,便于统一收集指标/日志。
  • 在onRequest()中把traceId、开始时间等放入attributes,在onResponse()/onError()中取出,配合 Micrometer / OpenTelemetry 上报:
    • 耗时:onResponse/onError时间戳 −onRequest时间戳
    • Token 消耗:tokenUsage.inputTokenCount()/outputTokenCount()/totalTokenCount()
    • 错误率:onError计数 +errorContext.error()
  • 监听器内不要抛异常(异常只会被 WARN 吞掉),应自行 try-catch 并落日志,避免影响监控链路完整性。

速查小结

  • 三个回调:onRequest(请求前,一次)、onResponse(成功响应,一次)、onError(出错,一次);重试不会重复触发。
  • 一个桥梁:attributes映射 —— 跨回调、跨监听器传值;流式场景线程不同,必须靠它传递上下文。
  • 两类参数:通用ChatRequestParameters/ChatResponseMetadata+ 提供商特有(如OpenAiChatRequestParameters、OpenAiChatResponseMetadata、OpenAiTokenUsage),用instanceof取。
  • 线程:非流式同线程同步;流式onResponse/onError在另一线程,且早于StreamingChatResponseHandler对应回调。
  • 容错:监听器抛异常只记 WARN,不影响后续监听器与主流程。
  • 语义约定:属性命名对齐 OpenTelemetry Generative AI 语义约定,便于接入现有 APM。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进