构建 Prompt、调用 LLM 生成决策仪表盘 JSON、解析响应。通过 LiteLLM Router 支持多模型负载均衡和 fallback。
| 文件 | 职责 |
|---|---|
llm_analyzer.py |
GeminiAnalyzer 主类,LLM 调用与响应处理(via LiteLLM Router) |
prompt_builder.py |
PromptBuilder Prompt 组装,system + user prompt |
response_parser.py |
JSON 提取与修复(json_repair) |
result_types.py |
分析结果数据类,字段校验与填充 |
trend_analyzer.py |
薄壳 — 仅 re-export quantcore.indicators.trend.{StockTrendAnalyzer, TrendSnapshot};纯算法已下沉到 quantcore |
__init__.py |
导出 |
LLM 分析器主类(名称历史遗留,实际支持所有 LLM)。
analyzer = GeminiAnalyzer(config: Config)
# 内部初始化 LiteLLM Router(从 channels/YAML/legacy key)-
analyze_stock(code: str, name: str, metadata: dict) -> dict— 单股 AI 分析- 流程:
PromptBuilder.build()组装 promptLiteLLM Router.completion()调用 LLMResponseParser.parse()提取 JSONResultTypes.validate_and_fill()校验填充- 保存到
analysis_results表
- 返回: 决策仪表盘 dict(包含 sentiment_score, trend_prediction, dashboard 等)
- 超时: 默认 6000s
- 重试: 默认 2 次
- 流程:
-
_init_router() -> litellm.Router— 初始化 LiteLLM Router- 优先级: LiteLLM YAML > 多通道 > Legacy 单 Key
- 日志: 打印已初始化的 deployment 数量和模型列表
-
_call_llm(system_prompt: str, user_prompt: str) -> str— 底层 LLM 调用- 参数: temperature, max_tokens=8192
- 返回: LLM 原始响应文本
Prompt 组装器。
-
build(code: str, name: str, metadata: dict) -> Tuple[str, str]— 构建 system + user prompt- 返回:
(system_prompt, user_prompt) - system_prompt: 分析师角色定义 + 评分规则 + 输出 JSON 格式
- user_prompt: 股票数据 + 量化评分 + 新闻情报
- 返回:
-
_build_system_prompt() -> str— 系统提示词- 角色: 专业投资分析师
- 核心规则: 评分不可修改、仓位映射表
- 分析框架: 5 步(确认评分→解读模型→判断环境→技术择时→操作建议)
- 布林带多周期框架
- 输出 JSON 格式定义
-
_build_user_prompt(code, name, metadata) -> str— 用户提示词- 包含的数据段:
- 股票基础信息(代码/名称/日期)
- 今日行情(OHLCV)
- 均线系统(MA5/10/20 + 形态)
- 实时行情增强(量比/换手率/PE/PB/市值)
- 市值趋势(30 天变化)
- PE 历史百分位
- 布林带多周期(7 个时间框架)
- 财报与分红
- 成长指标
- Finviz 前瞻数据
- 同行对比
- 趋势分析数据(MA50/MA200/MACD/RSI/BB Squeeze)
- 斐波那契回撤
- 量价变化
- 大盘环境
- 宏观环境
- 新闻情报
- 量化模型评分总览表
- 重点关注问题(5 个必答题)
- 决策仪表盘要求
- 包含的数据段:
LLM 响应解析器。
-
parse(response_text: str) -> dict— 从 LLM 响应中提取 JSON- 尝试顺序:
- 直接
json.loads() - 提取
```json ... ```代码块 - 使用
json_repair库修复
- 直接
- 返回: 解析后的 dict
- 尝试顺序:
-
extract_json_block(text: str) -> Optional[str]— 提取 JSON 代码块
分析结果数据类 + LLM 输出后置校验/纠偏函数。
pipeline/stages/llm_stage.py 与 pipeline/analysis_flow.py(Agent 路径)调用顺序:
fill_chip_structure_if_needed → fill_price_position_if_needed → enforce_trend_status_consistency → enforce_score_band_consistency → enforce_stop_loss_consistency。
| 函数 | 作用 |
|---|---|
check_content_integrity(result) |
检查必填字段(sentiment_score / operation_advice / dashboard.core_conclusion / risk_alerts / stop_loss)是否齐全 |
apply_placeholder_fill(result, missing) |
用占位字符串补齐缺失字段(弱模式兜底) |
enforce_stop_loss_consistency(result) |
battle_plan.sniper_points.stop_loss 为权威源,把 has_position 文本里不一致的止损价改写成同一个数字 |
enforce_trend_status_consistency(result, trend_result) |
以 trend_result.golden_cross 为权威,纠正 LLM 在 Death Cross 数据下错误返回的 is_bullish=true 与 "多头排列" 描述。返回被纠正的字段名列表 |
enforce_score_band_consistency(result) |
按 sentiment_score 区间硬性校准 operation_advice / trend_prediction:40-59 区间禁止"加仓/买入",0-19 区间强制"卖出/强烈看空",反向规则同理 |
fill_chip_structure_if_needed(result, chip_data) |
用 ChipDistribution 数据补齐 LLM 未返回的筹码字段 |
fill_price_position_if_needed(result, trend_result, realtime) |
用趋势/实时数据补齐 price_position 中的 MA5/10/20/50/200/current_price |
拆包公告(2026-05):本文件已退化为
quantcore.indicators.trend的纯 re-export shim。 实际类名是StockTrendAnalyzer(旧文档里写的"TrendAnalyzer"是简称)。新代码请:from quantcore.indicators import StockTrendAnalyzer, TrendSnapshot2026-05 删除:
stocklens.analyzer.trend_analyzerre-export shim 已下线,请直接from quantcore.indicators.trend import ...。
趋势分析器,纯 Python 计算(不依赖 LLM)。
-
analyze(df: pd.DataFrame, realtime_quote: dict = None) -> dict— 完整趋势分析- 返回:
{ "ma50": float, # MA50 值 "ma200": float, # MA200 值 "price_vs_ma50_pct": float, # 价格偏离 MA50 百分比 "price_vs_ma200_pct": float, # 价格偏离 MA200 百分比 "ma_cross": str, # "Golden Cross" / "Death Cross" / "无交叉" "ma50_slope": float, # MA50 斜率(%/日) "rsi_14": float, # RSI(14) "macd": dict, # {dif, dea, macd_bar, cross} "bb_squeeze": bool, # 布林带收窄 "bb_trend": float, # MA20 斜率 "volume_analysis": dict, # 量能分析 "relative_strength": dict, # 相对强度 vs SPY }
- 返回:
-
_calculate_rsi(series: pd.Series, period: int = 14) -> float— RSI 计算 -
_calculate_macd(series: pd.Series) -> dict— MACD 计算 -
_analyze_volume(df: pd.DataFrame) -> dict— 量能分析(量比/放量天数/连续放量) -
_calculate_relative_strength(df: pd.DataFrame) -> dict— 相对强度 vs SPY
- 依赖:
litellm、json_repair、pandas、config、storage(AnalysisRepo, LLMUsageRepo) - 被依赖:
pipeline(orchestrator.py)
| 变量 | 默认值 | 说明 |
|---|---|---|
LITELLM_MODEL |
自动推断 | 主模型 |
LLM_TEMPERATURE |
0.7 | 采样温度 |
LITELLM_FALLBACK_MODELS |
空 | 备用模型 |
GeminiAnalyzer名称是历史遗留,实际通过 LiteLLM Router 支持所有 LLM- LLM 返回的
sentiment_score必须与量化评分引擎计算的一致,Prompt 中有强制规则 json_repair库用于修复 LLM 输出的不规范 JSON(缺少引号、多余逗号等)- LLM 用量会自动记录到
llm_usage表(model/tokens/cost/latency)
_call_litellm 检测到 anthropic/ 系列模型时(包括 bedrock/anthropic.* 和
openrouter/anthropic/...),把 system 消息打包成 list-of-blocks 并附加
cache_control: {"type": "ephemeral"}。LiteLLM 透传该字段;非 Anthropic provider
(Gemini / OpenAI / DeepSeek 等)静默忽略,向后兼容。
# 触发条件由 _supports_prompt_caching(model) 判定
system_content = [
{
"type": "text",
"text": SYSTEM_PROMPT, # ~5000 chars / ~1500 tokens
"cache_control": {"type": "ephemeral"},
}
]usage 字典额外暴露 cache_creation_input_tokens / cache_read_input_tokens 用于
审计。日志在命中缓存时会输出 [LiteLLM] anthropic/claude-X prompt cache hit: NNN tokens。
预期效果:批量分析 30 只股票时,第 1 次调用为缓存创建(成本略高),第 2 次起 system 提示 部分按缓存读取计费(Anthropic 缓存读取 ≈ 输入 token 价格的 10%),整体输入 token 成本 下降约 70-90%。Gemini 通过 implicit context caching 自动获得类似收益,无需 client 标记。
由于 LLM 偶发自相矛盾(典型表现:Death Cross 数据下输出 is_bullish=true / 评分 57 输出"加仓"),系统采用 Prompt 显式约束 + Python 后置纠偏 双保险:
-
Prompt 端(
prompt_builder.py):- 趋势分析表中明确写出
is_bullish应取的布尔值,并在表后用块引用强调"权威结论"。 - 评分区间块根据当前
sentiment_score仅展开命中的区间,并写出"必须 / 严禁"措辞(如 40-59 严禁"加仓",仓位上限 ≤ 2 成)。 - RSI 缺失(None / 0)时显示
N/A(数据不足,请勿引用具体数值),避免 LLM 把 dataclass 默认 0.0 当成"极度超卖"。 - 估值结论必须引用同行业排名,对高增长股优先引用 Forward PE / PEG,禁止"PEG 低估 + 估值偏高"自相矛盾的描述。
- 趋势分析表中明确写出
-
Python 端(
result_types.py):enforce_trend_status_consistency在 LLM 返回后对照trend_analyzer算出的golden_cross校准dashboard.data_perspective.trend_status.{is_bullish, ma_alignment}。enforce_score_band_consistency对sentiment_score与operation_advice / trend_prediction做硬一致性,偏离区间直接降级为安全默认(如"观望/持有")。- 两个函数都返回被纠正的字段列表,触发 WARNING 日志便于观测 LLM 出错频率。
- 单测见
tests/unit/test_result_consistency.py与tests/unit/test_prompt_builder.py。
180 行的 system prompt 之前作为 GeminiAnalyzer.SYSTEM_PROMPT 类常量内嵌在
llm_analyzer.py 顶部。现在迁移到 stocklens/analyzer/system_prompt.py:
- prompt 工程师调整提示词不需要碰 Python 文件
- 阅读
llm_analyzer.py直接看到 LLM 调用 / 重试 / 解析逻辑,不再被 prompt 内容遮蔽 - prompt 可以被测试 / dashboard / 文档 import,无需实例化
GeminiAnalyzer
向后兼容:GeminiAnalyzer.SYSTEM_PROMPT = _SYSTEM_PROMPT 保留为 1 行 re-export,
任何 GeminiAnalyzer.SYSTEM_PROMPT 引用照常解析。
llm_analyzer.py 998 → 812 行(-19%)。
prompt_builder.py 的 _format_prompt(337 行单方法)保留主编排逻辑,把 6 个独立段落
抽离到新模块 stocklens/analyzer/prompt_sections.py:
| 函数 | 段落 |
|---|---|
build_realtime_section |
实时行情富化(量比 / 换手率 / PE/PB / 市值 / 60d 涨幅) |
build_mv_trend_section |
美股 30 天市值变化 |
build_chip_section |
A 股筹码分布表 |
build_yesterday_section |
量价昨日对比 |
build_us_market_env_section / build_us_macro_section |
US 市场环境 / 宏观快照 |
build_systemic_risk_guidance |
系统性 vs 个股风险区分指引 |
build_data_missing_section |
数据缺失警告 |
每个 builder 是无状态纯函数(接受 context: dict + 必要的格式 helper),便于单测。
_supports_prompt_caching / _build_system_message_content /
_build_integrity_complement_prompt / _build_integrity_retry_prompt 之前散在
llm_analyzer.py(842 行)中,本质都是 prompt 构建辅助,已迁移到
prompt_builder._PromptMixin。GeminiAnalyzer 通过 mixin 继承访问,调用方零改动。
迁移后 llm_analyzer.py 只剩 LLM 路由 / 重试 / 解析职责,prompt 相关全在
prompt_builder.py。
analyze / aanalyze 此前几乎逐字重复整个分析主循环(~350 行);
_call_litellm / _acall_litellm 同理重复 ~80 行。_format_prompt 仍有
~700 行内联段落构建。
llm_analyzer.py 抽出 7 个共享 helper:
| Helper | 用途 |
|---|---|
_resolve_stock_name(context, code) |
三段式名称兜底(context → realtime → STOCK_NAME_MAP) |
_unavailable_result(code, name) |
无 API key 时返回 dashboard-合规默认 result |
_error_result(code, name, exc) |
兜底异常时返回默认 result |
_decorate_result(result, ...) |
写 raw_response / market_snapshot / model_used + 调 apply_quant_score_override |
_next_integrity_action(...) |
决定「停 / 重试 / placeholder fill」三态 |
_build_call_plan(prompt, gen_cfg) |
解析 config + 生成 fallback model 列表 |
_build_call_kwargs(model, ...) / _should_use_router(...) / _apply_direct_call_params(...) / _extract_usage(response, model, async_tag) |
LLM 调用的 4 个共享步骤 |
analyze 与 aanalyze 现在只差 (i) time.sleep vs await asyncio.sleep (ii) self._call_litellm(...) vs await self._acall_litellm(...)。_call_litellm 与 _acall_litellm 同理。
result_types.py 新增 apply_quant_score_override(result, quant_score) — 把原本在 analyze/aanalyze 中各自重复 25 行的 score → trend/advice 映射收口为单一函数,与其他 enforce_* 函数并列。
prompt_sections.py 新增 4 个 builder(M6):
| Builder | 段落 |
|---|---|
build_financial_report_section |
财报快照 + TTM 派息(value-investing) |
build_growth_quarterly_section |
成长指标 + 最近 4 季报告 |
build_finviz_forward_section |
Finviz 前瞻数据(profitable / unprofitable 双路径) |
build_finviz_changes_section |
前瞻数据变更信号(vs 上次分析) |
build_peer_comparison_section |
同行对比表 |
prompt_builder.py 从 865 行降到 651 行(-25%),_format_prompt 主体几乎只剩
对 prompt_sections.build_* 的顺序调用。
- 任何 LLM 行为调整(新 telemetry 字段 / 新重试规则 / 新一致性 enforcer)只需要改一处。
apply_quant_score_override现在是 score band → 决策映射的唯一入口;测试只用断言这一个函数,不再需要分别 mock sync/async 两条路径。- 新加 prompt 段落只需要写一个
build_xxx函数 + 在_format_prompt加一行;不再编辑 700 行的巨大_format_prompt。 - 全部测试(unit + 包级)518/518 通过。