Files
higress/plugins/wasm-go/extensions/ai-statistics/README.md

26 KiB
Raw Blame History

title, keywords, description
title keywords description
AI可观测
higress
AI
observability
AI可观测配置参考

介绍

提供 AI 可观测基础能力,包括 metric, log, trace其后需接 ai-proxy 插件,如果不接 ai-proxy 插件的话,则需要用户进行相应配置才可生效。

运行属性

插件执行阶段:默认阶段 插件执行优先级:200

配置说明

插件默认请求符合 openai 协议格式,并提供了以下基础可观测值,用户无需特殊配置:

  • metric提供了输入 token、输出 token、首个 token 的 rt流式请求、请求总 rt 等指标,支持在网关、路由、服务、模型四个维度上进行观测
  • log提供了 input_token, output_token, model, llm_service_duration, llm_first_token_duration 等字段

用户还可以通过配置的方式对可观测的值进行扩展:

名称 数据类型 填写要求 默认值 描述
use_default_attributes bool 非必填 false 是否使用默认完整属性配置,包含 messages、answer、question 等所有字段。适用于调试、审计场景
use_default_response_attributes bool 非必填 false 是否使用轻量级默认属性配置(推荐),包含 model 和 token 统计,不缓冲流式响应体。适用于高并发生产环境
attributes []Attribute 非必填 - 用户希望记录在log/span中的信息
disable_openai_usage bool 非必填 false 非openai兼容协议时model、token的支持非标配置为true时可以避免报错
value_length_limit int 非必填 4000 记录的单个value的长度限制
enable_path_suffixes []string 非必填 [] 只对这些特定路径后缀的请求生效,可以配置为 "*" 以匹配所有路径(通配符检查会优先进行以提高性能)。如果为空数组,则对所有路径生效
enable_content_types []string 非必填 [] 只对这些内容类型的响应进行缓冲处理。如果为空数组,则对所有内容类型生效
session_id_header string 非必填 - 指定读取 session ID 的 header 名称。如果不配置,将按以下优先级自动查找:x-openclaw-session-keyx-clawdbot-session-keyx-moltbot-session-keyx-agent-session。session ID 可用于追踪多轮 Agent 对话

Attribute 配置说明:

名称 数据类型 填写要求 默认值 描述
key string 必填 - attribute 名称
value_source string 必填 - attribute 取值来源,可选值为 fixed_value, request_header, request_body, response_header, response_body, response_streaming_body
value string 必填 - attribute 取值 key value/path
default_value string 非必填 - attribute 默认值
rule string 非必填 - 从流式响应中提取 attribute 的规则,可选值为 first, replace, append
apply_to_log bool 非必填 false 是否将提取的信息记录在日志中
apply_to_span bool 非必填 false 是否将提取的信息记录在链路追踪 span 中
trace_span_key string 非必填 - 链路追踪 attribute key默认会使用key的设置
as_separate_log_field bool 非必填 false 记录日志时是否作为单独的字段,日志字段名使用key的设置

value_source 的各种取值含义如下:

  • fixed_value:固定值
  • request_header attribute 值通过 http 请求头获取value 配置为 header key
  • request_body attribute 值通过请求 body 获取value 配置格式为 gjson 的 jsonpath
  • response_header attribute 值通过 http 响应头获取value 配置为 header key
  • response_body attribute 值通过响应 body 获取value 配置格式为 gjson 的 jsonpath
  • response_streaming_body attribute 值通过流式响应 body 获取value 配置格式为 gjson 的 jsonpath

value_sourceresponse_streaming_body 时,应当配置 rule,用于指定如何从流式 body 中获取指定值,取值含义如下:

  • first:多个 chunk 中取第一个有效 chunk 的值
  • replace:多个 chunk 中取最后一个有效 chunk 的值
  • append:拼接多个有效 chunk 中的值,可用于获取回答内容

内置属性 (Built-in Attributes)

插件提供了一些内置属性键key可以直接使用而无需配置 value_sourcevalue。这些内置属性会自动从请求/响应中提取相应的值:

内置属性键 说明 适用场景
question 用户提问内容 支持 OpenAI/Claude 消息格式
system 系统提示词 支持 Claude /v1/messages 的顶层 system 字段
answer AI 回答内容 支持 OpenAI/Claude 消息格式,流式和非流式
tool_calls 工具调用信息 OpenAI/Claude 工具调用
reasoning 推理过程 OpenAI o1 等推理模型
reasoning_tokens 推理 token 数(如 o1 模型) OpenAI Chat Completionsoutput_token_details.reasoning_tokens 提取
cached_tokens 缓存命中的 token 数 OpenAI Chat Completionsinput_token_details.cached_tokens 提取
input_token_details 输入 token 详细信息(完整对象) OpenAI/Gemini/Anthropic包含缓存、工具使用等详情
output_token_details 输出 token 详细信息(完整对象) OpenAI/Gemini/Anthropic包含推理 token、生成图片数等详情

