mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +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:
@ -262,6 +262,9 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
|
||||
toolsUsed = append(toolsUsed, tc.Name)
|
||||
pluginName := a.resolveToolPlugin(tc.Name)
|
||||
log.Printf("[agent] executing tool: %s (plugin=%s, id=%s)", tc.Name, pluginName, tc.ID)
|
||||
if tc.RawArguments != "" {
|
||||
log.Printf("[agent] tool %s raw_arguments: %s", tc.Name, truncateStr(tc.RawArguments, 300))
|
||||
}
|
||||
|
||||
sdkTC := sdk.ToolCall{ID: tc.ID, Name: tc.Name, Plugin: pluginName, Arguments: tc.Arguments}
|
||||
stageCtx.ToolCalls = []sdk.ToolCall{sdkTC}
|
||||
@ -402,13 +405,22 @@ func accumulateStream(ctx context.Context, ch <-chan agentAPI.StreamChunk, a *Ag
|
||||
return
|
||||
}
|
||||
if acc.name == "" {
|
||||
log.Printf("[agent] stream tool_call idx=%d flushed with EMPTY name (args=%q) — dropped", idx, truncateStr(acc.argsRaw.String(), 120))
|
||||
delete(accs, idx)
|
||||
return
|
||||
}
|
||||
args, argsOK := parseToolArgsJSON(acc.argsRaw.String())
|
||||
raw := strings.TrimSpace(acc.argsRaw.String())
|
||||
// 空参诊断:区分「上游没发分片」(raw="")、「混拼污染」(解析失败) 与「合法空对象」({})。
|
||||
if !argsOK {
|
||||
log.Printf("[agent] stream tool_call %s (idx=%d) argument fragments invalid JSON: %q", acc.name, idx, truncateStr(raw, 200))
|
||||
} else if raw == "" {
|
||||
log.Printf("[agent] stream tool_call %s (idx=%d) received NO argument fragments", acc.name, idx)
|
||||
}
|
||||
tc := agentAPI.ToolCall{
|
||||
ID: acc.id,
|
||||
Name: acc.name,
|
||||
Arguments: parseToolArgsJSON(acc.argsRaw.String()),
|
||||
Arguments: args,
|
||||
}
|
||||
resp.ToolCalls = append(resp.ToolCalls, tc)
|
||||
delete(accs, idx)
|
||||
@ -446,9 +458,16 @@ func accumulateStream(ctx context.Context, ch <-chan agentAPI.StreamChunk, a *Ag
|
||||
}
|
||||
}
|
||||
|
||||
// 增量 tool call 分片:OpenAI 风格按 index 拼接 id/name/arguments
|
||||
for i, tc := range ck.ToolCalls {
|
||||
idx := i
|
||||
// 增量 tool call 分片:OpenAI 风格按 index 字段拼接 id/name/arguments。
|
||||
// 注意必须用分片自带的 StreamIndex(上游 JSON "index"),不能用 Go
|
||||
// range 序号:每个 SSE chunk 通常只含一个 tool_call 元素,slice 序号
|
||||
// 恒为 0,并行多工具调用(index=0,1,2...)的分片会全部污染到同一个桶,
|
||||
// 导致 name 相互覆盖、args 碎片混拼解析失败(空参数工具调用)。
|
||||
for _, tc := range ck.ToolCalls {
|
||||
idx := tc.StreamIndex
|
||||
if idx == 0 && tc.Name == "" && tc.RawArguments == "" {
|
||||
continue
|
||||
}
|
||||
acc := accs[idx]
|
||||
if acc == nil {
|
||||
acc = &toolCallAcc{}
|
||||
@ -483,16 +502,17 @@ func accumulateStream(ctx context.Context, ch <-chan agentAPI.StreamChunk, a *Ag
|
||||
}
|
||||
|
||||
// parseToolArgsJSON 将经过完整拼接的 tool call arguments JSON 字符串解析为 map。
|
||||
// 空字符串返回空 map。
|
||||
func parseToolArgsJSON(s string) map[string]interface{} {
|
||||
if s == "" {
|
||||
return map[string]interface{}{}
|
||||
// 第二个返回值 ok=false 表示分片拼接结果不是合法 JSON(分片污染/丢失),
|
||||
// 与「合法的空对象 {}」相区分。
|
||||
func parseToolArgsJSON(s string) (map[string]interface{}, bool) {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return map[string]interface{}{}, true
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(s), &m); err == nil && m != nil {
|
||||
return m
|
||||
return m, true
|
||||
}
|
||||
return map[string]interface{}{}
|
||||
return map[string]interface{}{}, false
|
||||
}
|
||||
|
||||
func convertToolCalls(tcs []agentAPI.ToolCall) []sdk.ToolCall {
|
||||
|
||||
Reference in New Issue
Block a user