通过higress AI统计插件学gjson表达式的分享

用过higress的都知道higress是一个功能强大的AI网关,而且支持wasm插件,其中有个插件是AI Statistics(AI统计),可以用来进行大模型调用相关的可观测,可以统计token使用量,可以提取请求内容写入日志。比如记录用户的输入。问题的由来就在这里了。

针对记录用户的输入,AI统计插件默认的配置如下:

bash 复制代码
attributes: 
- apply_to_log: true 
key: "question" 
value: "messages.@reverse.0.content" 
value_source: "request_body"

我们知道大模型调用,用户消息都在messages,它是一个数组,包括用户消息+AI返回,正常来说发送给大模型的messages最新的message是用户消息(role=user)。

但是对于toolcall调用的情况,最新消息就是tool消息(工具调用结果),而且在不规范调用的情况下,也可能最新就是AI消息(role=assistant,实际测试这种情况大模型也是可以正常调用的)。

在上面的情况下,官方默认的配置messages.@reverse.0.content就存在问题了,message先反转再取第0个元素,可能取到的就是AI消息或tool执行结果,而不是用户消息。

看了下插件代码

Go 复制代码
case RequestBody: 
    value = gjson.GetBytes(body, attribute.Value).Value()

可见是通过gjson方式以jsonpath实现的,那么我们就要gjson看是不是支持jsonpath带筛选条件(针对messages数组做role=user的筛选)。

现在都很少去百度搜索、csdn的,多数直接找个AI大模型去问了,可惜啊AI是给出了答案,但是各种试就是不行。

比如messages.@reverse.#(role=="user")#.0.content,结果返回了[],看着是进行了筛选然后取第一个元素,但实际不行

又attributeValue = `messages.@reverse.#(role=="user")#.content`,结果返回["今天天气怎么样?"],变成了数组

这里测试,都得在higress控制台修改配置,然后调用路由api,再去日志查看,比较费劲麻烦,于是写了个测试代码,方便调试。

Go 复制代码
package main

import (
	"fmt"

	"github.com/tidwall/gjson"
)

func main() {
	// 测试场景1: 只有一条 user 消息
	body := []byte(`{
		"messages": [
			{
				"role": "user",
				"content": "今天天气怎么样?"
			}
		],
		"model": "qwen3-30b-a3b-instruct-2507",
		"stream": false,
		"enable_thinking": false
	}`)

	fmt.Println("=== 测试结果 ===")
	attributeValue := `messages.@reverse.#(role=="user")#.0.content`
	attributeValue = `messages.@reverse.#(role=="user")#.0`
	attributeValue = `messages.@reverse.#(role=="user")#.content`
	attributeValue = `messages.@reverse.0.content`
	attributeValue = `messages.@reverse|#(role=="user")#|0.content`
	attributeValue = `messages.#(role=="user")#|@reverse|0.content`
	attributeValue = `messages.@reverse.#(role=="user")#|0.content`
	attributeValue = `messages.#(role=="user")#.@reverse|0.content`

	value := gjson.GetBytes(body, attributeValue).Value()
	fmt.Printf("result: %s\n", value)
}

然后开始启用Claude Code,还是这哥们给力,给出了两个可用的答案,同时还写了测试代码进行验证。

答案1:messages.@reverse.#(role=="user").content

答案2:messages.#(role=="user")#|@reverse|0.content

发现这两个答案筛选部分,一个带1个#一个带了2个#,于是追问,答案逐渐清晰

|--------|----------|----------|
| 特性 | 表达式 1 | 表达式 2 |
| 使用符号 | 单 # | 双 ## |
| 返回类型 | 直接返回对象 | 返回数组 |
| 是否需要索引 | ❌ 不需要 | ✅ 需要 0 |
| 何时取值 | 查询时就取第一个 | 过滤后需要手动取 |

这也解释了为啥答案1不需要带索引0

对于答案2,个人感觉思路更清晰,通过|管道模式,一步步解析得到最终结果。

至于为啥messages.@reverse.#(role="user")#.0.content不行,而且返回是[]?首先,messages.@reverse.0.content是可以返回结果的(不过可能不是role=user的结果),但是在增加#(role="user")#筛选后,无法再应用索引,.0直接识别为了0属性,那么肯定就不符合预期了。

然后,我又想到一个答案messages.#(role=="user")#|@reverse|0.content,就是先筛选再反转,取第0个元素(这里问了AI,也做了代码测试,元素顺序不会出问题)

然后又想到2个#的问题在于无法直接索引0那么只需要通过|进行索引0就好了,于是又有一个新答案:messages.#(role=="user")#.@reverse|0.content

至此,这篇关于gjson的学习分享就结束了。

相关推荐
一条GO8 小时前
简单的 defer 也有可能写出BUG
go
用户580559502108 小时前
深入理解 Go defer(下):编译器与runtime视角的实现原理
后端·go
tyung8 小时前
用 zhenyi-base 做一个带网页的群聊 Demo
websocket·go
AntBlack9 小时前
Ant-Browser : 发布一个开源免费的指纹浏览器 ,欢迎体验
后端·架构·go
程序员爱钓鱼12 小时前
Go排序核心库: sort包深度指南
后端·面试·go
ha6661 天前
golibs — Protocol & Registry 技术文档
go
程序员爱钓鱼1 天前
Go输出与格式化核心库:fmt包完整指南
后端·面试·go
程序员爱钓鱼2 天前
Go PDF处理利器: github.com/pdfcpu/pdfcpu 深度指南
后端·面试·go
江湖十年2 天前
使用 testing/synctest 测试并发代码
后端·面试·go
比特森林探险记3 天前
Go 语言依赖注入和java 区别
go