使用内置属性时,只需设置 keyapply_to_log 等参数,无需设置 value_sourcevalue

注意

  • reasoning_tokenscached_tokens 是从 token details 中提取的便捷字段,适用于 OpenAI Chat Completions API
  • input_token_detailsoutput_token_details 会以 JSON 字符串形式记录完整的 token 详情对象

配置示例

如果希望在网关访问日志中记录 ai-statistic 相关的统计值,需要修改 log_format在原 log_format 基础上添加一个新字段,示例如下:

'{"ai_log":"%FILTER_STATE(wasm.ai_log:PLAIN)%"}'

如果字段设置了 as_separate_log_field,例如:

attributes:
  - key: consumer
    value_source: request_header
    value: x-mse-consumer
    apply_to_log: true
    as_separate_log_field: true

那么要在日志中打印,需要额外设置 log_format

'{"consumer":"%FILTER_STATE(wasm.consumer:PLAIN)%"}'

空配置

监控

# counter 类型,输入 token 数量的累加值
route_upstream_model_consumer_metric_input_token{ai_route="ai-route-aliyun.internal",ai_cluster="outbound|443||llm-aliyun.internal.dns",ai_model="qwen-turbo",ai_consumer="none"} 24

# counter 类型,输出 token 数量的累加值
route_upstream_model_consumer_metric_output_token{ai_route="ai-route-aliyun.internal",ai_cluster="outbound|443||llm-aliyun.internal.dns",ai_model="qwen-turbo",ai_consumer="none"} 507

# counter 类型,流式请求和非流式请求消耗总时间的累加值
route_upstream_model_consumer_metric_llm_service_duration{ai_route="ai-route-aliyun.internal",ai_cluster="outbound|443||llm-aliyun.internal.dns",ai_model="qwen-turbo",ai_consumer="none"} 6470

# counter 类型,流式请求和非流式请求次数的累加值
route_upstream_model_consumer_metric_llm_duration_count{ai_route="ai-route-aliyun.internal",ai_cluster="outbound|443||llm-aliyun.internal.dns",ai_model="qwen-turbo",ai_consumer="none"} 2

# counter 类型,流式请求首个 token 延时的累加值
route_upstream_model_consumer_metric_llm_first_token_duration{ai_route="ai-route-aliyun.internal",ai_cluster="outbound|443||llm-aliyun.internal.dns",ai_model="qwen-turbo",ai_consumer="none"} 340

# counter 类型,流式请求次数的累加值
route_upstream_model_consumer_metric_llm_stream_duration_count{ai_route="ai-route-aliyun.internal",ai_cluster="outbound|443||llm-aliyun.internal.dns",ai_model="qwen-turbo",ai_consumer="none"} 1

以下是使用指标的几个示例:

流式请求首个 token 的平均延时:

irate(route_upstream_model_consumer_metric_llm_first_token_duration[2m])
/
irate(route_upstream_model_consumer_metric_llm_stream_duration_count[2m])

流式请求和非流式请求平均消耗的总时长:

irate(route_upstream_model_consumer_metric_llm_service_duration[2m])
/
irate(route_upstream_model_consumer_metric_llm_duration_count[2m])

日志

{
  "ai_log": "{\"model\":\"qwen-turbo\",\"input_token\":\"10\",\"output_token\":\"69\",\"llm_first_token_duration\":\"309\",\"llm_service_duration\":\"1955\"}"
}

如果请求中携带了 session ID header日志中会自动添加 session_id 字段:

{
  "ai_log": "{\"session_id\":\"sess_abc123\",\"model\":\"qwen-turbo\",\"input_token\":\"10\",\"output_token\":\"69\",\"llm_first_token_duration\":\"309\",\"llm_service_duration\":\"1955\"}"
}

链路追踪

配置为空时,不会在 span 中添加额外的 attribute

从非 openai 协议提取 token 使用信息

在 ai-proxy 中设置协议为 original 时,以百炼为例,可作如下配置指定如何提取 model, input_token, output_token

attributes:
  - key: model
    value_source: response_body
    value: usage.models.0.model_id
    apply_to_log: true
    apply_to_span: false
  - key: input_token
    value_source: response_body
    value: usage.models.0.input_tokens
    apply_to_log: true
    apply_to_span: false
  - key: output_token
    value_source: response_body
    value: usage.models.0.output_tokens
    apply_to_log: true
    apply_to_span: false

