跳到主要内容

AI 能力架构

概述​

大模型在本产品里只做研判,不做取数,也不做第一道否决。整条 AI 链路由五段组成:

关键设计:任何一环失败都不能给用户吃错误墙。凭据失效(401/403)时链路自动切到 本地规则口径降级稿或库中既有结果,并如实标注来源(见文末 provenance 一节)。

模型调用层​

backend/app/core/ai_config.py:

能力实现参数
配置来源设置页多管理员配置优先,缺省回退 .env(AI_BASE_URL / AI_API_KEY / AI_MODEL)load_model_config()
Key 池顺序① AI_API_KEYS(逗号分隔)② AI_API_KEY ③ 设置页 apiKey(去重追加)_build_key_pool
轮转round-robin index % len(pool),跳过冷却中的 Key;全冷却时取最快恢复者get_next_key()
Key 冷却mark_key_failed() 后冷却 30s_KEY_COOLDOWN_SEC
模型降级ModelFallbackManager:连续失败 3 次 → PRIMARY 切 FALLBACK,每 30s 探测回切_FAILURE_THRESHOLD / _PROBE_INTERVAL / AI_FALLBACK_MODEL
鉴权错误判定AuthenticationError / PermissionDeniedError / status_code ∈ (401, 403) / 文案关键字(invalid api key、unauthorized、invalid token 等)is_auth_error
客户端async/sync 客户端按 fingerprint 缓存重建,trust_env=False,timeout 60sget_async_client / get_sync_client
403 与 5xx 的分流纪律

is_auth_error 只认凭据类失败。超时/5xx 必须仍按真失败走重试与降级—— 把它们误判成"凭据失效"会直接跳到本地降级稿,把一次网络抖动变成一份自报"未调用大模型"的稿子。 同理,数据源侧的 @with_retry 对 403 也刻意不重试(403 是风控信号,重试只会加深冷却)。

凭据按管理员存储​

多管理员改造后,AI 凭据落在 backend/data/model_configs/{admin_uid}.json (🔴 禁止入库)。_admin_uid() 取 role=admin 中 id 最小者并缓存 60s; 读回退链 = 该管理员文件 → 存量 data/model_config.json → 目录内任一文件。 字段:provider baseUrl apiKey model enabledProviders providerModels providerApiFormats providerApiKeys providerBaseUrls。写入端点 POST /settings/model-config 仅管理员,空 Key 保留旧值、多 Provider 键增量合并。

技能体系​

backend/skills/ 下 37 个 SKILL.md,另有 skills.config.json 声明默认启用顺序(9 项)。

环节实现
加载core/skill_manager.py::load_skills(force_reload) 扫目录 + 模块级缓存
frontmattername description official(部分技能另带 version category triggers[])
匹配旧:match_skills(触发词包含);现:smart_match_skills(user_input, max_skills=3)——_skill_match_score 加权 + 财经白名单 _FINANCE_SKILL_IDS + 类别去重
注入build_skill_system_prompt(skill_ids) → chat_service.auto_match_skill_ids → 作为 extra 段拼进 messages[0]
技能市场另一套:data/skills/marketplace_installed.json + api/skills.py 的 install/uninstall/toggle

技能与 SOP 的关系:chat_service.build_sop_messages 以 get_prompt("equity_sop") 为 骨架(fast 档 Step1-3 / deep 档 Step1-5),技能段是追加在骨架之上的 extra—— 即"回答结构由 SOP 定,专业口径由技能定"。

Prompt 本体在 backend/data/prompts/{system_prompt,equity_sop,hunter_agent}.md, 可经 custom_prompts.json 覆写(core/agent_config.py)。

MCP​

后端同时提供两条 MCP 通道,都挂在 /mcp:

通道实现端点
项目自研 JSON-RPCapp/mcp/server.py::discover_tools() 按目录自动发现(须导出 tool_spec + handler,跳过 _ 前缀)GET /api/v1/mcp/tools、POST /api/v1/mcp/tools/call
FastMCP streamable HTTPservices/mcp_server.py,工具由 ai_tools 注册表桥接,另有 ping / available_dataapp.mount("/mcp", get_mcp_asgi_app())

自研侧 10 个工具:xuangu_ai_chat xuangu_find xuangu_hold xuangu_hot xuangu_info xuangu_macro xuangu_market xuangu_news xuangu_scan xuangu_schema。

xuangu_macro 是规则结论,不是研判

xuangu_macro 转述 services/macro_regime.py 的确定性判读(读本地 fox_macro_indicator,零网络、不含预测、不是买卖时点建议),与对话工具 query_macro_regime、系统提示注入段共用同一份出口 (regime_prompt_section 文本 / regime_brief 结构化 / macro_industry_matrix.exposure_top 矩阵裁剪)。判不出来时它如实回 available=false + 原因,不回落实时取数、不拼一个「中性」糊弄。

