feat(model): 工作模型与 JARVIS 语音模型分开配置 + 语音任务路由到快模型

模型配置加"用途"维度:工作主力(chat,要强)与 JARVIS 语音(voice,要快/低时延)各配各激活,
语音任务走语音模型池,未配置则透明回落工作模型——不影响现有功能。

- contract: ConfigKindVoice="voice" + Meta[model_profile]=voice(与 intent==report 同类路由)
- gateway: ServeConfig/broadcastActive 循环纳入 voice;submitVoiceTask 打 model_profile=voice 标记
- dispatcher: 第二个 llm.Pool(voicePool)吃 voice 配置热更新;board.useVoice 从 Meta 派生(含快照);
  Orchestrator.agentPool(b) 按黑板选池——语音且语音池就绪→语音池,否则工作池;
  agent 生成路径(graph/react/coordinator/compose)全改走 agentPool(b),报告/护栏/记忆固定工作池
- admin: 模型页三 Tab(工作主力/JARVIS语音/向量化),复用 ModelManager;api Kind 加 voice

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-22 11:39:17 +08:00
parent b6927247da
commit aec7ad949c
15 changed files with 102 additions and 50 deletions
@@ -18,6 +18,7 @@ type boardSnapshot struct {
UID string `json:"uid,omitempty"`
SID string `json:"sid,omitempty"`
Query string `json:"query,omitempty"`
UseVoice bool `json:"use_voice,omitempty"`
Profile string `json:"profile,omitempty"`
History []*schema.Message `json:"history,omitempty"`
KB string `json:"kb,omitempty"`
@@ -35,6 +36,7 @@ func (b *board) MarshalJSON() ([]byte, error) {
UID: b.uid,
SID: b.sid,
Query: b.query,
UseVoice: b.useVoice,
Profile: b.profile,
History: b.history,
KB: b.kb,
@@ -56,6 +58,7 @@ func (b *board) UnmarshalJSON(data []byte) error {
b.uid = s.UID
b.sid = s.SID
b.query = s.Query
b.useVoice = s.UseVoice
b.profile = s.Profile
b.history = s.History
b.kb = s.KB
@@ -56,9 +56,10 @@ func (o *Orchestrator) execComposeGraph(ctx context.Context, t *contract.Task, t
flow, ferr := dsl.Parse(t.Graph)
plan := dsl.Compile(t.Graph)
b := &board{
uid: meta(t, contract.MetaUserID),
sid: meta(t, contract.MetaSessionID),
query: plan.Query,
uid: meta(t, contract.MetaUserID),
sid: meta(t, contract.MetaSessionID),
query: plan.Query,
useVoice: meta(t, contract.MetaModelProfile) == contract.ModelProfileVoice, // 语音任务 → 走语音模型池
}
// 无图/空图:退化为 compose 单轮对话。
@@ -17,7 +17,7 @@ import (
// START → ChatModel 节点 → END,编译为 Runnable 后流式执行;可观测经 callbacks 桥到 ExecEvent。
// 模型未就绪 / 编译失败时降级回 runAgent(同样的流式回流,保证不回归)。
func (o *Orchestrator) runComposeConversation(ctx context.Context, taskID string, b *board, system string, tr *execTracer, node, label string) {
cm := o.pool.ChatModel()
cm := o.agentPool(b).ChatModel() // 语音任务走语音模型池
if cm == nil {
o.runAgent(ctx, taskID, b, system, tr, node, label) // 无模型 → 降级桩
return
@@ -154,7 +154,7 @@ func (o *Orchestrator) buildSpecialists(ctx context.Context, specs []specialistS
usedFuncNames := map[string]bool{}
for i, spec := range specs {
sys := firstNonEmpty(spec.System, defaultAgentSystem) + specialistCondensedSuffix
run := o.specialistRunner(ctx, spec, sys, byName)
run := o.specialistRunner(ctx, b, spec, sys, byName)
if run == nil {
tr.info("coordinator", "system", "专家跳过", "无可用模型:"+spec.Name)
continue
@@ -175,7 +175,7 @@ func (o *Orchestrator) buildSpecialists(ctx context.Context, specs []specialistS
// specialistRunner 构造专家执行闭包:带工具且模型支持函数调用→react.Agent.Generate;否则→ChatModel.Generate。
// 无可用模型返回 nil(该专家被跳过)。
func (o *Orchestrator) specialistRunner(ctx context.Context, spec specialistSpec, sys string, byName map[string]tool.BaseTool) func(context.Context, string) (string, error) {
func (o *Orchestrator) specialistRunner(ctx context.Context, b *board, spec specialistSpec, sys string, byName map[string]tool.BaseTool) func(context.Context, string) (string, error) {
var tools []tool.BaseTool
for _, tn := range spec.Tools {
if t, ok := byName[tn]; ok {
@@ -183,7 +183,7 @@ func (o *Orchestrator) specialistRunner(ctx context.Context, spec specialistSpec
}
}
if len(tools) > 0 {
if tcm := o.pool.ToolCallingModel(); tcm != nil {
if tcm := o.agentPool(b).ToolCallingModel(); tcm != nil {
ag, err := react.NewAgent(ctx, &react.AgentConfig{
ToolCallingModel: tcm,
ToolsConfig: compose.ToolsNodeConfig{Tools: tools},
@@ -202,7 +202,7 @@ func (o *Orchestrator) specialistRunner(ctx context.Context, spec specialistSpec
}
// 工具型专家但模型不支持函数调用 → 退纯对话(下方)
}
cm := o.pool.ChatModel()
cm := o.agentPool(b).ChatModel()
if cm == nil {
return nil
}
@@ -219,7 +219,7 @@ func (o *Orchestrator) specialistRunner(ctx context.Context, spec specialistSpec
// Anthropic 配方(分解→定制简报→并行派发→综合)。无 ToolCallingModel / 0 可用专家 → 降级单 agent。
func (o *Orchestrator) runCoordinator(ctx context.Context, taskID string, b *board, system string, n dsl.Node, tr *execTracer, node string) {
specs := parseSpecialists(n.Config)
tcm := o.pool.ToolCallingModel()
tcm := o.agentPool(b).ToolCallingModel()
if tcm == nil || len(specs) == 0 {
tr.info(node, "system", "协调者降级", "模型不支持函数调用或未配置专家,退回普通对话")
o.runAgent(ctx, taskID, b, system, tr, node, labelOf(n, "多智能体协调"))
+5 -3
View File
@@ -21,6 +21,7 @@ const defaultAgentSystem = "你是 sundynix-agentix 平台的 AI 助手。"
type board struct {
uid, sid string
query string
useVoice bool // 该任务用 JARVIS 语音模型池(网关 Meta[model_profile]==voice
profile string
history []*schema.Message
kb string // 最近一个检索节点的 owner 作用域库名(供 map 并行各项检索)
@@ -178,10 +179,11 @@ func (o *Orchestrator) runAgent(ctx context.Context, taskID string, b *board, sy
var reasoning strings.Builder
onReasoning := func(s string) { reasoning.WriteString(s) }
var err error
if o.pool.Ready() {
err = o.pool.ChatStream(ctx, toChatMessages(msgs), send, onReasoning)
pool := o.agentPool(b) // 语音任务走语音模型池(否则工作池)
if pool.Ready() {
err = pool.ChatStream(ctx, toChatMessages(msgs), send, onReasoning)
} else {
err = o.pool.StreamText(ctx, replyFor(msgs), func(tok []byte) { send(string(tok)) })
err = pool.StreamText(ctx, replyFor(msgs), func(tok []byte) { send(string(tok)) })
}
if rc := reasoning.String(); rc != "" {
tr.info(node, "model", "推理过程", fmt.Sprintf("思考 %d 字:%s", len([]rune(rc)), truncate(rc, 200)))
@@ -99,6 +99,7 @@ const specialistTimeout = 3 * time.Minute
// Orchestrator 把每个 DSL 任务动态编译为 Eino 图并执行(记忆召回 → 工具节点 → 注入 → 流式)。
type Orchestrator struct {
pool LLM
voicePool LLM // JARVIS 语音模型池(可为 nil → 语音任务回落 pool)
breaker *harness.CircuitBreaker
eval *harness.Evaluator
sink TokenSink
@@ -122,6 +123,19 @@ func NewOrchestrator(pool LLM, breaker *harness.CircuitBreaker, eval *harness.Ev
return &Orchestrator{pool: pool, breaker: breaker, eval: eval, sink: sink, tools: tools, exec: exec, status: status, approval: approval, evalSink: evalSink}, nil
}
// SetVoicePool 注入 JARVIS 语音模型池(可选)。注入且就绪时,标了 model_profile==voice 的任务
// 用它跑,抢首字时延;不注入或未就绪则语音任务透明回落工作模型池(o.pool)。
func (o *Orchestrator) SetVoicePool(p LLM) { o.voicePool = p }
// agentPool 按黑板选本任务该用的模型池:语音任务且语音池就绪 → 语音池;否则工作池。
// 只用于面向用户的 agent 生成(对话/协作);报告/护栏/记忆抽取等固定走工作池。
func (o *Orchestrator) agentPool(b *board) LLM {
if b != nil && b.useVoice && o.voicePool != nil && o.voicePool.Ready() {
return o.voicePool
}
return o.pool
}
// SetGuardian 注入输入护栏 Tier2 的 LLM 分类器(可选;不注入则灰区任务直接放行执行)。
func (o *Orchestrator) SetGuardian(c *harness.Classifier) { o.guard = c }
@@ -170,7 +170,7 @@ func (o *Orchestrator) discoverTools(subject func(string) string, b *board, task
// runReactAgent 执行带"自主工具"的 agent 节点:模型在 ReAct 循环里自行决定调哪些 MCP 工具。
// 模型不支持函数调用 / 无工具时降级回普通 runAgent。最终答复流式回流;工具调用由适配器落轨迹。
func (o *Orchestrator) runReactAgent(ctx context.Context, taskID string, b *board, system string, n dsl.Node, tr *execTracer, node string) {
tcm := o.pool.ToolCallingModel()
tcm := o.agentPool(b).ToolCallingModel()
tools := o.agentTools(b, taskID, tr)
if tcm == nil || len(tools) == 0 {
tr.info(node, "system", "ReAct 降级", "模型不支持函数调用或无可用工具,退回普通对话")