跳到主要内容

模型与 HTTP 可观测

什么时候阅读这一页

当你已经完成快速开始,并希望知道下面这些问题的数据来源时,阅读本页:

  • 模型总耗时和底层 HTTP 耗时为什么不同?
  • 流式请求什么时候才算结束?
  • Token、会话 ID 和账号 ID 保存在哪里?
  • 哪些字段适合做 Dashboard 分组,哪些字段只能用于单次 Trace 查询?

自动拦截位置

BaseChatModel 在构建责任链时把 ChatObservabilityInterceptor 放在最外层,后面依次是全局 Chat interceptor、实例 interceptor 和实际模型客户端。因此一次标准模型调用会形成:

text
上游业务 Span(可选)
└── provider.chat / provider.chatStream
    ├── 用户 ChatInterceptor
    └── http.client.request
        └── 模型服务

同步调用在 chat(...) 返回或抛出异常时结束模型 Span。流式调用在启动请求的线程中创建 Span,但不会把 线程上下文留到异步阶段;每个回调只在自己的线程中临时恢复 Span,并在 onCloseonError 结束。 因此流式耗时覆盖完整响应周期,而不是只统计发起 HTTP 请求所需的时间。

开关关系

模型可观测同时受两个开关控制:

java
config.setObservabilityEnabled(true);
properties
agentsflex.otel.enabled=true
全局开关模型开关Chat Span/MetricsAgentsFlexHttpClient Span/Metrics
false任意关闭关闭
truefalse关闭仍然开启
truetrue开启开启

模型开关只控制 Chat interceptor。需要完全关闭框架观测时,应使用全局开关。

Chat Span

同步 Span 名称为 {provider}.chat,流式 Span 名称为 {provider}.chatStream。provider 或 model 为空时 使用 unknown,避免因配置缺失导致埋点异常。

主要属性:

属性条件说明
gen_ai.provider.name始终提供模型服务的厂商或平台
gen_ai.request.model始终本次请求实际使用的模型;优先取 ChatOptions.model
gen_ai.operation.name始终当前为 chat
gen_ai.request.max_tokensChatOptions 已设置最大输出 Token 数
gen_ai.request.temperatureChatOptions 已设置temperature 参数
gen_ai.request.top_pChatOptions 已设置top-p 参数
gen_ai.request.top_kChatOptions 已设置top-k 参数
gen_ai.request.stop_sequencesChatOptions 已设置且非空停止序列数组
gen_ai.usage.input_tokens可取得 Usage输入 Token;优先服务端值,缺失时使用本地统计
gen_ai.usage.output_tokens可取得 Usage输出 Token;优先服务端值,缺失时使用本地统计
gen_ai.response.finish_reasons模型返回结束原因例如 stoplengthtool_calls
agentsflex.bot.idChatOptions 已设置宿主系统业务 Bot ID
gen_ai.conversation.idChatOptions 已设置会话关联 ID
enduser.idChatOptions 已设置账号或最终用户关联 ID
agentsflex.turn.idChatOptions 已设置当前会话中的单次交互 ID
agentsflex.gen_ai.response.content开启内容采集且存在响应脱敏后的响应正文,最多 500 个字符

上述模型语义属性采用 OpenTelemetry GenAI 语义约定,便于不同 APM 识别同一种数据。响应正文没有使用看似标准的 gen_ai.* 自定义名称,而是放在 agentsflex.* 下,避免与标准结构化消息属性产生冲突。旧的 llm.* 属性 不再写入。

当前 Chat interceptor 不把 Prompt 写入 Span。agentsflex.otel.capture.content=true 只会增加模型响应属性; 是否允许响应离开业务系统仍应由应用的数据策略决定。

返回 null、返回错误响应或抛出异常都会标记 Span 为 ERROR。抛出的异常还会通过 span.recordException(...) 记录。

Chat Metrics

Metric类型单位说明
agentsflex.gen_ai.request.countCounter模型请求总数,Agents-Flex 扩展指标
gen_ai.client.operation.durationHistogram同步或完整流式请求耗时
gen_ai.client.token.usageHistogramToken输入或输出 Token 用量
agentsflex.gen_ai.request.error.countCounter失败请求数,Agents-Flex 扩展指标

Metrics 属性包括:

  • gen_ai.provider.name
  • gen_ai.request.model
  • gen_ai.operation.name
  • gen_ai.token.type,仅 Token 指标存在,值为 inputoutput
  • error.type,仅失败请求存在

bot、conversation、account、turn 和任意用户 ID 不进入内置 Metrics,避免时间序列基数快速增长。这些 关联信息只保留在 Span 中。

如何结合 Span 和 Metric 判断问题

