fix(dispatcher): 多智能体专家超时 —— 卡死专家不再拖垮协调(T4.E)

- specialistTool 加 timeout 字段(默认 specialistTimeout=3min,专家可多轮 react+工具故给宽)
- InvokableRun 用 WithTimeout 包裹专家派发;超时(DeadlineExceeded)作为"观察"
  跳过该专家(err=nil),lead 据其余专家继续综合,不中断整个协调
- 2 单测:卡死专家 ~50ms 跳过并返回超时观察 / 正常专家不受影响
- timeout=0 时不包裹(向后兼容既有 specialistTool 构造)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-02 10:03:29 +08:00
parent de36ed4cb3
commit f9c849b14b
4 changed files with 82 additions and 8 deletions
@@ -3,6 +3,7 @@ package eino
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
@@ -69,10 +70,11 @@ func parseSpecialists(cfg map[string]any) []specialistSpec {
// specialistTool 把一个专家(react.Agent 或 ChatModel)包成 Eino InvokableToolagent-as-tool):
// 入参 brief = lead 写给它的定制简报;返回专家的精炼结论。每次派发落一条 agent 轨迹。
type specialistTool struct {
info *schema.ToolInfo
name string
run func(ctx context.Context, brief string) (string, error)
tr *execTracer
info *schema.ToolInfo
name string
run func(ctx context.Context, brief string) (string, error)
tr *execTracer
timeout time.Duration // 单次派发上限;0=不限。超时作为"观察"跳过该专家,不中断协调。
}
func (s *specialistTool) Info(context.Context) (*schema.ToolInfo, error) { return s.info, nil }
@@ -94,8 +96,19 @@ func (s *specialistTool) InvokableRun(ctx context.Context, argsJSON string, _ ..
bud.AddPrompt(brief)
}
end := s.tr.span("agent:"+s.name, "agent", "派发专家 "+s.name)
out, err := s.run(ctx, brief)
rctx := ctx
if s.timeout > 0 {
var cancel context.CancelFunc
rctx, cancel = context.WithTimeout(ctx, s.timeout)
defer cancel()
}
out, err := s.run(rctx, brief)
if err != nil {
// 超时单独提示:作为"观察"跳过该专家,lead 据其余专家继续综合(不中断协调)。
if s.timeout > 0 && errors.Is(err, context.DeadlineExceeded) {
end("专家超时", err)
return fmt.Sprintf("专家 %s 超时(>%s),本轮跳过其结论。", s.name, s.timeout), nil
}
end("专家执行失败", err)
return "专家 " + s.name + " 执行失败:" + err.Error(), nil
}
@@ -125,7 +138,7 @@ func (o *Orchestrator) buildSpecialists(ctx context.Context, specs []specialistS
continue
}
out = append(out, &specialistTool{
name: spec.Name, tr: tr, run: run,
name: spec.Name, tr: tr, run: run, timeout: specialistTimeout,
info: &schema.ToolInfo{
Name: spec.Name,
Desc: firstNonEmpty(spec.Use, "专家 "+spec.Name),