From 9eebd96ab7ace40931132fa7c10721023fe5e981 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 16:45:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20=E6=8F=92=E4=BB=B6=E6=8B=92?= =?UTF-8?q?=E7=BB=9D=E5=B7=A5=E5=85=B7=E6=97=B6=E6=8A=8A=20ctx.Response=20?= =?UTF-8?q?=E7=9A=84=E7=90=86=E7=94=B1=E9=80=8F=E7=BB=99=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit before_toolcall 的 ctx.Response 是插件写的**拒绝理由**,但工具结果被写死成 「工具 X 已被插件拒绝」,理由从不到达模型——模型于是不知道能不能重试, 会反复重试被拒的调用。抽出 denialResultText 并在有理由时原样透出。 --- internal/agent/core/task.go | 16 +++++++++++++++- internal/agent/core/tooloop_test.go | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/internal/agent/core/task.go b/internal/agent/core/task.go index d46595f..0faad80 100644 --- a/internal/agent/core/task.go +++ b/internal/agent/core/task.go @@ -671,7 +671,7 @@ func (a *Agent) stepToolBegin(f *TaskFrame) stepOutcome { f.StageCtx.ToolCalls = []sdk.ToolCall{sdkTC} f.StageCtx.ToolResults = nil if a.runStage(sdk.StageBeforeToolcall, f.StageCtx) { - result := fmt.Sprintf("工具 %s 已被插件拒绝", tc.Name) + result := denialResultText(f.StageCtx, tc.Name) f.Msgs = append(f.Msgs, agentAPI.Message{Role: "assistant", ToolCalls: []agentAPI.ToolCall{tc}}) f.Msgs = append(f.Msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result}) a.publishEvent(events.EventToolCall, map[string]interface{}{ @@ -702,6 +702,20 @@ func (a *Agent) stepToolBegin(f *TaskFrame) stepOutcome { return outcomeContinue } +// denialResultText 返回「工具被插件拒绝」时交给模型的工具结果。 +// +// 插件在 before_toolcall 里用 ctx.Response 写的是**拒绝理由**(为什么被拒、 +// 能不能重试)。此前这里一律丢成通用文案,模型看不到原因就会反复重试同一个 +// 调用——权限门精心写的"请不要重试"等于白写。有理由就用理由。 +func denialResultText(ctx *sdk.StageContext, toolName string) string { + if ctx != nil && ctx.Response != nil { + if reason := strings.TrimSpace(*ctx.Response); reason != "" { + return reason + } + } + return fmt.Sprintf("工具 %s 已被插件拒绝", toolName) +} + // stepToolExec 执行工具。**临界区**:见设计文档 §4.3。 func (a *Agent) stepToolExec(f *TaskFrame) stepOutcome { result := a.executeToolCall(f.CurTool, f.OutputChannel) diff --git a/internal/agent/core/tooloop_test.go b/internal/agent/core/tooloop_test.go index 005b375..7bd6c4a 100644 --- a/internal/agent/core/tooloop_test.go +++ b/internal/agent/core/tooloop_test.go @@ -5,6 +5,7 @@ import ( "testing" agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api" + sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk" ) // appendPlaceholder 复刻 process() 循环顶部的补位逻辑。 @@ -115,3 +116,23 @@ func TestIsOutputDeliveryTool(t *testing.T) { } } } + +// 插件在 before_toolcall 里给出的拒绝理由必须原样进入工具结果。 +// 若被通用文案覆盖,模型不知道「为什么被拒 / 能不能重试」,会反复重试同一个调用。 +func TestDenialReasonReachesModel(t *testing.T) { + reason := "QQ 权限策略拒绝私人资源工具 calendar_list;请不要重试" + ctx := &sdk.StageContext{Response: &reason} + if got := denialResultText(ctx, "calendar_list"); got != reason { + t.Fatalf("拒绝理由被丢弃,实际: %q", got) + } + + // 插件没给理由时退回通用文案(保持既有行为)。 + if got := denialResultText(&sdk.StageContext{}, "calendar_list"); !strings.Contains(got, "已被插件拒绝") { + t.Fatalf("无理由时应退回通用文案,实际: %q", got) + } + // 空白理由不算理由。 + blank := " " + if got := denialResultText(&sdk.StageContext{Response: &blank}, "x"); !strings.Contains(got, "已被插件拒绝") { + t.Fatalf("空白理由应退回通用文案,实际: %q", got) + } +}