通过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的学习分享就结束了。

相关推荐
Grassto1 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto3 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室4 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题4 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉5 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想
asaotomo6 天前
一款 AI 驱动的新一代安全运维代理 —— DeepSentry(深哨)
运维·人工智能·安全·ai·go
码界奇点7 天前
基于Gin与GORM的若依后台管理系统设计与实现
论文阅读·go·毕业设计·gin·源代码管理
迷迭香与樱花7 天前
Gin 框架
go·gin
只是懒得想了7 天前
用Go通道实现并发安全队列:从基础到最佳实践
开发语言·数据库·golang·go·并发安全
fenglllle8 天前
使用fyne做一个桌面ipv4网段计算程序
开发语言·go