监控

route_upstream_model_consumer_metric_input_token{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 343
route_upstream_model_consumer_metric_output_token{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 153
route_upstream_model_consumer_metric_llm_service_duration{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 3725
route_upstream_model_consumer_metric_llm_duration_count{ai_route="bailian",ai_cluster="qwen",ai_model="qwen-max"} 1

日志

此配置下日志效果如下:

{
  "ai_log": "{\"model\":\"qwen-max\",\"input_token\":\"343\",\"output_token\":\"153\",\"llm_service_duration\":\"19110\"}"
}

链路追踪

链路追踪的 span 中可以看到 model, input_token, output_token 三个额外的 attribute

配合认证鉴权记录 consumer

举例如下:

attributes:
  - key: consumer # 配合认证鉴权记录consumer
    value_source: request_header
    value: x-mse-consumer
    apply_to_log: true

记录问题与回答

仅记录当前轮次的问题与回答

attributes:
  - key: question # 记录当前轮次的问题(最后一条用户消息)
    value_source: request_body
    value: messages.@reverse.0.content
    apply_to_log: true
  - key: answer # 在流式响应中提取大模型的回答
    value_source: response_streaming_body
    value: choices.0.delta.content
    rule: append
    apply_to_log: true
  - key: answer # 在非流式响应中提取大模型的回答
    value_source: response_body
    value: choices.0.message.content
    apply_to_log: true

记录完整的多轮对话历史(推荐配置)

对于多轮 Agent 对话场景,使用内置属性可以大幅简化配置:

session_id_header: "x-session-id"  # 可选,指定 session ID header
attributes:
  - key: messages     # 完整对话历史
    value_source: request_body
    value: messages
    apply_to_log: true
  - key: question     # 内置属性,自动提取最后一条用户消息
    apply_to_log: true
  - key: answer       # 内置属性,自动提取回答
    apply_to_log: true
  - key: reasoning    # 内置属性,自动提取思考过程
    apply_to_log: true
  - key: tool_calls   # 内置属性,自动提取工具调用
    apply_to_log: true

内置属性说明:

插件提供以下内置属性 key无需配置 value_sourcevalue 字段即可自动提取:

内置 Key 说明 默认 value_source
question 自动提取最后一条用户消息 request_body
answer 自动提取回答内容(支持 OpenAI/Claude 协议) response_streaming_body / response_body
tool_calls 自动提取并拼接工具调用(流式场景自动按 index 拼接 arguments response_streaming_body / response_body
reasoning 自动提取思考过程reasoning_content如 DeepSeek-R1 response_streaming_body / response_body

注意:如果配置了 value_sourcevalue,将优先使用配置的值,以保持向后兼容。

日志输出示例:

{
  "ai_log": "{\"session_id\":\"sess_abc123\",\"messages\":[{\"role\":\"user\",\"content\":\"北京天气怎么样?\"}],\"question\":\"北京天气怎么样?\",\"reasoning\":\"用户想知道北京的天气,我需要调用天气查询工具。\",\"tool_calls\":[{\"index\":0,\"id\":\"call_abc123\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"location\\\":\\\"Beijing\\\"}\"}}],\"model\":\"deepseek-reasoner\"}"
}

流式响应中的 tool_calls 处理:

插件会自动按 index 字段识别每个独立的工具调用,拼接分片返回的 arguments 字符串,最终输出完整的工具调用列表。

记录 Token 详情

使用内置属性记录 OpenAI Chat Completions 的 token 详细信息:

attributes:
  # 使用便捷的内置属性提取特定字段
  - key: reasoning_tokens  # 推理token数o1等推理模型
    apply_to_log: true
  - key: cached_tokens  # 缓存命中的token数
    apply_to_log: true
  # 记录完整的token详情对象
  - key: input_token_details
    apply_to_log: true
  - key: output_token_details
    apply_to_log: true

日志示例

对于使用了 prompt caching 和推理模型的请求,日志可能如下:

{
  "ai_log": "{\"model\":\"gpt-4o\",\"input_token\":\"100\",\"output_token\":\"50\",\"reasoning_tokens\":\"25\",\"cached_tokens\":\"80\",\"input_token_details\":\"{\\\"cached_tokens\\\":80}\",\"output_token_details\":\"{\\\"reasoning_tokens\\\":25}\",\"llm_service_duration\":\"2000\"}"
}

其中:

  • reasoning_tokens: 25 - 推理过程产生的 token 数
  • cached_tokens: 80 - 从缓存中读取的 token 数
  • input_token_details: 完整的输入 token 详情JSON 格式)
  • output_token_details: 完整的输出 token 详情JSON 格式)

这些详情对于:

  1. 成本优化:了解缓存命中率,优化 prompt caching 策略
  2. 性能分析:分析推理 token 占比,评估推理模型的实际开销
  3. 使用统计:细粒度统计各类 token 的使用情况

流式响应观测能力

流式Streaming响应是 AI 对话的常见场景,插件提供了完善的流式观测支持,能够正确拼接和提取流式响应中的关键信息。

流式响应的挑战

流式响应将完整内容拆分为多个 SSE chunk 逐步返回,例如:

data: {"choices":[{"delta":{"content":"Hello"}}]}
data: {"choices":[{"delta":{"content":" 👋"}}]}
data: {"choices":[{"delta":{"content":"!"}}]}
data: [DONE]

要获取完整的回答内容,需要将各个 chunk 中的 delta.content 拼接起来。

自动拼接机制

插件针对不同类型的内容提供了自动拼接能力:

内容类型 拼接方式 说明
answer 文本追加append 将各 chunk 的 delta.content 按顺序拼接成完整回答
reasoning 文本追加append 将各 chunk 的 delta.reasoning_content 按顺序拼接
tool_calls 按 index 组装 识别每个工具调用的 index,分别拼接各自的 arguments

answer 和 reasoning 拼接示例

流式响应:

data: {"choices":[{"delta":{"content":"你好"}}]}
data: {"choices":[{"delta":{"content":",我是"}}]}
data: {"choices":[{"delta":{"content":"AI助手"}}]}

最终提取的 answer"你好我是AI助手"

tool_calls 拼接示例

流式响应(多个并行工具调用):

data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_001","function":{"name":"get_weather"}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":1,"id":"call_002","function":{"name":"get_time"}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":"}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"Beijing\"}"}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"city\":\"Shanghai\"}"}}]}}]}

