用 LangGraph 搭了一个多 Agent 股票分析系统,分享一下踩坑经验
最近在研究 LangGraph 的实际应用场景,基于 TauricResearch/TradingAgents 做了一个 A 股适配的 fork —— TradingAgents-CN-lite。过程中踩了不少坑,记录一下。
先说效果
给它一个股票代码和一个日期,它会派出一组 AI 分析师从各个角度分析这只股票,然后通过多轮辩论得出交易建议。最终输出买入/卖出/持有的结论,附带完整的分析报告(Markdown + HTML 中英文版)。
整个流程分 5 个阶段:
第一阶段 — 分析:四个专业分析师并行工作
| 分析师 | 做什么 | 数据来源 |
|---|---|---|
| 📊 基本面 | 财务报表、资产负债表、现金流 | AkShare / yfinance |
| 💬 情绪 | 社交媒体和论坛情绪 | 东方财富股吧 / Reddit |
| 📰 新闻 | 宏观和公司层面的新闻 | CCTV财经 / yfinance |
| 📈 技术面 | MACD、RSI、布林带等指标 | BaoStock / yfinance |
第二阶段 — 辩论:🐂 多头研究员列出看涨理由,🐻 空头研究员列出看跌理由,互相挑战对方的推理,进行 N 轮辩论。
第三阶段 — 决策:🧑💼 研究主管综合辩论结果,🤵 交易员做出初步交易判断。
第四阶段 — 风险审查:三个风险分析师从不同角度讨论这笔交易
| 🔴 激进派 | 🟡 保守派 | ⚪ 中性派 |
|---|---|---|
| 高风险偏好 | 风险厌恶 | 平衡视角 |
第五阶段 — 最终拍板:👑 投资组合经理做出最终的买入/卖出/持有决定,📝 系统生成结构化报告。
LangGraph 编排:状态图是核心
整个系统用 LangGraph 的 StateGraph 编排,核心是一个有向无环图:
START → [分析师序列] → Bull ↔ Bear(辩论) → Research Manager → Trader
→ Aggressive → Conservative → Neutral(风险辩论) → Portfolio Manager → END
几个值得记录的设计决策:
1. 分析师节点有 Tool Loop
每个分析师不是一次调用就结束,而是有一个 工具调用循环:LLM 决定调用什么数据工具 → 获取结果 → 继续推理,直到分析完成。这通过 LangGraph 的条件边实现:
workflow.add_conditional_edges(
current_analyst,
should_continue_analyst, # 检查是否还有工具要调
[current_tools, current_clear], # 继续调工具 or 清理进入下一阶段
)
workflow.add_edge(current_tools, current_analyst) # 工具结果回到分析师
2. 辩论节点不写 messages
这是踩的一个大坑。辩论节点(Bull/Bear)只更新 investment_debate_state,不写 messages 通道。而 LangGraph 的 stream_mode="values" 会把完整状态发到每个 chunk,导致 debug 输出永远显示上一个 clear 节点留下的 HumanMessage("Continue")。
解决方案:在 debug 输出循环里跟踪前一个 chunk 的辩论状态值,只有当 current_response 实际变化时才打印:
prev_debate_response = ""
for chunk in self.graph.stream(state, **args):
debate = chunk.get("investment_debate_state", {})
current = debate.get("current_response", "")
if current and current != prev_debate_response:
print(current)
prev_debate_response = current
3. 结构化输出 + 容错
研究主管、交易员、投资组合经理三个决策节点用 llm.with_structured_output(Schema) 返回 Pydantic 对象。但不同 LLM 的 structured output 能力差异很大:
- Claude / GPT:基本没问题
- DeepSeek:需要跳过
tool_choice - mimo-v2.5-pro:会把
Optional[float]字段填成字符串"None"而不是null
后者的修复:给 Pydantic model 加 model_validator:
@model_validator(mode="before")
@classmethod
def _coerce_none_strings(cls, data):
if isinstance(data, dict):
for key in ("entry_price", "stop_loss"):
if data.get(key) in ("None", "null", ""):
data[key] = None
return data
A 股数据管道:最大的适配工作
原版 TradingAgents 用 Yahoo Finance,A 股数据经常缺失。适配 A 股主要解决三个问题:
1. 数据源选择
| 数据类型 | 数据源 | 为什么不用 yfinance |
|---|---|---|
| 行情 / 技术指标 | BaoStock | yfinance 的 A 股数据经常缺 |
| 基本面 / 财报 / 新闻 | AkShare | 覆盖全面,更新及时 |
| 散户情绪 | 东方财富股吧 | A 股散户情绪的唯一可靠来源 |
| 宏观新闻 | CCTV 财经 | 中文宏观新闻的最佳来源 |
2. 市场自动识别
用正则匹配股票代码格式,自动路由到对应的数据源和基准指数:
def detect_market(ticker: str) -> str:
if re.match(r'^(60|68)\d{4}$', ticker):
return 'sh' # 沪市
elif re.match(r'^(00|30)\d{4}$', ticker):
return 'sz' # 深市
elif re.match(r'^8\d{5}$', ticker):
return 'bj' # 北交所
# ...
3. 东方财富反爬
东方财富股吧的反爬比较严格,普通的 requests.get() 会被拦截。解决方案是用 curl_cffi 模拟 Chrome 的 TLS 指纹:
import curl_cffi.requests as curl_requests
response = curl_requests.get(url, impersonate="chrome")
这个库在底层用 libcurl 发送请求,TLS 握手特征和 Chrome 一致,能绕过大部分反爬检测。
中文 LLM 适配
支持了 DeepSeek、通义千问(国际+国内双区)、智谱 GLM(双区)、MiniMax(双区)、小米 Mimo、Ollama 等。每个提供商的 API 细节不同,做了一个统一的 client 工厂:
# .env 配置
TRADINGAGENTS_LLM_PROVIDER=deepseek
TRADINGAGENTS_DEEP_THINK_LLM=deepseek-v4-pro
TRADINGAGENTS_QUICK_THINK_LLM=deepseek-v4-flash
TRADINGAGENTS_OUTPUT_LANGUAGE=Chinese
踩坑总结
- LangGraph 的 stream_mode 要选对:
"values"发完整状态,"updates"只发增量。debug 输出用"values"时要注意 chunk 里可能有过期数据。 - 结构化输出不能假设 LLM 会严格遵守 schema:一定要有 fallback 和容错(
model_validator、try-catch)。 - A 股数据源选型很重要:yfinance 对 A 股支持不行,BaoStock + AkShare 是目前最稳定的组合。
- 反爬要用对工具:
requests搞不定东方财富,curl_cffi的 TLS 指纹模拟是关键。
项目地址
GitHub:https://github.com/cy-Yin/TradingAgents-CN-lite
原版 TradingAgents:https://github.com/TauricResearch/TradingAgents
本项目仅供研究和学习使用,不构成任何投资建议。
更多推荐


所有评论(0)