后端: 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 前端:无 仓库:无
165 lines
5.5 KiB
Go
165 lines
5.5 KiB
Go
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"
|
||
}
|