Skip to main content
xbot
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage
Edit page

Retry Timeout + Streaming Engine 实施方案

目标

  1. 设置 RetryConfig.Timeout = 120s:确保每次 LLM 重试有独立的 120 秒超时窗口
  2. Engine 切换流式 LLM 调用:将 engine.go 的 Generate() 改为 GenerateStream(),收集 stream events 组装为 LLMResponse,过程中实时更新进度

任务 1:RetryConfig.Timeout = 120s

分析

  • DefaultRetryConfig()Timeout 字段当前为 0(不设超时)
  • perAttemptCtx 在 Timeout=0 时会尝试从父 ctx deadline 推导,父 ctx 也无 deadline 则不设超时
  • 需要:DefaultRetryConfig 设置 Timeout=120s,同时支持通过环境变量覆盖

改动

文件改动
llm/retry.goDefaultRetryConfig() 增加 Timeout: 120 * time.Second
config/config.go新增 LLMRetryTimeout time.Duration 配置项
config/config.goDefault() 增加默认值 120s + 环境变量 LLM_RETRY_TIMEOUT
main.gocreateLLM 调用处传入 Timeout: cfg.Agent.LLMRetryTimeout
llm/retry_test.go更新 TestDefaultRetryConfig 断言

任务 2:Engine 切换流式 LLM

分析

当前 engine.go 调用 cfg.LLMClient.Generate(),等待完整响应后才返回。 流式方案需要:

  1. 检查 LLMClient 是否实现 StreamingLLM 接口
  2. 调用 GenerateStream() 获取 <-chan StreamEvent
  3. 遍历 events 收集 content、tool_calls、usage,组装为 LLMResponse
  4. 遍历过程中,每收到一段 content 就实时更新进度(可选,不影响功能)

关键设计决策

Q: 是否需要回退到非流式? 是的。如果 LLMClient 不实现 StreamingLLM(如某些自定义实现),回退到 Generate()

Q: 流式收集逻辑放哪里? 新增 llm/stream.go,提供 CollectStream(ctx, eventCh) 函数,将 <-chan StreamEvent 转为 (*LLMResponse, error)。可被 engine 复用,也可被其他调用方复用。

Q: 进度展示? 流式过程中,每收到一批 content delta,更新进度行:> 💭 正在思考...> 💭 正在思考: [前100字摘要]... 收到 tool_call 时显示 > 🔧 调用工具: Read, Shell, ...

改动

文件改动
llm/stream.go新增 CollectStream(ctx, <-chan StreamEvent) (*LLMResponse, error)
llm/stream_test.go新增测试
agent/engine.goLLM 调用处:先尝试 GenerateStream + CollectStream,回退 Generate

CollectStream 逻辑

func CollectStream(ctx context.Context, eventCh <-chan StreamEvent) (*LLMResponse, error) {
    var resp LLMResponse
    var content strings.Builder
    var reasoningContent strings.Builder
    toolCalls := make(map[int]*ToolCallDelta)  // index → accumulated delta
    
    for ev := range eventCh {
        switch ev.Type {
        case EventContent:
            content.WriteString(ev.Content)
        case EventReasoningContent:
            reasoningContent.WriteString(ev.ReasoningContent)
        case EventToolCall:
            tc := toolCalls[ev.ToolCall.Index]
            if tc == nil {
                tc = &ToolCallDelta{Index: ev.ToolCall.Index}
                toolCalls[ev.ToolCall.Index] = tc
            }
            tc.ID = firstNonEmpty(tc.ID, ev.ToolCall.ID)
            tc.Name = firstNonEmpty(tc.Name, ev.ToolCall.Name)
            tc.Arguments += ev.ToolCall.Arguments
        case EventUsage:
            resp.Usage = *ev.Usage
        case EventDone:
            resp.FinishReason = ev.FinishReason
        case EventError:
            return nil, fmt.Errorf("stream error: %s", ev.Error)
        }
    }
    
    resp.Content = content.String()
    resp.ReasoningContent = reasoningContent.String()
    // 将 map 转为有序 slice
    for i := 0; i < len(toolCalls); i++ {
        if tc, ok := toolCalls[i]; ok {
            resp.ToolCalls = append(resp.ToolCalls, ToolCall{
                ID: tc.ID, Name: tc.Name, Arguments: tc.Arguments,
            })
        }
    }
    return &resp, nil
}

Engine 调用改动

// engine.go Run() 主循环中的 LLM 调用
var response *llm.LLMResponse
if streaming, ok := cfg.LLMClient.(llm.StreamingLLM); ok {
    eventCh, streamErr := streaming.GenerateStream(retryNotifyCtx, cfg.Model, messages, toolDefs, cfg.ThinkingMode)
    if streamErr != nil {
        err = streamErr
    } else {
        response, err = llm.CollectStream(ctx, eventCh)
    }
} else {
    response, err = cfg.LLMClient.Generate(retryNotifyCtx, cfg.Model, messages, toolDefs, cfg.ThinkingMode)
}

同理,input-too-long 重试路径也做相同改造。

不做的事

  • 不做实时进度推送:流式过程中不频繁 notifyProgress,因为 LLM 响应通常 10-60 秒,粒度太细反而刷屏。现有的 “💭 思考中…” 占位已经足够。后续可以在流式过程中逐步更新 thinking content 作为增强。
  • 不改 RunConfig.LLMClient 类型:保持 llm.LLM 接口,运行时 type assert 检查 StreamingLLM

执行顺序

  1. llm/retry.go: DefaultRetryConfig 增加 Timeout=120s
  2. llm/stream.go + llm/stream_test.go: CollectStream 实现 + 测试
  3. config/config.go + main.go: LLMRetryTimeout 配置
  4. agent/engine.go: LLM 调用切换到流式
  5. llm/retry_test.go: 更新测试断言
  6. go build ./... + go test ./... 验证