最终提取的 tool_calls

[
  {"index":0,"id":"call_001","function":{"name":"get_weather","arguments":"{\"city\":\"Beijing\"}"}},
  {"index":1,"id":"call_002","function":{"name":"get_time","arguments":"{\"city\":\"Shanghai\"}"}}
]

使用默认配置快速启用

插件提供两种默认配置模式:

轻量模式(推荐用于生产环境)

通过 use_default_response_attributes: true 启用轻量模式:

use_default_response_attributes: true

此配置是推荐的生产环境配置,特别适合高并发、高延迟的场景:

字段 说明
model 模型名称(从请求体提取)
reasoning_tokens 推理 token 数
cached_tokens 缓存命中 token 数
input_token_details 输入 token 详情
output_token_details 输出 token 详情

为什么推荐轻量模式?

LLM 请求有两个显著特点:延迟高(通常数秒到数十秒)和请求体大(多轮对话可能达到数百 KB 甚至 MB 级别)。

在高并发场景下,如果请求体和响应体都被缓存在内存中,积压的请求会占用大量内存:

  • 假设 QPS=100平均延迟=10秒请求体=500KB
  • 同时在处理的请求数 ≈ 100 × 10 = 1000 个
  • 如果缓存完整请求体+响应体1000 × 1.5MB ≈ 1.5GB 内存

轻量模式通过以下方式降低内存占用:

  • 缓冲请求体:仅用于提取 model 字段(很小),不提取 questionsystemmessages 等大字段
  • 不缓冲流式响应体:不提取 answerreasoningtool_calls 等需要完整响应的字段
  • 只统计 token:从响应的 usage 字段提取 token 信息

内存对比:

场景 完整模式 轻量模式
单次请求 (1MB 请求 + 500KB 响应) ~1.5MB ~1MB请求体
高并发 (100 QPS, 10s 延迟) ~1.5GB ~1GB
超高并发 (1000 QPS, 10s 延迟) ~15GB ~10GB

注意:轻量模式下 chat_round 字段会正常计算,model 会从请求体正常提取。

完整模式

通过 use_default_attributes: true 可以一键启用完整的流式观测能力:

use_default_attributes: true

此配置会自动记录以下字段,但会缓冲完整的请求体和流式响应体

