Files
smartmate/backend/newAgent/router/chat_route.go
LoveLosita 21eed5af75 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
前端:无
仓库:无
2026-04-15 11:04:27 +08:00

165 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
}