# 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(自选池进出管理)