fix(cloud-agent): 工具失败归类与可行动字段
工具失败此前只有一句自然语言(tool_failed + text),无法区分"模型自己出的错"和真实的 权限/状态/上游问题:模型拿不到可自纠的反馈,排查时分不清该改提示词、改权限还是等上游。 - 新增 backend/internal/app/cloud_agent_tool_error_class.go:把失败归成 invalid_model_output(幻觉工具名;本轮契约之外、只被 contextScope 挡掉的调用)、 schema_error(缺必填字段/类型错/未知字段/非法 JSON)、state_conflict(画布在读取之后被改动)、 permission_violation(只读轮发起写入等真实授权边界)、upstream_failure(上游 4xx/5xx、 超时、媒体上游拒绝),其余归 tool_error。 - 每类附 retryable 与稳定的 requiredAction(fix_arguments / reread_canvas / use_advertised_tools / ask_user / report_to_user),写进 tool_failed 的回执与载荷顶层。 只加标注:没有任何放行或拒绝逻辑读它。 - 缺必填字段的通用判定:各工具的校验点抛的多是普通 AppError,从错误类型看不出"模型漏了字段"。 现在用本轮实际暴露的 schema 的 required 与参数比对,命中即按参数契约错误处理(回传 schema 与 guidance 供模型自纠);只在业务侧确实按参数问题拒绝(400/422)时套用,已分类的参数错误不套壳。 - 幻觉工具名的拒绝文案更具体:过去一律"工具未获本轮权限授权",现在指向"不存在的工具 + 只使用 本轮工具表里列出的工具"。 - 前端工具卡失败时显示归类标签,比统一的"执行失败"更能说明下一步该做什么。
This commit is contained in:
@@ -950,6 +950,20 @@ func cloudAgentToolResult(runID string, state *cloudAgentRuntime, call cloudAgen
|
||||
if !ok || detail == nil {
|
||||
detail = map[string]any{}
|
||||
}
|
||||
// 模型漏了必填字段(或参数不是对象)时,即便业务校验抛的是普通 AppError,也按
|
||||
// "参数契约错误"处理:把本轮实际暴露的 schema 回给模型,它才改得对。
|
||||
// 只在业务侧确实按参数问题拒绝(400/422)时套用:上游 5xx 或网络错误即便同时漏字段,
|
||||
// 也该算上游故障,别把锅扣到参数上。已分类的参数错误不再套壳,避免覆盖更精确的反馈。
|
||||
var appErr *AppError
|
||||
var existingArgumentErr *cloudAgentArgumentError
|
||||
if missing := cloudAgentMissingRequiredArguments(state.Canonical.Tools, call); len(missing) > 0 &&
|
||||
!errors.As(err, &existingArgumentErr) &&
|
||||
errors.As(err, &appErr) && appErr != nil && (appErr.Status == 400 || appErr.Status == 422) {
|
||||
err = &cloudAgentFieldArgumentError{
|
||||
error: &cloudAgentArgumentError{err},
|
||||
Field: strings.Join(missing, ","), Issue: "required",
|
||||
}
|
||||
}
|
||||
message := cloudAgentSafeToolError(err)
|
||||
detail["error"] = message
|
||||
var admissionErr *cloudAgentMediaAdmissionError
|
||||
@@ -979,6 +993,16 @@ func cloudAgentToolResult(runID string, state *cloudAgentRuntime, call cloudAgen
|
||||
detail["exampleArguments"] = map[string]any{"snapshotHash": "<canvas_get_state.snapshotHash>", "ops": []any{map[string]any{"type": "add_node", "id": "<new-node-id>", "nodeType": "text", "content": "<content>"}}}
|
||||
}
|
||||
}
|
||||
// 稳定归类 + 可行动字段:只加标注,不改任何放行/拒绝判定。
|
||||
// allowed 与执行器用的是同一份判定(cloudAgentToolAllowed 是纯函数,结果一致)。
|
||||
if class, retryable, requiredAction := cloudAgentToolErrorClass(state.Request, call, err, cloudAgentToolAllowed(state.Request, call.Function.Name)); class != "" {
|
||||
detail["errorClass"], detail["errorClassLabel"] = class, cloudAgentToolErrorLabel(class)
|
||||
detail["retryable"] = retryable
|
||||
if requiredAction != "" {
|
||||
detail["requiredAction"] = requiredAction
|
||||
}
|
||||
payload["errorClass"] = class
|
||||
}
|
||||
result = detail
|
||||
kind = "tool_failed"
|
||||
payload["text"] = message
|
||||
@@ -1173,7 +1197,13 @@ func (s *Service) advanceCloudAgentTool(run *model.CloudAgentExecution, state *c
|
||||
var toolErr error
|
||||
switch {
|
||||
case !allowed:
|
||||
toolErr = BadAuthRequest("工具未获本轮权限授权")
|
||||
// 幻觉出来的工具名与"真被权限挡住"要分开反馈:前者模型根本不该发这个调用,
|
||||
// 后者是授权边界问题。两者仍然一律拒绝,只是给模型的下一步更明确。
|
||||
if !cloudAgentPlatformToolNames()[call.Function.Name] {
|
||||
toolErr = BadAuthRequest("模型调用了不存在的工具「" + truncateRunes(call.Function.Name, 60) + "」,本轮已拒绝;请只使用本轮工具表里列出的工具")
|
||||
} else {
|
||||
toolErr = BadAuthRequest("工具未获本轮权限授权")
|
||||
}
|
||||
case call.Function.Name == "canvas_apply_ops":
|
||||
result, toolErr = applyCloudAgentCanvas(repo, run.UserID, state.Request.CanvasID, call, policy, cloudAgentCanvasEventRecorder(run.ID, state))
|
||||
case call.Function.Name == "canvas_create_storyboard", call.Function.Name == "canvas_edit_storyboard":
|
||||
|
||||
Reference in New Issue
Block a user