66caeef35c
LLM 自主在 agent 间路由/委派:orchestrator=ReAct(Eino 出机器),编排认知按 Anthropic orchestrator-worker 配方(出脑子)。专家=包成工具的子 agent(agent-as-tool),lead 给 每个专家写定制简报(brief)后并行派发、综合。方案见 MULTI_AGENT.md。 为什么 agent-as-tool 而非 Eino host:host 的 specialist 拿原始输入(preHandler return state.msgs),传不了 lead 写的定制简报,而定制简报正是 Anthropic 多智能体的 精髓。agent-as-tool 让 orchestrator 自己 emit 工具调用、参数 brief 即简报。 = OpenAI agent.as_tool() / Anthropic 研究系统的 orchestrator-worker。 - coordinator.go: specialistTool(react.Agent/ChatModel 包成 InvokableTool,入参 brief, 精炼返回) + parseSpecialists/buildSpecialists(带工具→react,不带→ChatModel,MCP 工具 按 spec.tools 过滤) + runCoordinator(lead 提示词=Anthropic 配方) + leadOrchestratorPrompt。 - 双路接入 execDSLNode(compose)+ runGraph(graph.go)的 case coordinator。 - 护栏:禁套娃(专家是内联叶子)/ MaxStep / 专家 I/O 计入共享 Budget / 降级(无 ToolCallingModel 或 0 专家 → runAgent)。 - streamAgentReply:抽出 runReactAgent 与 runCoordinator 共用的流式回流尾段。 - 复用即得:evaluator-optimizer=harness 低分纠偏;成本天花板=预算护栏;上下文隔离= 专家独立 react.Agent;观测=每次派发落 agent 轨迹。 测试:parseSpecialists / agent-as-tool 包装(brief 透传+精炼返回+失败作观察) / 降级。 live 验证(真 deepseek):两专家**并行派发**、lead 给各自写**不同定制简报**、最终 **综合**(非拼接)成稿,评测 1.00。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
92 lines
3.4 KiB
Go
92 lines
3.4 KiB
Go
package eino
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/cloudwego/eino/schema"
|
|
|
|
"github.com/sundynix/sundynix-dispatcher/internal/dsl"
|
|
"github.com/sundynix/sundynix-dispatcher/internal/harness"
|
|
"github.com/sundynix/sundynix-dispatcher/internal/llm"
|
|
)
|
|
|
|
// TestParseSpecialists 钉死节点 config 的 agents 列表解析(含 tools 子集、无名跳过)。
|
|
func TestParseSpecialists(t *testing.T) {
|
|
cfg := map[string]any{"agents": []any{
|
|
map[string]any{"name": "legal", "use": "法律条款", "system": "你是律师", "tools": []any{"wiki_search", "kb_search"}},
|
|
map[string]any{"name": "finance", "use": "财务测算"},
|
|
map[string]any{"use": "无名字应被跳过"},
|
|
}}
|
|
specs := parseSpecialists(cfg)
|
|
if len(specs) != 2 {
|
|
t.Fatalf("应解析出 2 个专家(无名跳过),got %d", len(specs))
|
|
}
|
|
if specs[0].Name != "legal" || specs[0].Use != "法律条款" || len(specs[0].Tools) != 2 {
|
|
t.Fatalf("legal 解析有误: %+v", specs[0])
|
|
}
|
|
if specs[1].Name != "finance" || len(specs[1].Tools) != 0 {
|
|
t.Fatalf("finance 解析有误: %+v", specs[1])
|
|
}
|
|
}
|
|
|
|
// TestSpecialistToolInvoke 钉死 agent-as-tool 包装:lead 写的 brief 透传给专家、结论回传、
|
|
// 空 brief 兜底、专家失败作为"观察"返回而非中断协调。
|
|
func TestSpecialistToolInvoke(t *testing.T) {
|
|
var gotBrief string
|
|
st := &specialistTool{
|
|
name: "legal", tr: &execTracer{},
|
|
info: &schema.ToolInfo{Name: "legal"},
|
|
run: func(_ context.Context, brief string) (string, error) { gotBrief = brief; return "结论:条款合规", nil },
|
|
}
|
|
|
|
out, err := st.InvokableRun(context.Background(), `{"brief":"审查合同第3条违约金"}`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gotBrief != "审查合同第3条违约金" {
|
|
t.Fatalf("lead 简报应透传给专家,got %q", gotBrief)
|
|
}
|
|
if out != "结论:条款合规" {
|
|
t.Fatalf("专家结论应回传,got %q", out)
|
|
}
|
|
|
|
// 空 brief → 兜底文案(不把空串丢给专家)。
|
|
st.run = func(_ context.Context, brief string) (string, error) { gotBrief = brief; return "ok", nil }
|
|
_, _ = st.InvokableRun(context.Background(), `{}`)
|
|
if !strings.Contains(gotBrief, "未给简报") {
|
|
t.Fatalf("空 brief 应兜底,got %q", gotBrief)
|
|
}
|
|
|
|
// 专家失败 → 返回失败观察,err 为 nil(不中断 orchestrator 的 ReAct 循环)。
|
|
st.run = func(_ context.Context, _ string) (string, error) { return "", fmt.Errorf("boom") }
|
|
out, err = st.InvokableRun(context.Background(), `{"brief":"x"}`)
|
|
if err != nil {
|
|
t.Fatalf("专家失败不应上抛 error(应作观察返回): %v", err)
|
|
}
|
|
if !strings.Contains(out, "执行失败") {
|
|
t.Fatalf("应返回失败观察,got %q", out)
|
|
}
|
|
}
|
|
|
|
// TestRunCoordinatorDegrade 钉死降级:模型不支持函数调用(ToolCallingModel=nil)→ 退回普通对话出稿,
|
|
// 不因"多智能体"而卡死或空答。
|
|
func TestRunCoordinatorDegrade(t *testing.T) {
|
|
fs := &fakeSink{}
|
|
o := &Orchestrator{
|
|
pool: &fakeLLM{ready: true, stream: func([]llm.ChatMessage) string { return "降级直答" }},
|
|
breaker: harness.NewCircuitBreaker(),
|
|
sink: fs,
|
|
}
|
|
b := &board{query: "你好"}
|
|
n := dsl.Node{ID: "c", Kind: "coordinator", Config: map[string]any{
|
|
"agents": []any{map[string]any{"name": "x", "use": "y"}},
|
|
}}
|
|
o.runCoordinator(context.Background(), "t_co", b, "", n, &execTracer{}, "coordinator:c")
|
|
if !strings.Contains(b.answer, "降级直答") {
|
|
t.Fatalf("ToolCallingModel 缺失应降级 runAgent 出答案,got %q", b.answer)
|
|
}
|
|
}
|