mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
fix(llm): max_tokens 截断不再静默降级成空参数
长参数工具调用(整段脚本/大 JSON)被 core.llm.max_tokens 从中间切断时, 上游回 finish_reason=length,而旧实现把这个信号整个丢掉:残缺 JSON 解析失败 后静默降级成空 map,工具只看到参数为空并报 'path is required'。模型因此完全 看不出真因,原样重试四遍、次次撞同一堵墙(2026-09-19 实测 4 次 files_write 失败)。 注:files_read 并未失败——是写挂之后模型反复重写把读卷进同一轮,看起来像两者都报错。 改法: - finish_reason=length 时不再静默降级,改为塞入 __truncated_error 指引, 告诉模型「参数被截断 + 请拆成多次调用/追加写 + 勿原样重试」; - executeToolCallInner 见到该标记即短路,不拿空参数去调工具; - 非截断的残缺 JSON 保持旧行为(避免把「厂商不回 finish_reason」误判成截断)。 回归测试 2 条钉死这两面。
This commit is contained in:
@ -2,6 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
@ -26,7 +27,7 @@ func TestAccumulateStreamToolCalls(t *testing.T) {
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
resp, err := accumulateStream(context.Background(), ch, nil, "cli")
|
||||
resp, err := accumulateStream(context.Background(), ch, nil, "cli", 4096)
|
||||
if err != nil {
|
||||
t.Fatalf("accumulateStream: %v", err)
|
||||
}
|
||||
@ -56,7 +57,7 @@ func TestAccumulateStreamContent(t *testing.T) {
|
||||
ch <- agentAPI.StreamChunk{Done: true, FinishReason: "stop"}
|
||||
close(ch)
|
||||
}()
|
||||
resp, err := accumulateStream(context.Background(), ch, nil, "cli")
|
||||
resp, err := accumulateStream(context.Background(), ch, nil, "cli", 4096)
|
||||
if err != nil {
|
||||
t.Fatalf("accumulateStream: %v", err)
|
||||
}
|
||||
@ -67,3 +68,69 @@ func TestAccumulateStreamContent(t *testing.T) {
|
||||
t.Fatalf("reasoning: %q", resp.ReasoningContent)
|
||||
}
|
||||
}
|
||||
|
||||
// 回归(2026-09-19 实测事故):长参数工具调用被 max_tokens 从中间截断时,
|
||||
// 上游发 finish_reason="length"、参数 JSON 残缺。旧实现把残缺 JSON 静默降级成
|
||||
// 空 map,工具只报 "path is required",模型看不出真因、原样重试四次。
|
||||
//
|
||||
// 本测试钉死:截断必须变成带指引的 __truncated_error,而不是空参数。
|
||||
func TestAccumulateStreamTruncatedArgsSurfaced(t *testing.T) {
|
||||
ch := make(chan agentAPI.StreamChunk, 10)
|
||||
go func() {
|
||||
ch <- agentAPI.StreamChunk{ToolCalls: []agentAPI.ToolCall{
|
||||
{ID: "call_1", Name: "files_write", RawArguments: `{"path":"/tmp/a.py","content":"# -*- coding`},
|
||||
}}
|
||||
// 参数写到一半被切断,随后到达 length 终止块
|
||||
ch <- agentAPI.StreamChunk{ToolCalls: []agentAPI.ToolCall{
|
||||
{RawArguments: `: utf-8 -*-\nimport openpyxl\nfor i in range(80):\n w`},
|
||||
}}
|
||||
ch <- agentAPI.StreamChunk{Done: true, FinishReason: "length"}
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
resp, err := accumulateStream(context.Background(), ch, nil, "cli", 4096)
|
||||
if err != nil {
|
||||
t.Fatalf("accumulateStream: %v", err)
|
||||
}
|
||||
if len(resp.ToolCalls) != 1 {
|
||||
t.Fatalf("want 1 tool call, got %d", len(resp.ToolCalls))
|
||||
}
|
||||
msg, ok := resp.ToolCalls[0].Arguments["__truncated_error"].(string)
|
||||
if !ok || msg == "" {
|
||||
t.Fatalf("截断的参数必须带 __truncated_error,实际 Arguments=%v", resp.ToolCalls[0].Arguments)
|
||||
}
|
||||
// 指引必须可执行:说出真因(截断/max_tokens)并给出拆小方案
|
||||
for _, want := range []string{"截断", "max_tokens=4096", "拆成多次调用"} {
|
||||
if !strings.Contains(msg, want) {
|
||||
t.Errorf("指引缺少 %q:%s", want, msg)
|
||||
}
|
||||
}
|
||||
// 截断时绝不能把残缺 JSON 解析出的空 map 当参数交出去
|
||||
if _, hasPath := resp.ToolCalls[0].Arguments["path"]; hasPath {
|
||||
t.Error("截断参数不应残留任何可用字段(否则会以残缺参数执行)")
|
||||
}
|
||||
}
|
||||
|
||||
// 非截断的残缺 JSON 保持旧行为(静默降级成空 map,由工具自己的必填校验报错):
|
||||
// 这样不会把「厂商不回 finish_reason」的流也误判成截断。
|
||||
func TestAccumulateStreamInvalidArgsNotFlaggedAsTruncated(t *testing.T) {
|
||||
ch := make(chan agentAPI.StreamChunk, 10)
|
||||
go func() {
|
||||
ch <- agentAPI.StreamChunk{ToolCalls: []agentAPI.ToolCall{
|
||||
{ID: "call_1", Name: "files_write", RawArguments: `{"path":`},
|
||||
}}
|
||||
ch <- agentAPI.StreamChunk{Done: true, FinishReason: "tool_calls"}
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
resp, err := accumulateStream(context.Background(), ch, nil, "cli", 4096)
|
||||
if err != nil {
|
||||
t.Fatalf("accumulateStream: %v", err)
|
||||
}
|
||||
if len(resp.ToolCalls) != 1 {
|
||||
t.Fatalf("want 1 tool call, got %d", len(resp.ToolCalls))
|
||||
}
|
||||
if _, ok := resp.ToolCalls[0].Arguments["__truncated_error"]; ok {
|
||||
t.Error("finish_reason=tool_calls 时不应标记为截断")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user