fix(decision): add safe fallback when AI outputs only reasoning without JSON (#561)

## 问题 (Problem)
当 AI 只输出思维链分析没有 JSON 决策时,系统会崩溃并报错:
"无法找到JSON数组起始",导致整个交易周期失败,前端显示红色错误。

## 解决方案 (Solution)
1. 添加安全回退机制 (Safe Fallback)
   - 当检测不到 JSON 数组时,自动生成保底决策
   - Symbol: "ALL", Action: "wait"
   - Reasoning 包含思维链摘要(最多 240 字符)

2. 统一注释为简体中文 + 英文对照
   - 关键修复 (Critical Fix)
   - 安全回退 (Safe Fallback)
   - 退而求其次 (Fallback)

## 效果 (Impact)
- 修复前:系统崩溃,前端显示红色错误 "获取AI决策失败"
- 修复后:系统稳定,自动进入 wait 状态,前端显示绿色成功
- 日志记录:[SafeFallback] 标记方便监控和调试

## 设计考量 (Design Considerations)
- 仅在完全找不到 JSON 时触发(区分于格式错误)
- 有 JSON 但格式错误仍然报错(提示需要改进 prompt)
- 保留完整思维链摘要供后续分析
- 避免隐藏真正的问题(格式错误应该暴露)

## 测试 (Testing)
-  正常 JSON 输出:解析成功
-  纯思维链输出:安全回退到 wait
-  JSON 格式错误:继续报错(预期行为)
-  编译通过

## 监控建议 (Monitoring)
可通过日志统计 fallback 频率:
```bash
grep "[SafeFallback]" logs/nofx.log | wc -l
```

如果频率 > 5% 的交易周期,建议检查并改进 prompt 质量。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
0xYYBB | ZYY | Bobo
2025-11-06 00:08:23 +08:00
committed by GitHub
parent b2e4be9152
commit ca2c428357

View File

@@ -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 {