字段 说明 内存影响
messages 完整对话历史 ⚠️ 可能很大
question 最后一条用户消息 需要缓冲请求体
system 系统提示词 需要缓冲请求体
answer AI 回答(自动拼接流式 chunk ⚠️ 需要缓冲响应体
reasoning 推理过程(自动拼接流式 chunk ⚠️ 需要缓冲响应体
tool_calls 工具调用(自动按 index 组装) 需要缓冲响应体
reasoning_tokens 推理 token 数
cached_tokens 缓存命中 token 数
input_token_details 输入 token 详情
output_token_details 输出 token 详情

注意:完整模式适用于调试、审计等需要完整对话记录的场景,但在高并发生产环境可能消耗大量内存。

流式日志示例

启用默认配置后,一个流式请求的日志输出示例:

{
  "answer": "2 plus 2 equals 4.",
  "question": "What is 2+2?",
  "response_type": "stream",
  "tool_calls": null,
  "reasoning": null,
  "model": "glm-4-flash",
  "input_token": 10,
  "output_token": 8,
  "llm_first_token_duration": 425,
  "llm_service_duration": 985,
  "chat_id": "chat_abc123"
}

包含工具调用的流式日志示例:

{
  "answer": null,
  "question": "What's the weather in Beijing?",
  "response_type": "stream",
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"location\": \"Beijing\"}"
      }
    }
  ],
  "model": "glm-4-flash",
  "input_token": 50,
  "output_token": 15,
  "llm_first_token_duration": 300,
  "llm_service_duration": 500
}

流式特有指标

流式响应会额外记录以下指标:

  • llm_first_token_duration:从请求发出到收到首个 token 的时间(首字延迟)
  • llm_stream_duration_count:流式请求次数

可用于监控流式响应的用户体验:

# 平均首字延迟
irate(route_upstream_model_consumer_metric_llm_first_token_duration[5m])
/
irate(route_upstream_model_consumer_metric_llm_stream_duration_count[5m])

调试

验证 ai_log 内容

在测试或调试过程中,可以通过开启 Higress 的 debug 日志来验证 ai_log 的内容:

# 日志格式示例
2026/01/31 23:29:30 proxy_debug_log: [ai-statistics] [nil] [test-request-id] [ai_log] attributes to be written: {"question":"What is 2+2?","answer":"4","reasoning":"...","tool_calls":[...],"session_id":"sess_123","model":"gpt-4","input_token":20,"output_token":10}

通过这个debug日志可以验证

  • question/answer/reasoning 是否正确提取
  • tool_calls 是否正确拼接特别是流式场景下的arguments
  • session_id 是否正确识别
  • 各个字段是否符合预期

进阶

配合阿里云 SLS 数据加工,可以将 ai 相关的字段进行提取加工,例如原始日志为:

ai_log:{"question":"用python计算2的3次方","answer":"你可以使用 Python 的乘方运算符 `**` 来计算一个数的次方。计算2的3次方即2乘以自己2次可以用以下代码表示\n\n```python\nresult = 2 ** 3\nprint(result)\n```\n\n运行这段代码你会得到输出结果为8因为2乘以自己两次等于8。","model":"qwen-max","input_token":"16","output_token":"76","llm_service_duration":"5913"}

使用如下数据加工脚本,可以提取出 question 和 answer

e_regex("ai_log", grok("%{EXTRACTJSON}"))
e_set("question", json_select(v("json"), "question", default="-"))
e_set("answer", json_select(v("json"), "answer", default="-"))

提取后SLS 中会添加 question 和 answer 两个字段,示例如下:

ai_log:{"question":"用python计算2的3次方","answer":"你可以使用 Python 的乘方运算符 `**` 来计算一个数的次方。计算2的3次方即2乘以自己2次可以用以下代码表示\n\n```python\nresult = 2 ** 3\nprint(result)\n```\n\n运行这段代码你会得到输出结果为8因为2乘以自己两次等于8。","model":"qwen-max","input_token":"16","output_token":"76","llm_service_duration":"5913"}

question:用python计算2的3次方

answer:你可以使用 Python 的乘方运算符 `**` 来计算一个数的次方。计算2的3次方即2乘以自己2次可以用以下代码表示

result = 2 ** 3
print(result)

运行这段代码你会得到输出结果为8因为2乘以自己两次等于8。

路径和内容类型过滤配置示例

只处理特定 AI 路径

enable_path_suffixes:
  - "/v1/chat/completions"
  - "/v1/embeddings"
  - "/generateContent"

只处理特定内容类型

enable_content_types:
  - "text/event-stream"
  - "application/json"

处理所有路径(通配符)

enable_path_suffixes:
  - "*"

处理所有内容类型(空数组)

enable_content_types: []

完整配置示例

enable_path_suffixes:
  - "/v1/chat/completions"
  - "/v1/embeddings"
  - "/generateContent"
enable_content_types:
  - "text/event-stream"
  - "application/json"
attributes:
  - key: model
    value_source: request_body
    value: model
    apply_to_log: true
  - key: consumer
    value_source: request_header
    value: x-mse-consumer
    apply_to_log: true