mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
fix(agent): 流式并行 tool_call 按 JSON index 分桶,修复空参数调用
【根因】内核流式解析层丢弃了上游 SSE 分片的 OpenAI index 字段:
- openAIToolCall 结构体无 index 字段,JSON 解析即丢
- homed 的 openai.lua 转换为扁平结构时同样未透传 index
- accumulateStream 退而用 Go range slice 序号做累积桶 key,
但每个 SSE chunk 只含一个 tool_call 元素,序号恒为 0
于是并行多工具调用(index=0,1,2,3)的所有分片全部写入同一个桶:
name 相互覆盖、args 碎片混拼成非法 JSON → parseToolArgsJSON
失败返回空 map → 工具以空参数被调用(spawn_child 报'请提供 task'、
cmd_run 报'command is required'等),agent 只能串行重试自愈。
单工具场景只有一个 index 无污染,故简单请求一直正常;
pi 直连同一 llmsproxy 正常(其实现标准按 index 累积)。
【修复】
- ToolCall 增加 StreamIndex(json:stream_index),openAIToolCall
解析上游 index 并透传;openai.lua 输出 stream_index 字段
- accumulateStream 以 tc.StreamIndex 为累积 key
- flushToolCall 区分三种空参:未收到分片/碎片非合法 JSON/合法空
对象({}),分别打诊断日志,避免误报
- 回归测试 TestAccumulateStreamParallelToolCallsByIndex 模拟
4 路并行分片流验证按 index 正确分组与参数完整性
另含 spawn_child max_turns 参数、child_result 运行中状态区分、
provider 层非流式空参诊断日志。
This commit is contained in:
@ -7,6 +7,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
@ -191,6 +193,10 @@ type ToolCall struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
RawArguments string `json:"raw_arguments,omitempty"` // 流式分片原始 JSON 字符串
|
||||
// StreamIndex 是上游流式 tool_call 的 OpenAI index 字段(并行多工具调用
|
||||
// 时同一轮的分片用它区分归属)。lua 适配器以 stream_index 键透传;
|
||||
// 仅内核流式累积内部使用,不序列化到对外 API。
|
||||
StreamIndex int `json:"stream_index,omitempty"`
|
||||
}
|
||||
|
||||
type apiToolCall struct {
|
||||
@ -403,6 +409,14 @@ func (p *LuaAdaptedProvider) Chat(ctx context.Context, req *CompletionRequest) (
|
||||
return nil, fmt.Errorf("unmarshal unified response: %w (body: %s)", err, unifiedJSON)
|
||||
}
|
||||
|
||||
// 诊断:tool_calls 存在但参数为空——上游/适配器丢参数,打印原始响应片段定位
|
||||
for _, tc := range result.ToolCalls {
|
||||
if len(tc.Arguments) == 0 && tc.RawArguments == "" {
|
||||
log.Printf("[provider:%s] tool_call %s (%s) has empty arguments; raw body head: %s",
|
||||
p.name, tc.Name, tc.ID, string(rawResp[:min(len(rawResp), 400)]))
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@ -551,6 +565,7 @@ func parseOpenAICompatibleSSEBody(raw []byte) (*CompletionResponse, bool) {
|
||||
type openAIToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Index int `json:"index"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments interface{} `json:"arguments"`
|
||||
@ -584,6 +599,7 @@ func normalizeOpenAIToolCalls(raw []openAIToolCall) []ToolCall {
|
||||
Name: name,
|
||||
Arguments: parseToolArguments(argsRaw),
|
||||
RawArguments: rawArgsString(argsRaw),
|
||||
StreamIndex: tc.Index,
|
||||
})
|
||||
}
|
||||
return out
|
||||
@ -629,6 +645,7 @@ func normalizeStreamToolCalls(raw []openAIToolCall) []ToolCall {
|
||||
Type: typ,
|
||||
Name: name,
|
||||
RawArguments: rawArgsString(argsRaw),
|
||||
StreamIndex: tc.Index,
|
||||
})
|
||||
}
|
||||
return out
|
||||
@ -856,6 +873,7 @@ func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequ
|
||||
}
|
||||
}
|
||||
|
||||
debugSSE := os.Getenv("HOMED_DEBUG_SSE") == "1"
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || !strings.HasPrefix(line, "data:") {
|
||||
@ -865,6 +883,9 @@ func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequ
|
||||
if data == "" {
|
||||
continue
|
||||
}
|
||||
if debugSSE && strings.Contains(data, "tool_calls") {
|
||||
log.Printf("[provider:%s] SSE raw tool_call line: %s", p.name, truncateForLog(data, 400))
|
||||
}
|
||||
if data == "[DONE]" {
|
||||
if !doneSent {
|
||||
if !emit(StreamChunk{Done: true}) {
|
||||
@ -1208,3 +1229,11 @@ func getFloat(m map[string]interface{}, key string) float64 {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// truncateForLog 诊断日志用截断。
|
||||
func truncateForLog(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n] + "..."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user