观察结果更可能的方向下一步
Chat Span 慢,子 HTTP Span 也慢模型服务或网络慢server.address、model 比较 HTTP 延迟
Chat Span 慢,子 HTTP Span 正常响应消费、自定义 interceptor 或本地处理慢查看两段 Span 的时间差和应用日志
只有一条 Trace 慢,P95 正常个别请求或偶发依赖问题深入查看该 Trace 的工具和 HTTP 子节点
P95、P99 持续上涨系统性退化或容量问题对比请求量、错误率、部署版本和 provider
error count 上涨且 HTTP 为 429上游限流检查并发、配额、重试和模型服务限制

这里的 P95 表示:一段时间内 95% 的请求耗时不超过该值。初学者不必先掌握复杂统计,只需记住平均值容易 掩盖少量特别慢的请求,生产监控通常更关注 P95/P99。

业务关联字段

使用 ChatOptions 设置 Bot、会话、账号和本轮交互:

java
ChatOptions options = new ChatOptions();
options.setContextBotId("bot-1");
options.setContextConversationId("conversation-42");
options.setContextAccountId("account-7");
options.setContextTurnId("turn-3");

chatModel.chat(prompt, options);

流式调用使用相同配置:

java
chatModel.chatStream(prompt, listener, options);

这些字段会自动进入 Chat Span。需要让同一轮中的 Tool 和 HTTP Span 也携带这些属性时,应在业务执行外层 通过 Observability.useRuntime(route, attributes) 绑定同一组属性,因为 Span Attribute 不会自动从父 Span 复制到子 Span。

如果还需要 tenant、workflow、request type 等查询维度,可以注册自定义 Chat interceptor。内置 Observability interceptor 位于外层,所以用户 interceptor 中的 Span.current() 已经是当前模型 Span:

java
public final class TenantTraceInterceptor implements ChatInterceptor {
    @Override
    public AiMessageResponse intercept(
        BaseChatModel<?> model, ChatContext context, SyncChain chain) {

        Span.current().setAttribute("app.tenant.id", resolveTenant(context));
        return chain.proceed(model, context);
    }

    @Override
    public void interceptStream(
        BaseChatModel<?> model,
        ChatContext context,
        StreamResponseListener listener,
        StreamChain chain) {

        Span.current().setAttribute("app.tenant.id", resolveTenant(context));
        chain.proceed(model, context, listener);
    }
}

GlobalChatInterceptors.addInterceptor(new TenantTraceInterceptor());

全局 interceptor 只会进入注册后创建的 ChatModel。建议在应用启动阶段完成注册,不要在并发请求期间修改 全局列表。

HTTP Span

AgentsFlexHttpClient 为 GET、POST、PUT、DELETE 和 multipart 请求创建 CLIENT Span,名称为 http.client.request。在模型调用内部使用时,它会自动成为 Chat Span 的子节点。

主要属性:

属性说明
http.request.methodHTTP 方法
url.full已移除 user-info、query 和 fragment 的 URL
server.addresshost,显式端口存在时包含端口
http.response.status_codeHTTP 状态码

状态码大于等于 400 时 Span 标记为 ERROR。IO 异常会记录异常并继续按现有客户端契约抛给调用方。

getResponse(...) 把打开的 OkHttp Response 交给调用方,因此 Span 会持续到响应体读完、读取失败或 Response 被关闭。调用方必须使用 try-with-resources;如果不关闭响应,HTTP Span 和连接都可能长时间 占用。

java
try (Response response = client.getResponse(url, headers)) {
    String body = response.body() == null ? null : response.body().string();
}

HTTP Metrics

Metric类型单位说明
agentsflex.http.client.request.countCounterHTTP 请求总数,Agents-Flex 扩展指标
http.client.request.durationHistogram请求耗时;开放响应包含响应体消费时间
agentsflex.http.client.request.error.countCounter异常或状态码大于等于 400 的请求数

HTTP Metrics 属性为 http.request.methodserver.address,收到响应后还包含 http.response.status_code。完整 URL 不进入 Metrics,避免路径参数和查询参数造成高基数。

Trace Context 传播

HTTP 请求使用当前 OpenTelemetry 的 propagator 向请求头注入 Trace Context。Agents-Flex 自建 SDK 默认 使用 W3C Trace Context;复用应用 SDK 时使用应用配置的 propagator。

如果自定义 interceptor 把任务切换到其他线程或线程池,应用仍需按照 OpenTelemetry 规则显式传播 Context。框架只保证自身流式回调和 HTTP 客户端的上下文边界,不会自动修复任意用户异步代码。

一个完整示例

用户发起一次流式模型调用,模型 HTTP 在 1 秒后返回首批数据,10 秒后流式结束:

text
openai.chatStream             10.0s
└── http.client.request        1.0s 或持续到响应体关闭

此时 gen_ai.client.operation.duration 记录完整 10 秒,因为用户实际等待到流结束;HTTP Span 的结束时间取决于客户端如何 消费响应体。分析时不要把两者都简单理解为“模型服务器计算时间”。

相关文档