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

436 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: AI可观测
keywords: [higress, AI, observability]
description: 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 等字段
用户还可以通过配置的方式对可观测的值进行扩展:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|----------------|-------|------|-----|------------------------|
| `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-key``x-clawdbot-session-key``x-moltbot-session-key``x-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_source``response_streaming_body` 时,应当配置 `rule`,用于指定如何从流式 body 中获取指定值,取值含义如下:
- `first`:多个 chunk 中取第一个有效 chunk 的值
- `replace`:多个 chunk 中取最后一个有效 chunk 的值
- `append`:拼接多个有效 chunk 中的值,可用于获取回答内容
### 内置属性 (Built-in Attributes)
插件提供了一些内置属性键key可以直接使用而无需配置 `value_source``value`。这些内置属性会自动从请求/响应中提取相应的值:
| 内置属性键 | 说明 | 适用场景 |
|---------|------|---------|
| `question` | 用户提问内容 | 支持 OpenAI/Claude 消息格式 |
| `answer` | AI 回答内容 | 支持 OpenAI/Claude 消息格式,流式和非流式 |
| `tool_calls` | 工具调用信息 | OpenAI/Claude 工具调用 |
| `reasoning` | 推理过程 | OpenAI o1 等推理模型 |
| `reasoning_tokens` | 推理 token 数(如 o1 模型) | OpenAI Chat Completions`output_token_details.reasoning_tokens` 提取 |
| `cached_tokens` | 缓存命中的 token 数 | OpenAI Chat Completions`input_token_details.cached_tokens` 提取 |
| `input_token_details` | 输入 token 详细信息(完整对象) | OpenAI/Gemini/Anthropic包含缓存、工具使用等详情 |
| `output_token_details` | 输出 token 详细信息(完整对象) | OpenAI/Gemini/Anthropic包含推理 token、生成图片数等详情 |
使用内置属性时,只需设置 `key``apply_to_log` 等参数,无需设置 `value_source``value`
**注意**
- `reasoning_tokens``cached_tokens` 是从 token details 中提取的便捷字段,适用于 OpenAI Chat Completions API
- `input_token_details``output_token_details` 会以 JSON 字符串形式记录完整的 token 详情对象
## 配置示例
如果希望在网关访问日志中记录 ai-statistic 相关的统计值,需要修改 log_format在原 log_format 基础上添加一个新字段,示例如下:
```yaml
'{"ai_log":"%FILTER_STATE(wasm.ai_log:PLAIN)%"}'
```
如果字段设置了 `as_separate_log_field`,例如:
```yaml
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])
```
#### 日志
```json
{
"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` 字段:
```json
{
"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
```yaml
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
```
#### 日志
此配置下日志效果如下:
```json
{
"ai_log": "{\"model\":\"qwen-max\",\"input_token\":\"343\",\"output_token\":\"153\",\"llm_service_duration\":\"19110\"}"
}
```
#### 链路追踪
链路追踪的 span 中可以看到 model, input_token, output_token 三个额外的 attribute
### 配合认证鉴权记录 consumer
举例如下:
```yaml
attributes:
- key: consumer # 配合认证鉴权记录consumer
value_source: request_header
value: x-mse-consumer
apply_to_log: true
```
### 记录问题与回答
#### 仅记录当前轮次的问题与回答
```yaml
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 对话场景,使用内置属性可以大幅简化配置:
```yaml
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_source``value` 字段即可自动提取:
| 内置 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_source` 和 `value`,将优先使用配置的值,以保持向后兼容。
日志输出示例:
```json
{
"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 详细信息:
```yaml
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 和推理模型的请求,日志可能如下:
```json
{
"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 的使用情况
## 调试
### 验证 ai_log 内容
在测试或调试过程中,可以通过开启 Higress 的 debug 日志来验证 ai_log 的内容:
```bash
# 日志格式示例
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 路径
```yaml
enable_path_suffixes:
- "/v1/chat/completions"
- "/v1/embeddings"
- "/generateContent"
```
#### 只处理特定内容类型
```yaml
enable_content_types:
- "text/event-stream"
- "application/json"
```
#### 处理所有路径(通配符)
```yaml
enable_path_suffixes:
- "*"
```
#### 处理所有内容类型(空数组)
```yaml
enable_content_types: []
```
#### 完整配置示例
```yaml
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
```