观潮锐评记:让 AI 从「改语气」到「做判断」,7 项数据增强 173 行代码

观潮锐评记:让 AI 从「改语气」到「做判断」,7 项数据增强 173 行代码

接上回:观潮出海记——美股支持上线,AAPL 和 TSLA 能分析了。

出海那天很兴奋,但第二天小龙虾做了一件事让我冷静下来——它读完了观潮的全部代码,然后问了一个问题:

"你的 LLM 分析股票时,到底看到了什么数据?"

"这不是 AI 分析,这是 AI 改作文"

答案让人尴尬。

观潮的分析链路是这样的:数据层拉 K 线 → 技术分析引擎算评分 → 模板叙事生成一段中文文本 → LLM 把这段文本"润色"成更自然的语气。

问题在于,LLM 拿到的只有三样东西:

  1. 一段模板叙事文本("威海广泰近5日偏弱,均线空头排列...")
  2. 一个评分数字(-82)
  3. 持仓上下文("4600股,成本¥8.75,浮盈+13.2%")

它看不到新闻、看不到估值、看不到龙虎榜。PE/PB 字段写了但永远是 0。新闻数据明明拉回来了,但没人传给 LLM。get_lhb() 方法写好了,_analyze_stock_sync() 里根本没调。

用小龙虾的话说:"LLM 润色 ≠ LLM 分析,它只是在改写模板叙事的语气,不是基于原始数据做独立判断。"

这就是一次代码审计的价值——你自己写的东西,你以为它在"分析",其实它只是在"改作文"。

T1:先把存货用尽

最讽刺的发现是:很多数据代码已经写好了,只是没接到主分析流里。小龙虾把这些叫做"存货"——写好没用的代码比没写更浪费。

三项改动,一个下午:

龙虎榜接线。get_lhb() 方法一直在 data.py 里躺着,只是 _analyze_stock_sync() 的并发拉取里没调它。加一行 executor.submit(market_data.get_lhb, symbol) 就行——但 AKShare 更新了 API 签名,从 symbol 参数变成了日期范围查全市场再筛个股,多花了二十分钟做适配。

新闻喂 LLM。新闻数据明明在 news 变量里,polish_narrative() 的参数列表里就是没有它。加个 news 参数,在 prompt 里插入新闻标题列表,告诉 LLM"如有重大利好/利空务必提及"。

效果立竿见影。改之前,威海广泰的叙事是"近5日偏弱,均线空头排列"。改之后:

消息面倒是有些亮点,收购空港股权、获得国际奖项、无人机试飞成功,都算利好。

LLM 终于不是个"瞎子"了。

美股新闻。之前美股分析直接跳过了新闻(_skip_news = _is_us)。yfinance 的 ticker.news 是免费的,只是返回格式比较坑—— title 藏在 content.title 里面,不是顶层的 title 字段。一个 hotfix 解决。TSLA 分析里出现了"US jury finds Elon Musk misled Twitter shareholders"。

三个 fix 跟着三个 commit,T1 的教训是:第三方 API 永远在变,代码写好不代表能用,必须实际跑一遍验证。

T2:PE/PB 的惊喜

T2 的第一项是补基本面——PE/PB/ROE,小龙虾审计中标注的"最大缺口"。

原始规划是用 baostock 的 query_profit_data() 单独拉季报数据,预估 60 行代码。但我在验证数据质量时发现了一个惊喜:

# baostock K线查询,原来支持这些字段
rs = bs.query_history_k_data_plus(
    bs_code,
    "date,open,high,low,close,volume,pctChg,turn,peTTM,pbMRQ",
    ...
)

peTTM 和 pbMRQ 就在 K 线接口里。不需要额外请求、不需要新方法、不需要缓存策略——只要在查询字符串里多写两个字段名。

预估 60 行,实际改动 1 行查询字段 + 2 行列名映射 + 3 行 server.py 提取。PE 从永远的 0 变成了 24.07。

同一个 commit 里还顺手加了换手率(turn 字段),在技术分析里做了异常检测:换手率 >8% 配合方向判断是放量上攻还是主力出货,突然萧条(从 2% 跌到 0.5%)提示市场关注度下降。和已有的量比、OBV 形成三角交叉验证。

T2-3:从"改作文"到"做判断"

前面补的所有数据——新闻、PE/PB、换手率——最终都要汇聚到一个地方:LLM 的 prompt。

之前的 prompt 是这样的:

你是一位经验丰富的A股分析师,正在和朋友聊投资。
请将以下分析报告润色为更自然、更有"聊天感"的短评。
...
原始分析:
{template_narrative}

