From bd0f84c1f7d35c26b197044c9205430cfa883e1d Mon Sep 17 00:00:00 2001 From: root Date: Wed, 22 Jul 2026 17:05:04 +0800 Subject: [PATCH] fix: deduplicate assistant content in multi-tool turns to prevent premature loop exit - process.go: only emit resp.Content on the first tool call per batch, subsequent assistant messages use empty content (serialized as null) - provider.go: MarshalJSON outputs null content when empty with tool_calls to comply with DeepSeek/OpenAI expected format - output.go: update parameter interface (payload/type/meta) to match tool definitions (uncommitted from previous refactor) --- internal/agent/api/provider.go | 4 ++- internal/agent/core/output.go | 47 +++++++++++++++++++++++----------- internal/agent/core/process.go | 12 ++++++--- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/internal/agent/api/provider.go b/internal/agent/api/provider.go index 1c66139..ecc6573 100644 --- a/internal/agent/api/provider.go +++ b/internal/agent/api/provider.go @@ -47,8 +47,10 @@ func (m Message) MarshalJSON() ([]byte, error) { } if len(m.Blocks) > 0 { raw["content"] = m.Blocks - } else { + } else if m.Content != "" || len(m.ToolCalls) == 0 { raw["content"] = m.Content + } else { + raw["content"] = nil } if m.ReasoningContent != "" { raw["reasoning_content"] = m.ReasoningContent diff --git a/internal/agent/core/output.go b/internal/agent/core/output.go index b203c17..bfbb0ae 100644 --- a/internal/agent/core/output.go +++ b/internal/agent/core/output.go @@ -11,25 +11,46 @@ import ( func (a *Agent) executeOutputSendTool(tc agentAPI.ToolCall) string { channel := strings.TrimPrefix(tc.Name, "output_send__") - if channel == "" { - return "工具名称格式: output_send__{channel}" - } - content, _ := tc.Arguments["content"].(string) - if content == "" { - return "content 不能为空" + payload, _ := tc.Arguments["payload"].(string) + rawType, _ := tc.Arguments["type"].(string) + if channel == "" || payload == "" || rawType == "" { + return "工具名称格式: output_send__{channel},payload 和 type 不能为空" } + meta, _ := tc.Arguments["meta"].(string) caps := a.io.GetChannelCapabilities(channel) if caps == 0 { return fmt.Sprintf("通道 [%s] 不存在或不可用。可用输出工具列表见 output_list_channels", channel) } + switch rawType { + case "text": + if !caps.Supports(agentIO.CapText) { + return fmt.Sprintf("通道 [%s] 不支持文本输出(能力: %s)", channel, caps.String()) + } + case "voice", "audio": + if !caps.Supports(agentIO.CapAudio) { + return fmt.Sprintf("通道 [%s] 不支持语音输出(能力: %s)", channel, caps.String()) + } + case "image": + if !caps.Supports(agentIO.CapImage) { + return fmt.Sprintf("通道 [%s] 不支持图片输出(能力: %s)", channel, caps.String()) + } + case "file": + if !caps.Supports(agentIO.CapFile) { + return fmt.Sprintf("通道 [%s] 不支持文件输出(能力: %s)", channel, caps.String()) + } + } - if !caps.Supports(agentIO.CapText) { - return fmt.Sprintf("通道 [%s] 不支持文本输出(能力: %s)", channel, caps.String()) + args := map[string]interface{}{ + "payload": payload, + "type": rawType, + } + if meta != "" { + args["meta"] = meta } stageCtx := &sdk.StageContext{ - FinalText: content, + FinalText: payload, Phase: sdk.StageBeforeOutput, } a.runStage(sdk.StageBeforeOutput, stageCtx) @@ -39,11 +60,7 @@ func (a *Agent) executeOutputSendTool(tc agentAPI.ToolCall) string { if stageCtx.FinalText == "" { return "输出被插件清空" } - - args := map[string]interface{}{ - "payload": stageCtx.FinalText, - "type": "text", - } + args["payload"] = stageCtx.FinalText if dev := a.io.GetDevice(channel); dev != nil { result, err := dev.Execute("output", args) @@ -53,7 +70,7 @@ func (a *Agent) executeOutputSendTool(tc agentAPI.ToolCall) string { return fmt.Sprintf("已通过 [%s] 通道发送: %v", channel, result) } - a.io.EmitTextTo("agent_io", channel, stageCtx.FinalText) + a.io.EmitTextTo("agent_io", channel, payload) return fmt.Sprintf("已通过 [%s] 通道发送", channel) } diff --git a/internal/agent/core/process.go b/internal/agent/core/process.go index 6a7ca32..9417410 100644 --- a/internal/agent/core/process.go +++ b/internal/agent/core/process.go @@ -179,6 +179,7 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri return resp.Content, toolsUsed, nil } + contentOnce := true for _, tc := range resp.ToolCalls { if len(a.interceptCh) > 0 { for _, interrupt := range a.drainInterrupts() { @@ -203,7 +204,7 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri stageCtx.ToolResults = nil if a.runStage(sdk.StageBeforeToolcall, stageCtx) { result := fmt.Sprintf("工具 %s 已被插件拒绝", tc.Name) - msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{tc}}) + msgs = append(msgs, agentAPI.Message{Role: "assistant", ToolCalls: []agentAPI.ToolCall{tc}}) msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result}) a.publishEvent(events.EventToolCall, map[string]interface{}{ "tool": tc.Name, @@ -219,7 +220,7 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri if pluginName != "" && !a.pluginHealth.isHealthy(pluginName) { result := fmt.Sprintf("插件 %s 处于崩溃状态,已跳过执行,等待自动恢复重载", pluginName) log.Printf("[agent] skip tool %s: plugin %s unhealthy", tc.Name, pluginName) - msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{tc}}) + msgs = append(msgs, agentAPI.Message{Role: "assistant", ToolCalls: []agentAPI.ToolCall{tc}}) msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result}) continue } @@ -238,7 +239,12 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri argsJSON, _ := json.Marshal(tc.Arguments) a.recordToolCall(tc.Name, string(argsJSON), result) - msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{tc}}) + msgContent := "" + if contentOnce { + msgContent = resp.Content + contentOnce = false + } + msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: msgContent, ToolCalls: []agentAPI.ToolCall{tc}}) msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: tc.ID, Content: result}) a.publishEvent(events.EventToolCall, map[string]interface{}{