第 5 章 · 03 主类 AutoHedge 与三种输出格式 本节摘要:本节精读 (仅 52 行)——AutoHedge 对外暴露的主类。这个类是 CLI/脚本与底层 Agent 之间的「中间层」:它在 里创建输出目录与一个 swarms ,在 里把任务交给 ,再把结果写进 Conversation,最后按 参数(list/dict/str 三选一)从 Conversation 取出历史返回。本节逐行拆这个类,重点讲清三种输出格式的差异( / / )、异常处理策略(捕获-记日志-raise)、以及若干「声明了没用」的残留参数。读完本节,你理解 AutoHedge 的中间层抽象与它对下游消费的格式化支持。 内容来源:原项目源码 、 ,精读并套用体系化模板。
本节摘要:本节精读
autohedge/main.py(仅 52 行)——AutoHedge 对外暴露的主类。这个类是 CLI/脚本与底层 Agent 之间的「中间层」:它在__init__里创建输出目录与一个 swarmsConversation(time_enabled=True),在run(task)里把任务交给director_agent.run,再把结果写进 Conversation,最后按output_type参数(list/dict/str 三选一)从 Conversation 取出历史返回。本节逐行拆这个类,重点讲清三种输出格式的差异(return_messages_as_list/return_messages_as_dictionary/return_history_as_string)、异常处理策略(捕获-记日志-raise)、以及若干「声明了没用」的残留参数。读完本节,你理解 AutoHedge 的中间层抽象与它对下游消费的格式化支持。
内容来源:原项目源码
autohedge/main.py、autohedge/__init__.py,精读并套用体系化模板。
阅读完本节,你应当能够:
main.py 的 import 与 AutoHedge.__init__。output_dir、output_type、output_file_path 各自的实际作用(含未用的)。run(task) 的四步调用链。return_messages_as_list / _as_dictionary / _history_as_string 三种输出。先看 autohedge/__init__.py(7 行):
from autohedge.env_loader import load_env load_env() from autohedge.main import AutoHedge __all__ = ["AutoHedge"]
它做两件事:加载 .env(第 2 章讲过),再把 AutoHedge 类暴露为 autohedge.AutoHedge。所以外部代码只需 from autohedge import AutoHedge,就同时完成了「.env 加载 + 拿到主类」——这就是 example.py 与 cli.py 都用这一行的原因。
再看 main.py 顶部 import(第 1-6 行):
from pathlib import Path from loguru import logger from swarms import Conversation from autohedge.workers import director_agent
Path:跨平台路径,建输出目录用。logger:loguru 的日志器。Conversation:swarms 的消息历史容器(第 3 章第 4 节讲过)。director_agent:整个 main.py 唯一 import 的 Agent——五个 Agent 里只有 Director 被主类直接调用,其余四个通过 Director 的 handoffs 间接驱动。main.py 第 9-31 行:
class AutoHedge: """ Main trading system that coordinates all agents and manages the trading cycle. Tickers to analyze are derived from the task by the director (no predefined list). """ def __init__( self, name: str = "autohedge", description: str = "fully autonomous hedgefund", output_dir: str = "outputs", output_file_path: str = None, output_type: str = "list", ): self.name = name self.description = description self.output_type = output_type self.output_file_path = output_file_path self.output_dir = Path(output_dir) self.output_dir.mkdir(exist_ok=True) logger.info("Initializing Automated Trading System") self.conversation = Conversation(time_enabled=True)
逐项拆解:
| 参数 | 默认值 | 实际用途 |
|---|---|---|
name |
"autohedge" |
仅存为属性,未在任何逻辑里使用 |
description |
"fully autonomous hedgefund" |
仅存为属性,未使用(注意「fully autonomous」是营销语) |
output_dir |
"outputs" |
转 Path 并 mkdir(exist_ok=True) 建目录 |
output_file_path |
None |
仅存为属性,全程未写文件 |
output_type |
"list" |
决定 run 返回格式(list/dict/str) |
⚠️ 现实澄清:
name、description、output_file_path三个参数声明了但代码里没用——典型的「API 预留位」但没实现。output_dir建了目录但没人往里写(因为output_file_path没被用)。所以__init__里真正起作用的只有两件事:设self.output_type(影响返回格式)与创建self.conversation(消息历史)。
self.output_dir.mkdir(exist_ok=True) ... self.conversation = Conversation(time_enabled=True)
exist_ok=True 意味着目录已存在不报错。这个目录现在没用,但作者预留了「未来把每次交易周期结果写进 outputs/」的意图。time_enabled=True 给每条消息加时间戳(第 3 章第 4 节讲过)。这是 main.py 持有的唯一状态——消息历史。Tickers to analyze are derived from the task by the director (no predefined list).
这句点明:AutoHedge 不预设股票池。要分析哪只股票由 Director 从任务文本自行推断(对应 prompts.py 里的 DIRECTOR_TICKER_DISCOVERY_PROMPT 模板)。这是与传统量化系统(预先配 tickers=["NVDA","MSFT"])的根本区别——把「选股」也交给 LLM。
main.py 第 33-63 行的 run 方法,是整个类的核心:
def run(self, task: str, *args, **kwargs): """ Execute one complete trading cycle for all stocks. ... """ logger.info("Starting trading cycle") self.conversation.add(role="user", content=f"Task: {task}") try: output = director_agent.run(task=task) self.conversation.add(role="director", content=output) if self.output_type == "list": return self.conversation.return_messages_as_list() if self.output_type == "dict": return self.conversation.return_messages_as_dictionary() if self.output_type == "str": return self.conversation.return_history_as_string() return self.conversation.return_messages_as_list() except Exception as e: logger.error(f"Error in trading cycle: {str(e)}") raise
逐段解读:
logger.info("Starting trading cycle") self.conversation.add(role="user", content=f"Task: {task}")
loguru 打一条 INFO 日志(便于追溯),然后把任务以 role="user" 写进 Conversation。注意 content 是 f"Task: {task}"——加了 Task: 前缀。这意味着历史里存的不是裸任务,而是带前缀的。但下一步交给 Director 的 task=task 是不带前缀的原任务——前缀只出现在历史记录里。
output = director_agent.run(task=task)
全系统的心脏。这一行触发 swarms 内部的整套编排:Director 推理 → 可能 handoff 给四专家 → 专家各自推理(情绪会调 exa_search)→ 产出回交 → Director 综合。返回值 output 是 Director 的最终文本回复。这一步可能耗时几十秒到几分钟,期间会发生多次 LLM 调用。
self.conversation.add(role="director", content=output)
以 role="director" 存进 Conversation。注意只记两层:user(任务)与 director(最终回复)。四个专家的中间产出不进这个 Conversation——它们存在 swarms 内部的另一套历史里,AutoHedge 这层看不到也不存。
if self.output_type == "list": return self.conversation.return_messages_as_list() if self.output_type == "dict": return self.conversation.return_messages_as_dictionary() if self.output_type == "str": return self.conversation.return_history_as_string() return self.conversation.return_messages_as_list()
按构造时设的 output_type 选返回格式(详见下节)。最后一行是默认兜底——如果 output_type 是别的值(拼错或未实现),也返回 list。
except Exception as e: logger.error(f"Error in trading cycle: {str(e)}") raise
捕获所有异常,用 loguru 记 ERROR 日志,然后 raise(重新抛出)。这是一种「记了但不吞」的策略——日志留痕便于排查,但异常仍向上传播,让调用方(CLI 或脚本)决定怎么处理(比如 CLI 用红字打印)。
💡 核心心法:「捕获-记日志-raise」是库代码的推荐做法——库不应该吞掉异常(会让上层不知情),但应该先记录上下文(便于调试)。对比 cli.py 里
run_repl的except Exception as e: console.print(f"[red]Error: {e}[/]")——它捕获后不 raise,因为它是最外层(用户界面),不能再往上了,只能打印给用户。库 vs 应用的异常处理边界在此。
output_type 是这个类最有意义的参数。三种值的差异(基于 Conversation 的方法名语义):
output_type="list"(默认)return self.conversation.return_messages_as_list()
返回 list[dict]——每条消息一个 dict,装在列表里。最结构化、最适合程序再处理。典型形态:
[ {"role": "user", "content": "Task: Analyze NVDA ...", "timestamp": "2026-08-05T14:30:12"}, {"role": "director", "content": "...(Director 的最终回答)...", "timestamp": "2026-08-05T14:31:45"}, ]
下游可以 for msg in result: if msg["role"]=="director": print(msg["content"]) 这样取用。
output_type="dict"return self.conversation.return_messages_as_dictionary()
返回一个聚合 dict(具体聚合方式由 swarms 决定,方法名暗示「按某种键聚合」,如按 role 分组)。适合「按角色查看」的场景。形态可能是:
{ "user": ["Task: Analyze NVDA ..."], "director": ["...(回答)..."], }
output_type="str"return self.conversation.return_history_as_string()
返回一长串字符串——整个历史拼成的文本,最适合直接 print 给人看。形态可能是:
[user] Task: Analyze NVDA ... [director] ...(回答)...
| 格式 | 方法 | 返回类型 | 适合场景 |
|---|---|---|---|
list(默认) |
return_messages_as_list |
list[dict] |
程序再处理(过滤、提取、转存) |
dict |
return_messages_as_dictionary |
dict |
按角色/键聚合查看 |
str |
return_history_as_string |
str |
直接 print 给人 |
💡 核心心法:同一个 Conversation,三种取法服务于三种下游——这正是「数据与呈现分离」的体现。Conversation 是数据(消息历史),
return_*方法是呈现(怎么取出来)。output_type让调用方按需选呈现,而不必重写数据层。
综合来看,main.py 有几处「半成品」痕迹:
output_file_path 全程未用:声明、存属性,但没写文件。本意大概是「把结果写到这个文件」,没实现。output_dir 建了没用:目录创建了,但 run 没往里写。与 output_file_path 一起,是「预留的输出持久化」未完成。name/description 仅存不用:可能想用于日志或元数据,没接上。run 的 docstring 写 for all stocks,但实际 Director 决定股票,docstring 措辞滞后。这些不影响的 run 的核心功能(任务 → Director → 返回),但能看出 AutoHedge 的中间层是「最小可用(MVP) + 一堆未实现的预留位」。生产化时,output_file_path 应该真正写文件(每次 run 存一份 JSON),name/description 应进日志上下文。
把本节与上一章串起来,一次完整的 CLI 任务执行:
用户敲 autohedge → cli.main() → run_repl() → _welcome()(展示历史) │ 用户输入任务 ▼ _append_recent(task)(写历史) │ from autohedge import AutoHedge system = AutoHedge() ← 本节 __init__ result = system.run(task=task) ← 本节 run │ Panel(str(result)[:2000]) ← 截断打印
cli.py 是门面(交互/展示),main.py 是核心(调度 Agent/管理历史)。两者通过 AutoHedge 类解耦——CLI 可以换成 Web/API/脚本,只要调 AutoHedge().run(task) 就能复用全部 Agent 能力。
from autohedge import AutoHedge 同时完成 .env 加载与主类导入;main.py 只 import 了 director_agent(其余四专家由 handoffs 间接驱动)。name/description/output_file_path 声明未用(残留);output_dir 建目录但没人写;真正起作用的是 output_type + 创建 Conversation(time_enabled=True)。Task: 前缀)→ director_agent.run(task)(心脏)→ add(director)→ 按 output_type 返回;用户任务前缀只进历史不进 Director。list[dict],程序处理,默认)、dict(按键聚合)、str(长字符串,给人读);同一数据三种呈现,「数据与呈现分离」。下一章,我们钻进 Agent 调用的「工具」——从 exa_search 开始,看 LLM 工具调用体系怎么工作。