☰
Higress AI统计插件配置指南:gjson表达式从入门到实战
2026/10/2 3:45:20 网站建设 项目流程

做AI网关的同学应该都有过这种经历:模型路由、限流、计量计费,这些事只要业务一上量,光靠翻日志根本盯不过来。我当时落地higress作为AI网关方案时,第一件想搞清楚的事情就是每个模型的调用量、token消耗、错误率到底是多少。higress自带的AI统计插件恰好就是干这个的,但配置过程中有个绕不过去的门槛——插件规则里要用gjson表达式来定位JSON里面的字段。不熟悉这套语法的人,第一眼就会被“到底怎么写”卡住。

这篇文章就是我在配置higress AI统计插件过程中,对gjson表达式从一头雾水到能熟练编写的一份完整梳理。我会说清楚AI统计插件为什么需要gjson、gjson的核心语法有哪些、在插件实际配置里怎么用、以及我踩过的那些坑。不管你是刚开始接触网关配置,还是已经用过但总在表达式上报错,都能从里面找到能直接抄走的解法。

1. higress AI统计插件为什么需要gjson表达式

1.1 AI统计插件到底在统计什么

higress是云原生网关,AI场景下最常用的能力就是把请求转发给上游的大模型服务,比如OpenAI兼容格式的接口。这种接口的请求体和响应体里会携带很多有价值的信息:模型名称、usage里的token统计、生成内容的finish_reason、响应状态码、错误信息等等。

AI统计插件做的事情,就是把这些藏在JSON body里的字段提取出来,整理成监控指标。举个例子,一次正常的模型响应体长这样:

{ "model": "gpt-4", "choices": [ { "message": {"content": "你好"}, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 120, "completion_tokens": 80, "total_tokens": 200 } }

如果我想知道这次请求总共消耗了多少token,就得从响应体里把usage.total_tokens这个字段取出来。放在AI统计插件里,这个“取出来”的动作靠的就是gjson表达式。

我最初接触这个插件时,心里想的是:不就是取个字段吗,用正则写不就行了?但真正配置起来才发现,插件设计得比我想象的更讲究。它让你写的是一个高层的路径描述,而不是对整段文本做匹配。这个设计思路的不同,直接决定了为什么我们要学gjson。

1.2 配置核心:从请求或响应体中提取字段

AI统计插件的规则配置,核心逻辑可以拆成三部分:数据从哪里来、要取哪个字段、提取后怎么用。

数据来源一般在source里指定,常见的是response_body和request_body。要取哪个字段,就是在expr或类似的字段里写gjson表达式。比如我想统计total_tokens,最自然的写法就是:

usage.total_tokens

这个表达式很好理解,点号左边是JSON里的键名,点号右边是更深一层的键名。gjson会从根节点开始,一级一级往下找。

如果你要统计的是第一个生成结果的内容,那表达式要稍微绕一下:

choices.0.message.content

这里可以看到gjson表达式的两个特点:数组用数字下标访问,不需要中括号;嵌套路径用点号连接,清晰且短。这样的表达方式,在配置一些监控规则时非常直观,比写正则要省心太多。

1.3 为什么是gjson而不是正则、JSONPath

这个问题我一开始也想不通。后来仔细对比了一下,发现gjson在几个核心维度上确实更贴合网关这种场景。

先对比正则。正则匹配JSON的痛点是结构不固定、转义多。一个"usage":{"total_tokens":200}可能有各种换行和空格,你要写准了很费劲。而且就算你匹配到了数字,还得再处理一下才能变成指标。gjson是基于JSON结构来定位的,换行空格对它来说没有影响,字段路径写对了就一定找得到。

再对比JSONPath。JSONPath功能更强,比如支持$.store.book[0].title这种完整写法,但代价就是语法相对复杂。功能强归强,在网关插件这种配置型场景里,绝大多数用户只想知道“怎么简洁地取到某个字段”,gjson这种精简路径语法反而更友好。

还有很重要的一点:higress本身是Go语言实现的,而gjson就是Go生态里一个成熟的高性能JSON解析库。一个Go写的网关网关,内部插件用同样Go生态的表达式库,这本身就是最合理的技术选型。用过gjson库的应该都知道,它在解析大JSON时性能表现相当好,这点对于网关这种高并发场景尤其重要。

如果用一个生活化的类比:JSON就是一棵按目录存放文件的树,gjson表达式就是“文件夹/子文件夹/文件名”这种路径写法。知道了所有文件的存放位置,你要取哪个东西都只是写路径的问题。

2. gjson表达式基础语法拆解

2.1 点路径与数组索引

先看最基本的点路径。对于JSON结构:

{ "name": { "first": "Tom", "last": "Jerry" }, "age": 30, "tags": ["devops", "gateway"], "address": { "city": "Hangzhou", "code": 310000 } }
  • name.first返回"Tom"
  • age返回30
  • address.city返回"Hangzhou"

这些路径本质上就是一层层向下访问键名。需要注意的是,gjson对JSON的键名严格区分大小写。写错了大小写,表达式返回的就是空结果,并且不会报错。这一点后面我会专门讲,因为太容易踩了。

数组的访问用数字下标。比如tags.0返回"devops",tags.1返回"gateway"。这里有个和很多语言习惯不一致的地方:gjson不需要也不支持tags[0]这样的中括号写法。我第一次用的时候习惯性地写tags[0],结果什么都没有,查了一会儿才意识到要写tags.0。

如果你想要数组的长度,可以用#号。tags.#返回2。这在统计一批结果的数量时很有用。

还有一种特殊场景:键名本身包含点号。比如JSON里有个键叫"a.b",那写a.b会被解析成先找a再找b,自然就找不到了。我的建议是尽量避免设计这种键名,如果上游接口固定返回这种结构,那可以考虑用通配符或者调整提取思路。gjson在这块处理起来确实别扭,不要硬刚。

2.2 通配符与条件查询

通配符*可以匹配任意字段名。比如:

{ "data": { "item1": {"value": 10}, "item2": {"value": 20}, "item3": {"value": 30} } }
  • data.*返回item1、item2、item3三个对象的数组
  • data.*.value返回[10, 20, 30]

这在AI网关场景里很实用。比如模型响应里可能有多个choices,你想把每个choice的finish_reason都取出来,就可以写:

choices.*.finish_reason

返回的就是类似["stop", "length"]这样的数组。

条件查询是gjson里比较有特色的能力,语法是#(...)。比如:

{ "items": [ {"name": "a", "age": 25}, {"name": "b", "age": 35}, {"name": "c", "age": 30} ] }
  • items.#(age>30)#返回年龄大于30的元素个数,结果是1
  • items.#(age>30).name返回年龄大于30的元素的name字段,结果是["b"]
  • items.#(name=="b")#返回name等于"b"的元素个数,结果是1

注意字符串比较要用双引号把值包起来,数字比较直接写数字,布尔值写true或者false。这个细节在调试插件时经常遇到,少写了双引号表达式就不会生效。

#(...)放在末尾后面再跟一个#,是gjson里的一个固定写法,表示“统计符合条件的数量”。第一次看到items.#(age>30)#这个表达式,里面出现两个#,很容易懵。简单理解就是:第一个#进入条件查询模式,最后的#表示取数量结果。

2.3 修饰符与多路径提取

gjson的修饰符是通过@符号调用的函数式能力。常用的几个:

  • @this:返回当前匹配到的值本身
  • @reverse:把数组倒序
  • @join:把字符串数组用分隔符拼起来
  • @flatten:把多维数组扁平化
  • @slice:对数组做切片

举个例子,items.*.name返回所有name字段后,如果想倒序排列,可以写:

items.*.name|@reverse

这里|是管道符号,表示把前一个表达式的结果传给修饰符处理。@join也很有用,如果想把多个name拼成一个带逗号的字符串,可以写:

items.*.name|@join:","

在插件配置里,修饰符的使用频率其实不算特别高,但遇到“需要将多个值合并成一个标签”的场景时会很方便。

多路径提取的语法是用花括号{}包住多个查询路径。比如:

{name.first, name.last}

返回的结果是一个JSON对象:

{ "name.first": "Tom", "name.last": "Jerry" }

这在插件配置里可以用于同时提取多个字段,一次查询拿到多个值,减少对body的重复解析。

2.4 聚合统计:让表达式不止于取值

gjson表达式不仅能取值,还能做简单的聚合。除了前面讲到的#统计数量,还有一个常见的场景是统计数组长度。

考虑这样一个响应体,表示一次流式输出过程中的多段结果:

{ "stream_choices": [ {"index": 0, "text": "你好"}, {"index": 1, "text": "世界"}, {"index": 2, "text": "!"} ] }

stream_choices.#返回3,也就是输出段数。结合AI统计插件,你可以把这个值作为一次会话的“流式返回块数”来监控。

再想想一个更常见的用法:我需要知道一次请求返回的choices里有多少个是被截断的(finish_reason为length),这直接反映了生成内容是否超长被截。表达式写出来是:

choices.#(finish_reason=="length")#

这在调试大模型输出的“戛然而止”问题时,给了我很大的帮助。之前我只能靠日志一条条翻,有了这种条件聚合表达式,直接就能让监控面板告诉我截断发生的次数和频率。

聚合统计这部分,让我对gjson的理解从“取字段”上升到“表达意图”。表达式写得好,一个路径就能顶一段日志分析的工作量。

3. 在AI统计插件里写gjson的实战

3.1 一个完整的配置示例

这里给出一份基于我实际使用结构的配置示例。不同的higress版本,插件的字段名可能略有差异,但核心思路是一样的。

metrics: - name: llm_total_tokens source: response_body expr: usage.total_tokens - name: llm_prompt_tokens source: response_body expr: usage.prompt_tokens - name: llm_completion_tokens source: response_body expr: usage.completion_tokens - name: llm_finish_reason_stop source: response_body expr: choices.#(finish_reason=="stop")#

这个配置的意图很明确:从每个模型的响应体里提取token数据和finish_reason情况,全部作为指标输出。配置完并接入监控系统后,我在Grafana里添加一个面板,直接对llm_total_tokens做sum聚合,就能看到一段时间内所有模型调用加起来消耗的总token数。

这里有一个重要的实操心得:AI统计插件只负责“提取和暴露指标”,如果要做累计、平均、分位数这些计算,建议放到Prometheus查询语句里做,不要在插件里做复杂处理。插件侧尽量保持简单、明确,每个指标只做一件事。

3.2 统计token消耗:从单次到全局

token消耗是AI网关最关心的指标之一,因为它直接和成本挂钩。很多模型服务的计费方式非常直接:你的账单就是按prompt_tokens和completion_tokens分别计费的。

假设你接的模型返回结构是:

{ "model": "qwen-max", "usage": { "prompt_tokens": 112, "completion_tokens": 233, "total_tokens": 345 } }

在AI统计插件里提取total_tokens的表达式只有一行:

usage.total_tokens

但如果你给两个不同模型做对比测试,希望分别看到它们的token消耗,传统方法是每部署一个插件实例就配一个固定的表达式。但更好的做法,是把model字段提取出来作为一个标签,在监控系统里按模型维度分组。

AI统计插件通常会支持自定义标签字段,这样你可以把响应体里的model提取出来,指标就自动带上了模型名称这个维度。我实际配置时是这么干的:

metrics: - name: llm_requests_total source: response_body expr: model labels: - model

这样在监控面板上,每一种模型的调用量、token消耗量就可以清清楚楚分开看。想象一下,线上接入三家模型供应商,每个模型的成本都不一样,没有这种按模型维度的指标,月底算账就是一场灾难。

还有一个小技巧:因为model字段在请求体和响应体里都有,如果你想统计的是“用户请求了哪个模型”,这个从请求体取更准确:

metrics: - name: llm_calls_requests source: request_body expr: model

如果是从响应体取,那你统计的是“实际被调用的模型”,能发现网关是否做了模型路由兜底。我后来就靠这个区分了“用户请求的模型”和“实际处理的模型”,在排查路由配置问题时非常有用。

3.3 按模型维度聚合与错误统计

前面讲了怎么按model字段分组。其实错误统计的思路完全一样。

当上游模型返回错误时,标准的响应体结构通常是:

{ "error": { "message": "Rate limit reached", "type": "rate_limit", "code": 429 } }

要统计错误类型,表达式就是:

error.type

要拿到具体的错误信息,写:

error.message

这里有个容易忽略的点:gjson的message本身是一种查询语法吗?其实不是,它就是一个普通字段名,不会跟gjson内部的功能冲突。所以放心用。

我建议在配置里单独建一组错误指标,把错误类型作为标签提取出来:

metrics: - name: llm_error_total source: response_body expr: error.type labels: - type

配置完成后,我在监控面板上就能直接看到“哪些错误类型占了大多数”,比如是不是rate_limit暴增,是不是context_length_exceeded频繁出现。这种可视化的排查方式,比我之前靠日志grep错误码的效率高出几个量级。

还有一种情况是上游网关返回的错误没有结构化,只是纯文本。这种情况下gjson表达式提取不到字段,指标会为空。我的建议是设置一个默认的兜底指标,比如请求总数,再结合非空的错误指标做比率计算,这样就算拿不到错误细分,至少还能知道错误率的整体变化趋势。

3.4 更多场景:响应延迟、内容长度、缓存命中

除了token和错误,AI网关还有很多值得统计的指标。

响应延迟虽然可以直接在网关层拿到耗时数据,但有些模型服务会在响应体里返回更细粒度的时间信息,比如总共花了多少秒。假设响应体里有这样的字段:

{ "timings": { "total_ms": 890, "prompt_ms": 120, "completion_ms": 770 } }

你可以在插件里提取timings.total_ms,再换算成秒或者直接作为耗时指标。这个比网关层的连接耗时更贴近“模型真实处理时间”,因为网关耗时还包括了网络传输和排队。

还有一个有意思的指标是生成内容长度。比如你关心一次请求的输出有多少字,但响应体里没有直接给出长度字段。这时候常见的做法是统计choices里content的长度,但这需要更复杂的计算能力。gjson本身不支持函数计算字符串长度,所以我一般用请求和响应的字节数作为近似。有些模型供应商会在usage里额外返回completion_tokens,这个数字其实就约等于输出长度,所以直接用usage.completion_tokens就够了。

对于大模型的流式响应,还有一种情况是每一段chunk里都有usage信息。这时候可以用通配符把每一段的token数都取出来,虽然不能直接加总,但可以在监控系统里对指标做sum。比如:

chunks.*.usage.total_tokens

这个表达式返回的是所有chunk的total_tokens数组,在Prometheus里对每个实例做sum,就可以得到一次完整流式响应累计消耗的token总量。我在配置这类指标时踩过坑,因为没有意识到返回的是一个数组,导致监控面板上显示的数据很奇怪。后来才明白,这种场景下gjson的返回值类型是数组,下游监控系统能不能正确处理数组,需要在配置时特别确认。

4. 调试方法与常见坑

4.1 快速验证gjson表达式的三种方式

写gjson表达式最忌讳的就是直接改配置、上生产,然后盯着监控看有没有数据。正确流程应该是先在本地验证表达式是否符合预期。

第一种方式,在线调试。gjson官网提供了一个在线调试页面,网址是gjson.dev。你可以在左边粘贴一段JSON样例,右边写表达式,立刻就能看到解析结果。这个页面我几乎每次配置插件都要打开,非常稳。

第二种方式,本地写一段小代码验证。如果你本地有Go环境,可以用gjson库写一个十来行的测试脚本。这里给个简单示例:

package main import ( "fmt" "github.com/tidwall/gjson" ) func main() { json := `{"usage":{"total_tokens":200},"choices":[{"finish_reason":"stop"}]}` result := gjson.Get(json, "choices.#(finish_reason==\"stop\")#") fmt.Println(result.String()) }

第三种方式,利用higress本地部署的调试能力。higress支持本地运行,可以在本地搭建好网关配置后,用真实的模型响应体去触发插件,看日志里打印的指标。这个方法最接近线上环境,适合调通了表达式但还需要验证整体链路的情况。

我的经验是:先用在线调试把表达式的语法验证明白,再放到本地higress里跑一遍真实请求,最后才上生产。三步下来踩坑率会低很多。

4.2 常见错误写法速查

我把实际配置中最容易踩的几个坑整理成了表格,方便对照排查:

错误场景错误写法示例正确写法错误原因
数组访问用了中括号choices[0]choices.0gjson不使用中括号语法
字符串比较少了双引号#(name==b)##(name=="b")#字符串必须用双引号包裹
大小写写错Usage.total_tokensusage.total_tokensJSON键名严格区分大小写
条件查询末尾少了#items.#(age>30)items.#(age>30)#统计数量要用双#结尾
点路径把数字当键名data.0.name需要确认根结构如果data不是数组会取不到
忽略多层嵌套choices.finish_reasonchoices.0.finish_reason数组必须先指定下标或用通配符

这里重点说下第一个坑。choices[0]这个错误非常常见,因为大多数语言的JSON解析都是用中括号访问数组元素。但gjson走的是点路径风格,所以choices.0.message.content才是对的。虽然这个表达习惯刚开始会让人不适,但用熟了会发现它写起来更简洁,尤其适合在配置类文件里使用。

还有一个坑是条件查询里布尔值的写法。比如JSON里的字段值是true,表达式要写成#(status==true)#,不要试图用"true"这种字符串形式。数字比较和字符串比较的规则我在前面已经说过,这里再强调一下:类型不匹配会导致条件永远为假。

4.3 空值、大小写与结构差异处理

gjson的一个特性是,当路径不存在时,它不会抛异常,而是返回一个空结果。这对插件来说既是优点也是隐患。优点是即使上游模型返回的JSON结构不完整,插件也能正常运行,不会因为解析失败导致网关崩溃。隐患是,如果你没注意到返回为空,监控面板上就会莫名其妙出现“某指标一直为零”的情况。

我排查过这类问题。有一次配置了一个统计请求延迟的指标,配置完以后发现一直都是0。在线调试工具里手写了一段模拟JSON,路径是正确的,能正常提取到数据。后来拉了一个真实线上日志,才发现那家模型服务在响应体里根本没有timings字段,它是放在响应头里返回的。这种情况表达式再怎么写都是白搭,因为数据源就不在body里。

还有一个小细节:有些模型供应商在不同接口版本里返回的JSON结构会略有不同。比如老版本的finish_reason字段可能在choices数组的元素里,新版本却变成了choices元素下面的message对象里。如果你的网关同时接了多个版本的接口,表达式就必须考虑到这种结构差异。我的应对办法是准备一个兜底表达式,在无法提取到值时返回一个默认值,让指标至少能被监控到,不至于完全空白。

gjson本身不提供复杂的分支判断语法,所以在插件层面解决结构差异并不容易。我的建议是:要么在上游做数据标准化,让所有模型服务返回统一结构;要么在网关前面的流程里做一次转换;要么就分开配置不同的指标,分别对应不同版本。

4.4 性能与维护建议

网关是流量入口,每个请求都会经过插件处理。虽然gjson性能在Go库里面算是很优秀的,但你也不能随便乱用,在高并发场景下还是要注意几个点。

首先,尽量减小解析范围。如果你的JSON body有几十KB,而你只需要提取其中两个字段,那表达式路径越短越好。gjson的查询是基于逐层解析的,路径越深,需要遍历的节点越多。

其次,避免在一个表达式里做太多聚合运算。比如choices.*.message.content返回全体内容的数组,这个数组可能非常大,如果只是想知道有多少个choices,这个表达式就是浪费。用choices.#就够了。

第三,如果多个指标要提取同一个祖先路径下的不同子字段,尽量合并成多路径提取,减少对body的重复解析。比如{usage.prompt_tokens, usage.completion_tokens}这种写法在一次查询里拿到两个值,比分别写两条表达式调用两次解析要高效。

最后,维护角度的一点建议:表达式不要写得花里胡哨,复杂的条件嵌套、多级修饰符串联,容易让后来的人看不懂。我的习惯是每个表达式旁边写一个简短注释,说明它在统计什么,对应哪个业务指标。这比把表达式写得很炫技但没人敢碰要好得多。

一点个人体会

gjson这个表达式库,单独看语法其实非常小,半天就能掌握。难的不是语法本身,而是你能否在真实场景里理解“结构的形状”。我在配置higress AI统计插件时最深的感受是:每一个gjson表达式背后,都是在跟一组真实存在的JSON数据结构对话。你越了解你的AI网关后端返回什么样的body,你的表达式就写得越顺手。

如果你正准备上手higress的AI统计插件,我的建议很简单:先找一条你真实环境下的模型响应体,把JSON慢慢展开,一行一行看清楚,然后从最简单的model、usage.total_tokens开始写。写完以后用在线调试工具过一遍,确认提取结果符合预期,再放进插件配置里。过程中遇到的任何报错,先看是不是斜杠、双引号、大小写的问题。等你把第一组指标跑出来,看到监控面板上出现那些漂亮的曲线时,你就会觉得前面花的这些功夫完全值得。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询