mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 12:53:35 +00:00
fix(seq): 类型不匹配的错误改成模型可执行的话(真机实跑发现)
真机实跑(独立实例)发现:模型把 group 的 `in` 传成字符串 "{}",
拿到的是 encoding/json 的原始报错:
json: cannot unmarshal string into Go struct field rawSeq.groups.0.in
of type map[string]string
这句说的是**事实**(string 解不成 map)而不是**该怎么做**
(in 应该写成对象 {"键":"类型"});残留的 `rawSeq` / `Go struct field`
更是 Go 内部实现细节,对模型无意义且会误导它去猜一个叫 rawSeq 的东西。
模型为此**重试了 3 次**才改对。
这与本仓反复吃亏的那类问题同源:`20s` 少引号 → 静默降级 →
cmd_run 失败率 34%。**报事实不报改法,模型只能猜。**
改动(parse.go):新增 friendlyJSONError,把原始报错翻译成可执行文案
· in / out 类型不符 ⇒ 说明"应写成对象 {键:类型};无入参请写 {}"
· tools 类型不符 ⇒ 说明"应写成字符串(内容是 ; 分隔的 JSON 对象)"
· groups / name / when / missing / timeout / on_error ⇒ 逐个说明期望
· 未知字段 ⇒ 列出 group 允许的全部字段名(拼写错误最常见)
· shortFieldName 剥掉 `rawSeq` 这类包内类型名前缀
· jsonKind / goTypeName 把 Go 类型翻译成模型看得懂的说法
判据(parse_test.go,2 条):
· in 传字符串 ⇒ 错误须指名字段、须说明该传对象、**且不得残留 Go 内部类型名**
· tools 传数组 ⇒ 错误须指明 tools 且说明它是字符串
★ 变异验证时踩了一次坑:第一次变异让 friendlyJSONError 不被调用,
结果**编译失败**(函数变成未使用),判据压根没跑,我却看到 "ok"。
改用可编译的变异(函数保留、开头直接 return err)后判据正确 FAIL。
★ 教训:**"变异后判据通过"要先确认变异真的生效**——编译失败 ≠ 判据通过。
真机复验:模型读一次即懂,并明确说"提示里的意思很明确";
修复前它为此重试 3 次。
回归:internal/plugins/... internal/agent/core internal/sdk 全绿。
This commit is contained in:
@ -13,7 +13,9 @@ package seq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
@ -105,7 +107,7 @@ func Parse(data []byte) (*Sequence, error) {
|
||||
dec.DisallowUnknownFields()
|
||||
var raw rawSeq
|
||||
if err := dec.Decode(&raw); err != nil {
|
||||
return nil, fmt.Errorf("序列 JSON 解析失败: %w", err)
|
||||
return nil, fmt.Errorf("序列 JSON 解析失败: %w", friendlyJSONError(err))
|
||||
}
|
||||
|
||||
if strings.TrimSpace(raw.Name) == "" {
|
||||
@ -385,3 +387,101 @@ func trunc(s string, n int) string {
|
||||
}
|
||||
return s[:n] + "..."
|
||||
}
|
||||
|
||||
// friendlyJSONError 把 encoding/json 的原始报错翻译成**模型可执行**的话。
|
||||
//
|
||||
// 为什么必须翻译(真机实跑证据):模型把 group 的 `in` 传成字符串 "{}"
|
||||
// 时,原始报错是
|
||||
//
|
||||
// json: cannot unmarshal string into Go struct field rawSeq.groups.0.in
|
||||
// of type map[string]string
|
||||
//
|
||||
// 这句话说的是"事实"(string 解不成 map),不是"该怎么做"
|
||||
// (in 应该写成对象 {"键":"类型"})。模型为此重试了 **3 次**才改对。
|
||||
// 残留的 `rawSeq` / `Go struct field` 更是 Go 内部实现细节,
|
||||
// 对模型无意义且会误导它去猜一个叫 rawSeq 的东西。
|
||||
//
|
||||
// 这与本仓反复吃亏的那类问题同源:`20s` 少引号 → 静默降级 →
|
||||
// cmd_run 失败率 34%。**报事实不报改法,模型只能猜。**
|
||||
func friendlyJSONError(err error) error {
|
||||
// ① 未知字段:拼写错误最常见,且必须显式(DisallowUnknownFields 已启用)
|
||||
if strings.Contains(err.Error(), "unknown field") {
|
||||
return fmt.Errorf("%w(注意字段名拼写;每个 group 允许的字段为 "+
|
||||
"name/description/in/out/when/parallel/missing/timeout/on_error/retries/tools)", err)
|
||||
}
|
||||
|
||||
// ② 类型不匹配:给出该字段**应该**是什么
|
||||
var te *json.UnmarshalTypeError
|
||||
if errors.As(err, &te) {
|
||||
field := shortFieldName(te.Field)
|
||||
switch field {
|
||||
case "in", "out":
|
||||
return fmt.Errorf("group 的 %s 应写成**对象**,形如 {\"键\":\"类型\"}"+
|
||||
"(键=参数名,值=类型如 string/bool/integer/array);"+
|
||||
"收到的是 %s —— 无入参请写 {},不要写成字符串", field, jsonKind(te.Value))
|
||||
case "tools":
|
||||
return fmt.Errorf("group 的 tools 应写成**字符串**(内容是若干以 ';' 分隔的 JSON 对象),"+
|
||||
"而不是数组;例如 \"{\\\"tool\\\":\\\"cmd_run\\\",\\\"args\\\":{},\\\"as\\\":\\\"x\\\"} ;\";"+
|
||||
"收到的是 %s", jsonKind(te.Value))
|
||||
case "groups":
|
||||
return fmt.Errorf("groups 应是数组,形如 [ {…}, {…} ];收到的是 %s", jsonKind(te.Value))
|
||||
case "name", "description", "when", "missing", "timeout", "on_error":
|
||||
return fmt.Errorf("%s 应写成字符串;收到的是 %s", field, jsonKind(te.Value))
|
||||
default:
|
||||
return fmt.Errorf("%s 的类型不对(应为 %s,收到 %s)",
|
||||
field, goTypeName(te.Type), jsonKind(te.Value))
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// shortFieldName 把 "rawSeq.groups.0.in" 缩成 "in"。
|
||||
//
|
||||
// 剥掉 Go 内部类型名(rawSeq):那是本包的实现细节,模型无从得知,
|
||||
// 照抄反而会去猜"rawSeq 是什么"。
|
||||
func shortFieldName(f string) string {
|
||||
if f == "" {
|
||||
return "某个字段"
|
||||
}
|
||||
if i := strings.LastIndex(f, "."); i >= 0 {
|
||||
f = f[i+1:]
|
||||
}
|
||||
// 去掉数组下标:groups.0.in → in(上面已取最后一段,兜底再剥一次)
|
||||
f = strings.TrimSuffix(f, "]")
|
||||
return f
|
||||
}
|
||||
|
||||
// jsonKind 描述**收到的** JSON 值长什么样(用模型看得懂的说法)。
|
||||
func jsonKind(v string) string {
|
||||
switch v {
|
||||
case "string":
|
||||
return "字符串"
|
||||
case "array":
|
||||
return "数组"
|
||||
case "object":
|
||||
return "对象"
|
||||
case "number":
|
||||
return "数字"
|
||||
case "bool":
|
||||
return "布尔值"
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// goTypeName 去掉包名前缀(map[string]string → map)。
|
||||
func goTypeName(t reflect.Type) string {
|
||||
if t == nil {
|
||||
return "未知"
|
||||
}
|
||||
if t.Kind() == reflect.Map {
|
||||
return "对象"
|
||||
}
|
||||
if t.Kind() == reflect.Slice {
|
||||
return "数组"
|
||||
}
|
||||
s := t.String()
|
||||
if i := strings.LastIndex(s, "."); i >= 0 {
|
||||
s = s[i+1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@ -272,3 +272,55 @@ func TestBracesInsideStringDoNotAffectDepth(t *testing.T) {
|
||||
t.Errorf("command 被破坏: %q(期望 %q)", got, command)
|
||||
}
|
||||
}
|
||||
|
||||
// ①' ★ 字段类型不匹配的错误必须**可执行**(真机实跑发现的缺口)。
|
||||
//
|
||||
// 实测:模型把 `in` 传成字符串 "{}",拿到的是 encoding/json 的原始报错:
|
||||
//
|
||||
// j 序列 JSON 解析失败: json: cannot unmarshal string into Go struct
|
||||
// field rawSeq.groups.0.in of type map[string]string
|
||||
//
|
||||
// 这条文案对模型**不可执行**:它说"string 不能解到 map",却没说
|
||||
// "`in` 应该是对象 {键:类型}"。模型为此重试了 **3 次**才改对。
|
||||
//
|
||||
// 这正是本仓反复吃亏的那类问题(`20s` 少引号 → 静默降级 → 34% 失败率):
|
||||
// 报"事实"而不报"该怎么做",模型只能猜。
|
||||
func TestParseTypeErrorIsActionable(t *testing.T) {
|
||||
// in 传字符串(模型最常犯:以为能写 "{}")
|
||||
bad := `{"name":"t","groups":[{"name":"g","in":"{}","out":{"x":"string"},` +
|
||||
`"tools":"{\"tool\":\"cmd_run\",\"args\":{},\"as\":\"x\"} ;"}]}`
|
||||
_, err := Parse([]byte(bad))
|
||||
if err == nil {
|
||||
t.Fatal("in 传字符串却通过了")
|
||||
}
|
||||
msg := err.Error()
|
||||
// 必须指出**哪个字段**
|
||||
if !strings.Contains(msg, "in") {
|
||||
t.Errorf("错误应指明出错的字段 in,实际: %s", msg)
|
||||
}
|
||||
// 必须说明**该传什么**
|
||||
if !strings.Contains(msg, "对象") && !strings.Contains(msg, "{") {
|
||||
t.Errorf("错误应说明该传对象(如 {\"键\":\"类型\"}),实际: %s", msg)
|
||||
}
|
||||
// 必须剥掉 Go 内部类型名 rawSeq(那是实现细节,对模型无意义且误导)
|
||||
if strings.Contains(msg, "rawSeq") || strings.Contains(msg, "Go struct field") {
|
||||
t.Errorf("错误里残留 Go 内部类型信息(模型无法据此行动): %s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// ②' 同类问题:tools 传数组而非字符串(模型可能照直觉传数组)。
|
||||
func TestParseToolsTypeErrorIsActionable(t *testing.T) {
|
||||
bad := `{"name":"t","groups":[{"name":"g","in":{},"out":{"x":"string"},` +
|
||||
`"tools":[{"tool":"cmd_run","args":{},"as":"x"}]}]}`
|
||||
_, err := Parse([]byte(bad))
|
||||
if err == nil {
|
||||
t.Fatal("tools 传数组却通过了")
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "tools") {
|
||||
t.Errorf("错误应指明 tools 字段,实际: %s", msg)
|
||||
}
|
||||
if !strings.Contains(msg, "字符串") {
|
||||
t.Errorf("错误应说明 tools 是**字符串**(内含 ; 分隔的 JSON 对象),实际: %s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user