mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-31 13:29:35 +08:00
- Add new AI Querier module for structured information extraction from screenshots - Support custom output schema for structured data response - Implement automatic type conversion and data validation - Add comprehensive test suite with various data structure examples - Refactor callModelWithLogging to utils.go as shared function for planner, asserter, and querier - Eliminate code duplication across AI modules (30+ lines of repeated code) - Improve maintainability with unified logging and timing logic - Add environment variable checks in test setup to handle missing API keys gracefully Key features: - Custom output schema support with JSON Schema generation - Automatic data type conversion with reflection - Fallback mechanisms for robust parsing - Comprehensive documentation and usage examples - Backward compatibility with existing functionality
299 lines
8.0 KiB
Markdown
299 lines
8.0 KiB
Markdown
# HttpRunner AI Querier - 自定义输出格式功能
|
||
|
||
## 功能概述
|
||
|
||
HttpRunner 的 AI Querier 模块支持自定义输出格式功能,允许用户指定特定的数据结构,让 AI 模型返回结构化的数据响应。适用于:
|
||
|
||
- **UI 元素分析**:自动化测试中的界面元素提取
|
||
- **游戏界面分析**:网格类游戏(连连看、消消乐、2048等)数据提取
|
||
- **表单数据提取**:从表单截图中提取结构化信息
|
||
- **图像内容分析**:任何需要从截图中提取结构化信息的场景
|
||
|
||
## 核心数据结构
|
||
|
||
```go
|
||
// QueryOptions - 查询选项
|
||
type QueryOptions struct {
|
||
Query string `json:"query"` // 查询文本
|
||
Screenshot string `json:"screenshot"` // Base64编码的截图
|
||
Size types.Size `json:"size"` // 屏幕尺寸
|
||
OutputSchema interface{} `json:"outputSchema,omitempty"` // 自定义输出格式(可选)
|
||
}
|
||
|
||
// QueryResult - 查询结果
|
||
type QueryResult struct {
|
||
Content string `json:"content"` // 人类可读的分析结果
|
||
Thought string `json:"thought"` // AI 推理过程
|
||
Data interface{} `json:"data,omitempty"` // 结构化数据(使用OutputSchema时自动转换为指定类型)
|
||
}
|
||
```
|
||
|
||
## 基本用法
|
||
|
||
### 标准查询
|
||
|
||
```go
|
||
// 创建查询器
|
||
modelConfig, err := ai.GetModelConfig(option.OPENAI_GPT_4O)
|
||
querier, err := ai.NewQuerier(ctx, modelConfig)
|
||
|
||
// 执行查询
|
||
result, err := querier.Query(ctx, &ai.QueryOptions{
|
||
Query: "请分析这张截图中的内容",
|
||
Screenshot: screenshot,
|
||
Size: size,
|
||
// 不指定 OutputSchema
|
||
})
|
||
|
||
fmt.Printf("分析结果: %s\n", result.Content)
|
||
fmt.Printf("推理过程: %s\n", result.Thought)
|
||
// result.Data 为 nil
|
||
```
|
||
|
||
### 自定义格式查询
|
||
|
||
```go
|
||
// 定义输出结构
|
||
type GameAnalysis struct {
|
||
Content string `json:"content"` // 分析描述
|
||
Thought string `json:"thought"` // 思考过程
|
||
Rows int `json:"rows"` // 行数
|
||
Cols int `json:"cols"` // 列数
|
||
Icons []string `json:"icons"` // 图标类型
|
||
}
|
||
|
||
// 执行查询
|
||
result, err := querier.Query(ctx, &ai.QueryOptions{
|
||
Query: "分析这个游戏界面的网格结构和图标类型",
|
||
Screenshot: screenshot,
|
||
Size: size,
|
||
OutputSchema: GameAnalysis{}, // 指定输出格式
|
||
})
|
||
|
||
// 直接类型断言获取结构化数据
|
||
if gameData, ok := result.Data.(*GameAnalysis); ok {
|
||
fmt.Printf("行数: %d, 列数: %d\n", gameData.Rows, gameData.Cols)
|
||
fmt.Printf("图标类型: %v\n", gameData.Icons)
|
||
}
|
||
```
|
||
|
||
## 应用场景示例
|
||
|
||
### UI 元素分析
|
||
|
||
```go
|
||
type UIAnalysis struct {
|
||
Content string `json:"content"`
|
||
Thought string `json:"thought"`
|
||
Elements []UIElement `json:"elements"`
|
||
}
|
||
|
||
type UIElement struct {
|
||
Type string `json:"type"` // button, text, input等
|
||
Text string `json:"text"` // 文本内容
|
||
BoundBox BoundingBox `json:"boundBox"` // 位置坐标
|
||
Clickable bool `json:"clickable"` // 是否可点击
|
||
}
|
||
|
||
type BoundingBox struct {
|
||
X, Y, Width, Height int `json:"x,y,width,height"`
|
||
}
|
||
```
|
||
|
||
### 网格游戏分析
|
||
|
||
```go
|
||
type GridGame struct {
|
||
Content string `json:"content"`
|
||
Thought string `json:"thought"`
|
||
Grid [][]Cell `json:"grid"` // 网格数据
|
||
Stats Statistics `json:"statistics"` // 统计信息
|
||
}
|
||
|
||
type Cell struct {
|
||
Type string `json:"type"` // 单元格类型
|
||
Value string `json:"value"` // 单元格值
|
||
Row int `json:"row"` // 行索引
|
||
Col int `json:"col"` // 列索引
|
||
}
|
||
|
||
type Statistics struct {
|
||
TotalCells int `json:"totalCells"`
|
||
UniqueTypes int `json:"uniqueTypes"`
|
||
}
|
||
```
|
||
|
||
### 表单数据提取
|
||
|
||
```go
|
||
type FormAnalysis struct {
|
||
Content string `json:"content"`
|
||
Thought string `json:"thought"`
|
||
Fields []FormField `json:"fields"`
|
||
Actions []Action `json:"actions"`
|
||
}
|
||
|
||
type FormField struct {
|
||
Label string `json:"label"` // 字段标签
|
||
Type string `json:"type"` // 字段类型
|
||
Value string `json:"value"` // 当前值
|
||
Required bool `json:"required"` // 是否必填
|
||
BoundBox BoundingBox `json:"boundBox"` // 位置
|
||
}
|
||
```
|
||
|
||
## 核心特性
|
||
|
||
### 自动类型转换
|
||
- 指定 `OutputSchema` 时,`QueryResult.Data` 自动转换为指定类型
|
||
- 支持直接类型断言:`result.Data.(*YourType)`
|
||
- 无需手动调用转换函数
|
||
|
||
### 多级回退机制
|
||
1. 优先解析为指定的结构化类型
|
||
2. 失败时尝试通用JSON解析
|
||
3. 最终回退到纯文本响应
|
||
|
||
### 向后兼容
|
||
- 不指定 `OutputSchema` 时行为不变
|
||
- 现有代码无需修改
|
||
|
||
## 最佳实践
|
||
|
||
### 1. 结构体设计
|
||
|
||
```go
|
||
// 推荐:包含标准字段
|
||
type YourSchema struct {
|
||
Content string `json:"content"` // 必须:人类可读描述
|
||
Thought string `json:"thought"` // 必须:AI推理过程
|
||
// 自定义字段...
|
||
Data CustomData `json:"data"`
|
||
}
|
||
|
||
// 使用描述性的JSON标签
|
||
type Element struct {
|
||
Type string `json:"elementType"` // 清晰的字段名
|
||
Position Point `json:"gridPosition"` // 描述性标签
|
||
Visible bool `json:"isVisible"` // 布尔值清晰性
|
||
}
|
||
```
|
||
|
||
### 2. 查询指令
|
||
|
||
```go
|
||
// 推荐:详细的查询指令
|
||
opts := &ai.QueryOptions{
|
||
Query: `分析这张截图并提供结构化信息:
|
||
1. 识别界面类型和主要元素
|
||
2. 提取所有可交互元素的位置和属性
|
||
3. 统计各类元素的数量`,
|
||
Screenshot: screenshot,
|
||
Size: size,
|
||
OutputSchema: YourSchema{},
|
||
}
|
||
```
|
||
|
||
### 3. 错误处理
|
||
|
||
```go
|
||
result, err := querier.Query(ctx, opts)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 类型断言
|
||
if data, ok := result.Data.(*YourSchema); ok {
|
||
// 使用结构化数据
|
||
processData(data)
|
||
} else {
|
||
// 回退到文本结果
|
||
log.Printf("结构化解析失败,使用文本结果: %s", result.Content)
|
||
}
|
||
```
|
||
|
||
## 完整示例
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"log"
|
||
|
||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||
"github.com/httprunner/httprunner/v5/uixt/ai"
|
||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||
)
|
||
|
||
type ScreenAnalysis struct {
|
||
Content string `json:"content"`
|
||
Thought string `json:"thought"`
|
||
Elements []string `json:"elements"`
|
||
Categories []string `json:"categories"`
|
||
Count int `json:"count"`
|
||
}
|
||
|
||
func main() {
|
||
ctx := context.Background()
|
||
|
||
// 创建查询器
|
||
modelConfig, err := ai.GetModelConfig(option.OPENAI_GPT_4O)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
querier, err := ai.NewQuerier(ctx, modelConfig)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
// 加载截图
|
||
screenshot, size, err := builtin.LoadImage("screenshot.png")
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
// 执行结构化查询
|
||
result, err := querier.Query(ctx, &ai.QueryOptions{
|
||
Query: "分析截图中的UI元素,提取元素类型和分类信息",
|
||
Screenshot: screenshot,
|
||
Size: size,
|
||
OutputSchema: ScreenAnalysis{},
|
||
})
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
// 使用结构化数据
|
||
if analysis, ok := result.Data.(*ScreenAnalysis); ok {
|
||
fmt.Printf("发现 %d 个元素\n", analysis.Count)
|
||
fmt.Printf("元素类型: %v\n", analysis.Elements)
|
||
fmt.Printf("分类: %v\n", analysis.Categories)
|
||
} else {
|
||
fmt.Printf("文本结果: %s\n", result.Content)
|
||
}
|
||
}
|
||
```
|
||
|
||
## 辅助函数
|
||
|
||
对于特殊情况,提供了类型转换辅助函数:
|
||
|
||
```go
|
||
// 手动类型转换(通常不需要)
|
||
converted, err := ai.ConvertQueryResultData[YourType](result)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
```
|
||
|
||
**注意**:使用 `OutputSchema` 时,`Data` 字段已自动转换为正确类型,通常不需要手动调用此函数。
|
||
|
||
## 技术限制
|
||
|
||
- 需要支持结构化输出的AI模型(如 OpenAI GPT-4)
|
||
- 复杂嵌套结构需要清晰的查询指令
|
||
- AI模型可能不总是严格遵循指定格式
|
||
- UI-TARS 模型使用不同的响应格式处理 |