稳健理财助理 Agent:把 11 位投资大师蒸馏成 Skill,再让 TradingAgent 替你多空博弈
最近在做(并且一直在用)一个个人项目:稳健理财助理 Agent。它是一个跑在微信小程序 + Web 双端的投资研究助手,后端是 FastAPI,里面内置了一个多 Agent 的 TradingAgents 框架。在线版本部署在 agent.xiaofenglei.cn,可以直接体验。
这篇文章想做三件事:
- 交代清楚它有哪些功能、为什么这么做、为什么这么做是有效的;
- 把几个核心功能的实现拆开看——尤其是「持仓怎么进上下文」「红黑榜综合了哪些因素」「为什么只看市值前 200」;
- 重点聊三个技术点:Skill 系统(大师人格怎么提取 / 怎么加载)、记忆机制(什么时候记 / 记什么 / 怎么存 / 什么时候用 / 什么时候更新)、上下文管理,并且都拿来和 ClaudeCode 对比,讲讲我为什么这么取舍。
代码基于本仓库(
SoundFinancialPlanning,下称「本项目」),文中所有结论我都给了文件:行号证据。核心是后端SoundFinancialPlanningBE(Python 3.11 / FastAPI)。
〇、一句话架构
先给一张总览图,后面所有章节都是对它的展开:
flowchart TD
subgraph CLIENT["客户端"]
MP["微信小程序"]
WEB["Next.js Web"]
end
subgraph BE["FastAPI 后端 SoundFinancialPlanningBE"]
RT["大师圆桌
roundtable_pipeline"]
TA["TradingAgents 多空博弈
vendored LangGraph"]
RB["红黑榜
research_leaderboard"]
DR["持仓深度复盘
deep_review: 1 Lead + 4 Teammate"]
MEM["四层记忆
agent_memory"]
SKILL["大师人格 Skill
.claude/skills/*.md"]
end
subgraph DATA["数据与存储"]
DS["东财 / 腾讯 / 通达信mootdx / 新浪 / 同花顺 / 财联社
零 akshare 依赖"]
DB[("MySQL + Redis RQ / pubsub / 缓存")]
end
MP --> BE
WEB --> BE
RT --> SKILL
RT --> TA
RT --> MEM
DR --> MEM
RB --> TA
TA --> DS
RB --> DS
BE --> DB
四大功能块:
| 功能 | 干什么 | 入口 |
|---|---|---|
| 个股大师圆桌 | 多位大师并行对一只股票/一个问题发言,合成结论 | POST /web/tradingagent/chat |
| TradingAgent 多空博弈 | 7 个分析师 + 多空研究员辩论 + 风险三方辩论,产出 BUY/SELL/HOLD 评级 | 内嵌于圆桌 / 独立 /analysis |
| 红黑榜 | 每日扫描市值前 200,出多头最强 top10 + 空头最强 bottom10 | GET /web/research/red-black-list |
| 持仓分析 / 深度复盘 | 导入持仓 → 1 Lead + 4 Teammate 复盘集中度/估值/现金/板块 | POST /web/portfolio/deep-review |
一、为什么做这个项目,以及为什么这么做是有效的
在拆实现之前,先把「为什么」讲清楚,因为后面很多设计取舍都源于此。
1.1 普通投资者的真实处境:在不擅长的地方赚钱
大多数人投资的状态是这样的:
- 容易冲动。看到一根大阳线、一条小道消息、一个「朋友说」就冲进去,买卖靠情绪不靠框架。
- 花在投资上的时间极少。白天要上班,晚上要带娃,真正能静下来研究一家公司财报、读完一份年报的时间,一年可能不到十个小时。
- 于是本质是:你在一个你并不擅长、投入也极少的领域,试图赚到钱。 这个期望本身就是不现实的——这跟一个没怎么写过代码的人想靠写代码接单赚钱是一样的。
投资这件事,门槛不在「买卖」这个动作(点两下屏幕就完成了),门槛在认知:你得理解商业模式、会看财务三张表、懂得估值、控制得住仓位和情绪。这些恰恰是需要长期积累的「专业知识」。
1.2 大师的公开言论,是可以被「蒸馏」的
那普通人的出路是什么?一个被反复验证过的答案是:站在巨人的肩膀上。
巴菲特、芒格、段永平、达利欧、彼得·林奇、霍华德·马克斯……这些人几十年如一日地公开输出:致股东信、股东大会问答、演讲、访谈、专栏。他们的思维框架其实高度稳定、可复用:
- 段永平就两句话:「做对的事,把事做对」;
- 巴菲特就看四样:护城河、管理层、财务、安全边际;
- 芒格就一套多元思维模型 + 逆向思考 + 25 种误判心理学。
这些东西不是秘密,它们被充分公开、反复讲过。问题在于:普通人没有时间去读完、消化、并且在自己做决策的那一刻能调用出来。
而「把一个人反复公开表达的思维框架提炼出来」,恰好是 LLM 极其擅长的事情。这就是本项目的第一个支点:
把大师的公开言论蒸馏成一份份可运行的 Skill,让 LLM 在你做决策的当下,以大师的视角替你想一遍。
「如何提取人格 Skill」是本文第四章的重点(用了女娲 + 达尔文两个工具),这里先记住结论:这是可行的,而且效果可以被量化地优化。
1.3 AI 在打破信息壁垒 —— 从 Coding 类比
我自己是程序员,对这一点体会很深。在 Coding 领域,AI 已经是一个「孜孜不倦的专家」:
- 你给它一个模糊需求,它能帮你出方案;
- 你确认方案,它能帮你写代码;
- 它 24 小时不累,读过全世界的开源代码,知识没有盲区。
作为程序员,我其实知道问题的边界在哪——我知道这个需求能不能做、大概多复杂、AI 给的方案靠不靠谱、哪一行可能有问题。我和 AI 是「内行 + 超级外挂」的关系,我能驾驭它。
但投资不一样。 投资是我(以及大多数人)的能力圈之外:
- 我没有受过系统的财务训练,看不穿财报里的门道;
- 我不具备宏观、行业、估值的专业知识;
- 而且投资容错率极低——代码写错了可以改、可以回滚,钱亏掉了就是真的亏掉了。
所以投资比 Coding 更需要谨慎、更需要专业知识。两个最典型的、普通投资者最容易踩坑的问题恰恰是:
- 持仓占整体财富的比例——单票 / 单板块押多重?加杠杆了吗?这决定了你会不会一次归零;
- 到底投哪只股票——这个需要真研究,而不是听消息。
这两件事,恰恰是 AI 能帮上忙、且能帮到点子上的地方。这就是本项目要做的:让一个不懂投资的普通人,也能在做决策的那一刻,调动起一群「大师级」的、谨慎的、有框架的视角,帮他把关「买哪只」和「押多重」。
这,就是「为什么这么做是有效的」:它把一个你不擅长、没时间、容错率又低的决策,交给了一个被蒸馏过的、24 小时在线的、永远克制的大师圆桌。
二、功能实现拆解(一):个股大师圆桌
这是用户感知最强的功能。先讲它跑起来的完整流程,再讲一个很多人会误解的关键点。
2.1 完整流水线
圆桌的主流水线在 run_roundtable_sync(SoundFinancialPlanningBE/app/services/roundtable_pipeline.py:947)。一次圆桌分析按顺序经历这些阶段:
sequenceDiagram
participant U as 用户
participant P as run_roundtable_sync
participant F as 基本面采集
participant M as 大师们
participant T as TradingAgents
participant W as WebSocket
U->>P: 提问(可带持仓上下文)
P->>P: 记忆注入(四层记忆拼到prompt头)
P->>P: 从原始query解析ticker
P->>F: _load_roundtable_fundamentals
F-->>P: 共享基本面底稿 shared_analysis
P->>W: 推送基本面底稿帧
par 大师并发(信号量=3)
loop 每位大师
P->>M: drain steer + 共享底稿
M-->>W: 流式发言(report_delta逐token)
end
end
P->>T: 命中缓存? 否→run_analysis_sync跑图
T-->>P: final_trade_decision (5档评级)
P->>P: _build_final_text/_build_final_decision
P->>W: 圆桌结论帧 + done帧
逐步说明:
① 记忆注入 + 持仓追加(roundtable_pipeline.py:965-982):把四层记忆(后面第五章详述)拼到 prompt 头部;如果带了持仓,再追加一段「持仓上下文」。
② 解析标的(roundtable_pipeline.py:985-994):从用户原始 query 解析出股票代码。这里有个刻意的设计——注释明确说不能用拼了记忆的 prompt 去解析,否则用户档案里的 preferred_tickers: ["600519.SH"](贵州茅台)会「劫持」解析器,用户明明问的是「蜜雪冰城」却返回茅台的数据。从裸 query 解析,让解析器忠于意图而非历史(roundtable_pipeline.py:987-993)。
③ 基本面预加载(_load_roundtable_fundamentals,roundtable_pipeline.py:780-843):先采集一份共享基本面底稿(估值、财务、资金流、概念板块等),这份底稿之后会广播给每一位大师,避免每个大师重复采数据。
④ 大师并发发言(roundtable_pipeline.py:1006-1086):用 asyncio.Semaphore 限流,默认并发 3(MASTER_CONCURRENCY=3,roundtable_pipeline.py:44)。每位大师拿到槽位后:先「drain」运行中用户追加的 steer 指令、合并共享底稿,然后流式生成发言,逐 token 通过 Redis pub/sub → WebSocket 推给前端。
⑤ TradingAgents 阶段(roundtable_pipeline.py:1140-1235):大师发言全部结束后,如果识别到了 ticker,就调用 TradingAgents 引擎(见第三章)。这里有个重要的性能设计——先查缓存:ta_cache 夜间已经批量预热过,命中就直接复用(「burning 10+ LLM minutes to re-run it would be wasteful」,roundtable_pipeline.py:1150-1156);没命中才现场跑图,超时 30 分钟。
⑥ 合成结论(_build_final_text / _build_final_decision,roundtable_pipeline.py:1359-1435):产出最终的圆桌结论和 BUY/SELL/HOLD 信号。
2.2 一个关键洞察:大师负责「叙事」,TradingAgent 负责「打分」
这是我最想强调、也最反直觉的一点。很多人以为「圆桌」是大师们你一言我一语地辩论,最后投票出结果。实际不是。
圆桌里的多位大师,是「各自独立并行发言 + 最后程序性汇总」,不是互相辩论。 证据:
- 每位大师拿到的是同一份 prompt + 同一份基本面底稿,互相看不到对方的发言(
roundtable_pipeline.py:1033-1052,master_prompt里只有用户问题 + 记忆 + steer,不含其他大师的输出); - 全流程没有任何「大师 A 反驳大师 B」的 prompt 或逻辑;
- 真正的多空辩论发生在 TradingAgents 子系统里,和大师人格圆桌是解耦的两套多智能体。
更关键的是最终的 BUY/SELL/HOLD 信号,完全来自 TradingAgents,不从大师的发言聚合。看 _build_final_decision(roundtable_pipeline.py:1400-1435):
if ta_result:
decision = _extract_ta_decision(ta_result)
...
signal = str(decision.get("signal") or decision.get("action") or "HOLD").upper()
if signal not in {"BUY", "SELL", "HOLD"}:
signal = "HOLD"
return {"signal": signal, "title": title, "text": summary, "ticker": ticker}
# 没有 ta_result 时:
return {"signal": "HOLD", ...} # 大师观点只进 text,不进 signal
也就是说:有 TradingAgents 结果时,signal 直接取自 TA;大师们的发言只作为「核心观点」文字展示。 没有 TA 结果时,signal 一律兜底成 HOLD。
为什么这么设计? 因为「大师人格」擅长的是叙事、视角、框架——它能用巴菲特的口吻讲清楚「这门生意好不好」,但你不能让一个角色扮演去给出可执行的、可量化的买卖信号,那既不可靠也不负责。真正可量化的多空判断,交给一套专门的多 Agent 打分引擎(TradingAgents)去做,它有结构化的评级 schema、有风险辩论、有事后复盘。
一句话:大师给你「想清楚」,TradingAgent 给你「打分」,最后程序把两者拼成一页结论。 叙事层和决策层分离,是这个系统能既「像大师」又「不乱喊单」的关键。
三、功能实现拆解(二):TradingAgent 多空博弈——多 Agent 怎么"博弈"、消息怎么传递
这是整个系统的「打分引擎」,也是最终 BUY/SELL/HOLD 的真正来源。它基于开源 TradingAgents(LangGraph 多智能体交易框架)深度改造成 A 股专用,vendored 在 app/agents/tradingagents/。这一章不光列角色,重点讲清楚这么多 Agent 之间是怎么传递和共享消息的——这是看懂任何多 Agent 系统的关键。
3.1 编排底座:LangGraph StateGraph + 一块共享黑板
整个博弈跑在一张 LangGraph 的 StateGraph 上(graph/setup.py:117):节点是 Agent,边是执行顺序。而所有 Agent 之间交换信息,靠的不是互相发私信,而是读写同一块「共享状态」——AgentState(agents/utils/agent_states.py:46)。
AgentState 继承 LangGraph 的 MessagesState,在上面挂了一排类型化字段,每个字段就是黑板上的一栏「公告」:
class AgentState(MessagesState):
company_of_interest: str # 分析哪只票
trade_date: str
sender: str # 上一个发言者
# —— 7 份分析师报告(每个分析师写自己那一栏)——
market_report: str # 技术面
sentiment_report: str # 情绪面
news_report: str # 新闻
fundamentals_report: str # 基本面
policy_report: str # 政策(A股特有)
hot_money_report: str # 游资资金(A股特有)
lockup_report: str # 解禁减持(A股特有)
data_quality_summary: str # 质量门评级
# —— 两轮辩论各自的子状态 ——
investment_debate_state: InvestDebateState
investment_plan: str
trader_investment_plan: str
risk_debate_state: RiskDebateState
final_trade_decision: str
past_context: str # 历史教训(事后复盘注入)
这是典型的黑板(blackboard)架构:每个 Agent 干完活,把产出写到黑板对应栏;下游 Agent 要用什么,直接从黑板读。没有点对点私信,协作全靠这块共享状态解耦。整体流程:
flowchart TD
START((START)) --> AN["7 个分析师串行
各自 ReAct 工具循环, 把报告写到黑板"]
AN --> QG["Quality Gate
给 7 份报告打 A-F 评级"]
QG --> DEB["多空辩论 Bull ⇄ Bear
(读写 InvestDebateState)"]
DEB --> RM["Research Manager 裁判
deep model · 5 档"]
RM --> TR["Trader 交易员
3 档 + 价位/止损/仓位"]
TR --> RD["风险三方辩论
Aggressive ⇄ Conservative ⇄ Neutral"]
RD --> PM["Portfolio Manager 终裁
deep model · 5 档"]
PM --> END((END))
3.2 消息共享的三种机制(重点)
这块共享状态上其实跑着三种粒度不同的消息,搞混了就看不懂这个框架:
① 持久的「黑板报告」——分析师的产出,全流程可见。
7 个分析师串行执行(setup.py:144-162),各自把报告写进黑板对应字段。一旦写上,后面所有 Agent(辩论者、裁判、交易员、组合经理)都能读。它是持久的、累积的。
② 易变的「工具调用消息」——每个分析师用完即清。
AgentState 还继承了 messages 通道(MessagesState 自带),用来跑每个分析师的 ReAct 工具循环:分析师发工具调用 → tools_x 节点执行 → 结果回到分析师 → 直到不再调工具(setup.py:150-155)。关键是,每换一个分析师,上一个的工具消息会被一个 Msg Clear 节点清空(create_msg_delete,setup.py:56,122-124)。为什么?因为工具消息又长又占 context,下一个分析师根本不需要看上一个「调了几次 API、返回了什么原始 JSON」——它只需要黑板上那份提炼后的报告。
这一手很讲究:框架刻意把「易变的过程消息」和「持久的结论文档」分开——前者每个阶段后丢弃,后者沉淀到黑板。这和第八章「上下文管理」的思想一脉相承:不该带进下一阶段的,果断丢。
③ 辩论的「子状态」——两轮辩论各维护一份对话状态。
多空、风险两轮辩论各有一个子对象。以多空为例(agent_states.py:7-17):
class InvestDebateState(TypedDict):
bull_history: str # 多头发言史
bear_history: str # 空头发言史
history: str # 整段辩论史(双方合并)
current_response: str # 最新一条发言(对方刚说了什么)
judge_decision: str # 裁判结论
count: int # 发言计数(控制轮数)
3.3 多空博弈是怎么"博弈"的:读黑板 → 反驳对方 → 写回黑板
现在能精确回答「多空怎么博弈」了。以多头为例,它的节点函数 bull_node(bull_researcher.py:4-64)干三件事:
第一步:读黑板。 读全部 7 份报告 + 数据质量摘要 + history(整段辩论史)+ current_response(对方空头上一轮的论点)。
第二步:构造 prompt,显式要求反驳。 prompt 里有一句决定性的话:
prompt = f"""You are a Bull Analyst advocating for investing in this A-share stock ...
Conversation history of the debate: {history}
Last bear argument: {current_response} ← 把对方上一轮论点喂进来
... Refute the bear's concerns ... engaging directly with the bear analyst's points ..."""
注意 Last bear argument: {current_response}——它把空头上一轮的发言显式塞进多头的 prompt,并要求「Refute(反驳)」。这就是「真博弈」和「各说各话」的分水岭:多头不是自说自话喊多,而是针对空头刚提出的具体质疑逐条反驳。空头完全对称(它的 prompt 里是 Last bull argument)。
第三步:写回黑板。 调完 LLM,把发言追加到共享辩论状态:
argument = f"Bull Analyst: {response.content}"
return {"investment_debate_state": {
"history": history + "\n" + argument, # 追加整段辩论史
"bull_history": bull_history + "\n" + argument, # 追加多头史
"current_response": argument, # 覆盖"最新发言", 供空头读
"count": investment_debate_state["count"] + 1, # 轮数 +1
}}
LangGraph 把这个 partial dict merge 回全局 AgentState,空头下一轮就能从 current_response 读到多头这段反驳——消息就是这样"传"过去的。
轮到谁说,谁说了算? 不是 Agent 自己,而是图里的条件边 should_continue_debate(conditional_logic.py:70-79):
def should_continue_debate(self, state):
if state["investment_debate_state"]["count"] >= 2 * self.max_debate_rounds:
return "Research Manager" # 到轮数 → 交裁判
if state["investment_debate_state"]["current_response"].startswith("Bull"):
return "Bear Researcher" # 多头刚说完 → 该空头反驳
return "Bull Researcher" # 否则 → 该多头
它看 current_response 的前缀判断「上一句是谁说的」,路由给另一方——多头说一句、空头驳一句,直到 count 到阈值(默认 max_debate_rounds=1,多空各一轮)就移交 Research Manager。
还有个细节证明「质量门」不是摆设:多/空头的 prompt 里都写了「⚠️ 若数据质量评估把某份报告标为 C/D/F,就减少采信并注明数据局限」(bull_researcher.py:47)。质量门评级会实时影响辩论者对每份报告的信任权重。
3.4 两轮辩论 + 三级结构化产出
多空辩论只是第一轮。完整打分是两轮辩论 + 三级结构化产出:
| 阶段 | 角色 | 职责 | 用的模型 |
|---|---|---|---|
| 分析师层 ×7 | Market / Social / News / Fundamentals / Policy / Hot Money / Lockup | 技术面、情绪、新闻、基本面、政策、游资、解禁(后三个 A 股特有) | quick |
| 质量门 | Quality Gate | 给 7 份报告打 A–F | quick |
| 多空辩论 | Bull ⇄ Bear | 多头 vs 空头交锋 | quick |
| 裁判 | Research Manager | 裁决多空 → 投资计划(5 档) | deep |
| 交易员 | Trader | 变成提案(入场价/止损/仓位,3 档) | quick |
| 风险辩论 | Aggressive / Conservative / Neutral | 激进/保守/中性三方风险交锋 | quick |
| 终裁 | Portfolio Manager | 综合两轮 → 最终决策(5 档) | deep |
产出全是结构化的,不是自由文本。三个决策型 Agent 用 LangChain 的 with_structured_output 产出 Pydantic 对象(agents/schemas.py,带熔断:连续 3 次结构化失败退回自由文本):
- ResearchPlan(研究经理):
recommendation(Buy/Overweight/Hold/Underweight/Sell 5 档)+ rationale + strategic_actions; - TraderProposal(交易员):
action(只有 Buy/Hold/Sell 3 档)+ entry_price / stop_loss / position_sizing; - PortfolioDecision(组合经理,终裁):
rating(5 档)+ executive_summary + investment_thesis + price_target + time_horizon。
第二轮风险辩论的机制与多空同构,只是子状态换成 RiskDebateState、轮转逻辑换成 should_continue_risk_analysis(Aggressive→Conservative→Neutral 轮替,conditional_logic.py:81-91)。组合经理读风险三方辩论史 + 投资计划 + 交易提案 + 历史教训 past_context,并把 A 股硬约束写进 prompt(T+1、涨跌停、ST 风险、融资融券资格,portfolio_manager.py:48-54)。
最后 process_signal 用确定性正则从组合经理的 markdown 提取评级(先找 Rating: X 标签再找评级词,默认 Hold),不再额外调 LLM——打分这一步是纯解析,不花 token。
3.5 双模型分工:快脑干活,慢脑拍板
框架用了两个 LLM(trading_graph.py):quick_thinking_llm 给 7 个分析师、多空研究员、交易员、风险三方(调用量大、要快);deep_thinking_llm 只给两个"裁判"——Research Manager 和 Portfolio Manager(setup.py:107,114)。道理很直接:裁判要做权衡拍板,值得用更强的慢模型;一线大量调用用快模型压成本。
3.6 事后复盘闭环 + 基本面数据(零 akshare)
它会"事后复盘"自己(trading_graph.py:265 + graph/reflection.py):每次分析一只新票前,先回补同一只票之前的 pending 决策,用 yfinance 拉它之后的真实收益(基准沪深 300 算 alpha),生成一段「反思」写回 memory log;下一次组合经理做决策时,这段历史教训作为 past_context 注入。也就是 决策 → 用真实收益复盘 → 注入下次决策,让这个 Agent 能随时间积累对某只票的判断质量。
数据层(dataflows/a_stock.py:1-13 文件头明确「no akshare」)直连一批公开源:mootdx(通达信协议,K线/财务/F10)、腾讯财经(PE/PB/市值/换手)、东方财富(龙虎榜/解禁,带节流防封)、新浪/同花顺/财联社(财报/一致预期 EPS/北向/新闻)。选直连而非 akshare,是因为这些接口各有封 IP 脾气,自己控制节流和兜底更可控——对一个要 7×24 预热几百只票的系统很重要。
四、功能实现拆解(三):持仓分析
持仓是最「私人」的功能,也是最能体现「AI 帮普通人把关」的地方。它回答的核心问题正是第一章说的那两件:「买哪只」和「押多重」。
4.1 持仓怎么进来:7 种录入方式
为了把「录入持仓」这件事的门槛降到几乎为零,系统支持了一长串导入方式(Web 端 /portfolio/new):手动录入、CSV、XLSX、富途 JSON、富途实盘、图片 OCR、多图 OCR。
后端统一汇入 save_import_rows(holding_import_service.py:9-76),按 (owner, account, ticker, cost, qty) 查重后插入或更新 Holding 表。几个细节:
- CSV(
csv_parser.py):自动尝试utf-8-sig/utf-8/gb18030/gbk解码;并用正则把 16–19 位卡号、邮箱、手机号打码成***(csv_parser.py:8-12)——脱敏是内建在第一道的。 - 图片 OCR(
ocr.py):默认走 MiniMax 视觉模型,可切 Anthropic,key 缺失降级 mock。prompt 特意要求「忽略顶部总资产/今日收益/总盈亏等汇总信息,只提取单个持仓行」,返回结构化 JSON。识别不出时 fallback 到中文文本解析。 - 富途:
fetch_from_futu_bridge注释明说「not wired in this build」——实际是把富途持仓 JSON 粘贴进来解析,不是真的 API 同步(futu.py:31-40)。这是很务实的取舍:个人项目不碰券商 API 的合规和稳定性。
每次写入后,都会 fire-and-forget 触发一次「持仓快照写记忆」(见 4.2 的路径 B)。
4.2 重点:持仓是怎么「进上下文」的
这是本章的核心。持仓进入 LLM 上下文有两条独立路径:
路径 A —— 直接拼成 prompt 块(深度复盘专用)。 _format_portfolio_context(portfolio_deep_review.py:107-132)把当前账户持仓渲染成一段中文上下文,注入到每个 teammate 和 Lead 的 system prompt:
【账户】我的美股账户(共 6 只持仓)
【汇总】总市值 1,234,567 持仓成本 1,100,000 浮动盈亏 +134,567 (+12.23%)
【逐只持仓】
1. 苹果 (AAPL us) 数量 100 成本 180.0 现价 210.5 市值 21,050 盈亏 +3,050 (+16.9%)
2. ...
路径 B —— 通过「长期记忆快照」注入(更通用)。 这条路更精巧:
portfolio_snapshot.py:24-63把持仓 + 现金序列化成一个结构化快照,显式算出现金占比cash_pct和每只票的权重weight = 市值 / 总资产;snapshot_portfolio_to_memory(portfolio_snapshot.py:66-82)把它作为一条type="fact"的长期记忆写进agent_user_memories表(按天去重,一天只留最新一条);- 之后任何一次对话,这条快照都会随「记忆块」一起注入 prompt 头部(第五章详述注入机制)。
为什么要算 weight 和 cash_pct? 因为这正是回答「押多重」的原始数据。有了每只票占总资产的比例和现金比例,模型才能判断「这只票是不是已经过重了」「现金是不是太少、没有子弹补仓」。把「占比」在数据层算好再喂给模型,比让模型自己拿着市值和总资产去算要可靠得多(LLM 算术不可靠,这是常识)。
4.3 持仓深度复盘:1 Lead + 4 Teammate 的 Agent Teams
持仓的「深度复盘」(POST /web/portfolio/deep-review)用的是多 Agent 协作(Agent Teams)模式:1 个 Lead + 4 个 Teammate,全部并发协程 + Redis 共享状态。这和大师圆桌的「独立发言」不同——这里是真协作。
4 个 Teammate 角色(teammate_role.py:38-125),明确覆盖了「财富比例 / 集中度」这个核心关切:
| 角色 | focus | 涉及比例/集中度? |
|---|---|---|
| 🌐 板块轮动 | 板块资金流向、行业 beta、景气度拐点 | 板块暴露 |
| 📊 个股估值 | 逐仓 PE/PB/PS/PEG + 目标价/止损 | — |
| ⚠️ 风险敞口 | 单股集中度、行业集中度、相关系数、Beta、回撤情景、尾部风险 | ✅ |
| 💰 现金管理 | 现金比例、机会成本、加减仓触发价 | ✅ |
协作机制用 Redis 实现(inbox.py):每个 Teammate 每一轮先「drain」收件箱读队友私信、读队友已写的共享草稿,再调 LLM,最后把自己的分析写进共享草稿的自己的 field。4 个 Teammate 全部完成后,Lead 先轮询等 4 份草稿齐(最多 300s),再多轮综合,输出一份带「共识 / 冲突(标出谁说 A 谁说 B)/ 调仓方案(每只持仓动作 + 比例 + 触发价)/ 尾部风险」的报告。
所以整个持仓分析是这样闭环的:7 种方式把持仓弄进来 → 算好每只票的权重和现金占比 → 两条路径注入上下文 → 4 个专家并行从板块/估值/风险/现金四个角度分析(其中两个专盯「押多重」)→ Lead 汇总成调仓方案。
五、功能实现拆解(四):红黑榜,以及为什么只看前 200
红黑榜是一个每日全市场扫描榜单:对每个市场(A股/港股/美股)的「市值前 200」股票池,用 LLM 批量打分,选出多头最强的红榜 top10 和空头最强的黑榜 bottom10,并对每只票做深度详情页。
5.1 红黑榜综合了哪些因素
打分是多层叠加的,最终一个 compositeScore 决定红与黑(核心在 research_leaderboard.py):
第 1 层 —— 初筛(screen)。 每 10 只一批喂给 LLM,直接产出 bullScore(0–100 多头)和 bearScore(0–100 空头)。此时 compositeScore = bull - bear(分差)。
第 2 层 —— 大师精评(refine)。 取分差最高/最低各 15 只进精评,每 3 只一批让 5 位大师(段永平/巴菲特/达利欧/林奇/芒格,DEFAULT_MASTERS,research_leaderboard.py:54)各打 0–100 分 + 一句理由。合并公式:
compositeScore = spread + (avg_master_score - 50.0)
第 3 层 —— Berkshire 研究注入(50% 权重)。 这是最明确的一个权重(_blend_berkshire_into_existing,research_leaderboard.py:1249-1259):
blended = Berkshire四维分(0-100) * 0.5 + 原多空分差(0-100) * 0.5
而 Berkshire 四维本身就是一个「四大师并行研究」的 prompt(ai_berkshire_service.py:99-190),四个维度各由一位大师负责:
- 段永平 → 商业模式 & 护城河(收入结构/飞轮/品牌/转换成本/网络效应);
- 巴菲特 → 财务 & 估值(ROE/毛利率/现金流/负债率/PE/安全边际);
- 芒格 → 行业格局 & 风险反转(市场规模/竞争格局/5+ 失败场景);
- 李录 → 管理层 & 长期确定性(CEO 能力圈/治理/10 年确定性)。
外加一张 10 项巴菲特买入 checklist(护城河已验证 / 管理层诚信 / 现金充足 / 负债可控 / ROE 可持续 / 现金流为正 / 估值合理 / 业务可懂 / 长期视角 / 整体通过)。
其他因子:财务健康度(PE/PB/ROE/营收利润增速/资产负债率/现金流,带启发式规则如 ROE≥10 盈利可接受、PE≥60 估值偏贵、负债率≥60 杠杆偏高);舆情新闻(利好/利空,仅展示不计分);市值排名本身甚至作为一个多头理由(「市值排名靠前,流动性和关注度更强」,research_leaderboard.py:1636)。
还有一个工程细节值得提:指标防污染(_is_metric_polluted,research_leaderboard.py:882-977)。它专门剔除「把股票代码数字当成指标」这类抽取错误(比如 00700 被解析成 PE=700)和超界值(PE>500、PB>50)。因为红黑榜是批量、自动化跑的,一旦脏数据进榜,整个榜单就没法看了——这是自动化 LLM 榜单必须做的脏活。
小结:红黑榜的打分 = ①LLM 多空分差 ②5 大师精评分 ③Berkshire 四维(护城河/财务估值/行业风险/管理层,占 50% 权重)④财务健康因子 ⑤舆情(仅展示)。最明确的数值权重是 Berkshire 研究 50% + 多空分差 50%。
5.2 为什么只看「市值前 200」—— 一个值得展开的设计决策
这是我被问得最多的一个问题:为什么不扫全市场,只看前 200?答案分工程和投资逻辑两层,后者更值得展开。
先看代码事实:前 200 来自配置 leaderboard_top_n = 200(config.py:61)。universe 的获取(prewarm_universe.py)也很说明问题——
- A股/港股:用东方财富公开接口按总市值降序(
fid:"f20",f20 就是总市值排序字段)拉前 N 名; - 美股:
prewarm_universe.py里硬编码的FALLBACK_US_TOP200,注释写明「基于 S&P 500 成分股市值权重 + 主要科技/金融消费龙头」。
注意美股这个 fallback 的措辞——它直接锚定标普 500。这不是巧合,正是设计意图所在。下面展开讲投资逻辑。
5.2.1 标普 500 本身就是「按市值选头部」的机器
先想清楚一个事实:标普 500 指数本身就是一台「按市值选头部公司」的机器。 它是市值加权的——公司越大、涨得越多,权重越高。买标普 500,约等于自动、持续地把筹码向「涨得最多的大公司」集中。
而标普 500 的长期收益,高度集中在头部。这就是著名的「Magnificent 7 / 七巨头」现象:近几年标普 500 的涨幅越来越由苹果、微软、英伟达等少数头部公司贡献,前十大成分股的权重常年在 30% 以上(2023–2025 年更高)。换句话说,指数的收益大头,本来就来自市值最靠前的那一小撮。
5.2.2 学术证据:绝大多数个股长期是「价值毁灭」
更硬核的证据来自学术界。Hendrik Bessembinder 那篇被广泛引用的研究 Do Stocks Outperform Treasury Bills?(2018)统计了 1926 年以来的美股全样本,结论相当反直觉:
- 大约 58% 的股票,整个生命周期的收益跑输一个月期国库券——也就是说,一大半个股长期看是负贡献;
- 股市创造的全部净财富,几乎集中在前约 4% 的公司身上——极少数头部赢家贡献了全部净收益。
这两个结论合起来指向一件事:股票市场的收益分布是极度右偏的,长尾的几千只小票整体上是「价值毁灭区」,真正的钱集中在头部少数公司。
5.2.3 把这个事实翻译成设计决策
把这个事实放到「一个普通人 + 有限算力」的约束下,结论就很自然了:
- 押注胜率最高的池子。 既然超额收益高度集中在头部大公司,那么把 LLM 的算力集中在市值前 200,就是「把有限的 token 预算,押在全市场胜率最高、信息最透明的池子」。对 A 股同理:妖股、壳股、ST 股噪声极大、基本面数据质量差、还容易被「代码数字当 PE」这类脏数据污染;而前 200 是机构主战场,研报覆盖、数据可得性、信息透明度都是最好的。
- 工程可行性的甜点。 每只上榜票要做初筛 + 精评 + Berkshire 四维 + 5 大师圆桌 + 深度详情页,单票是 10 分钟级的 LLM 调用(
redblack_cache.py:5-6注释)。全市场几千只根本跑不动;前 200 × 3 个市场,是「覆盖度 vs 成本」之间一个刚刚好的甜点。代码注释也说,拉 500 行对 top-200 是「plenty of headroom」(prewarm_universe.py:398-401)。 - 前 200 本身就是多头信号。 如前面提到的,市值排名靠前被直接写进了多头理由——流动性好、关注度高、踩雷概率低。对一个面向普通人的「稳健」理财助手来说,主动把长尾的赌博区排除掉,本身就是一种风控。
所以「只看前 200」不是一个偷懒的妥协,而是一个被收益集中度事实 + 工程成本共同推导出来的、主动的风控设计:稳健理财,先在胜率最高的池子里把功课做深,而不是在全市场大海捞针。
六、技术深潜(一):Skill 系统
前面反复提到「大师人格 Skill」,这是整个系统最有意思的技术资产。这一章讲三件事:Skill 是什么、人格怎么生产出来、为什么把它塞进大模型会有效。
6.1 Skill 到底是什么
在本项目里,一个 Skill 就是一份带 YAML frontmatter 的 Markdown 文件,路径是 .claude/skills/<key>-skill/SKILL.md。它描述一位投资大师的:思维模型、表达 DNA(话术风格)、决策框架、工作流、检查点、失败模式、黑名单。
一共 11 位大师,每位对应一个 skill 文件(注册表在 app/master_skills/catalog.py:28-101):段永平、巴菲特、芒格、达利欧、索罗斯、彼得·林奇、霍华德·马克斯、约翰·博格、彼得·蒂尔、格雷厄姆、李录。
可以把它理解成:把一个人的「认知操作系统」外置成一份可版本管理、可迭代、可被 LLM 加载的文档。 模型本身不动,改的是这份外置文档——这就是「冻结模型 + 外置可训练状态」的思路。
6.2 人格是怎么生产出来的:女娲提取 → 达尔文进化
这是生产管线,用到了两个外部工具(都是 GitHub 上的开源 skill)。
第一步:女娲(nuwa)—— 把公开言论「蒸馏」成人格
女娲(nuwa-skill,GitHub alchaincyf/nuwa-skill)的定位一句话说得很漂亮:「女娲不是复制人,是提炼思维框架」,而且**「捕捉的是 HOW they think,不是 WHAT they said」**。
它把一个人拆成五层(这正好对应第一节说的「大师言论可蒸馏」):
- 心智模型:他用什么镜片看世界?
- 决策启发式:他靠什么直觉规则做判断?
- 表达 DNA:他怎么说话?
- 反模式:他绝对不做什么?
- 诚实边界:这个 Skill 做不到什么?
执行流程是一条多 Agent 流水线:Phase 1 六路并行采集(①著作 ②对话 ③表达 ④他者评价 ⑤决策 ⑥时间线,且知乎/微信公众号/百度百科进黑名单,永远排除)→ Phase 2 三重验证(一个观点要收录,必须跨 2+ 领域复现 + 有生成力——能推断他对新问题的立场 + 有排他性)→ Phase 3 组装成 SKILL.md → Phase 4 质量验证(用已知问题做 sanity check,要求心智模型 3–7 个、内在张力 ≥2 对、一手来源占比 >50%)→ Phase 5 双 Agent 精炼。
本项目的证据很硬:pre-Darwin 的芒格 skill(app/master_skills/library.deprecated/munger/SKILL.md)和女娲官方示例 nuwa-skill/examples/munger-perspective/SKILL.md 逐字一致,catalog 也标注 repo="alchaincyf/munger-skill"(catalog.py:91)。其余 10 位大师多来自 Panmax/*-skill 的社区 fork(同样遵循女娲方法论)。
第二步:达尔文(darwin)—— 把人格「进化」到可用
提取出来的初版人格,往往「有料但不好用」。达尔文(darwin-skill,GitHub alchaincyf/darwin-skill)负责把它打磨到生产可用。核心理念是把 Skill 当作「冻结模型的外部可训练状态」,用一套循环持续优化:评估 → 改进 → 实测验证 → 人类确认 → 保留或回滚。
它用一把 9 维评分量规(满分 100),其中几个维度特别能说明问题:
- d5 可执行具体性(17 分,权重最高):禁止「建议 / 可以考虑 / 根据情况 / 灵活把握」这类软化措辞——skill 必须可直接执行;
- d3 失败模式编码(12 分):必须显式写「如果 X 失败 → Y」的分支,只写正向流程要扣分;
- d4 检查点设计(6 分):关键决策前要暂停确认,且必须显式标记 🔴 / STOP / CHECKPOINT,光靠「如果…建议…」不算;
- d9 反例与黑名单(6 分):必须有「不要做什么」的反例清单,只写「应该做 X」要扣分;
- d8 实测表现(23 分):不能只看文档规不规范,要拿测试 prompt 真跑,看输出质量是否真的变好。
几个机制保证了它不会越改越差:棘轮机制(用 git 版本控制,改进必须严格高于旧分才保留,退步自动回滚)、独立子 Agent 盲评(避免「自己改自己评」的偏差)、人在回路(每个 skill 优化完暂停,人确认再继续)。
这套量规不是拍脑袋,背后有论文支撑:它吸收了微软研究院 SkillLens(arXiv 2605.23899)的发现——LLM-as-judge 评估 skill 的准确率只有 46.4%(接近随机),加入 meta-skill 维度后能提到 73.8%——所以量规刻意强化了「失败模式」「可执行具体性」「黑名单」这几个 meta 维度。
本项目的实战证据(git log 不会说谎)
这条「女娲提取 → 达尔文进化」的管线,在本项目 git 历史里留下了完整痕迹:
8fb053d sync 11 master-perspective skills ← 女娲/社区提取的初版
5bd9ec4 darwin v2.0 round-1 — d3/d9/d1 ← 均分 44.1 → 60.9 (+16.8)
b8f075c darwin round-2 — d2 workflow + d4 🔴 checkpoints ← 60.9 → 71.0 (+10.1)
3c4fdfe route loader to .claude/skills/<key>-skill/SKILL.md
Round 1 给 11 个 skill 统一加了「失败模式表」和「黑名单」;Round 2 加了「🔴 工作流」5 步流水线和「🔴 检查点」,11 个 skill × 5 个 = 55 个显式 🔴 STOP。而且 commit 明确说 d5/d7 这两维刻意不动,「以免磨掉人格语气」——既要工程上的可执行,又要保住人格的味道,这个平衡很讲究。
show 一下:达尔文化之后的段永平 Skill 长什么样
以段永平为例(.claude/skills/duanyongping-skill/SKILL.md),达尔文化之后的结构是这样的——它已经不是一个「人物介绍」,而是一套可运行的决策程序:
- 角色定义:「步步高创始人、OPPO/vivo 幕后推手、第一个与巴菲特共进午餐的中国人。整套思想压缩为两句:做对的事,把事做对。回答时请克制、留余地,用反问引导而非直接下判断。」
- 思维模型(6 个):本分哲学 / 价值投资(只买看得懂的、不借钱不做空不做波段)/ 极度克制("不做什么"比"做什么"更重要)/ 用户导向(看用户不看对手)/ 诚信经营 / 分权管理。
- 表达 DNA:极度克制 / 反问引导 / 不确定就说不确定(常带「可能」「我觉得」)/ 价值观先行。
- 🔴 工作流(5 步流水线):对错判定 → 能力圈判定 → 长期视角(十年后回头看)→ 列不做清单 → 简洁建议,每步都有明确的输入/处理/输出。
- 🔴 检查点(5 个显式 STOP):比如「用户问具体股票代码/买卖时点 → STOP,明说不推荐个股」「用户想打价格战 → STOP,明说这不是对的事」。
- 失败模式表:8 行「用户场景 | 失败信号 | 应对路径」,比如「问到不懂的领域(加密货币/量化高频)→ 明确说不懂 + 推荐 buffett-skill」。
- 黑名单(8 条 🚫):不推荐股票代码、不鼓励投机套利、不在不懂的领域强给意见、不说「一定赚钱」、不支持烧钱补贴大战……
注意这里面有多少是「不做什么」。这正是价值投资人格的精髓,也是达尔文量规(d3/d4/d9)刻意强化的方向:一个稳健理财助手,「克制」比「聪明」重要得多。 一个动不动就喊「买入」「一定赚」的大师,是这个系统最不需要的。
6.3 为什么把 Skill 塞进大模型是有效的?塞在哪?
为什么有效
这背后的道理其实和「提示词工程为什么有效」是同一个:大模型在预训练时已经「读过」这些大师的东西,但它不会在你提问的当下,自动以那个特定人格、那套特定框架来回答。 Skill 的作用是在推理时,把模型「引导」到那个已被它内化、但需要被显式激活的知识子空间。
更具体地:
- 降维:把一个人几十年的思想,压缩成「6 个思维模型 + 决策框架 + 表达 DNA」这几页纸,模型不需要重新「读完」所有语料,就能抓住骨架;
- 纠偏:黑名单和失败模式,是在负向约束模型——它把「一个像段永平的人绝不会说的话」提前堵死,这比正向描述更能防止人格崩坏;
- 可执行:工作流和检查点把「怎么想」变成了「按哪几步走」,让输出从「谈谈感想」变成「按框架给结论」。
塞在哪:system prompt,不是 user prompt
这是个很实际的问题,答案在代码里很明确:Skill 原文被拼进 system prompt(build_master_system_prompt,master_skill_loader.py:71-101):
return (
f"你现在扮演{spec.name}风格的投资研究助手。\n"
"下面是必须遵循的 skill 原文,请把它作为你的主要行为和风格约束。\n\n"
f"{skill_text}\n\n" # ← 整份 SKILL.md 原文,进 system
"额外约束:\n"
"1. 使用中文回答。\n"
"2. 面向投资研究场景,优先讨论商业模式、估值、风险和仓位。\n"
"3. 不要编造数据…\n"
"5. 【输出节制】同一只股票的基本面只在首次给出…"
)
为什么是 system 而不是 user? 因为 Skill 定义的是「你是谁、你怎么行为」——这是身份和行为约束,属于模型的「人格设定」,天然该放在 system。而 user prompt 放的是「这次要处理什么」——用户的具体问题、持仓、行情数据。把「人格」和「任务」分到 system / user 两层,是让模型既稳定保持人格、又能灵活处理不同任务的标准做法。
补充:曾经的「反差点」已经修掉了
这里原本有个坑值得记录:早期版本里,默认的 MiniMax 链路,大师发言用的是一段轻量人设 prompt(_system_prompt,只用大师姓名 + 风格 + 原则拼一段,不读 SKILL.md);只有回落到 Anthropic 时才走 build_master_system_prompt 注入完整 skill——精心达尔文化的 11 份 SKILL.md,在默认链路上其实没被完整吃到。
这个坑后来修掉了(commit d0f08ed):所有发言链路统一改成走 build_master_system_prompt——不管是 MiniMax、Anthropic 还是现在的 DeepSeek,每条链路都注入完整的 SKILL.md 原文,而那段轻量的 _system_prompt 函数被整个删除。现在「人格资产」和「人格是否真的进了 prompt」终于对上了。这个教训值得留下:Skill 资产和「Skill 是否真的进了 prompt」是两件事,后者必须在运行时被验证。
七、记忆机制:4 层存储模型 + 5 个问题 + 对比 ClaudeCode
记忆是这个 Agent「越用越懂你」的关键。先直接回答一个问题:这套记忆到底是"几层"? 答案是——存储侧是 4 层(外加 1 张独立的长期记忆表),代码里就注释为「Agent 四层记忆」(agent_memory.py:1-19);而注入侧(真正喂给模型时)是 5 层,那是下一章的事。把「存储」和「注入」分开,是看懂整套系统的钥匙。
7.1 存储侧:4 层记忆 + 1 张长期记忆表
| 层 | 存什么 | 落在哪 | 持久化 | 何时写 |
|---|---|---|---|---|
| L1 会话归属 | 这次分析属于哪个用户 | analysis_tasks.user_id |
是 | 建任务时 |
| L2 用户档案卡 | 语言/风格/风险偏好/关注标的/历史决策/画像(1 行/人) | agent_user_profile_cards |
是 | 每 5 轮抽一次 |
| L3 对话摘要 | 近期对话摘要(LRU 5 条) | agent_conversation_summaries |
是 | 每轮都抽 |
| L4 运行时注入 | 当前交互窗口(拼装中的 prompt) | 不落表 | 否 | 每轮临时拼 |
| (独立)长期记忆 | 用户偏好/指令/事实/出处(≤50 条) | agent_user_memories |
是 | 每 5 轮抽 + 用户手动钉 |
注意 L4 这一层不落库——它只是"本轮把 L2/L3/长期记忆拼进 prompt"这个动作,拼完就丢。这也是为什么我说「存储 4 层、注入 5 层」不是一回事:L4 是存储和注入的交界。
下面用 5 个问题把这套记忆的生命周期讲完。
7.2 什么时候决定需要记忆?—— 触发式 + LLM 判断
触发点:每轮圆桌分析发完 done 帧之后,异步调一次 extract_and_persist_after_done(roundtable_pipeline.py:1343-1352),而且分频率:对话摘要(L3)每轮都抽,用户档案(L2)+ 长期记忆每 5 轮才抽一次(PROFILE_FRESH_TURNS=5,agent_memory.py:38,488),并发用 Redis SET NX EX 30 抢锁防重复抽取。
除了这条「对话后抽取」的主触发流,后来还补了三个让长期记忆真正「喂得饱」的触发点(commit 22edad5):① onboarding 完成时(POST /web/agent/onboarding-complete)一次性写入 3 条——风险画像(fact)、决策偏好(preference)、禁区(preference);② 持仓快照自动回写——持仓/现金的 6 个写端点 commit 后 fire-and-forget,把最新持仓按天去重写成一条 fact 记忆(portfolio_snapshot_{date}),不占满 50 条上限;③ 抽取 prompt 增强——明确识别「我决定 / 我打算 / 我计划 / 暂不 / 不投 / 以后再说」这类结构为 instruction,并把「触发重新评估的条件」也记进去。
最关键的是:「要不要记、记什么」不是规则硬编码,而是交给一个专门的 LLM extractor 判断(_PROFILE_EXTRACT_SYSTEM,agent_memory.py:415-448)。prompt 明确写了「不要收录:一次性提问、当前价格/净值等可重算数据、寒暄」。整个抽取全程 try/except 吞异常,绝不阻塞主流程。
7.3 记哪些东西?—— 结构化档案 + 四类长期记忆
- 用户档案(L2):语言、投资风格一句话、风险承受度(conservative/balanced/aggressive)、关注标的、历史关键决策(带 BUY/HOLD/SELL 信号 + 时间)、一段 ≤80 字画像(直接喂下轮 LLM)。
- 用户长期记忆:分四类
preference / instruction / fact / reference,每条 ≤12 字标题 + ≤60 字内容(含原因 Why)。prompt 特别强调识别「我决定… / 我打算… / 暂不… / 等…再…」(agent_memory.py:444-446)——因为用户做过的决策和计划最值得长期记住、也最该被将来尊重。第四章的「持仓快照」就是作为一种fact存进来的。
7.4 记忆怎么存储?—— 关系型数据库,不是 Redis
存关系型数据库(raw SQL + text(),刻意绕开 ORM——因为 gunicorn/uvicorn 多 worker 下 ORM session 会跨 event loop 报错,agent_memory.py:15-18)。三张表(db.py:225-277)。形态是结构化字段 + JSON 列混合(关注标的、关键决策这些列表存 JSON 字符串)。Redis 只用于抽取锁,不存记忆本体。
7.5 记忆什么时候更新 / 合并 / 去重?
- 档案卡 upsert:关键决策去重只留最近 20 条、关注标的留最近 30 个、
turn_count每次 +1(驱动"每 5 轮抽一次"); - 长期记忆去重:同
(user, type, title)已存在则更新,否则新增; - 容量淘汰:长期记忆 ≥50 条时先淘汰最老的一条 agent 自动记忆,用户手动钉的(user 来源)优先保留;
- 摘要 LRU:插新摘要后删掉不在「最近 5 条」里的旧摘要;
- 用户可改/删:
/web/agent/memoriesCRUD + onboarding 一键写入。
7.6 对比 ClaudeCode(存储侧):理念借鉴,形态重写
这套记忆在理念上大量借鉴 ClaudeCode(注释明说 taxonomy 借鉴 memdir),但存储形态走了不同的路。
相同:① 都是后台 LLM 抽取,不让用户手动维护;② 都有记忆类型 taxonomy(ClaudeCode 分 user/feedback/project/reference,本项目分 preference/instruction/fact/reference);③ 都有去重、容量上限、注入上限;④ 都是「长期记忆」和「短期历史」分离,从不混合压缩。
不同(存储侧):
| ClaudeCode | 本项目 | 为什么 | |
|---|---|---|---|
| 载体 | 文件(MEMORY.md + topic files),人类可读可编辑 |
关系型数据库(3 表,结构化 + JSON 列) | 本项目是多用户产品,记忆要按 user_id 隔离、高并发读写、和持仓/账户 join。文件适合单机 CLI,DB 适合多租户服务。 |
| 短期记忆 | 内存消息 + transcript JSONL + session memory 三层协作 | 不存短期原文进记忆层,短期历史走另一条路(见第八章) | 聊天轮次短,不需要 transcript/session-memory 那套恢复机制。 |
| 触发 | 一轮完整 query 结束时 extract | 「每 5 轮抽档案、每轮抽摘要」分频触发 | 短聊天用「固定频率 + LLM 判断」更主动、成本更可控。 |
八、上下文管理:5 层注入模型 + 历史管理 + 对比 ClaudeCode
存储是 4 层,注入是 5 层。每一次对话,真正组装进 LLM 的上下文是 1 层人格(system)+ 5 层情境(user)。这一章把这 6 层讲清楚,再和 ClaudeCode 的分层对照。
8.1 注入侧总览:1 + 5 层
flowchart TD
SYS["system prompt(1 层人格)
L0 人格层: 大师 Skill 原文 + 输出约束"]
SYS --> U1
subgraph USER["user prompt(5 层情境, 自顶向下)"]
U1["L1 长期记忆层
top 15 · 来自 agent_user_memories"]
U1 --> U2["L2 用户档案层
画像/风险偏好/关注标的/历史决策 · 来自 profile_cards"]
U2 --> U3["L3 对话摘要层
最近 5 条 · 来自 conversation_summaries"]
U3 --> U4["L4 短期历史层
圆桌 8 条 / 单大师阈值压缩 · 前端带或 DB"]
U4 --> U5["L5 当前输入层
本轮提问 + 持仓 + 实时行情 + steer"]
end
注意它们和第七章存储层的对应关系:存储侧的 L2 档案 → 注入侧 L2,存储侧 L3 摘要 → 注入侧 L3,独立的长期记忆表 → 注入侧 L1;而注入侧的 L4(短期历史)不来自记忆层,来自会话本身;L5(当前输入 + 实时数据)是每轮现取的。人格(L0)永远在 system,情境(L1–L5)永远在 user——这条边界和第六章「Skill 进 system」是同一个设计。
8.2 5 层情境各是什么、给多少
| 层 | 内容 | 来自 | 数量上限 |
|---|---|---|---|
| L1 长期记忆 | 用户明确偏好/指令/事实/出处(含持仓快照) | agent_user_memories |
top 15(全量 ≤50) |
| L2 用户档案 | 画像 + 结构化档案卡 | agent_user_profile_cards |
关注标的 ≤10、关键决策 ≤5 |
| L3 对话摘要 | 近期对话摘要 | agent_conversation_summaries |
最近 5 条,每条截 240 字 |
| L4 短期历史 | 当前会话原文 | 圆桌=前端带 / 单大师=DB 全量+阈值压缩 | 8 条(圆桌)/ 阈值触发(单大师) |
| L5 当前输入 | 本轮提问 + 持仓 + 实时基本面/行情 + steer | 每轮现取 | — |
组装代码(roundtable_pipeline.py:965-982)就是把 L1–L3 拼成 memory_block、 prepend 到 user prompt 头部,再叠上历史(L4)和本轮问题+持仓+实时数据(L5)。每一层都有硬上限——这是上下文不膨胀的关键。
8.3 对话历史怎么管:两条链路 + 阈值触发的多层压缩
L4(短期历史)有两条策略不同的链路:
- 圆桌链路:历史由前端带给后端,后端不持久化,每次只取最近 8 条(
history[-8:])。跨会话的长期连续性交给 L3 摘要层补足。 - 单大师聊天链路:历史全量落库(
master_chat_messages表,含压缩摘要),prompt 时不再用粗暴的滑动窗口,而是走一套 Claude Code 风格的阈值触发多层压缩(chat_compaction.build_llm_history)。持仓上下文不落库,只作 system preamble 每次临时注入,避免堆积过期持仓(master_chat_service.py:341-345)。
这套压缩(chat_compaction.py)替代了旧的 history[-12:] 一刀切,核心规则是:
- 阈值触发:默认 200K 上下文窗口,估算 token 超过「有效窗口 − 摘要预留 − 缓冲」(≈167K)才压缩;低于阈值不压缩、不滑窗,全量保留——这是和旧滑窗「第 12 条之后就无条件丢弃」的本质区别。token 估算是 CJK 感知的(中文≈1 token、其他≈1/4),无第三方依赖。
- 多层降级:L0 阈值门 → L1 单条过长消息预算(规则截断,防超长粘贴)→ 时间触发(闲置 >60min 走零成本规则层,省一次 LLM 调用)→ L4 自动压缩(LLM 生成结构化摘要,主机制)→ L2 微压缩兜底(规则:把更老的长回复换成占位符)→ L5 snip(最老直接丢)。
- 失败熔断:自动摘要连续失败 N 次(默认 3)就停手,改走零成本规则层,避免上下文不可救药时每轮白打一次摘要 LLM。
- 不删历史、可链式再压缩:摘要以
compact_summary角色追加进 DB(原始对话不动,UI 仍展示全量);发给 LLM 的上下文 = 「最近一次摘要 + 其后的近期原样消息」。下次再超限,就把「上次摘要 + 新增旧消息」再压成新摘要,形成压缩链。
摘要 prompt 也针对投资场景定制了 9 段:核心问题与意图 / 讨论过的标的 / 各大师结论 / 涉及的框架(护城河、估值、安全边际)/ 用户补充的关键信息(买入价、仓位、风险偏好)/ 逐条用户消息要点 / 待跟进问题 / 最近焦点 / 建议下一步。
8.4 对比 ClaudeCode(注入侧):都是分层,但分层方式不同
如果按「进入模型的内容」来分,ClaudeCode 的上下文大致也是 5 层,正好可以和本项目对照:
| 层 | ClaudeCode(注入侧) | 本项目(注入侧) |
|---|---|---|
| 人格/指令 | 系统指令(identity/tools/env:git status、date、cwd) | L0 大师 Skill 原文(system) |
| 长期记忆 | MEMORY.md 索引,进 system prompt |
L1 长期记忆 top 15,进 user 头部 |
| 辅助上下文 | 相关记忆/session memory/userContext,包成 <system-reminder> 的 meta user message |
L2 档案 + L3 摘要,进 user 头部 |
| 会话历史 | 经 session memory 压缩的 messages + 工具结果 | L4 阈值触发多层压缩(现在也压缩了) |
| 当前输入 | 当前 user turn | L5 提问 + 持仓 + 实时行情 + steer |
可以看到,两边都是「分层 + 索引/摘要常驻 + 按需召回 + 不全量塞」,但有两个关键差异:
- 长期记忆的注入位置:ClaudeCode 把
MEMORY.md索引放进 system prompt;本项目把人格放 system、记忆/情境放 user。动机不同——ClaudeCode 的 system 里没有"人格"要占座,所以记忆索引可以进 system;本项目的 system 被大师人格占了,记忆只能去 user。 - 历史压缩:这一点后来对齐了——早期本项目不压缩历史、只靠滑动窗口 + 独立摘要层;现在单大师聊天链路也引入了 Claude Code 风格的阈值触发多层压缩(见 8.3),思路和 CC 的 session memory compaction 同源。区别只剩范围:CC 压的是含工具结果的长任务历史,本项目压的是纯对话(只有 user/assistant 消息,没有工具结果/大文件/计划态,所以 CC 的"大文件层"被去掉了)。
8.5 为什么我不需要任务规划
这是和 ClaudeCode 最本质的区别,也是本项目「简单」的根源。
ClaudeCode 的上下文管理之所以复杂,是因为它要支撑长程、多步、会分叉的 agentic 任务:需要 TodoWrite 跟踪进度、需要任务状态、需要子 agent sidechain、需要在上下文快满时 auto-compaction、还要把工具结果/system-reminder/hook 输出都纳入。它管理的是一个正在执行中的、有状态的工程任务。
而本项目可以非常简单,因为场景本质不同——我做的是「聊天类交互」,不是「任务执行」:
- 没有任务规划、没有 todo、没有任务状态机。 在
master_agent.py/roundtable_pipeline.py/master_chat_service.py全文搜todo | task_plan | plan_step | task_state | checklist,零匹配。 - 「复杂的部分」不在对话里,而在确定性流水线里。 用户说「分析一下茅台」,这句话很简单;真正复杂的多空博弈、7 个分析师、风险辩论,全由
run_roundtable_sync/ TradingAgents 这条程序编排的流水线去跑,不是让 LLM 去"规划"步骤。LLM 不需要 todo list——流程是代码写死的,它只需在指定节点把指定那件事做好。 - 所以不需要「任务状态 vs 普通聊天」的区分。 每轮都是独立的「问 → 答」,长期连续性由记忆层(而非历史窗口)负责。
结论很干净:当「智能的编排」由代码(而非 LLM 的规划)承担时,上下文管理可以收敛成「按需压缩的历史 + 独立记忆层」这样的形态。 我把复杂度从「运行时上下文」挪到了「设计时流水线」,这是这个项目能保持简单、可控、便宜的根本原因。这不是说我的方案更优——两者面向的场景不同:ClaudeCode 必须处理开放式、跨几十步的工程任务,所以需要那套复杂机制;本项目处理一次投资咨询对话,一套简单机制就够。按需设计,不盲目照搬,这本身也是「本分」。
九、其他值得一提的技术点
除了上面三大块,还有几个工程细节,虽然不显眼,但都是让一个 LLM 产品「能用」和「好用」拉开差距的地方:
-
Steer:运行中改需求。 用户可以在一次分析跑到一半时追加指令(「再帮我看看现金流」「别讲技术面了」)。这些指令缓冲在 Redis list 里,流水线在大师发言的「检查点」把它们 drain 出来、合并进还没发言的大师的 prompt(
roundtable_pipeline.py:1029-1049)。语义和 ChatGPT 的 steer 一致:只影响还没发生的工作,不打扰正在流式输出的大师。 -
缓存体系:LLM 产品的成本生命线。 这个系统大量用缓存把「贵」的 LLM 调用摊薄:
ta_cache夜间对几百只票批量预热 TradingAgents 结果,圆桌命中就免去 10+ 分钟的现场跑图;红黑榜用 write-on-update 避免冷启动 502;行情有独立的 quote cache。对于一个「每日扫前 200 × 3 市场 + 每个详情页 10 分钟」的系统,没有这层缓存,成本根本撑不住。 -
Provider 链降级 + 熔断。 现在用的是一条可配的 provider 链(
FailoverOpenAIClient,openai 客户端的 drop-in 替换):DeepSeek 主、MiniMax 备,调用先走主链,遇到配额 / 鉴权 / 5xx / 超时这类值得降级的错误,就切到备链并自动把模型 id 换成备链自己的(MiniMax 的模型 id 会被 DeepSeek 拒,反之亦然)。整条链共享一个进程级熔断器(is_primary_down/trip_primary),一次故障全进程感知;发言一旦流出第一个 token 就不再重试(避免两半拼接),保证「即使 LLM 全挂,页面也不会白屏」。另外 DeepSeek 链路会显式关掉 thinking 模式——推理 token 也计入max_tokens,预算紧的对话会被顶空。 -
数据脱敏内建在第一道。 CSV 导入时就把卡号、邮箱、手机号打码(
csv_parser.py:8-12);红黑榜有专门的指标防污染(research_leaderboard.py:882-977)剔除「把股票代码当 PE」这类脏数据。这些「脏活」不性感,但对一个自动化处理真实持仓、批量生成榜单的系统是生死线。 -
大师名单的「三个版本」坑。 代码里有三份不一致的大师名单:catalog 注册了 11 位、圆桌默认轮换 10 位(不含李录)、Web 端默认 5 位。李录注册了却进不了默认圆桌。这是一个典型的「演进中留下的不一致」,属于我知道但还没来得及收敛的技术债——写出来提醒自己,也提醒读者:配置分散在多处时,要以 catalog 为唯一事实源收敛。
十、结尾:这套系统到底在解决什么
回看整个项目,它其实就干了一件事:把一个普通人「不擅长、没时间、容错率又低」的投资决策,交给一套被精心设计过的 AI 系统去把关。
- 用女娲 + 达尔文,把 11 位大师几十年公开的言论,蒸馏并进化成可运行的 Skill;
- 用大师圆桌提供「叙事和视角」,用 TradingAgents 多空博弈提供「可量化的打分」,两者分离,既不啰嗦也不乱喊单;
- 用红黑榜在市值前 200 这个「胜率最高的池子」里做每日扫描——这个「只看前 200」的决定,背后是收益集中度的事实和工程成本的双重推导;
- 用持仓分析(7 种导入 + 1 Lead 4 Teammate)盯住普通人最容易踩的两个坑:「买哪只」和「押多重」;
- 用一套数据库化的四层记忆让它越用越懂你,又用极简的上下文管理保持整个系统的简单和便宜。
如果说有什么贯穿始终的设计哲学,那大概就是段永平 Skill 里被反复强化的那两个字——本分:知道自己能力圈的边界(不看长尾小票、不乱给买卖建议、不确定就说不确定)、把复杂留给设计时而不是运行时、把「不做什么」看得比「做什么」更重。
这套系统还在持续迭代(大师名单要收敛、MiniMax 链路要吃全量 Skill、红黑榜要接更多维度)。如果你也在做类似的「把专家知识蒸馏成 Agent」的尝试,希望这篇拆解对你有用。
附:核心代码索引
- 圆桌流水线:
SoundFinancialPlanningBE/app/services/roundtable_pipeline.py:947- TradingAgents:
SoundFinancialPlanningBE/app/agents/tradingagents/(编排graph/trading_graph.py)- 红黑榜:
SoundFinancialPlanningBE/app/services/research_leaderboard.py、prewarm_universe.py- 持仓/复盘:
holding_import_service.py、portfolio_snapshot.py、deep_review/coordinator.py- Skill:
master_skills/catalog.py、services/master_skill_loader.py、.claude/skills/*-skill/SKILL.md- 记忆:
services/agent_memory.py、services/agent_user_memories.py、db.py:225-277- 人格生产管线:外部
nuwa-skill(女娲提取)+darwin-skill(达尔文进化)