diff --git a/docs/llm-execution-protocol.md b/docs/llm-execution-protocol.md new file mode 100644 index 00000000..d807904b --- /dev/null +++ b/docs/llm-execution-protocol.md @@ -0,0 +1,254 @@ +# LLM 执行协议(开发规范) + +> 任何"LLM 输出 → 代码校验 → 写入 DB"的场景,必须遵循本协议。 +> 本协议从重评流程(batch_reassess)提取,适用于所有 LLM→DB 交互。 + +--- + +## 一、协议总览 + +``` +输入数据 → Prompt模板 → LLM输出 → 节标题解析 → 代码校验 → DB写入 → 确认反馈 +``` + +**核心原则**: +1. **LLM 只输出文本**,不直接写 DB +2. **代码层做所有校验**(LLM 输出不可信) +3. **写入前快照**,失败可恢复 +4. **白名单控制写入权限**(不是所有 source 都能写所有字段) + +--- + +## 二、输入数据层 + +### 2.1 数据来源规范 + +| 数据类型 | 来源 | 层级 | 示例 | +|---|---|---|---| +| 价格 | stock_daily.close / live_prices.price | 采集层 | 收盘价/盘中价 | +| 基本面 | stock_fundamentals.pe/mcap | 加工层 | PE/市值 | +| 技术指标 | ta.full_analysis() / stock_indicators | 加工层 | 支撑阻力/MA/RSI | +| 策略参数 | holding_strategies | 业务层 | entry/stop/tp | +| 消息面 | signal_news | 采集层 | 新闻摘要 | +| 资金流 | capital_flow_cache | 采集层 | 净流入/主力 | +| 组合状态 | portfolio_summary + holdings | 业务层 | 总资产/持仓股数 | + +**铁律**:使用层不得直接调 API 采集数据。所有数据必须从 DB 表读取(由采集层写入)。 + +### 2.2 数据收集函数 + +每个 LLM 任务实现一个 `collect_data(code)` 函数: +- 从各表读取数据 +- 填充 `data` dict +- 返回给 build_prompt 使用 + +--- + +## 三、Prompt 模板规范 + +### 3.1 结构 + +``` +角色定义(一句话) + +【输入数据段1】(系统提供,非 LLM 生成) +【输入数据段2】 +... +【输出格式要求】(节标题列表 + 每个字段的约束) + +⚠️ 输出纪律(必须遵守) +1. 直接以【节标题】开头 +2. 禁止寒暄/开场白 +3. 所有【】节标题一个都不能少 +``` + +### 3.2 节标题设计原则 + +- **每个字段一个节标题**(如 `【买入区间】`、`【建议止损】`) +- **节标题行首匹配**(不使用关键词包含) +- **一个字段一个值**(禁止节标题内嵌套多个值) +- **"无"显式表达**(如 `【买入区间】无`,不写 `0.0~0.0`) + +### 3.3 输出纪律 + +prompt 末尾必须包含: +``` +⚠️ 输出纪律: +1. 直接以【xxx】开头 +2. 禁止寒暄 +3. 所有【】节标题一个都不能少 +4. 每个字段只填一个值 +``` + +--- + +## 四、解析层(parse_response) + +### 4.1 节标题解析器 + +```python +def _section_line(text, name): + """从 LLM 输出中提取指定节标题的内容""" + for line in text.split("\n"): + if re.match(r'^\s*【' + name + r'】', line): + return line + return "" +``` + +**规则**: +- 行首匹配(可含空白) +- 只认精确节标题(不用 "包含xxx的第一行") +- 返回整行内容(含节标题本身) + +### 4.2 解析约束 + +每个字段的解析必须包含: +- **类型校验**:数字字段必须是数字 +- **范围校验**:如仓位必须 1-30% +- **一致性校验**:止损 < 区间下沿 < 区间上沿 < 止盈 +- **空值处理**:显式 "无" → 清空;空字符串 → 跳过(保留原值) + +--- + +## 五、校验层(save_result / 门禁) + +### 5.1 门禁清单 + +| 门禁 | 规则 | 失败行为 | +|---|---|---| +| 空输出 | LLM 返回空/无节标题 | 拒绝写入 | +| 区间合理性 | 下沿 < 上沿 < 下沿 × 3 | 跳过区间写入 | +| 区间偏离 | 区上沿 < 现价 × 0.5 或 区下沿 > 现价 × 1.5 | 拒写并清空 | +| 止损锚定 | 止损 ≥ 区间下沿 | 用技术位修正 / 拒写 | +| 信号一致性 | 操作建议否定买入但信号是"买入" | 降级为"关注" | +| 数量正整数 | 股数必须 > 0 且为整数 | 拒绝写入 | +| 代码存在性 | 股票代码必须在 stock_daily 中存在 | 拒绝写入 | + +### 5.2 门禁输出 + +每个门禁失败必须: +1. 打印 `⚠️` 日志(含具体原因和数值) +2. 跳过该字段写入(保留原值)或拒绝整个写入 +3. **不能静默失败** + +--- + +## 六、写入层 + +### 6.1 写入前操作 + +```python +snapshot_strategy_history(conn, code, source_trigger) # 快照 +``` + +### 6.2 写入权限(白名单) + +```python +_PARAM_WHITELIST = ('per_stock_12d', 'batch_12d', 'promote') +if source_trigger not in _PARAM_WHITELIST: + # 非白名单 source:保留 DB 当前参数值 +``` + +### 6.3 写入后操作 + +- 返回确认信息给调用方 +- 触发必要的联动(如快照、信号同步) + +--- + +## 七、错误处理 + +### 7.1 LLM 调用失败 + +``` +重试 → 升级模型 → 最终失败则跳过该股票 +``` + +### 7.2 解析失败 + +``` +节标题未找到 → 该字段跳过(保留原值) +所有字段跳过 → 拒绝写入 +``` + +### 7.3 写入失败 + +``` +DB 异常 → 回滚到快照 → 打印错误 +``` + +--- + +## 八、可复用组件 + +### 8.1 `_section_line(text, name)` — 节标题解析器 +- 位置:batch_reassess.py +- 用途:从 LLM 输出提取指定节内容 +- 通用性:✅ 任何 LLM 输出解析都可复用 + +### 8.2 `snapshot_strategy_history(conn, code, source)` — 写入前快照 +- 位置:mofin_db.py +- 用途:记录修改前状态,失败可恢复 +- 通用性:✅ 任何 DB 写入都应先快照 + +### 8.3 `write_holding_strategy(..., source_trigger)` — 白名单写入 +- 位置:mofin_db.py +- 用途:按 source_trigger 控制写入权限 +- 通用性:✅ 所有策略卡写入都走这个函数 + +### 8.4 门禁模式 + +```python +# 区间合理性门禁 +if _el > 0 and _eh > _el and _eh < _el * 3: + # 合理,写入 +elif _el > 0 or _eh > 0: + print("⚠️ 解析异常,跳过") +``` + +通用性:✅ 任何数值字段都可套用此模式 + +--- + +## 九、新场景接入清单 + +新增"LLM 输出 → 写入 DB"场景时,按以下清单逐项完成: + +- [ ] 实现 `collect_data(code)` — 收集输入数据 +- [ ] 实现 `build_prompt(data)` — 定义输出格式(节标题) +- [ ] 定义节标题列表 — 每个字段一个标题 +- [ ] 实现 `parse_response(text)` — 按节标题解析 +- [ ] 实现门禁逻辑 — 校验每个字段的合理性 +- [ ] 选择写入函数 — 复用已有或新建 +- [ ] 定义 source_trigger — 写入权限控制 +- [ ] 测试:正常输入 → 正常输出 +- [ ] 测试:异常输入(空/越界/格式错误)→ 正确拒绝 +- [ ] 测试:并发写入 → 不互相覆盖 + +--- + +## 十、示例:截图识别更新持仓 + +### 输入 +用户发交易截图 → 知微 LLM 解析 + +### 输出格式 +``` +【交易动作】买入 / 卖出 +【股票代码】600262 +【股票名称】北方稀土 +【交易数量】500 +【交易价格】15.20 +【交易时间】2026-08-20 14:30 +``` + +### 门禁 +- 交易动作:必须是"买入"或"卖出" +- 股票代码:6位数字,必须在 stock_daily 中存在 +- 交易数量:正整数,卖出时不超过当前持仓 +- 交易价格:正数,与现价偏差不超过 ±10% + +### 写入 +- 买入 → INSERT OR REPLACE holdings(shares += 交易数量) +- 卖出 → UPDATE holdings(shares -= 交易数量,shares=0 时标记 is_active=0) +- 写入后自动调用 clean_watchlist(自选池进出管理) diff --git a/specs/schemas/reassess-output.json b/specs/schemas/reassess-output.json new file mode 100644 index 00000000..b539003f --- /dev/null +++ b/specs/schemas/reassess-output.json @@ -0,0 +1,13 @@ +{ + "description": "重评 LLM 输出的节标题格式(parse_response 使用)", + "sections": [ + {"name": "综合结论", "field": "signal", "type": "enum", "values": ["买入","关注","观望","卖出","止盈","持有","弱势持有","可加仓","可买入"]}, + {"name": "买入区间", "field": "entry", "type": "range", "constraint": "下沿<上沿<下沿x3"}, + {"name": "建议止损", "field": "stop_loss", "type": "number", "constraint": "必须低于买入区下沿"}, + {"name": "建议止盈", "field": "take_profit", "type": "number", "constraint": "必须高于买入区上沿"}, + {"name": "操作建议", "field": "action_advice", "type": "string", "max_length": 200}, + {"name": "建议仓位", "field": "position", "type": "percentage", "constraint": "1-30%,仅买入信号时填写"}, + {"name": "策略判断", "field": "strategy_judge", "type": "enum", "values": ["维持原策略","修改参数","策略失效需更换"]}, + {"name": "策略失效理由", "field": "strategy_reason", "type": "string", "required_if": "strategy_judge≠维持原策略"} + ] +} diff --git a/specs/schemas/trade-capture-output.json b/specs/schemas/trade-capture-output.json new file mode 100644 index 00000000..04f73731 --- /dev/null +++ b/specs/schemas/trade-capture-output.json @@ -0,0 +1,11 @@ +{ + "description": "交易截图识别 LLM 输出格式", + "sections": [ + {"name": "交易动作", "field": "action", "type": "enum", "values": ["买入","卖出"]}, + {"name": "股票代码", "field": "code", "type": "string", "constraint": "6位数字"}, + {"name": "股票名称", "field": "name", "type": "string"}, + {"name": "交易数量", "field": "shares", "type": "integer", "constraint": "必须>0"}, + {"name": "交易价格", "field": "price", "type": "number", "constraint": "必须>0,与现价偏差±10%"}, + {"name": "交易时间", "field": "time", "type": "datetime", "format": "YYYY-MM-DD HH:MM"} + ] +}