最近在研究 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
行情 / 技术指标BaoStockyfinance 的 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

踩坑总结

  1. LangGraph 的 stream_mode 要选对"values" 发完整状态,"updates" 只发增量。debug 输出用 "values" 时要注意 chunk 里可能有过期数据。
  2. 结构化输出不能假设 LLM 会严格遵守 schema:一定要有 fallback 和容错(model_validator、try-catch)。
  3. A 股数据源选型很重要:yfinance 对 A 股支持不行,BaoStock + AkShare 是目前最稳定的组合。
  4. 反爬要用对工具requests 搞不定东方财富,curl_cffi 的 TLS 指纹模拟是关键。

项目地址

GitHub:https://github.com/cy-Yin/TradingAgents-CN-lite

原版 TradingAgents:https://github.com/TauricResearch/TradingAgents


本项目仅供研究和学习使用,不构成任何投资建议。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