Version: 0.9.18.dev.260415
后端: 1. ChatNode 路由从 GenerateJSON 重构为流式控制码路由 - 新建 backend/newAgent/router/chat_route.go:流式增量控制码解析器 StreamRouteParser,复用 agent 的 <SMARTFLOW_ROUTE> 正则模式 - 更新 backend/newAgent/node/chat.go:RunChatNode 从 GenerateJSON(阻塞等完整 JSON)改为 Stream + 控制码解析 + 分支流式处理 - streamAndDispatch 核心循环:逐 chunk 喂解析器,控制码解析后按 route 分发 - handleDirectReplyStream:thinking=false 同一流续传,thinking=true 关流后二次 thinking 调用 - handleDeepAnswerStream:移除"让我想想"过渡语,直接关流后发起第二次流式调用(thinking 由 effectiveThinking 控制) - handleRouteExecuteStream / handleRoutePlanStream:关流 → 推送 status → 设 Phase - 更新 backend/newAgent/prompt/chat.go:路由 prompt 从 JSON 格式改为控制码标签格式 - 更新 backend/newAgent/model/chat_contract.go:ChatRoutingDecision 新增 Thinking / Raw 字段,移除 Speak / Reason 2. Thinking 参数从 bool 扩展为 string 三态 - 更新 backend/model/agent.go:UserSendMessageRequest.Thinking 从 bool 改为 string - 更新 backend/service/agentsvc/agent.go:AgentChat / runNormalChatFlow 适配 string 类型,新增 thinkingModeToBool 兼容旧链路 - 更新 backend/service/agentsvc/agent_newagent.go:runNewAgentGraph 接收 thinkingMode string 并注入 CommonState 3. CommonState 新增 ThinkingMode / ExecuteThinking 字段 - 更新 backend/newAgent/model/common_state.go:ThinkingMode 控制下游 thinking 行为("true" 强开 / "false" 强关 / "auto"交路由决策) - ChatNode 通过 resolveEffectiveThinking 合并前端偏好与路由决策,传递给所有下游处理函数 4. 新增真流式推送方法 - 更新 backend/newAgent/stream/emitter.go:新增 EmitStreamAssistantText / EmitStreamReasoningText,桥接 StreamReader → SSE chunk 前端:无 仓库:无
This commit is contained in:
@@ -3,19 +3,22 @@ package newagentstream
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
infrallm "github.com/LoveLosita/smartflow/backend/infra/llm"
|
||||
)
|
||||
|
||||
// PayloadEmitter 是真正向外层 SSE 管道写 chunk 的最小接口。
|
||||
//
|
||||
// 说明:
|
||||
// 1. 这里刻意不用 chan/string 绑死实现;
|
||||
// 2. 上层既可以传“写 channel”的函数,也可以传“写 gin stream”的函数;
|
||||
// 2. 上层既可以传"写 channel"的函数,也可以传"写 gin stream"的函数;
|
||||
// 3. 只要签名是 `func(string) error`,都能接进来。
|
||||
type PayloadEmitter func(payload string) error
|
||||
|
||||
// StageEmitter 是 graph/node 对“当前阶段”进行推送的兼容接口。
|
||||
// StageEmitter 是 graph/node 对"当前阶段"进行推送的兼容接口。
|
||||
//
|
||||
// 设计说明:
|
||||
// 1. 旧调用侧仍然只关心 stage/detail 两段文本,因此这里先保留;
|
||||
@@ -23,7 +26,7 @@ type PayloadEmitter func(payload string) error
|
||||
// 3. 这样能兼顾当前兼容性和后续协议升级空间。
|
||||
type StageEmitter func(stage, detail string)
|
||||
|
||||
// PseudoStreamOptions 描述“整段文字伪流式输出”的切块与节奏配置。
|
||||
// PseudoStreamOptions 描述"整段文字伪流式输出"的切块与节奏配置。
|
||||
//
|
||||
// 字段语义:
|
||||
// 1. MinChunkRunes:达到该最小长度后,若命中标点/换行等边界,可提前切块;
|
||||
@@ -51,7 +54,7 @@ func DefaultPseudoStreamOptions() PseudoStreamOptions {
|
||||
// ChunkEmitter 是 newAgent 统一的 SSE chunk 发射器。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责把“正文 / 思考 / 工具事件 / 确认请求 / 中断提示”统一转换成 OpenAI 兼容 payload;
|
||||
// 1. 负责把"正文 / 思考 / 工具事件 / 确认请求 / 中断提示"统一转换成 OpenAI 兼容 payload;
|
||||
// 2. 负责在必要时把结构化事件附带成 extra,同时给当前前端提供可读的降级文本;
|
||||
// 3. 不负责决定什么时候发什么,也不负责持久化状态。
|
||||
type ChunkEmitter struct {
|
||||
@@ -365,7 +368,92 @@ func (e *ChunkEmitter) EmitDone() error {
|
||||
return e.emit("[DONE]")
|
||||
}
|
||||
|
||||
// EmitStageAsReasoning 把“阶段提示”伪装成 reasoning chunk 推给前端。
|
||||
// EmitStreamAssistantText 从 StreamReader 逐 chunk 读取并实时推送 assistant 正文。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 负责把 StreamReader 的每个 chunk 实时转换为 SSE payload 推送;
|
||||
// 2. 负责累计完整文本并返回,供调用方写入 history;
|
||||
// 3. 不负责打开/关闭 StreamReader,调用方负责生命周期管理。
|
||||
func (e *ChunkEmitter) EmitStreamAssistantText(
|
||||
ctx context.Context,
|
||||
reader infrallm.StreamReader,
|
||||
blockID, stage string,
|
||||
) (string, error) {
|
||||
if e == nil || reader == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var fullText strings.Builder
|
||||
firstChunk := true
|
||||
|
||||
for {
|
||||
chunk, err := reader.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return fullText.String(), err
|
||||
}
|
||||
|
||||
// 推送 reasoning content。
|
||||
if chunk != nil && strings.TrimSpace(chunk.ReasoningContent) != "" {
|
||||
if emitErr := e.EmitReasoningText(blockID, stage, chunk.ReasoningContent, firstChunk); emitErr != nil {
|
||||
return fullText.String(), emitErr
|
||||
}
|
||||
firstChunk = false
|
||||
}
|
||||
|
||||
// 推送 assistant 正文。
|
||||
if chunk != nil && chunk.Content != "" {
|
||||
if emitErr := e.EmitAssistantText(blockID, stage, chunk.Content, firstChunk); emitErr != nil {
|
||||
return fullText.String(), emitErr
|
||||
}
|
||||
fullText.WriteString(chunk.Content)
|
||||
firstChunk = false
|
||||
}
|
||||
}
|
||||
|
||||
return fullText.String(), nil
|
||||
}
|
||||
|
||||
// EmitStreamReasoningText 从 StreamReader 逐 chunk 读取并实时推送 reasoning 文字。
|
||||
//
|
||||
// 与 EmitStreamAssistantText 结构相同,但只推送 ReasoningContent,不推送 Content。
|
||||
// 用于只需展示思考过程而无需展示正文的场景。
|
||||
func (e *ChunkEmitter) EmitStreamReasoningText(
|
||||
ctx context.Context,
|
||||
reader infrallm.StreamReader,
|
||||
blockID, stage string,
|
||||
) (string, error) {
|
||||
if e == nil || reader == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var fullText strings.Builder
|
||||
firstChunk := true
|
||||
|
||||
for {
|
||||
chunk, err := reader.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return fullText.String(), err
|
||||
}
|
||||
|
||||
if chunk != nil && strings.TrimSpace(chunk.ReasoningContent) != "" {
|
||||
if emitErr := e.EmitReasoningText(blockID, stage, chunk.ReasoningContent, firstChunk); emitErr != nil {
|
||||
return fullText.String(), emitErr
|
||||
}
|
||||
fullText.WriteString(chunk.ReasoningContent)
|
||||
firstChunk = false
|
||||
}
|
||||
}
|
||||
|
||||
return fullText.String(), nil
|
||||
}
|
||||
|
||||
// EmitStageAsReasoning 把"阶段提示"伪装成 reasoning chunk 推给前端。
|
||||
//
|
||||
// 兼容说明:
|
||||
// 1. 保留旧函数签名,方便当前旧链路直接复用;
|
||||
@@ -378,7 +466,7 @@ func EmitStageAsReasoning(emit PayloadEmitter, requestID, modelName string, crea
|
||||
// EmitAssistantReply 把一段完整正文作为 assistant chunk 推出。
|
||||
//
|
||||
// 注意:
|
||||
// 1. 这里保持“整段发”,不主动切块;
|
||||
// 1. 这里保持"整段发",不主动切块;
|
||||
// 2. 若后续某条链路需要更自然的阅读节奏,应直接调用 EmitPseudoAssistantText;
|
||||
// 3. 为兼容老调用侧,这里 blockID 和 stage 都留空。
|
||||
func EmitAssistantReply(emit PayloadEmitter, requestID, modelName string, created int64, content string, includeRole bool) error {
|
||||
@@ -493,7 +581,7 @@ func (e *ChunkEmitter) emitPseudoText(ctx context.Context, text string, options
|
||||
return nil
|
||||
}
|
||||
|
||||
// SplitPseudoStreamText 按“标点优先、长度兜底”的策略切分整段文本。
|
||||
// SplitPseudoStreamText 按"标点优先、长度兜底"的策略切分整段文本。
|
||||
//
|
||||
// 步骤说明:
|
||||
// 1. 优先在句号、问号、感叹号、分号、换行等自然边界切块,保证阅读顺畅;
|
||||
|
||||
Reference in New Issue
Block a user