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:
LoveLosita
2026-04-15 11:04:27 +08:00
parent b72e202822
commit 21eed5af75
9 changed files with 658 additions and 234 deletions

View File

@@ -0,0 +1,164 @@
package newagentrouter
import (
"fmt"
"regexp"
"strings"
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
)
var (
// chatRouteHeaderRegex 从模型流式输出中解析 SMARTFLOW_ROUTE 控制码头部。
//
// 格式示例:
// <SMARTFLOW_ROUTE nonce="abc" route="execute" rough_build="true" refine="false" reorder="false" thinking="true"/>
//
// 属性说明:
// 1. nonce防注入校验必须与调用方传入的 nonce 精确匹配;
// 2. route路由目标direct_reply / execute / deep_answer / plan
// 3. rough_build可选仅 route=execute 时有效,默认 false
// 4. refine可选仅 rough_build=true 时有效,默认 false
// 5. reorder可选仅 route=execute 时有效,默认 false
// 6. thinking可选仅 route=execute 时有效,默认 false。
chatRouteHeaderRegex = regexp.MustCompile(
`(?is)<\s*SMARTFLOW_ROUTE\b` +
`[^>]*\bnonce\s*=\s*["']?([a-zA-Z0-9\-]+)["']?` +
`[^>]*\broute\s*=\s*["']?(direct_reply|execute|deep_answer|plan)["']?` +
`(?:[^>]*\brough_build\s*=\s*["']?(true|false)["']?)?` +
`(?:[^>]*\brefine\s*=\s*["']?(true|false)["']?)?` +
`(?:[^>]*\breorder\s*=\s*["']?(true|false)["']?)?` +
`(?:[^>]*\bthinking\s*=\s*["']?(true|false)["']?)?` +
`[^>]*/\s*>`)
)
// StreamRouteParser 从 LLM 流式输出中增量提取路由决策。
//
// 协议约定:模型输出以 SMARTFLOW_ROUTE 控制码标签开头,标签结束后是用户可见内容。
// 例如:<SMARTFLOW_ROUTE nonce="abc" route="direct_reply"/>你好!很高兴见到你...
//
// 职责边界:
// 1. 只负责从流式 chunk 中提取控制码并解析为 ChatRoutingDecision
// 2. 不负责推送 SSE chunk不负责决定后续走哪条链路
// 3. 控制码解析失败时标记 fallback由上层决定降级策略。
type StreamRouteParser struct {
buf strings.Builder
nonce string
routeFound bool
decision *newagentmodel.ChatRoutingDecision
}
// NewStreamRouteParser 创建流式路由解析器。
func NewStreamRouteParser(nonce string) *StreamRouteParser {
return &StreamRouteParser{
nonce: strings.ToLower(strings.TrimSpace(nonce)),
}
}
// Feed 写入一段 chunk content。
//
// 返回值:
// - visible控制码标签之后的内容用户可见文本
// - routeReady路由决策是否已确定
// - err解析错误。
//
// 调用方应在 routeReady=true 后调用 Decision() 获取路由决策,
// 并根据 route 进入对应分支处理 visible 及后续 chunk。
func (p *StreamRouteParser) Feed(content string) (visible string, routeReady bool, err error) {
if p.routeFound {
// 路由已解析,后续 chunk 直接透传。
return content, true, nil
}
p.buf.WriteString(content)
text := p.buf.String()
match := chatRouteHeaderRegex.FindStringSubmatchIndex(text)
if match == nil {
// 控制码尚未完整,检查是否应该 fallback。
if len(text) > 500 {
// 超过 500 字符仍未匹配到控制码 -> fallback 到 plan。
p.routeFound = true
p.decision = &newagentmodel.ChatRoutingDecision{
Route: newagentmodel.ChatRoutePlan,
Raw: text,
}
return text, true, fmt.Errorf("控制码解析超时fallback 到 plan")
}
return "", false, nil
}
// 提取匹配到的子组。
groups := chatRouteHeaderRegex.FindStringSubmatch(text)
if len(groups) < 3 {
return "", false, fmt.Errorf("控制码正则子组不足: %d", len(groups))
}
// nonce 校验。
parsedNonce := strings.ToLower(strings.TrimSpace(groups[1]))
if parsedNonce != p.nonce {
return "", false, fmt.Errorf("nonce 不匹配: got=%s expected=%s", parsedNonce, p.nonce)
}
// 解析 route。
route := newagentmodel.ChatRoute(strings.TrimSpace(groups[2]))
// 解析可选布尔属性(默认 false
roughBuild := parseOptionalBool(groups, 3)
refine := parseOptionalBool(groups, 4)
reorder := parseOptionalBool(groups, 5)
thinking := parseOptionalBool(groups, 6)
p.decision = &newagentmodel.ChatRoutingDecision{
Route: route,
NeedsRoughBuild: roughBuild,
NeedsRefineAfterRoughBuild: refine,
AllowReorder: reorder,
Thinking: thinking,
Raw: groups[0],
}
// 归一化与校验。
if validateErr := p.decision.Validate(); validateErr != nil {
// 校验失败 -> fallback 到 plan。
p.decision.Route = newagentmodel.ChatRoutePlan
p.decision.NeedsRoughBuild = false
p.decision.NeedsRefineAfterRoughBuild = false
p.decision.AllowReorder = false
p.decision.Thinking = false
}
p.routeFound = true
// 控制码标签之后的文本作为 visible 返回。
fullMatch := groups[0]
tagEndIdx := strings.Index(text, fullMatch)
if tagEndIdx >= 0 {
afterTag := text[tagEndIdx+len(fullMatch):]
// 去掉标签后紧跟的换行符(如果有)。
afterTag = strings.TrimPrefix(afterTag, "\r\n")
afterTag = strings.TrimPrefix(afterTag, "\n")
return afterTag, true, nil
}
return "", true, nil
}
// RouteReady 返回路由决策是否已确定。
func (p *StreamRouteParser) RouteReady() bool {
return p.routeFound
}
// Decision 返回已解析的路由决策RouteReady=true 后可用)。
func (p *StreamRouteParser) Decision() *newagentmodel.ChatRoutingDecision {
return p.decision
}
// parseOptionalBool 从正则子组中解析可选布尔值。
// 如果子组不存在或为空,返回 false。
func parseOptionalBool(groups []string, index int) bool {
if index >= len(groups) {
return false
}
return strings.TrimSpace(groups[index]) == "true"
}