"请润色"——定位就是改语气。LLM 再聪明,你只给它一段文字让它改写,它也只能改写。

改造后的 prompt:

你是一位经验丰富的A股分析师,正在和朋友聊投资。
请基于以下数据和分析,输出一段自然的、有"聊天感"的投资短评。

要求:
- 基于结构化数据做独立交叉验证,不要只改写模板叙事
- 如果数据之间有矛盾(技术vs基本面、资金vs价格),要重点分析
...

结构化数据:
方向: 看空 | 综合评分: -82 (原始-77, 体制调整-5)
价格: 9.91 | 5日涨跌: -5.8% | 20日位置: 6%
均线: 空头排列 | MA5偏离: -1.3% | MA20偏离: -8.0%
动量: RSI 30 | MACD 死叉
量能: 量比 0.75x | OBV斜率 -0.839 | 换手率 0.9% (5日均 1.2%)
估值: PE(TTM) 72.1 | PB(MRQ) 5.12
布林: 位置 6% | ATR 0.39
体制: 熊市 — 趋势向下 | 建议仓位 ≤20%
冲突: 技术面看空但主力资金净流入(可能洗盘)

模板分析(供参考,不要照搬):
{template_narrative}

关键变化:"请润色"变成了"请基于数据做独立判断",模板叙事从"原始分析"降级为"供参考,不要照搬"。

效果对比:

Before(T1 之后,有新闻但无结构化数据):

威海广泰这两天确实有点难受,连续走弱跌了5.8%...消息面倒是有些亮点,收购空港股权...

After(T2-3,完整结构化数据):

技术面这么空,主力资金却在悄悄吸筹,这可能是在洗盘。你8.75的成本还不错,现在浮盈13.2%,但我建议先减个20-30手锁定部分利润。原因是现在大盘还在熊市格局,反弹很可能是陷阱,而且这票PE72倍估值不算便宜。

区别一目了然:

  • 会用 PE 判断估值了("72倍不算便宜")
  • 会发现矛盾信号了("技术面空但主力吸筹 → 可能洗盘")
  • 会给具体操作建议了("减20-30手",而不是笼统的"减仓")
  • 会结合体制做风险判断了("大盘熊市,反弹可能是陷阱")

这不再是"改作文",是真正的"做判断"。

一个 bug 和一个教训

T2-3 上线后,小龙虾做了 re-review,给了 9.5/10。但它抓到了一个隐藏 bug:

# 我写的
f"MACD {'金叉' if _mom.get('macd_cross') == 'golden' else '死叉' if _mom.get('macd_cross') == 'death' else '无交叉'}"

# technical.py 实际返回的
macd_cross: "金叉" / "死叉" / "无"

我在用英文 golden/death 去匹配中文 金叉/死叉——永远匹配不上,LLM 看到的永远是"无交叉"。一个本应该在写代码时就发现的问题,因为两个模块之间的格式约定不一致,悄悄丢失了一个重要的技术指标。

修复只需要一行:

f"动量: RSI {_mom.get('rsi_14', 50):.0f} | MACD {_mom.get('macd_cross', '无')}"

教训很简单:数据从 A 模块流到 B 模块时,格式约定必须一致。不要在消费端硬编码生产端的值——直接用生产端返回的原始值。

173 行代码的全景

回看这一天的改动——5 个文件、173 行代码、8 个 commit:

层改动效果
data.pyK 线加 turn/peTTM/pbMRQ 字段 + 美股新闻 + 龙虎榜适配数据从"有但没用"变成"有且可用"
technical.py换手率异常检测 + 三角交叉验证评分维度从 8 维扩展到 9 维
server.pyPE/PB 提取 + 结构化摘要构建 + data_summary 传递LLM 看到完整数据画面
llm.pyprompt 升级 + news/data_summary 注入从"改作文"到"做判断"

最让我印象深刻的不是代码量,而是发现 baostock K 线自带 PE/PB 那一刻——最好的优化不是写新代码,而是发现数据就在手边只是没取。预估 60 行的改动变成了 1 行,这才是工程效率的真正体现。

下一步:回测引擎——用历史数据验证优化后的信号策略是否比优化前更准。173 行代码改了 LLM 的"视野",但信号评分体系没变,胜率会不会因为新数据(换手率±5分)而提升?只有回测能给出答案。


观潮系列:诞生记 → 云端部署 → Dashboard + 信号 → 信号验证 → 出海记 → 锐评记

Comments