10.7.1 LangGraph 条件边 (Conditional Edges) 详解
本文档基于 TradingAgents 项目,通过问答形式深入解析 LangGraph 中条件边的工作原理,帮助你理解多智能体系统中节点间的逻辑跳转机制。
目录
1. 系统概览
Q1: TradingAgents 系统包含哪些主要阶段?
A: 系统分为 4 个主要阶段,共 13+ 个节点:
| 阶段 | 节点名称 | 功能 |
|---|---|---|
| 分析师层 | Market Analyst, Social Analyst, News Analyst, Fundamentals Analyst | 收集和分析各类市场数据 |
| 投资辩论层 | Bull Researcher, Bear Researcher, Research Manager | 看涨/看跌方辩论,经理做决策 |
| 交易执行层 | Trader | 制定具体交易计划 |
| 风险评估层 | Risky Analyst, Safe Analyst, Neutral Analyst, Risk Judge | 三方风险评估,最终裁决 |
Q2: 什么是条件边?为什么需要条件边?
A: 条件边是 LangGraph 中根据当前状态动态决定下一个执行节点的机制。
# 普通边:固定的下一跳
workflow.add_edge("A", "B") # A 永远跳转到 B
# 条件边:根据状态动态决定
workflow.add_conditional_edges(
"A", # 源节点
router_function, # 路由函数:根据 state 返回目标节点名
{"B": "B", "C": "C"} # 可能的目标节点映射
)核心原理: 路由函数接收当前状态 state,返回下一个节点的名称字符串。
2. 状态定义
Q3: 系统使用了哪些状态类型?
A: 系统定义了 3 个核心状态类型:
# 文件: tradingagents/agents/utils/agent_states.py
# 1. 投资辩论状态
class InvestDebateState(TypedDict):
bull_history: str # 看涨方历史发言
bear_history: str # 看跌方历史发言
history: str # 完整辩论历史
current_response: str # 最近一次发言(关键!用于判断下一个发言者)
judge_decision: str # 经理决策
count: int # 辩论轮数(关键!用于判断是否结束)
# 2. 风险辩论状态
class RiskDebateState(TypedDict):
risky_history: str # 激进分析师历史
safe_history: str # 保守分析师历史
neutral_history: str # 中立分析师历史
history: str # 完整辩论历史
latest_speaker: str # 最后发言者(关键!用于轮换)
current_risky_response: str
current_safe_response: str
current_neutral_response: str
judge_decision: str
count: int # 辩论轮数(关键!用于判断是否结束)
# 3. 主状态
class AgentState(MessagesState):
company_of_interest: str
trade_date: str
sender: str
# 分析报告
market_report: str
sentiment_report: str
news_report: str
fundamentals_report: str
# 嵌套的辩论状态
investment_debate_state: InvestDebateState
risk_debate_state: RiskDebateState
# 最终结果
investment_plan: str
trader_investment_plan: str
final_trade_decision: strQ4: 初始状态是如何创建的?
A: 通过 Propagator 类创建:
# 文件: tradingagents/graph/propagation.py
def create_initial_state(self, company_name: str, trade_date: str):
return {
"messages": [("human", company_name)],
"company_of_interest": company_name,
"trade_date": str(trade_date),
# 投资辩论初始状态
"investment_debate_state": {
"history": "",
"current_response": "", # 空字符串,第一个发言者将是 Bull
"count": 0 # 从 0 开始计数
},
# 风险辩论初始状态
"risk_debate_state": {
"history": "",
"current_risky_response": "",
"current_safe_response": "",
"current_neutral_response": "",
"count": 0
},
# 各分析报告初始为空
"market_report": "",
"fundamentals_report": "",
"sentiment_report": "",
"news_report": "",
}3. 第一阶段:分析师层的条件边
Q5: 分析师节点为什么需要条件边?
A: 因为分析师使用工具(tools)获取数据。LLM 可能调用工具,也可能直接生成最终报告。
工作流程:
- 分析师 LLM 收到任务
- 如果需要数据 → 生成
tool_calls→ 跳转到工具节点执行 - 如果已完成分析 → 生成最终报告 → 跳转到消息清除节点
Q6: 分析师的条件边是如何实现的?
A: 每个分析师都有对应的条件函数:
# 文件: tradingagents/graph/conditional_logic.py
class ConditionalLogic:
def should_continue_market(self, state: AgentState):
"""Market 分析师的路由逻辑"""
messages = state["messages"]
last_message = messages[-1]
# 关键判断:最后一条消息是否包含工具调用?
if last_message.tool_calls:
return "tools_market" # 有工具调用 → 执行工具
return "Msg Clear Market" # 无工具调用 → 清除消息,进入下一阶段
# 其他分析师的逻辑完全相同,只是返回的节点名不同
def should_continue_social(self, state: AgentState):
# ... 同样的逻辑
if last_message.tool_calls:
return "tools_social"
return "Msg Clear Social"Q6.5: tool_calls 到底是什么?返回的格式是什么样的?
A: tool_calls 是 LangChain 中 AIMessage 的一个属性,当 LLM 决定调用工具时,会返回一个包含工具调用信息的列表。
数据结构
# tool_calls 是一个列表,每个元素是一个字典
tool_calls: List[Dict] = [
{
"id": "call_abc123", # 唯一标识符
"name": "get_stock_data", # 工具函数名
"args": { # 工具参数(字典格式)
"symbol": "AAPL",
"start_date": "2024-01-01",
"end_date": "2024-01-31"
}
},
# 可能有多个工具调用...
]具体案例:Market Analyst 调用工具
假设我们让 Market Analyst 分析苹果公司(AAPL)的股票:
场景 1:LLM 决定先获取股票数据
# LLM 返回的 AIMessage
result = AIMessage(
content="", # 调用工具时,content 通常为空
tool_calls=[
{
"id": "call_xK9mZ2",
"name": "get_stock_data",
"args": {
"symbol": "AAPL",
"start_date": "2024-11-01",
"end_date": "2024-12-01"
}
}
]
)
# 条件判断
if result.tool_calls: # True,因为列表非空
# → 跳转到 tools_market 节点执行工具场景 2:LLM 决定同时获取多个指标
# LLM 可以一次调用多个工具
result = AIMessage(
content="",
tool_calls=[
{
"id": "call_aB3cD4",
"name": "get_stock_data",
"args": {
"symbol": "AAPL",
"start_date": "2024-11-01",
"end_date": "2024-12-01"
}
},
{
"id": "call_eF5gH6",
"name": "get_indicators",
"args": {
"symbol": "AAPL",
"indicator": "rsi",
"curr_date": "2024-12-01",
"look_back_days": 30
}
}
]
)
# len(result.tool_calls) == 2
# 条件判断仍然为 True场景 3:LLM 完成分析,直接输出报告
# 当 LLM 认为数据足够时,直接生成报告
result = AIMessage(
content="""
## AAPL 技术分析报告
基于过去30天的数据分析:
- RSI: 65.2(中性偏多)
- MACD: 正向交叉
- 50日均线: 上升趋势
| 指标 | 数值 | 信号 |
|------|------|------|
| RSI | 65.2 | 中性 |
| MACD | 1.23 | 买入 |
建议:持有观望,等待回调买入机会。
""",
tool_calls=[] # 空列表!
)
# 条件判断
if result.tool_calls: # False,因为列表为空
# → 跳转到 Msg Clear Market 节点工具定义与 tool_calls 的对应关系
项目中定义的工具:
# 文件: tradingagents/agents/utils/core_stock_tools.py
@tool
def get_stock_data(
symbol: Annotated[str, "ticker symbol of the company"],
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
end_date: Annotated[str, "End date in yyyy-mm-dd format"],
) -> str:
"""Retrieve stock price data (OHLCV) for a given ticker symbol."""
...
# 文件: tradingagents/agents/utils/technical_indicators_tools.py
@tool
def get_indicators(
symbol: Annotated[str, "ticker symbol of the company"],
indicator: Annotated[str, "technical indicator to get"],
curr_date: Annotated[str, "The current trading date, YYYY-mm-dd"],
look_back_days: Annotated[int, "how many days to look back"] = 30,
) -> str:
"""Retrieve technical indicators for a given ticker symbol."""
...对应关系表:
| 工具定义 | tool_calls 中的字段 |
|---|---|
函数名 get_stock_data | "name": "get_stock_data" |
参数 symbol: str | "args": {"symbol": "AAPL", ...} |
参数 start_date: str | "args": {..., "start_date": "2024-01-01", ...} |
参数 end_date: str | "args": {..., "end_date": "2024-01-31"} |
完整的工具调用循环
┌─────────────────────────────────────────────────────────────────┐
│ Market Analyst 节点 │
│ │
│ 1. LLM 接收任务: "分析 AAPL 股票" │
│ 2. LLM 思考: "我需要先获取股票数据" │
│ 3. LLM 返回: │
│ AIMessage( │
│ content="", │
│ tool_calls=[{ │
│ "id": "call_123", │
│ "name": "get_stock_data", │
│ "args": {"symbol": "AAPL", ...} │
│ }] │
│ ) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ should_continue_market(state) │
│ │
│ last_message.tool_calls = [{ │
│ "name": "get_stock_data", │
│ ... │
│ }] │
│ │
│ if tool_calls: ✓ True │
│ return "tools_market" │
└───────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ tools_market 节点 │
│ (ToolNode 自动执行工具) │
│ │
│ 执行: get_stock_data("AAPL",..)│
│ 返回: ToolMessage( │
│ content="Date,Open,High,...",│
│ tool_call_id="call_123" │
│ ) │
└───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Market Analyst 节点(第二次) │
│ │
│ 1. LLM 看到工具返回的数据 │
│ 2. LLM 思考: "数据够了,可以生成报告" │
│ 3. LLM 返回: │
│ AIMessage( │
│ content="## AAPL 分析报告\n...", │
│ tool_calls=[] ← 空! │
│ ) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────┐
│ should_continue_market(state) │
│ │
│ last_message.tool_calls = [] │
│ │
│ if tool_calls: ✗ False │
│ return "Msg Clear Market" │
└───────────────────────────────┘
│
▼
进入下一阶段...Q7: 分析师节点返回什么?如何触发条件判断?
A: 分析师节点返回包含 messages 的字典:
# 文件: tradingagents/agents/analysts/market_analyst.py
def market_analyst_node(state):
# ... 构建 prompt ...
# 关键:使用 bind_tools 让 LLM 可以调用工具
chain = prompt | llm.bind_tools(tools)
result = chain.invoke(state["messages"])
# result 可能包含 tool_calls(需要调用工具)
# 也可能不包含 tool_calls(直接生成报告)
report = ""
if len(result.tool_calls) == 0:
report = result.content # 没有工具调用时,内容就是报告
return {
"messages": [result], # 这个 result 会被条件函数检查
"market_report": report,
}Q8: 工具节点执行后会去哪里?
A: 工具节点执行后会回到同一个分析师节点,形成循环:
# 文件: tradingagents/graph/setup.py
# 条件边:分析师 → 工具节点 或 清除节点
workflow.add_conditional_edges(
"Market Analyst",
self.conditional_logic.should_continue_market,
["tools_market", "Msg Clear Market"],
)
# 普通边:工具节点 → 分析师(循环回来继续处理工具结果)
workflow.add_edge("tools_market", "Market Analyst")完整循环图:
Market Analyst
│
├─(有 tool_calls)──→ tools_market ──→ Market Analyst(循环)
│
└─(无 tool_calls)──→ Msg Clear Market ──→ Social Analyst(下一个)Q9: 消息清除节点的作用是什么?
A: 清除累积的消息,避免上下文过长:
# 文件: tradingagents/agents/utils/agent_utils.py
def create_msg_delete():
def delete_messages(state):
"""清除消息并添加占位符"""
messages = state["messages"]
# 删除所有消息
removal_operations = [RemoveMessage(id=m.id) for m in messages]
# 添加最小占位消息(某些 LLM 要求至少有一条消息)
placeholder = HumanMessage(content="Continue")
return {"messages": removal_operations + [placeholder]}
return delete_messagesQ10: 多个分析师之间如何串联?
A: 通过普通边连接清除节点到下一个分析师:
# 文件: tradingagents/graph/setup.py
for i, analyst_type in enumerate(selected_analysts):
# ... 添加条件边和工具循环 ...
# 连接到下一个分析师或进入辩论阶段
if i < len(selected_analysts) - 1:
# 不是最后一个分析师 → 连接到下一个
next_analyst = f"{selected_analysts[i+1].capitalize()} Analyst"
workflow.add_edge(current_clear, next_analyst)
else:
# 是最后一个分析师 → 进入投资辩论阶段
workflow.add_edge(current_clear, "Bull Researcher")4. 第二阶段:投资辩论层的条件边
Q11: 投资辩论的条件边是如何工作的?
A: Bull 和 Bear 研究员轮流发言,直到达到最大轮数:
# 文件: tradingagents/graph/conditional_logic.py
def should_continue_debate(self, state: AgentState) -> str:
"""投资辩论的路由逻辑"""
debate_state = state["investment_debate_state"]
# 条件 1:检查是否达到最大轮数
# max_debate_rounds=1 时,总共 2 次发言(Bull 1次 + Bear 1次)
if debate_state["count"] >= 2 * self.max_debate_rounds:
return "Research Manager" # 辩论结束,交给经理决策
# 条件 2:根据最后发言者决定下一个发言者
if debate_state["current_response"].startswith("Bull"):
return "Bear Researcher" # Bull 说完,轮到 Bear
return "Bull Researcher" # Bear 说完,轮到 BullQ12: Bull Researcher 节点如何更新状态?
A: Bull 研究员更新 investment_debate_state,特别是 current_response 和 count:
# 文件: tradingagents/agents/researchers/bull_researcher.py
def bull_node(state) -> dict:
investment_debate_state = state["investment_debate_state"]
history = investment_debate_state.get("history", "")
# ... 构建 prompt 并调用 LLM ...
response = llm.invoke(prompt)
# 关键:添加 "Bull Analyst:" 前缀
argument = f"Bull Analyst: {response.content}"
# 更新辩论状态
new_investment_debate_state = {
"history": history + "\n" + argument,
"bull_history": bull_history + "\n" + argument,
"bear_history": investment_debate_state.get("bear_history", ""),
"current_response": argument, # 标记最后发言者是 Bull
"count": investment_debate_state["count"] + 1, # 轮数 +1
}
return {"investment_debate_state": new_investment_debate_state}Q13: 条件边是如何连接 Bull 和 Bear 的?
A: 两个研究员都指向同一个条件函数,形成轮换:
# 文件: tradingagents/graph/setup.py
# Bull → Bear 或 Research Manager
workflow.add_conditional_edges(
"Bull Researcher",
self.conditional_logic.should_continue_debate,
{
"Bear Researcher": "Bear Researcher",
"Research Manager": "Research Manager",
},
)
# Bear → Bull 或 Research Manager
workflow.add_conditional_edges(
"Bear Researcher",
self.conditional_logic.should_continue_debate,
{
"Bull Researcher": "Bull Researcher",
"Research Manager": "Research Manager",
},
)Q14: 辩论的完整流程是什么样的?(假设 max_debate_rounds=1)
A:
初始状态:count=0, current_response=""
1. Bull Researcher 执行
- 生成 "Bull Analyst: ..."
- count=1, current_response="Bull Analyst: ..."
- 条件判断:count(1) < 2*1=2,且 response 以 "Bull" 开头
- → 跳转到 Bear Researcher
2. Bear Researcher 执行
- 生成 "Bear Analyst: ..."
- count=2, current_response="Bear Analyst: ..."
- 条件判断:count(2) >= 2*1=2
- → 跳转到 Research Manager
3. Research Manager 执行
- 综合双方观点,做出决策
- 输出 investment_plan5. 第三阶段:交易执行层
Q15: 交易执行层使用条件边吗?
A: 不使用。这是一个简单的顺序流程:
# 文件: tradingagents/graph/setup.py
# Research Manager → Trader(普通边)
workflow.add_edge("Research Manager", "Trader")
# Trader → Risky Analyst(普通边,进入风险评估阶段)
workflow.add_edge("Trader", "Risky Analyst")Q16: Trader 节点做什么?
A: Trader 接收投资计划,生成具体的交易建议:
# 文件: tradingagents/agents/trader/trader.py
def trader_node(state, name):
investment_plan = state["investment_plan"] # 来自 Research Manager
# ... 构建 prompt,包含投资计划和历史记忆 ...
result = llm.invoke(messages)
return {
"messages": [result],
"trader_investment_plan": result.content, # 供风险评估使用
"sender": name,
}6. 第四阶段:风险评估层的条件边
Q17: 风险评估层有何不同?
A: 风险评估层有 3 个分析师(而非 2 个),按固定顺序轮换:
Risky Analyst → Safe Analyst → Neutral Analyst → Risky Analyst → ...Q18: 风险评估的条件边是如何实现的?
A: 根据 latest_speaker 字段决定下一个发言者:
# 文件: tradingagents/graph/conditional_logic.py
def should_continue_risk_analysis(self, state: AgentState) -> str:
"""风险分析的路由逻辑"""
risk_state = state["risk_debate_state"]
# 条件 1:检查是否达到最大轮数
# max_risk_discuss_rounds=1 时,总共 3 次发言
if risk_state["count"] >= 3 * self.max_risk_discuss_rounds:
return "Risk Judge" # 结束,交给风险经理
# 条件 2:根据最后发言者决定下一个发言者(三方轮换)
if risk_state["latest_speaker"].startswith("Risky"):
return "Safe Analyst" # Risky → Safe
if risk_state["latest_speaker"].startswith("Safe"):
return "Neutral Analyst" # Safe → Neutral
return "Risky Analyst" # Neutral → RiskyQ19: 风险分析师如何更新状态?
A: 每个分析师更新自己的响应和 latest_speaker:
# 文件: tradingagents/agents/risk_mgmt/aggresive_debator.py (Risky Analyst)
def risky_node(state) -> dict:
risk_debate_state = state["risk_debate_state"]
# ... 获取其他分析师的观点,构建 prompt ...
response = llm.invoke(prompt)
argument = f"Risky Analyst: {response.content}"
new_risk_debate_state = {
"history": history + "\n" + argument,
"risky_history": risky_history + "\n" + argument,
"safe_history": risk_debate_state.get("safe_history", ""),
"neutral_history": risk_debate_state.get("neutral_history", ""),
# 关键字段
"latest_speaker": "Risky", # 标记最后发言者
"current_risky_response": argument, # 更新当前响应
"current_safe_response": risk_debate_state.get("current_safe_response", ""),
"current_neutral_response": risk_debate_state.get("current_neutral_response", ""),
"count": risk_debate_state["count"] + 1, # 轮数 +1
}
return {"risk_debate_state": new_risk_debate_state}Q20: 三个风险分析师的条件边是如何连接的?
A: 每个分析师都可能跳转到另一个分析师或结束:
# 文件: tradingagents/graph/setup.py
# Risky → Safe 或 Risk Judge
workflow.add_conditional_edges(
"Risky Analyst",
self.conditional_logic.should_continue_risk_analysis,
{
"Safe Analyst": "Safe Analyst",
"Risk Judge": "Risk Judge",
},
)
# Safe → Neutral 或 Risk Judge
workflow.add_conditional_edges(
"Safe Analyst",
self.conditional_logic.should_continue_risk_analysis,
{
"Neutral Analyst": "Neutral Analyst",
"Risk Judge": "Risk Judge",
},
)
# Neutral → Risky 或 Risk Judge
workflow.add_conditional_edges(
"Neutral Analyst",
self.conditional_logic.should_continue_risk_analysis,
{
"Risky Analyst": "Risky Analyst",
"Risk Judge": "Risk Judge",
},
)Q21: 风险评估的完整流程是什么样的?(假设 max_risk_discuss_rounds=1)
A:
初始状态:count=0, latest_speaker=""
1. Risky Analyst 执行
- count=1, latest_speaker="Risky"
- 条件判断:count(1) < 3*1=3,且 speaker 是 "Risky"
- → 跳转到 Safe Analyst
2. Safe Analyst 执行
- count=2, latest_speaker="Safe"
- 条件判断:count(2) < 3,且 speaker 是 "Safe"
- → 跳转到 Neutral Analyst
3. Neutral Analyst 执行
- count=3, latest_speaker="Neutral"
- 条件判断:count(3) >= 3*1=3
- → 跳转到 Risk Judge
4. Risk Judge 执行
- 综合三方观点,做出最终交易决策
- 输出 final_trade_decision
- → ENDQ22: Risk Judge 如何结束整个流程?
A: Risk Judge 连接到 END 节点:
# 文件: tradingagents/graph/setup.py
workflow.add_edge("Risk Judge", END) # 流程结束Risk Judge 节点的返回值:
# 文件: tradingagents/agents/managers/risk_manager.py
def risk_manager_node(state) -> dict:
# ... 综合所有信息,调用 LLM ...
response = llm.invoke(prompt)
return {
"risk_debate_state": new_risk_debate_state,
"final_trade_decision": response.content, # 最终决策
}7. 完整流程图
Q23: 能否画出完整的流程图?
A:
START
│
▼
┌─────────────────────┐
│ Market Analyst │◄──────────────┐
└─────────────────────┘ │
│ │
┌───────────┴───────────┐ │
│ should_continue_market │ │
└───────────┬───────────┘ │
有 tool_calls │ 无 tool_calls │
▼ │ ▼ │
┌──────────────┐ │ ┌──────────────────┐ │
│ tools_market │────┘ │ Msg Clear Market │ │
└──────────────┘ └────────┬─────────┘ │
│ │
... 类似流程重复 ... ▼ │
Social → News → Fundamentals │
│ │
▼ │
┌─────────────────────┐ │
│ Bull Researcher │◄──────────┐ │
└─────────────────────┘ │ │
│ │ │
┌───────────┴───────────┐ │ │
│ should_continue_debate│ │ │
└───────────┬───────────┘ │ │
count < max │ count >= max │ │
& Bull spoke │ │ │
▼ │ │ │
┌──────────────┐ │ │ │
│Bear Researcher│───┤ │ │
└──────────────┘ │ │ │
│ │ │ │
count < max │ │ │
& Bear spoke ─────┴─────────────────────┘ │
│ │
▼ │
┌─────────────────────┐ │
│ Research Manager │ │
└─────────────────────┘ │
│ │
▼ │
┌─────────────────────┐ │
│ Trader │ │
└─────────────────────┘ │
│ │
▼ │
┌───────────────────────────────────────┐ │
│ Risk Analysis Loop │ │
│ ┌─────────────────────────────┐ │ │
│ │ Risky Analyst │◄────┼─────┼───────┐
│ └─────────────────────────────┘ │ │ │
│ │ │ │ │
│ ▼ │ │ │
│ ┌─────────────────────────────┐ │ │ │
│ │ Safe Analyst │ │ │ │
│ └─────────────────────────────┘ │ │ │
│ │ │ │ │
│ ▼ │ │ │
│ ┌─────────────────────────────┐ │ │ │
│ │ Neutral Analyst │─────┼─────┼───────┘
│ └─────────────────────────────┘ │ │
│ │ count >= max │ │
└─────────────────┼─────────────────────┘ │
▼ │
┌─────────────────────┐ │
│ Risk Judge │ │
└─────────────────────┘ │
│ │
▼ │
END │总结
Q24: 条件边的核心设计模式是什么?
A: TradingAgents 展示了 3 种条件边设计模式:
| 模式 | 应用场景 | 判断依据 | 示例 |
|---|---|---|---|
| 工具循环 | 分析师层 | last_message.tool_calls | 有工具调用 → 执行工具 → 回到分析师 |
| 二方辩论 | 投资辩论层 | current_response.startswith("Bull/Bear") + count | Bull/Bear 交替,直到轮数达标 |
| 三方轮换 | 风险评估层 | latest_speaker + count | Risky→Safe→Neutral 循环 |
Q25: 实现条件边的关键要点是什么?
A:
- 状态设计要包含判断依据:如
count、current_response、latest_speaker - 节点返回时更新关键字段:让条件函数能做出正确判断
- 条件函数返回字符串:必须与
add_conditional_edges的目标映射匹配 - 考虑初始状态:确保第一次进入时条件函数能正确路由
Q26: 条件边的代码如何组织?
A: 推荐的项目结构:
tradingagents/
├── graph/
│ ├── setup.py # 图的构建和边的连接
│ ├── conditional_logic.py # 所有条件函数集中管理
│ └── propagation.py # 初始状态创建
└── agents/
└── utils/
└── agent_states.py # 状态类型定义这种组织方式使条件逻辑集中、易于维护和测试。
作者: LearnGraph 基于项目: TradingAgents 版本: 1.0