Skip to content

Custom Response Models

sungaowei edited this page Oct 16, 2026 · 2 revisions

自定义响应模型 (Custom Response Models)

English Version | 简体中文


SystemOneAs[T] 允许你将评估结果直接解码到自定义的 Go 结构体中,让编译器在编译期掌握每个业务问题答案的精确数据类型:

type TicketAnalysis struct {
	Model    string                  `json:"model"`
	Usage    typesafe.Usage          `json:"usage"`
	Tone     typesafe.ChoiceAnswer   `json:"tone"`     // 自动从 answers["tone"] 提取并提升
	Billing  *typesafe.NoulAnswer    `json:"billing"`  // 可选字段:使用指针表示可能不存在
	Target   *typesafe.BoxAnswer     `json:"target"`   // 支持 Box 目标检测答案
	Summary  string                  `json:"summary,omitempty"`
}

ticket, err := typesafe.SystemOneAs[TicketAnalysis](ctx, client, state, typesafe.Questions{
	"tone":    typesafe.NewChoice(map[string]any{"calm": nil, "angry": nil}),
	"billing": typesafe.NewNoul("该内容是否与账单相关?"),
	"target":  typesafe.NewBox("图片中的目标物体"),
}, typesafe.WithCallImages(true), typesafe.WithModel(typesafe.ModelJevPreview))
if err != nil {
	return err
}

fmt.Printf("tone=%s billing=%.2f\n", ticket.Tone.Choice, ticket.Billing.Noul)
if ticket.Target != nil {
	fmt.Printf("box: Top=%.2f Bottom=%.2f\n", ticket.Target.Box.Top, ticket.Target.Box.Bottom)
}

注意:SystemOneAs 是包级泛型函数而非 Client 的方法,因为 Go 语言规范不允许在结构体的方法上声明独立的类型参数。


答案字段自动提升 (Answer Lifting)

泛型结构体 T 可以声明顶层信封字段(model、usage、answers),并且针对每一个具体问题,都可以按问题名称声明对应的答案结构体字段。

每个同名字段都会自动从 answers 字典中提取对应条目并赋值。

  • 真实的信封字段(如 model、usage)优先级高于同名问题,绝不会被意外覆盖;
  • 本版本未建模或未知的答案类型会被静默跳过,与 client.SystemOne 规则保持一致。

保留完整的答案字典 (typesafe.Answers)

若需要动态遍历或保留以问题名称为键的完整答案集合,可以在结构体中声明 Answers typesafe.Answers \json:"answers"``:

type GenericAnalysis struct {
	Model   string           `json:"model"`
	Usage   typesafe.Usage   `json:"usage"`
	Answers typesafe.Answers `json:"answers"`
}

analysis, err := typesafe.SystemOneAs[GenericAnalysis](ctx, client, state, questions)
if err != nil {
	return err
}

for name, answer := range analysis.Answers {
	switch typed := answer.(type) {
	case *typesafe.NoulAnswer:
		fmt.Printf("%s: p(true)=%.2f\n", name, typed.Noul)
	case *typesafe.ChoiceAnswer:
		fmt.Printf("%s: %s (置信度 %.2f)\n", name, typed.Choice, typed.Confidence)
	case *typesafe.ScoreAnswer:
		fmt.Printf("%s: 评分=%.2f\n", name, typed.Score)
	case *typesafe.BoxAnswer:
		fmt.Printf("%s: 边界框=[%.2f, %.2f, %.2f, %.2f]\n",
			name, typed.Box.Top, typed.Box.Bottom, typed.Box.Left, typed.Box.Right)
	}
}

typesafe.Answers 是 SDK 提供的专用字典类型,它在反序列化时能根据 JSON 中的 "type" 字段("noul"、"choice"、"score"、"box")动态构建正确的具体结构体指针。普通的 map[string]typesafe.Answer 无法做到这一点,因为接口类型无法得知该实例化哪一个具体实现。


必填字段与校验机制

在 T 的顶层字段中,所有字段默认均为必需字段,除非:

  1. 该字段可自然表示“缺省”(如指针 *T、map、slice、interface 等,未出现时为 nil);
  2. 字段被打上了 json:",omitempty" 或 json:",omitzero" 标签。

未打标签的非导出私有字段或匿名嵌入字段不会被强校验。

若服务端返回的响应中缺失了必填的答案字段,SDK 会抛出 *typesafe.APIResponseValidationError,明确指出缺失字段的路径(validationErr.FieldPath),防止程序因反序列化出零值而做出错误的业务判断(例如将缺失的概率字段作为 0.0 判定)。

var validationErr *typesafe.APIResponseValidationError
if errors.As(err, &validationErr) {
	log.Printf("响应缺失必填字段: %s", validationErr.FieldPath)
}

Clone this wiki locally