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:
@@ -71,7 +71,7 @@ export async function me(): Promise<AuthUser | null> {
|
||||
}
|
||||
|
||||
// ---- 模型配置(id 为雪花字符串)----
|
||||
export type Kind = "chat" | "embedding";
|
||||
export type Kind = "chat" | "voice" | "embedding";
|
||||
|
||||
export interface Model {
|
||||
id: string;
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { useState } from "react";
|
||||
import { ModelManager } from "../components/ModelManager";
|
||||
import type { Kind } from "../api";
|
||||
|
||||
// 模型配置页:工作主力(chat) / JARVIS 语音(voice) / 向量化(embedding) 三 Tab 统一管理。
|
||||
// 工作与语音分开:编排/报告用强模型(chat),JARVIS 语音对话用快模型(voice, 低时延)——各配各激活。
|
||||
const TABS: { key: Kind; label: string }[] = [
|
||||
{ key: "chat", label: "工作主力模型" },
|
||||
{ key: "voice", label: "JARVIS 语音模型" },
|
||||
{ key: "embedding", label: "向量化模型" },
|
||||
];
|
||||
|
||||
// 模型配置页:Chat / Embedding 双 Tab 统一管理。
|
||||
export function ModelsPage() {
|
||||
const [tab, setTab] = useState<"chat" | "embedding">("chat");
|
||||
const [tab, setTab] = useState<Kind>("chat");
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -11,46 +19,46 @@ export function ModelsPage() {
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-gray-150 pb-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-gray-800">模型管理</h3>
|
||||
<p className="text-xs text-gray-400">配置全平台的对话模型(LLM)与向量化模型(Embedding)</p>
|
||||
<p className="text-xs text-gray-400">工作主力模型跑编排/报告(要强)· JARVIS 语音模型跑实时对话(要快)· 向量化模型跑 RAG</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex rounded-lg border border-gray-200 bg-gray-50/50 p-1">
|
||||
<button
|
||||
onClick={() => setTab("chat")}
|
||||
className={`rounded-md px-4 py-1.5 text-xs font-medium transition-all ${
|
||||
tab === "chat"
|
||||
? "bg-white text-violet-700 shadow-sm"
|
||||
: "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
对话模型 (Chat)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab("embedding")}
|
||||
className={`rounded-md px-4 py-1.5 text-xs font-medium transition-all ${
|
||||
tab === "embedding"
|
||||
? "bg-white text-violet-700 shadow-sm"
|
||||
: "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
向量化模型 (Embedding)
|
||||
</button>
|
||||
{TABS.map((t) => (
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => setTab(t.key)}
|
||||
className={`rounded-md px-4 py-1.5 text-xs font-medium transition-all ${
|
||||
tab === t.key ? "bg-white text-violet-700 shadow-sm" : "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模型管理组件 */}
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
{tab === "chat" ? (
|
||||
{tab === "chat" && (
|
||||
<ModelManager
|
||||
kind="chat"
|
||||
title="对话模型配置 (chat → Dispatcher)"
|
||||
title="工作主力模型 (chat → Dispatcher 编排/报告)"
|
||||
baseUrlHint="https://api.deepseek.com"
|
||||
modelHint="deepseek-v4-pro"
|
||||
/>
|
||||
)}
|
||||
{tab === "voice" && (
|
||||
<ModelManager
|
||||
kind="voice"
|
||||
title="JARVIS 语音模型 (voice → 语音对话,选快模型抢首字时延)"
|
||||
baseUrlHint="https://api.deepseek.com"
|
||||
modelHint="deepseek-chat"
|
||||
/>
|
||||
) : (
|
||||
)}
|
||||
{tab === "embedding" && (
|
||||
<ModelManager
|
||||
kind="embedding"
|
||||
title="向量化模型配置 (embedding → mcp-go RAG)"
|
||||
title="向量化模型 (embedding → mcp-go RAG)"
|
||||
baseUrlHint="https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
modelHint="text-embedding-v3"
|
||||
/>
|
||||
@@ -59,4 +67,3 @@ export function ModelsPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ func main() {
|
||||
|
||||
natsURL := envOr("NATS_URL", "nats://localhost:4222")
|
||||
|
||||
pool := llm.NewPool() // LLM Pool: vLLM / Ollama 集群
|
||||
pool := llm.NewPool() // 工作主力模型池(chat)
|
||||
voicePool := llm.NewPool() // JARVIS 语音模型池(voice;未配置则空、语音任务回落 pool)
|
||||
breaker := harness.NewCircuitBreaker() // Harness: 熔断降级中心
|
||||
// Harness: LLM 自动化评测(规则 + LLM-as-judge,模型就绪时启用)。
|
||||
llmChat := func(ctx context.Context, sys, user string) (string, error) {
|
||||
@@ -53,6 +54,12 @@ func main() {
|
||||
log.Printf("[dispatcher] subscribe model config: %v", err)
|
||||
}
|
||||
go sub.FetchModelConfigWithRetry(context.Background(), pool.SetConfig)
|
||||
// 语音模型(JARVIS)配置:同机制热更新 + 后台重试。未配置语音模型时拉不到、voicePool 保持空,
|
||||
// 语音任务在 agentPool 里透明回落工作模型池——不影响现有功能。
|
||||
if _, err := sub.SubscribeVoiceConfigUpdated(voicePool.SetConfig); err != nil {
|
||||
log.Printf("[dispatcher] subscribe voice model config: %v", err)
|
||||
}
|
||||
go sub.FetchVoiceConfigWithRetry(context.Background(), voicePool.SetConfig)
|
||||
|
||||
// Prompt 控制面:拉激活集覆盖内置默认 + 订阅热更新(管理端激活某版即生效,不重启)。
|
||||
go sub.FetchPromptsWithRetry(context.Background(), prompts.ApplyOverrides)
|
||||
@@ -66,8 +73,9 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("[dispatcher] build eino graph: %v", err)
|
||||
}
|
||||
orch.SetGuardian(guardian) // 输入护栏 Tier2
|
||||
orch.SetUsageSink(sub) // 成本护栏:token 用量回写网关累计/计费
|
||||
orch.SetGuardian(guardian) // 输入护栏 Tier2
|
||||
orch.SetUsageSink(sub) // 成本护栏:token 用量回写网关累计/计费
|
||||
orch.SetVoicePool(voicePool) // JARVIS 语音任务用快模型(未配置则回落工作模型)
|
||||
|
||||
// HITL 持久化中断/恢复:开 checkpoint 存储 + 审批决定流,审批节点改走中断模型
|
||||
// (compose.Interrupt 落盘释放 goroutine、抗 dispatcher 重启)。任一步失败则降级回阻塞模型。
|
||||
|
||||
@@ -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, "多智能体协调"))
|
||||
|
||||
@@ -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 降级", "模型不支持函数调用或无可用工具,退回普通对话")
|
||||
|
||||
@@ -146,6 +146,16 @@ func (s *Subscriber) FetchModelConfigWithRetry(ctx context.Context, apply func(*
|
||||
s.inner.RequestConfigWithRetry(ctx, contract.ConfigKindChat, apply)
|
||||
}
|
||||
|
||||
// SubscribeVoiceConfigUpdated 订阅 JARVIS 语音模型配置热更新(可空——未配置语音模型时不生效)。
|
||||
func (s *Subscriber) SubscribeVoiceConfigUpdated(onUpdate func(*contract.ModelConfig)) (func() error, error) {
|
||||
return s.inner.SubscribeConfigUpdated(contract.ConfigKindVoice, onUpdate)
|
||||
}
|
||||
|
||||
// FetchVoiceConfigWithRetry 后台重试拉取初始语音模型配置(未配置则一直拿不到,语音任务回落工作模型)。
|
||||
func (s *Subscriber) FetchVoiceConfigWithRetry(ctx context.Context, apply func(*contract.ModelConfig)) {
|
||||
s.inner.RequestConfigWithRetry(ctx, contract.ConfigKindVoice, apply)
|
||||
}
|
||||
|
||||
// FetchPromptsWithRetry 后台重试拉取初始激活 prompt 集(覆盖内置默认)。
|
||||
func (s *Subscriber) FetchPromptsWithRetry(ctx context.Context, apply func(map[string]string)) {
|
||||
s.inner.RequestPromptsWithRetry(ctx, apply)
|
||||
|
||||
@@ -74,8 +74,8 @@ func main() {
|
||||
bus := nats.MustConnect(natsURL) // 接入 NATS 零拷贝骨干网 + 声明任务流
|
||||
defer bus.Close()
|
||||
|
||||
// 配置控制面:按 kind 响应消费方(Dispatcher=chat / mcp-go=embedding)的配置请求。
|
||||
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding} {
|
||||
// 配置控制面:按 kind 响应消费方(Dispatcher=chat/voice / mcp-go=embedding)的配置请求。
|
||||
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding, contract.ConfigKindVoice} {
|
||||
k := kind
|
||||
if _, err := bus.ServeConfig(k, func() *contract.ModelConfig {
|
||||
return db.ActiveConfig(context.Background(), k) // chat 含 Fallbacks(其它模型作备用)
|
||||
|
||||
@@ -566,7 +566,7 @@ func (h *Handler) TestModel(c *gin.Context) {
|
||||
// broadcastActive 重新广播各 kind 当前激活配置,触发对应消费方热更新。
|
||||
// chat 配置带 Fallbacks(其它已登记 chat 模型作备用),dispatcher 据此重建 failover 链。
|
||||
func (h *Handler) broadcastActive(ctx context.Context) {
|
||||
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding} {
|
||||
for _, kind := range []string{contract.ConfigKindChat, contract.ConfigKindEmbedding, contract.ConfigKindVoice} {
|
||||
if cfg := h.db.ActiveConfig(ctx, kind); cfg != nil {
|
||||
// 广播失败 = dispatcher/mcp-go 拿不到新配置,症状是"控制台改了模型却不生效",
|
||||
// 而改配置的人这边一切正常。必须留痕,否则只能靠猜。
|
||||
|
||||
@@ -65,6 +65,7 @@ func (s *voiceSession) submitVoiceTask(transcript, graphOverride string) (string
|
||||
task.Meta[contract.MetaUserID] = s.uid
|
||||
task.Meta[contract.MetaTenantID] = billingTenant
|
||||
task.Meta[contract.MetaSessionID] = s.sessionID
|
||||
task.Meta[contract.MetaModelProfile] = contract.ModelProfileVoice // 语音任务走 JARVIS 快模型(未配则回落工作模型)
|
||||
|
||||
if err := s.h.launchCore(ctx, s.uid, task); err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -153,8 +153,14 @@ const (
|
||||
|
||||
// 配置控制面按 kind 寻址:sundynix.config.<kind>.get / .updated。
|
||||
// Gateway 持有配置,消费方(Dispatcher/mcp-go)经 NATS 取用/订阅变更。
|
||||
ConfigKindChat = "chat" // 对话模型(Dispatcher 用)
|
||||
ConfigKindChat = "chat" // 工作主力对话模型(Dispatcher 默认用,要强)
|
||||
ConfigKindEmbedding = "embedding" // 向量模型(mcp-go RAG 用)
|
||||
ConfigKindVoice = "voice" // JARVIS 语音对话模型(Dispatcher 给语音任务用,要快/低时延)
|
||||
|
||||
// MetaModelProfile 指定该任务用哪档模型:为 ModelProfileVoice 时 Dispatcher 走语音模型池
|
||||
// (未配置语音模型则透明回落工作模型)。语音任务由网关置此标记,与 intent==report 同类路由。
|
||||
MetaModelProfile = "model_profile"
|
||||
ModelProfileVoice = "voice"
|
||||
|
||||
// 报告生成:Task.Meta[MetaIntent]==IntentReport 时,Dispatcher 走专用多步编排
|
||||
// (规划大纲 → 各章节并行检索+撰写 → 汇聚 → 渲染 Word),而非通用对话图。
|
||||
|
||||
Reference in New Issue
Block a user