1. 多 Agent 炒股这件事,到底在解决什么问题
第一次看到“多 Agent 炒股”这个说法,很多人脑子里冒出来的画面大概是几个机器人围在一张桌子前盯盘、吵架、下单。这个想象不算离谱,但真正落到工程实现上,它要解决的核心问题其实非常朴素:单一大模型做交易决策时,视角太单一、容易拍脑袋、缺乏交叉验证。
我自己最早用单个 LLM 跑行情分析的时候,最大的感受就是“它什么都敢说”。你给它一段 K 线数据,它能给你编出一套看起来逻辑自洽的买入理由,但换一天同样的数据,它可能给出完全相反的结论。这不是模型笨,而是单 Agent 架构天然缺少制衡机制——没有人和它唱反调,它就没有动力去审视自己的假设。
多 Agent 编排要解决的,就是把这个“一个人拍板”的过程,拆成“一群人各司其职、互相质疑、最后投票或汇总”的过程。TradingAgents 这类项目之所以能冲到十万级 Star,本质上是因为它踩中了一个真实痛点:大家想让 AI 真的下地干活,而不是只在聊天框里纸上谈兵。
这篇文章我会围绕 TradingAgents 这个项目,把多 Agent 炒股系统的整体设计思路、LangGraph 编排的核心机制、CLI 交互方式、回测验证方法,以及我在实际搭建过程中踩过的坑,完整地拆一遍。适合两类人看:一类是想理解多 Agent 编排到底怎么落地的开发者,另一类是想把 AI 用到量化交易场景里、但不知道从哪下手的朋友。哪怕你之前没接触过 LangGraph,跟着思路走也能明白个七八成。
2. 整体架构设计:为什么是“多 Agent + LangGraph + CLI”这套组合
2.1 单 Agent 为什么不够用
先说说单 Agent 方案的死穴。假设你让一个 Agent 同时负责“看财报、读新闻、分析技术指标、判断情绪、决定买卖”,它会面临几个问题。
第一是上下文污染。财报数据、新闻标题、技术指标混在一个 prompt 里,模型很容易被最近读到的信息带偏。比如刚读完一条利空新闻,它可能就忽略了技术面上明显的支撑位。
第二是角色冲突。一个 Agent 既要做多又要做空,既要激进又要保守,最后往往变成“和稀泥”,给出的建议模棱两可。
第三是无法追溯。决策错了,你根本不知道是哪一步推理出了问题,因为所有思考都揉在一段输出里。
多 Agent 的思路就是把这些问题拆开:让不同的 Agent 承担不同的角色,各自有独立的上下文和明确的职责边界,最后通过一个协调机制汇总。这就像一家投资公司里,研究员、交易员、风控各干各的,谁也别想一个人说了算。
2.2 LangGraph 在中间扮演什么角色
多 Agent 说起来简单,但“谁先跑、谁后跑、谁的结果传给谁、出现分歧怎么办”这些问题,需要一个编排框架来管。LangGraph 就是干这个的。
它把整个流程建模成一张有向图:节点是 Agent 或工具调用,边是数据流向和条件分支。相比传统的链式调用,LangGraph 最大的好处是支持循环和条件跳转。比如“看多 Agent”和“看空 Agent”辩论三轮,如果还没达成一致,就交给“裁判 Agent”拍板——这种带循环的逻辑,用普通 Chain 很难写,用 LangGraph 就是一个带条件的回边。
我选 LangGraph 而不是自己手写状态机,主要看中三点:状态管理是内置的,多个 Agent 共享一个 State 对象,不用自己维护全局变量;支持检查点,跑一半崩了能恢复;和 LangChain 生态无缝衔接,工具调用、模型切换都很顺。
2.3 CLI 为什么比 Web 界面更适合这个场景
很多人第一反应是做个网页界面多好看。但真正跑过交易分析的人会告诉你,CLI 才是效率最高的交互方式。
原因很实际:交易分析往往是批量、重复、需要脚本化的。你今天想跑 50 只股票的多 Agent 分析,用网页得点 50 次,用 CLI 一行命令加个循环就搞定了。而且 CLI 的输出可以直接重定向到文件、管道给下一个工具,方便做后续处理。
TradingAgents 的 CLI 设计得比较克制,核心就是几个命令:指定股票代码、选择分析深度、指定时间范围、输出格式。这种“少即是多”的设计,反而让它更容易被集成进已有的工作流。
2.4 整体数据流长什么样
把上面几块拼起来,一个典型的多 Agent 炒股流程是这样的:
- 用户通过 CLI 输入股票代码和分析参数
- 数据层拉取行情、财报、新闻等原始数据
- 多个分析 Agent 并行或串行处理不同维度的信息
- 多空双方 Agent 基于分析结果展开辩论
- 裁判 Agent 综合辩论内容给出最终决策
- 决策结果连同推理过程一起输出,可选写入回测系统
这个流程里,LangGraph 负责把 3 到 5 步串起来,State 对象在节点之间传递,每个 Agent 读取自己需要的字段、写入自己的结论。下面这张表能帮你快速理解各角色的分工:
| 角色 | 职责 | 输入 | 输出 |
|---|---|---|---|
| 数据 Agent | 拉取并清洗原始数据 | 股票代码、时间范围 | 结构化行情/财报/新闻 |
| 技术分析 Agent | 计算指标、识别形态 | 行情数据 | 技术面多空信号 |
| 基本面 Agent | 解读财报、估值 | 财报数据 | 基本面评分 |
| 情绪 Agent | 分析新闻与舆情 | 新闻文本 | 情绪倾向 |
| 多头 Agent | 构建看多论证 | 上述分析结果 | 买入理由 |
| 空头 Agent | 构建看空论证 | 上述分析结果 | 卖出理由 |
| 裁判 Agent | 综合辩论给结论 | 多空论证 | 最终决策与置信度 |
3. 核心细节拆解:LangGraph 编排与 Agent 通信的实操要点
3.1 State 设计:多 Agent 共享的“白板”
LangGraph 里最关键的抽象是 State。你可以把它理解成一块白板,所有 Agent 都能往上写、也能从上面读。设计得好,Agent 之间通信就很顺;设计得烂,就会出现字段冲突、数据覆盖。
我一般会把 State 设计成几个区块:原始数据区、分析结果区、辩论记录区、最终决策区。每个 Agent 只写自己负责的区块,读的时候按需读取。这样职责清晰,调试的时候也容易定位问题。
from typing import TypedDict, Annotated from operator import add class TradingState(TypedDict): ticker: str market_data: dict technical_signal: str fundamental_score: float sentiment: str bull_arguments: Annotated[list, add] bear_arguments: Annotated[list, add] debate_round: int final_decision: str confidence: float注意bull_arguments和bear_arguments用了Annotated[list, add],这是告诉 LangGraph 这两个字段用“追加”而不是“覆盖”的方式更新。辩论是多轮的,每轮都要保留记录,如果用默认覆盖,第二轮就会把第一轮的内容冲掉。这个坑我踩过,当时调试了半天才发现是 State 更新策略的问题。
3.2 节点与边的编排逻辑
节点就是一个个 Agent 函数,边决定执行顺序。TradingAgents 这类系统的编排通常分三段:并行分析段、辩论段、决策段。
并行分析段里,技术、基本面、情绪三个 Agent 可以同时跑,因为它们互不依赖。LangGraph 支持从同一个节点发散出多条边,实现并行。辩论段则是一个循环结构:多头发言、空头发言、判断是否继续,如果轮次没到上限且分歧还大,就回到多头继续。
from langgraph.graph import StateGraph, END graph = StateGraph(TradingState) graph.add_node("fetch_data", fetch_data_node) graph.add_node("technical", technical_node) graph.add_node("fundamental", fundamental_node) graph.add_node("sentiment", sentiment_node) graph.add_node("bull", bull_node) graph.add_node("bear", bear_node) graph.add_node("judge", judge_node) graph.set_entry_point("fetch_data") graph.add_edge("fetch_data", "technical") graph.add_edge("fetch_data", "fundamental") graph.add_edge("fetch_data", "sentiment") graph.add_edge(["technical", "fundamental", "sentiment"], "bull") graph.add_edge("bull", "bear") graph.add_conditional_edges("bear", should_continue_debate, { "continue": "bull", "judge": "judge" }) graph.add_edge("judge", END)add_edge的第一个参数传一个列表,表示要等这几个节点都跑完才进入下一个节点。这是 LangGraph 做“扇入”的标准写法。should_continue_debate是一个普通函数,根据debate_round和分歧程度返回"continue"或"judge"。
3.3 工具调用:让 Agent 真的能查数据
Agent 光会说话没用,得能调工具。LangGraph 里工具调用一般走 LangChain 的 Tool 接口。比如给技术分析 Agent 配一个计算 MACD 的工具、给情绪 Agent 配一个新闻搜索工具。
这里有个经验:工具的描述要写得非常具体。模型决定调不调工具、调哪个工具,全靠工具描述。我见过有人把工具描述写成“获取数据”,结果模型经常调错。改成“根据股票代码获取最近 30 天的日线行情,返回开盘价、收盘价、成交量”,调用准确率立刻上去了。
from langchain_core.tools import tool @tool def get_stock_price(ticker: str, days: int = 30) -> dict: """根据股票代码获取最近 N 天的日线行情数据。 返回包含 date, open, close, high, low, volume 的列表。 days 默认 30,最大 365。""" # 实际实现略 return {"ticker": ticker, "data": [...]}3.4 辩论机制:怎么让多空双方真的吵起来
辩论是多 Agent 炒股最有意思的部分,也是最容易做砸的部分。如果两个 Agent 的 prompt 写得太像,它们会互相附和,辩论就变成了走过场。
我的做法是给多空双方设定对立的人格和约束。多头 Agent 的 prompt 里明确要求“你必须找出至少三条支持买入的理由,并反驳空头的每一条论点”;空头 Agent 则相反。同时给它们不同的信息侧重:多头多看增长数据,空头多看风险指标。
还有一个技巧是限制发言长度。不限制的话,Agent 会写出一大段废话,既费 token 又稀释重点。我一般限制在 200 字以内,逼它说重点。
3.5 裁判 Agent 的决策逻辑
裁判 Agent 不是简单投票,而是要做加权综合。它需要读取多空双方的论证,评估哪方的证据更扎实,然后给出决策和置信度。
这里有个细节:置信度不能让它随便给。我试过直接问“你有多确信”,模型经常给 0.9 这种虚高的数字。后来改成让它先列出支持决策的关键证据数量、反驳成功的论点数量,再基于这些计数给出置信度,数字就靠谱多了。
4. 从零跑通一个多 Agent 分析:完整实操流程
4.1 环境准备与依赖安装
先把环境搭起来。Python 3.10 以上,主要依赖是 langgraph、langchain、langchain-openai(或你用的其他模型 SDK)、pandas、以及数据源相关的库。
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install langgraph langchain langchain-openai pandas yfinance模型 API Key 通过环境变量注入,不要硬编码在代码里。这一点是基本安全习惯,尤其是你要把代码传到公开仓库的时候。
export OPENAI_API_KEY="your-key-here"4.2 数据层搭建:行情、财报、新闻三路数据
数据是整个系统的地基。行情数据我用 yfinance 快速起步,财报数据可以用现成接口,新闻数据可以接搜索工具。关键是统一数据格式,让下游 Agent 不用关心数据从哪来。
我一般会定义一个fetch_all_data(ticker)函数,返回一个字典,包含price_df、financials、news_list三个字段。这样数据层和 Agent 层就解耦了,以后换数据源只改这一个函数。
4.3 逐个 Agent 的实现与调试
不要一上来就把所有 Agent 写完再跑,那样出问题很难定位。我的习惯是一次只加一个 Agent,跑通再加下一个。
先写数据 Agent,确认能拿到正确数据。再加技术分析 Agent,单独调用它,看输出是否合理。然后加基本面、情绪。等这三个都稳了,再加多空辩论。最后加裁判。
每加一个 Agent,都用同一只股票、同一时间段测试,这样能对比出新增 Agent 带来的变化。调试单个 Agent 的时候,可以直接调用它的节点函数,不用跑整张图,省时间。
4.4 组装 LangGraph 并首次运行
所有节点写好后,按第 3 节的编排逻辑组装图。首次运行建议开debug=True,LangGraph 会打印每个节点的输入输出,方便你观察数据流转。
app = graph.compile() result = app.invoke({ "ticker": "AAPL", "debate_round": 0, "bull_arguments": [], "bear_arguments": [] }) print(result["final_decision"]) print(result["confidence"])第一次跑大概率会遇到字段缺失、类型不匹配的问题。别慌,看 debug 输出,定位到具体节点,检查它的输入字段是不是上游没写进去。
4.5 CLI 封装:让分析可以批量跑
核心逻辑跑通后,用 argparse 或 click 包一层 CLI。我推荐 click,写起来更清爽。
import click @click.command() @click.option("--ticker", required=True, help="股票代码") @click.option("--rounds", default=3, help="辩论轮次") @click.option("--output", default="result.json", help="输出文件") def analyze(ticker, rounds, output): """对指定股票运行多 Agent 分析""" result = app.invoke({ "ticker": ticker, "debate_round": 0, "bull_arguments": [], "bear_arguments": [] }) with open(output, "w") as f: json.dump(result, f, ensure_ascii=False, indent=2) click.echo(f"决策: {result['final_decision']}, 置信度: {result['confidence']}")封装好之后,批量分析就是一行 shell 循环的事:
for code in AAPL MSFT GOOG; do python analyze.py --ticker $code --output ${code}_result.json done4.6 回测验证:用 backtrader 检验决策质量
Agent 说买,不代表真的该买。必须回测。我用 backtrader 做多股回测,思路是把多 Agent 的决策信号转成买卖信号,喂给 backtrader 的策略类。
关键点在于避免未来函数。多 Agent 分析用到的数据,必须严格限制在决策时点之前。我见过有人图省事,把整段历史数据一次性喂给 Agent,结果回测收益高得离谱,实盘一跑就亏——因为 Agent “偷看”了未来。
回测时还要注意交易成本。A 股有印花税和佣金,美股有佣金和滑点。不扣成本的回测结果都是耍流氓。我一般按单边 0.1% 到 0.2% 估算,具体看标的。
| 回测参数 | 建议值 | 说明 |
|---|---|---|
| 初始资金 | 100000 | 便于计算收益率 |
| 单边成本 | 0.1%-0.2% | 含佣金与滑点 |
| 持仓上限 | 单票不超过 20% | 控制集中度风险 |
| 回测区间 | 至少 2 年 | 覆盖不同市场环境 |
| 基准 | 沪深300 或标普500 | 对比超额收益 |
5. 常见问题与排查技巧实录
5.1 Agent 输出格式不稳定怎么办
这是最高频的问题。模型有时候返回 JSON,有时候返回带 markdown 代码块的 JSON,有时候干脆返回一段散文。解决办法有三层:第一,在 prompt 里明确要求“只返回 JSON,不要任何其他文字”;第二,用 Pydantic 定义输出结构,配合模型的 structured output 功能;第三,加一层解析容错,用正则先把 JSON 抠出来再解析。
我实测下来,structured output 加解析容错双保险最稳。纯靠 prompt 约束,十次里总有一两次翻车。
5.2 辩论陷入死循环怎么破
如果多空双方谁也不服谁,should_continue_debate一直返回 continue,图就跑不完了。必须设硬性上限,比如最多 5 轮,到点强制进裁判。另外可以在分歧度计算里加个阈值,双方论点重合度高于某个值就认为“没必要再吵了”,直接进裁判。
5.3 工具调用失败或超时
数据源接口不稳定是常态。我的做法是给每个工具调用加重试和降级。重试三次还失败,就返回一个“数据不可用”的标记,让 Agent 知道这块信息缺失,而不是直接崩掉整个流程。Agent 拿到缺失标记后,可以在论证里说明“因数据缺失,此维度未纳入考量”,反而更诚实。
5.4 回测结果好得不像真的
先别高兴,大概率是未来函数或者幸存者偏差。检查三件事:Agent 用到的数据时间戳是否都早于决策时点;股票池是否包含了已退市的票;交易成本是否扣了。这三项都确认没问题,再谈策略有效性。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 图跑不完 | 辩论无终止条件 | 检查 should_continue 逻辑 |
| 字段丢失 | State 更新策略错误 | 检查 Annotated 配置 |
| 决策反复横跳 | 温度参数过高 | 降低 temperature 到 0.2 以下 |
| 工具调用错乱 | 工具描述模糊 | 细化工具 docstring |
| 回测虚高 | 未来函数/未扣成本 | 核对时间戳与成本设置 |
| 输出解析失败 | 格式约束不足 | 加 structured output |
5.6 几个我踩过的坑
第一个坑是并行节点的 State 写入冲突。技术、基本面、情绪三个 Agent 并行跑,如果它们都往同一个字段写,后写的会覆盖先写的。解决办法是给每个 Agent 分配独立字段,或者用Annotated指定合并策略。
第二个坑是模型温度设太高。做交易决策,温度必须低,我一般设 0.1 到 0.2。温度高了,同样的输入每次输出都不一样,回测根本没法复现。
第三个坑是忽略 token 成本。多 Agent 多轮辩论,token 消耗是单 Agent 的好几倍。跑批量分析前,先用一两只股票估算单次成本,心里有数再铺开。我一开始没算,跑了一晚上账单出来吓了一跳。
6. 多 Agent 炒股系统的边界与我的实际体会
说了这么多技术细节,最后想聊聊这类系统的边界。多 Agent 炒股不是印钞机,它解决的是“决策过程的结构化和可追溯”,不是“预测涨跌”。市场里充满随机性,再好的分析框架也不能保证盈利。
我自己的用法是把它当成一个辅助研究工具:让它帮我快速梳理一只股票的多空逻辑,把散落在各处的信息整合成结构化报告,然后我自己做最终判断。它最大的价值是省时间、减少遗漏,而不是替我做决定。
另外,这套多 Agent 编排的思路其实不限于炒股。任何需要多视角交叉验证的决策场景——比如选品、投资尽调、方案评审——都可以套用同样的架构:多个角色独立分析、结构化辩论、裁判综合。LangGraph 提供的编排能力是通用的,炒股只是它一个比较抓眼球的应用。
如果你打算动手,我的建议是从最小可用版本开始:先跑通“数据 + 单分析 Agent + 裁判”的三节点流程,确认整条链路通了,再逐步加角色、加辩论、加回测。一上来就追求完整的多 Agent 系统,很容易在调试地狱里放弃。先把一个 Agent 调明白,比同时调七个 Agent 高效得多。