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

相关推荐
lekami_兰2 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘5 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤6 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt1118 小时前
AI DDD重构实践
go
Grassto2 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto4 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室5 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题5 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想
asaotomo7 天前
一款 AI 驱动的新一代安全运维代理 —— DeepSentry(深哨)
运维·人工智能·安全·ai·go