用户侧 MCP 配置(设置页里配的外部 MCP server)是另一回事:存 backend/data/mcp_servers.json,市场目录数据 backend/data/mcp_marketplace.json (与前端 mcpRegistry.ts 的内置 _BUILTIN_MARKETPLACE 对齐), 端点 GET/POST /api/v1/mcp/servers、GET /api/v1/mcp/marketplace。

POST /mcp/servers 的请求体是信封不是裸数组

提交裸数组会恒 422(历史 bug:设置页"同步 MCP 配置"长期因此从未真正生效)。 正确体是 {"servers": [...]}。改动前对照 api/mcp.py 的 Pydantic 模型。

对话链路​

backend/app/api/chat.py:

方法与路径作用
POST /chat/ask非流式对话 / SOP 分析
POST /chat/ask/streamSSE 流式(思考 + 联网搜索 + 工具调用)
POST /chat/analyze-stock强制 deep 档个股分析,带缓存
POST /chat/generate-title首轮对话生成标题
POST /chat/quick-valuation30s 快速估值
POST /chat/batch-compare≤10 只横向对比

SSE 帧为 data: {json}\n\n,结束帧 data: [DONE];事件类型: thinking skills reasoning content search_status(searching|done|degraded) source_progress search_results tool_call tool_result chart evidence error。

测试里不要用 TestClient.stream() 读 SSE

本环境 httpx/starlette 会在 stream().__enter__ 死锁(实测挂满超时把整个 pytest 会话拖死)。 帧生成器已独立成 api/tasks.py::_sse_frames 那样的函数,测试直接 await __anext__() 并配 asyncio.wait_for 上限。

System prompt 由 build_chat_messages 并行拼装多段:用户画像 / 意图 / 市场环境 / 技能段 / SOP 骨架 / hunter 段。历史无服务端表——由请求体 history 传入且 >16 条截断, 前端持久化在 localStorage(src/store/atoms.ts)。

「市场环境」段(core/agent_config.py::build_market_environment_context)除阶段/趋势/ 情绪/量能外还拼一份宏观状态判读(macro_regime.regime_prompt_section):文本自带 「确定性规则 {rule_version} · 输入数据截至 {data_as_of} · 非大模型推断 · 不含预测 · 不是买卖时点」的口径声明,且声明放在前 200 字内(预算 1200 字只切尾部排除清单, 切不到声明)。五维分析(ai_analyzer)的两条拼装路径(非流式 / 流式)注入的是同一个 函数,措辞不可能分家。判读不可用时该段返回空串、连服务抛异常也吞成空串—— 没有输入就没有结论,硬塞一句"宏观不可用"会被模型当成一条信息去解读,而宏观绝不允许 成为个股分析的新失败模式。

联网搜索侧:TAVILY_API_KEY / WEB_SEARCH_PROVIDER=auto / MODEL_WEB_SEARCH, search_status 在搜索不可用时如实回 degraded。

五维分析​

services/ai_analyzer.py 的五维(DIMENSION_KEYS):

key中文
national_will国家意志
era_trend时代潮流
industry_turning行业拐点
competition竞争格局
stage认清阶段

总分 = 五维均值。读缓存 get_cached(code) 取 app_analysis_results 中该 code 最近一行 JSON。

凭据失效时怎么办(_auth_fallback)​

优先级行为标记
①回吐库中既有结果llmError + fallbackOf="cached"
②无既有结果 → 调 quant_prefill.prefill_one(纯规则)fallbackOf="quant_prefill"

两条路径都不写库(降级不是新产出,不该覆盖历史)。

量化预填(规则秒开)​

services/quant_prefill.py 用纯确定性规则给板块全部个股预生成五维载荷, 让页面秒开(读路径不变,前端无需改造)。零 HTTP:只读 fox_stock_wide / fox_kline_daily / fox_finance_indicator,所有规则输入(basis.metrics 原始值 + basis.percentiles 批内分位 + dataAsOf)随载荷落库,每个展示出的数字可回溯。

预填的四条纪律

① 载荷必须带 provenance="quant_prefill" + provenanceLabel="量化预填 · 非大模型生成", 前端渲染该标签并改写结论标题/免责声明——缺这个标记就是把规则结论当 LLM 研判展示; ② 写路径绕过 AIAnalyzer._save_to_db(即绕过 persist_from_analysis): 规则结果不得进决策卡复盘与经验注入闭环,否则规则话术会被当成 AI 战绩回灌给下次分析; ③ 不覆盖非预填行:写前用 LOCATE('quant_prefill', result_json) > 0 认领 (app_analysis_results 无 UNIQUE 约束,ON DUPLICATE KEY UPDATE 实为纯插入); ④ 打分前过 gates.check_gates,被否决时 recommendation 强制 bearish 且总分封顶 GATE_SCORE_CAP=4.4(原始均分留在 basis.rawTotalScore 并由结论点名), 避免"8.2 分 + 看空"这种自相矛盾的展示。

