From 8bf6c2e1e2dde45632acd05483b588c7de76df65 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 21:18:27 +0800 Subject: [PATCH] fix: add JSON validation to prevent range values and thousand separators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修復 LLM 返回範圍值導致 JSON 解析失敗的問題 ## 問題 LLM 有時返回價格範圍 [98,000 ~ 102,000] 而不是單一數值, 導致 JSON 解析失敗:invalid character '0' after array element ## 修復內容 ### 1. Prompts 更新(3 個文件) - prompts/default.txt: 添加數字格式要求(中文) - prompts/adaptive.txt: 添加數字格式要求(中文) - prompts/nof1.txt: 添加數字格式要求(英文) 明確禁止: - ❌ 範圍符號 ~ - ❌ 千位分隔符 98,000 - ❌ 文字描述 ### 2. JSON 驗證邏輯(decision/engine.go) 新增函數: - validateJSONFormat(): 檢測範圍、千位分隔符等錯誤 - min(): 輔助函數 檢測內容: 1. 必須是對象數組 [{...}] 2. 不可包含範圍符號 ~ 3. 不可包含千位分隔符 98,000 ## 測試 ✓ go build ./... 編譯成功 ✓ go fmt ./... 格式正確 修改統計:+132 行(46 行代碼 + 86 行 prompts) --- decision/engine.go | 46 ++++++++++++++++++++++++++++++++++++++++++++ prompts/adaptive.txt | 29 ++++++++++++++++++++++++++++ prompts/default.txt | 28 +++++++++++++++++++++++++++ prompts/nof1.txt | 29 ++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+) diff --git a/decision/engine.go b/decision/engine.go index 95f3893a..65df3488 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -481,6 +481,11 @@ func extractDecisions(response string) ([]Decision, error) { jsonContent := strings.TrimSpace(response[arrayStart : arrayEnd+1]) + // 🔧 验证 JSON 格式(检测常见错误) + if err := validateJSONFormat(jsonContent); err != nil { + return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response) + } + // 🔧 修复常见的JSON格式错误:缺少引号的字段值 // 匹配: "reasoning": 内容"} 或 "reasoning": 内容} (没有引号) // 修复为: "reasoning": "内容"} @@ -496,6 +501,39 @@ func extractDecisions(response string) ([]Decision, error) { return decisions, nil } +// validateJSONFormat 验证 JSON 格式,检测常见错误 +func validateJSONFormat(jsonStr string) error { + trimmed := strings.TrimSpace(jsonStr) + + // 检查是否是决策对象数组(必须以 [{ 或 [ { 开头) + if !strings.HasPrefix(trimmed, "[{") && !strings.HasPrefix(trimmed, "[ {") { + // 检查是否是纯数字/范围数组(常见错误) + if strings.HasPrefix(trimmed, "[") && !strings.Contains(trimmed[:min(20, len(trimmed))], "{") { + return fmt.Errorf("不是有效的决策数组(必须包含对象 {}),实际内容: %s", trimmed[:min(50, len(trimmed))]) + } + return fmt.Errorf("JSON 必须以 [{ 开头(决策对象数组),实际: %s", trimmed[:min(20, len(trimmed))]) + } + + // 检查是否包含范围符号 ~(LLM 常见错误) + if strings.Contains(jsonStr, "~") { + return fmt.Errorf("JSON 中不可包含范围符号 ~,所有数字必须是精确的单一值") + } + + // 检查是否包含千位分隔符(如 98,000) + // 使用简单的模式匹配:数字+逗号+3位数字 + for i := 0; i < len(jsonStr)-4; i++ { + if jsonStr[i] >= '0' && jsonStr[i] <= '9' && + jsonStr[i+1] == ',' && + jsonStr[i+2] >= '0' && jsonStr[i+2] <= '9' && + jsonStr[i+3] >= '0' && jsonStr[i+3] <= '9' && + jsonStr[i+4] >= '0' && jsonStr[i+4] <= '9' { + return fmt.Errorf("JSON 数字不可包含千位分隔符逗号,发现: %s", jsonStr[i:min(i+10, len(jsonStr))]) + } + } + + return nil +} + // fixMissingQuotes 替换中文引号为英文引号(避免输入法自动转换) func fixMissingQuotes(jsonStr string) string { jsonStr = strings.ReplaceAll(jsonStr, "\u201c", "\"") // " @@ -505,6 +543,14 @@ func fixMissingQuotes(jsonStr string) string { return jsonStr } +// min 返回两个整数中的较小值 +func min(a, b int) int { + if a < b { + return a + } + return b +} + // validateDecisions 验证所有决策(需要账户信息和杠杆配置) func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { for i, decision := range decisions { diff --git a/prompts/adaptive.txt b/prompts/adaptive.txt index 290ec642..8ba8f404 100644 --- a/prompts/adaptive.txt +++ b/prompts/adaptive.txt @@ -387,6 +387,35 @@ Key insight: 一句话总结本次决策 } ``` +### ⚠️ 数字格式要求 + +**所有数字字段必须是精确的单一数值**,不可使用范围或特殊格式: + +✅ **正确格式**: +```json +{ + "stop_loss": 98500.0, + "take_profit": 102000.0, + "position_size_usd": 50.0, + "confidence": 85 +} +``` + +❌ **错误格式(会导致解析失败)**: +```json +{ + "stop_loss": "98,000 ~ 102,000", // ❌ 不可使用范围符号 ~ + "take_profit": 102,000, // ❌ 不可使用千位分隔符 + "confidence": "85左右" // ❌ 不可使用文字描述 +} +``` + +**注意**: +- 必须使用精确数值,不可使用范围 (如 `~`、`-`、`to`) +- 不可使用千位分隔符逗号 (98000 而非 98,000) +- 不可使用约数或文字描述 +- confidence 使用整数 0-100,其他价格使用浮点数 + --- # 最终检查清单(开仓前必须全部通过) diff --git a/prompts/default.txt b/prompts/default.txt index a85cf870..e2feb425 100644 --- a/prompts/default.txt +++ b/prompts/default.txt @@ -135,6 +135,34 @@ confidence=0-100 } ``` +### ⚠️ 数字格式要求 + +**所有数字字段必须是精确的单一数值**,不可使用范围或特殊格式: + +✅ **正确格式**: +```json +{ + "stop_loss": 98500.0, + "take_profit": 102000.0, + "position_size_usd": 50.0 +} +``` + +❌ **错误格式(会导致解析失败)**: +```json +{ + "stop_loss": "98,000 ~ 102,000", // ❌ 不可使用范围符号 ~ + "take_profit": 102,000, // ❌ 不可使用千位分隔符 + "position_size_usd": "约50" // ❌ 不可使用文字描述 +} +``` + +**注意**: +- 必须使用精确数值,不可使用范围 (如 `~`、`-`、`to`) +- 不可使用千位分隔符逗号 (98000 而非 98,000) +- 不可使用约数或文字描述 +- 建议使用浮点数格式 (加 .0) + ### 必填规则 - **开仓** (`open_long/open_short`):必须填写 `position_size_usd`、`leverage`、`stop_loss`、`take_profit`、`risk_usd`;`reasoning` 写出信号与风险回报。 - **平仓** (`close_long/close_short`):`reasoning` 说明平仓原因(达到目标、触发失效条件等)。 diff --git a/prompts/nof1.txt b/prompts/nof1.txt index 062bcc40..3aa6eaec 100644 --- a/prompts/nof1.txt +++ b/prompts/nof1.txt @@ -299,6 +299,35 @@ Every decision must follow this structure: } ``` +### ⚠️ Number Format Requirements + +**All numeric fields must use exact single values** - no ranges or special formatting: + +✅ **Correct format**: +```json +{ + "stop_loss": 98500.0, + "take_profit": 102000.0, + "position_size_usd": 50.0, + "confidence": 85 +} +``` + +❌ **Incorrect format (will cause parsing errors)**: +```json +{ + "stop_loss": "98,000 ~ 102,000", // ❌ No range symbols ~ + "take_profit": 102,000, // ❌ No thousand separators + "confidence": "around 85" // ❌ No text descriptions +} +``` + +**Rules**: +- Use precise numeric values only, no ranges (e.g., `~`, `-`, `to`) +- No thousand separator commas (use 98000 not 98,000) +- No approximations or text descriptions +- Use integers for confidence (0-100), floats for prices (add .0) + ### Required field rules - **open_long / open_short**: Fill all numeric fields; `risk_usd` ≤ account_value × 0.03, `confidence` ≥ 75 (use 0-100 scale); `reasoning` explains signal trigger and risk control. - **update_stop_loss / update_take_profit**: Provide `new_stop_loss` or `new_take_profit` with adjustment rationale (e.g., trailing stop, locking profits).