mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
fix: 流式渲染回合生命周期 + LLM 瞬断重试与 SSE body 兜底
问题一(webui 不是真流式): - sendChat 的 finally 在 POST 结束(15s ackTimer abort)时就复位 chatLoading,但 agent 生成窗口 15~190s,后续 SSE delta 全部走 全量重建路径、停止按钮提前消失、用户误发重复消息。 - GUI app.js 完全没有 content_delta/reasoning_delta 监听器, 只能等聚合帧一次性显示。 修复:三端统一回合生命周期——POST 只是触发,收尾由 SSE 驱动: - dashboard/GUI 新增 endChatTurn/armTurnWatchdog;拿到同步兜底 响应立即收尾,否则保持回合打开等 agent_output final / reset 帧 / 120s watchdog 兜底 - GUI 补齐 delta 监听器;agent_output 聚合分支 += 改覆盖; reasoning 聚合帧改覆盖(多轮工具调用时旧逻辑会重复累加) - agent_output 误杀分支(final 无 source 即 return 丢弃新输出) 改为内容比较去重,多轮连发时新一轮回复不再被吞 - waiter reasoning_delta reset 从清空全部消息改为 sealLastAgent 问题二(三条只成功一条): - handleChat 60s ctx 含排队时间,agent 串行处理下第 N 条必超时 (实测第 3 条 62s 超时 504);放宽到 300s(客户端 abort 时立即取消) - LLM 单 provider 瞬断无重试:process.go provider 循环内加同源 重试(2 次、退避 2s),401/403 凭证错误与用户中断不重试 - llmsproxy auto 链在非流式请求下可能返回 SSE body(上游恢复后 吐已生成的 chunk 流),非流式解析报 invalid character 'd' 丢掉 整段回复;新增 parseOpenAICompatibleSSEBody 拼接为完整响应 - 顺带修 normalizeStreamToolCalls 分片续传 bug:name 不重发时 argsRaw 被顶层 Arguments(nil) 覆盖丢失 function.arguments 验证: - 连发 3 条 + 单条共 4 条全部成功(首条 190s 重试扛住瞬断) - sse_body_test.go 锁定 SSE body 解析契约(content/usage/tool call 分片)
This commit is contained in:
@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
|
||||
@ -116,28 +117,61 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
|
||||
fbProvider.Name(), pi, len(providers)-1)
|
||||
}
|
||||
|
||||
fCtx, fCancel := context.WithCancel(a.ctx)
|
||||
a.llmMu.Lock()
|
||||
a.cancelLLM = fCancel
|
||||
a.llmMu.Unlock()
|
||||
|
||||
resp, llmErr = chatStreamWithFallback(fCtx, fbProvider, req, a)
|
||||
|
||||
a.llmMu.Lock()
|
||||
a.cancelLLM = nil
|
||||
a.llmMu.Unlock()
|
||||
fCancel()
|
||||
|
||||
if llmErr == nil {
|
||||
a.providerManager.ResetAvailability(fbProvider.Name())
|
||||
if fbProvider != a.provider {
|
||||
a.provider = fbProvider
|
||||
log.Printf("[agent] switched active provider to %q after fallback",
|
||||
fbProvider.Name())
|
||||
// 同源瞬时错误重试:网关瞬断(502/503/504/429/网络抖动)通常秒级恢复,
|
||||
// 直接跳下一个 provider(或直接报错)会丢掉本可成功的请求。
|
||||
// 凭证错误(401/403)与用户中断不重试。
|
||||
const maxAttempts = 2
|
||||
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||||
if attempt > 1 {
|
||||
log.Printf("[agent] provider %q transient failure, retry %d/%d in 2s: %v",
|
||||
fbProvider.Name(), attempt, maxAttempts, llmErr)
|
||||
select {
|
||||
case <-time.After(2 * time.Second):
|
||||
case <-a.ctx.Done():
|
||||
llmErr = a.ctx.Err()
|
||||
}
|
||||
if llmErr == nil || errors.Is(llmErr, context.Canceled) || errors.Is(llmErr, context.DeadlineExceeded) {
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
fCtx, fCancel := context.WithCancel(a.ctx)
|
||||
a.llmMu.Lock()
|
||||
a.cancelLLM = fCancel
|
||||
a.llmMu.Unlock()
|
||||
|
||||
resp, llmErr = chatStreamWithFallback(fCtx, fbProvider, req, a)
|
||||
|
||||
a.llmMu.Lock()
|
||||
a.cancelLLM = nil
|
||||
a.llmMu.Unlock()
|
||||
fCancel()
|
||||
|
||||
if llmErr == nil {
|
||||
a.providerManager.ResetAvailability(fbProvider.Name())
|
||||
if fbProvider != a.provider {
|
||||
a.provider = fbProvider
|
||||
log.Printf("[agent] switched active provider to %q after fallback",
|
||||
fbProvider.Name())
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// 用户中断:立即终止,不重试也不换 provider
|
||||
if errors.Is(llmErr, context.Canceled) {
|
||||
break
|
||||
}
|
||||
// 凭证错误:重试无意义,跳出重试循环进入 provider 标记/切换
|
||||
var pe *agentAPI.ProviderError
|
||||
if errors.As(llmErr, &pe) && (pe.StatusCode == 401 || pe.StatusCode == 403) {
|
||||
break
|
||||
}
|
||||
// 其余错误(含 5xx/429/网络):还有重试机会则继续,否则跳出
|
||||
}
|
||||
|
||||
if llmErr == nil {
|
||||
break
|
||||
}
|
||||
if errors.Is(llmErr, context.Canceled) {
|
||||
break
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user