From 4b924f6133d1a431a0006f59107e2969dd3e9d8c Mon Sep 17 00:00:00 2001 From: 0xYYBB | ZYY | Bobo <128128010+zhouyongyou@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:08:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(decision):=20add=20safe=20fallback=20when?= =?UTF-8?q?=20AI=20outputs=20only=20reasoning=20without=20JSON=20(#561)=20?= =?UTF-8?q?##=20=E9=97=AE=E9=A2=98=20(Problem)=20=E5=BD=93=20AI=20?= =?UTF-8?q?=E5=8F=AA=E8=BE=93=E5=87=BA=E6=80=9D=E7=BB=B4=E9=93=BE=E5=88=86?= =?UTF-8?q?=E6=9E=90=E6=B2=A1=E6=9C=89=20JSON=20=E5=86=B3=E7=AD=96?= =?UTF-8?q?=E6=97=B6=EF=BC=8C=E7=B3=BB=E7=BB=9F=E4=BC=9A=E5=B4=A9=E6=BA=83?= =?UTF-8?q?=E5=B9=B6=E6=8A=A5=E9=94=99=EF=BC=9A=20"=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E6=89=BE=E5=88=B0JSON=E6=95=B0=E7=BB=84=E8=B5=B7=E5=A7=8B"?= =?UTF-8?q?=EF=BC=8C=E5=AF=BC=E8=87=B4=E6=95=B4=E4=B8=AA=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E5=91=A8=E6=9C=9F=E5=A4=B1=E8=B4=A5=EF=BC=8C=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E7=BA=A2=E8=89=B2=E9=94=99=E8=AF=AF=E3=80=82?= =?UTF-8?q?=20##=20=E8=A7=A3=E5=86=B3=E6=96=B9=E6=A1=88=20(Solution)=201.?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E5=AE=89=E5=85=A8=E5=9B=9E=E9=80=80?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=20(Safe=20Fallback)=20=20=20=20-=20=E5=BD=93?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E4=B8=8D=E5=88=B0=20JSON=20=E6=95=B0?= =?UTF-8?q?=E7=BB=84=E6=97=B6=EF=BC=8C=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90?= =?UTF-8?q?=E4=BF=9D=E5=BA=95=E5=86=B3=E7=AD=96=20=20=20=20-=20Symbol:=20"?= =?UTF-8?q?ALL",=20Action:=20"wait"=20=20=20=20-=20Reasoning=20=E5=8C=85?= =?UTF-8?q?=E5=90=AB=E6=80=9D=E7=BB=B4=E9=93=BE=E6=91=98=E8=A6=81=EF=BC=88?= =?UTF-8?q?=E6=9C=80=E5=A4=9A=20240=20=E5=AD=97=E7=AC=A6=EF=BC=89=202.=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=B3=A8=E9=87=8A=E4=B8=BA=E7=AE=80=E4=BD=93?= =?UTF-8?q?=E4=B8=AD=E6=96=87=20+=20=E8=8B=B1=E6=96=87=E5=AF=B9=E7=85=A7?= =?UTF-8?q?=20=20=20=20-=20=E5=85=B3=E9=94=AE=E4=BF=AE=E5=A4=8D=20(Critica?= =?UTF-8?q?l=20Fix)=20=20=20=20-=20=E5=AE=89=E5=85=A8=E5=9B=9E=E9=80=80=20?= =?UTF-8?q?(Safe=20Fallback)=20=20=20=20-=20=E9=80=80=E8=80=8C=E6=B1=82?= =?UTF-8?q?=E5=85=B6=E6=AC=A1=20(Fallback)=20##=20=E6=95=88=E6=9E=9C=20(Im?= =?UTF-8?q?pact)=20-=20=E4=BF=AE=E5=A4=8D=E5=89=8D=EF=BC=9A=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=B4=A9=E6=BA=83=EF=BC=8C=E5=89=8D=E7=AB=AF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=BA=A2=E8=89=B2=E9=94=99=E8=AF=AF=20"=E8=8E=B7?= =?UTF-8?q?=E5=8F=96AI=E5=86=B3=E7=AD=96=E5=A4=B1=E8=B4=A5"=20-=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=90=8E=EF=BC=9A=E7=B3=BB=E7=BB=9F=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=EF=BC=8C=E8=87=AA=E5=8A=A8=E8=BF=9B=E5=85=A5=20wait?= =?UTF-8?q?=20=E7=8A=B6=E6=80=81=EF=BC=8C=E5=89=8D=E7=AB=AF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=BB=BF=E8=89=B2=E6=88=90=E5=8A=9F=20-=20=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=AE=B0=E5=BD=95=EF=BC=9A[SafeFallback]=20=E6=A0=87?= =?UTF-8?q?=E8=AE=B0=E6=96=B9=E4=BE=BF=E7=9B=91=E6=8E=A7=E5=92=8C=E8=B0=83?= =?UTF-8?q?=E8=AF=95=20##=20=E8=AE=BE=E8=AE=A1=E8=80=83=E9=87=8F=20(Design?= =?UTF-8?q?=20Considerations)=20-=20=E4=BB=85=E5=9C=A8=E5=AE=8C=E5=85=A8?= =?UTF-8?q?=E6=89=BE=E4=B8=8D=E5=88=B0=20JSON=20=E6=97=B6=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=EF=BC=88=E5=8C=BA=E5=88=86=E4=BA=8E=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E9=94=99=E8=AF=AF=EF=BC=89=20-=20=E6=9C=89=20JSON=20=E4=BD=86?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E9=94=99=E8=AF=AF=E4=BB=8D=E7=84=B6=E6=8A=A5?= =?UTF-8?q?=E9=94=99=EF=BC=88=E6=8F=90=E7=A4=BA=E9=9C=80=E8=A6=81=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=20prompt=EF=BC=89=20-=20=E4=BF=9D=E7=95=99=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E6=80=9D=E7=BB=B4=E9=93=BE=E6=91=98=E8=A6=81=E4=BE=9B?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E5=88=86=E6=9E=90=20-=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E9=9A=90=E8=97=8F=E7=9C=9F=E6=AD=A3=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=88=E6=A0=BC=E5=BC=8F=E9=94=99=E8=AF=AF=E5=BA=94=E8=AF=A5?= =?UTF-8?q?=E6=9A=B4=E9=9C=B2=EF=BC=89=20##=20=E6=B5=8B=E8=AF=95=20(Testin?= =?UTF-8?q?g)=20-=20=E2=9C=85=20=E6=AD=A3=E5=B8=B8=20JSON=20=E8=BE=93?= =?UTF-8?q?=E5=87=BA=EF=BC=9A=E8=A7=A3=E6=9E=90=E6=88=90=E5=8A=9F=20-=20?= =?UTF-8?q?=E2=9C=85=20=E7=BA=AF=E6=80=9D=E7=BB=B4=E9=93=BE=E8=BE=93?= =?UTF-8?q?=E5=87=BA=EF=BC=9A=E5=AE=89=E5=85=A8=E5=9B=9E=E9=80=80=E5=88=B0?= =?UTF-8?q?=20wait=20-=20=E2=9C=85=20JSON=20=E6=A0=BC=E5=BC=8F=E9=94=99?= =?UTF-8?q?=E8=AF=AF=EF=BC=9A=E7=BB=A7=E7=BB=AD=E6=8A=A5=E9=94=99=EF=BC=88?= =?UTF-8?q?=E9=A2=84=E6=9C=9F=E8=A1=8C=E4=B8=BA=EF=BC=89=20-=20=E2=9C=85?= =?UTF-8?q?=20=E7=BC=96=E8=AF=91=E9=80=9A=E8=BF=87=20##=20=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E5=BB=BA=E8=AE=AE=20(Monitoring)=20=E5=8F=AF=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E6=97=A5=E5=BF=97=E7=BB=9F=E8=AE=A1=20fallback=20?= =?UTF-8?q?=E9=A2=91=E7=8E=87=EF=BC=9A=20```bash=20grep=20"[SafeFallback]"?= =?UTF-8?q?=20logs/nofx.log=20|=20wc=20-l=20```=20=E5=A6=82=E6=9E=9C?= =?UTF-8?q?=E9=A2=91=E7=8E=87=20>=205%=20=E7=9A=84=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E5=91=A8=E6=9C=9F=EF=BC=8C=E5=BB=BA=E8=AE=AE=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E5=B9=B6=E6=94=B9=E8=BF=9B=20prompt=20=E8=B4=A8=E9=87=8F?= =?UTF-8?q?=E3=80=82=20Co-authored-by:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index 598658d1..98a56f73 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -481,15 +481,15 @@ func extractDecisions(response string) ([]Decision, error) { s := removeInvisibleRunes(response) s = strings.TrimSpace(s) - // 🔧 關鍵修復:在正則匹配之前就先修復全角字符! - // 否則正則表達式 \[ 無法匹配全角的 [ + // 🔧 关键修复 (Critical Fix):在正则匹配之前就先修复全角字符! + // 否则正则表达式 \[ 无法匹配全角的 [ s = fixMissingQuotes(s) // 1) 优先从 ```json 代码块中提取 if m := reJSONFence.FindStringSubmatch(s); m != nil && len(m) > 1 { jsonContent := strings.TrimSpace(m[1]) jsonContent = compactArrayOpen(jsonContent) // 把 "[ {" 规整为 "[{" - jsonContent = fixMissingQuotes(jsonContent) // 二次修復(防止 regex 提取後還有全角) + jsonContent = fixMissingQuotes(jsonContent) // 二次修复(防止 regex 提取后还有残留全角) if err := validateJSONFormat(jsonContent); err != nil { return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response) } @@ -500,16 +500,32 @@ func extractDecisions(response string) ([]Decision, error) { return decisions, nil } - // 2) 退而求其次:全文寻找首个对象数组 - // 注意:此時 s 已經過 fixMissingQuotes(),全角字符已轉換為半角 + // 2) 退而求其次 (Fallback):全文寻找首个对象数组 + // 注意:此时 s 已经过 fixMissingQuotes(),全角字符已转换为半角 jsonContent := strings.TrimSpace(reJSONArray.FindString(s)) if jsonContent == "" { - return nil, fmt.Errorf("无法找到JSON数组起始(已嘗試修復全角字符)\n原始響應前200字符: %s", s[:min(200, len(s))]) + // 🔧 安全回退 (Safe Fallback):当AI只输出思维链没有JSON时,生成保底决策(避免系统崩溃) + log.Printf("⚠️ [SafeFallback] AI未输出JSON决策,进入安全等待模式 (AI response without JSON, entering safe wait mode)") + + // 提取思维链摘要(最多 240 字符) + cotSummary := s + if len(cotSummary) > 240 { + cotSummary = cotSummary[:240] + "..." + } + + // 生成保底决策:所有币种进入 wait 状态 + fallbackDecision := Decision{ + Symbol: "ALL", + Action: "wait", + Reasoning: fmt.Sprintf("模型未输出结构化JSON决策,进入安全等待;摘要:%s", cotSummary), + } + + return []Decision{fallbackDecision}, nil } - // 🔧 規整格式(此時全角字符已在前面修復過) + // 🔧 规整格式(此时全角字符已在前面修复过) jsonContent = compactArrayOpen(jsonContent) - jsonContent = fixMissingQuotes(jsonContent) // 二次修復(防止 regex 提取後還有殘留全角) + jsonContent = fixMissingQuotes(jsonContent) // 二次修复(防止 regex 提取后还有残留全角) // 🔧 验证 JSON 格式(检测常见错误) if err := validateJSONFormat(jsonContent); err != nil {