晨报 / 复盘 / 日报​

表关键列
app_morning_notes / app_daily_reviewsdate(unique) title content sections_json source(默认 "ai") source_summary_json created_at updated_at

任务时点:morning_note 06:30(仅交易日,重试 2 次 × 300s)、daily_review 16:05 (收盘稿已发布、腾讯日 K 15:30 就绪,且避开 16:00/16:10/16:20 的 fox 域重任务起点)。

区块标记契约:正文按段落 sections_json,解析器为 api/morning.py::_parse_morning_sections (headline / news / events / trade_ideas,行首匹配、空段不覆盖)与 api/review.py::_parse_review_sections(core_data / breadth / capital_flow / sectors / intraday / summary / outlook)。前端按这些 key 渲染卡片, 新增段落必须同时改解析器与前端,否则正文有、页面无。

降级稿(services/local_digest.py)​

零 HTTP、零 LLM,只读 fox_market_snapshot 五类 + fox_market_turnover_daily + 市场阶段 + 日终数据,复用页面既有区块契约渲染本地规则口径稿。三条硬约定:

约定实现
缺数据整段省略不许出现只有标题的空区块、更不许编数;正文自报「未调用大模型」+ 降级原因(_disclaimer)
绝不覆盖已有非规则稿AI 稿 / 人工编辑都是数据;同日已有则原样复用并回 message 说明
如实标注落库 source="local_rule",前端渲染琥珀色「本地规则口径 · 非大模型生成」
降级任务的记账口径很讲究

调度侧记 partial:记 success 会让用户以为 AI 在正常写稿,记 failed 会触发 "失败率过高"重试。但 app_sync_log 仍记 success + 降级原因(否则 LogsPanel 天天飘红, 把"凭据失效"读成"任务坏了"),且 last_success_date 不前移——降级不是 AI 产出, 查"某日是否成功产出"的调用方看到的应仍是真实 AI 产出日。

决策卡闭环​

  • decision_card.persist_from_analysis(code, result, source="ai_analyzer"): trace_id = {source}_{code}_{decision_date} 唯一键;回填 name/industry/ direction(=recommendation)/total_score/price_at_decision/conclusion(≤2000)/ gate_blocked/gate_reason
  • review_pending_cards():REVIEW_HORIZON_DAYS=10 个交易日,cutoff = 今天 − 10×1.7 自然日, 单次 ≤300 张,只选 review_outcome IS NULL 且方向为 bullish/bearish 的卡; 纯读 fox_kline_daily(零 HTTP),回填 review_outcome / review_profit_pct / review_actual_trend(±1.0% 内记「震荡」)
  • build_experience_block(code, industry, limit=3):预算硬约束——≤3 条、 MISS 反例最多 1 条且降权 0.5、总长 ≤1200 字,候选先取 60 行再排序; 经 experience_prompt_section 注入 ai_analyzer 的 system prompt

全链路 db_available() 守卫 + 异常吞掉:mock 模式或 DB 故障不影响主分析链。

provenance 三档(诚实标注)​

同一份页面可能装着三种来源的稿子,互斥且不互相覆盖:

档位取值语义前端呈现
实时调用source 缺省 / "ai"、provenance 缺省本次真的调了用户配置的模型「AI 生成」
规则口径local_rule(晨报/复盘)、quant_prefill(五维)确定性规则产出,未调模型琥珀「本地规则口径 / 量化预填 · 非大模型生成」
助手离线agent_batch(批量)、deep_ai(单只深度)AI 写的,但不是本次点击所配模型产出中性蓝「AI 撰写 · 助手离线生成」+「数据截止 sections.data_as_of」

人工编辑另有 source="manual",无数据占位为 "empty"。

助手离线稿的两条铁律

① 落库前必过数字回溯闸门——正文每个数字须在事实包(tools/_docpack*.py 产出 + 机算派生量)里按所写精度对上(tools/_doccheck.py::must_trace、tools/_batch_verify.py: 结构 / 硬门控一致 / 数字可回溯),对不上即认定编造、非零退出不许落库。 环比/均线/分布等派生量一律机算不手算; ② 不进闭环——写路径(tools/_batch_write.py)绕过 _save_to_db / persist_from_analysis, 与 quant_prefill 同一条纪律;且与 local_rule 互不覆盖(local_digest 只认领 source in (None,"empty","local_rule") 的行)。

时点语义:16:00 之前写的「今日晨报」= 昨日 A 股收盘 + 今日凌晨美股, 不得引用截至时点之后才发生的行情(如当日港股收盘)。口径单一: 成交额统一取 fox_market_turnover_daily;源不可信(如已冻结的 em_northbound_daily) 时显式写「本节不计入」,而不是混进合计。

相关文档​