Vibe-Trading 开源项目跑不起来:自检表、限额与数据源降级

2026-08-05

本文基于 Vibe-Trading 仓库 commit 3a752d5(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/HKUDS/Vibe-Trading 最新代码与文档为准。

**HKUDS 开源的这个 Vibe-Trading(不是「凭感觉交易」那类说法,是 GitHub 上的一个具体仓库)里,只有一件事会真正阻止它启动:LLM 提供方那一项检查没过。其余所有红字都不阻塞启动,它们只是在提前告诉你哪一块功能会缺。**把这条分清楚,你的排查时间大概能省掉一半——因为绝大多数人看到自检表上一排 FAIL 就开始逐个修,而其中大部分根本不影响你当下要做的事。

Vibe-Trading 是 HKUDS 放出的开源个人交易 Agent(MIT 许可证),本质是把金融研究工作流拆成技能、工具和多智能体团队的一套 Agent 工程。它的代码量不小:全仓 2030 个受版本控制的文件,其中 agent/ 1805 个、frontend/ 155 个,agent/src/ 下有 23 个模块目录。这种体量的项目,故障排查最怕的是没有顺序地乱翻。好在它自己给了顺序。

一、先把「跑不起来」拆成三层

工程上,「跑不起来」在这个仓库里对应三种完全不同的现象,修法也完全不同:

第一层是进程起不来。你敲了命令,终端打完一张表就退出了,或者交互模式直接返回。这一层由 agent/src/preflight.py 负责判定,它有明确的致命项定义。

第二层是进程起来了、模型答得不对。工具确实调用了,返回也确实回来了,但模型基于半截数据下了结论。这一层的根在 agent/src/config/limits.py 定的工具结果长度上限——单次工具结果超过上限会被截断。这不是崩溃,但表现出来的样子非常像「这个工具坏了」。

第三层是某一类数据取不到。典型是 A 股的资金流、龙虎榜、融资融券、北向数据,主源不通时会走备源。这一层由 agent/src/tools/tushare_fallbacks.py 承载,它决定了「取不到」到底是彻底失败,还是悄悄换了个来源。

按这个顺序查,是因为下层的结论依赖上层:进程都没起来时去研究数据源是浪费时间;而模型输出可疑时去 ping 数据源,也查不到截断这种问题。

站内已有几篇讲通用故障面的文章可以对照着看——Agent 失败模式的分类方法讲的是怎么把一次失败归到正确的类别里,MCP Server 启动失败的排查讲的是协议层握不上手的情况,API 超时与重试终止讲的是网络层的判定;本篇不重复这些通用方法,只讲 Vibe-Trading 这一个仓库把这些判定固化在了哪几个文件里、按什么顺序读。

下面这张表是三层的速查地图,路径都是仓库里的真实位置:

组成部分它负责什么对应仓库位置你什么时候会碰到它
启动前自检探测 LLM 提供方与各数据源,打印状态表,判定是否致命agent/src/preflight.py每次进交互模式、每次单次运行、API 服务启动时
自检调用点决定自检结果如何影响流程(阻塞返回还是仅提示)agent/cli/_legacy.pyagent/cli/main.pyagent/api_server.py命令直接退出、或欢迎横幅之后表格才刷出来时
免出网就绪探针只做配置校验、刻意不发出站请求的健康检查agent/src/api/system_routes.py用探针轮询服务健康状态时
工具结果限额定义单次工具结果上限并在截断处留下明确告知agent/src/config/limits.py模型说「数据只有这些」而你知道不止时
分页兜底把记录整条整条地装进一页,并在信封里说明如何续取agent/src/tools/_result_paging.pyagent/src/tools/load_skill_tool.py长文档、长财报序列读不全时
数据源降级主源不可用时改用另一家提供方,并保持返回结构兼容agent/src/tools/tushare_fallbacks.pyA 股资金流/龙虎榜/两融/北向取数异常时
配置样板所有环境变量的参考写法agent/.env.example自检提示某个变量没设时

二、第一层:启动前自检表怎么读

run_preflight() 是这一层的唯一入口。它按固定顺序跑七个检查函数:_check_llm_provider_check_okx_check_yfinance_check_tushare_check_akshare_check_ccxt_check_content_filter_threshold,每个返回一个 CheckResult。这个数据类的字段值得记住,因为终端上那张表就是它渲染出来的:

@dataclass(frozen=True)
class CheckResult:
    """Result of a single preflight check."""

    name: str
    status: str  # "ready", "error", "not_configured", "skipped"
    message: str
    impact: str  # what breaks if this fails
    critical: bool = False

四个状态对应终端上的四种显示:ready 是 OK,error 是 FAIL,not_configured 是 N/A,skipped 是 SKIP。而 critical 这个字段,在整个文件里只有 _check_llm_provider 会把它置为 True。判定语句写得很直白:any(r.critical and r.status != "ready" for r in results)。也就是说,OKX 不通、yfinance 装不上、Tushare 没配、akshare 缺失、ccxt 缺失,这些全都只是让对应功能不可用,不阻塞启动。

impact 字段是这张表最容易被忽略的一列。它写的是「这项失败会坏掉什么」,而且只在 errornot_configured 时才会拼进显示文本。比如 OKX 检查失败时 impact 是 crypto backtest unavailable,yfinance 缺失时是 US/HK equity backtest unavailable,akshare 缺失时是 A-share/forex fallback unavailable。你要做的判断很简单:这条 impact 描述的功能,是不是你这次要用的。不是,就跳过。

LLM 那一项的内部顺序比其它几项复杂,值得单独讲,因为它是唯一会拦住你的:先调 _ensure_dotenv() 加载 .env,然后调 reset_env_config()。这里有个仓库自己写在注释里的坑——环境配置的单例可能在 .env 被加载之前就被缓存了(注释点名的例子是 theme.py 在 import 期调用 _is_dark_terminal),所以必须重置一次让它从已填充的 os.environ 重建。接着读 LANGCHAIN_PROVIDERLANGCHAIN_MODEL_NAME,任一为空就直接判致命,提示写的是这两个变量没设在 .env 里。

两个变量都在的话,走 _sync_provider_env()provider_diagnostics(),把 base URL、超时、重试次数、代理这几项拼成一串诊断提示挂在消息尾巴上。这串东西在排查网络问题时很有用:代理那一项显示的是排序后的代理键名,没有代理时显示 none——如果你在公司网络下连不上,先看这里是不是 none。

再往后分两条路。提供方是 openai-codex 时走 OAuth 分支,调 get_openai_codex_login_status(),没拿到登录态就判致命,impact 直接写成要你执行的命令 vibe-trading provider login openai-codex。其余提供方要求 OPENAI_BASE_URLOPENAI_API_BASE 有值,然后做一次探活:把 base URL 末尾的 /v1 去掉再 requests.getallow_redirects=False。注释写明了这次请求的意图——只测 TCP 加 SSL 能不能通,不是测鉴权。所以这一项 OK 不代表你的密钥是对的,它只代表这个域名你能连上。密钥错误要到真正发起模型调用时才暴露,这是排查时最容易误判的一处。

自检失败时终端会打一行提示,指向 agent/.env.example 作为配置参考。这个文件确实是最该先打开的东西:各家提供方的 LANGCHAIN_PROVIDERLANGCHAIN_MODEL_NAME 组合、数据源的可选配置、各类开关,都以注释形式排在里面。

还有一处行为差异要知道:自检不是每条路径都同步跑的。agent/cli/main.py 里的 _start_preflight_async()run_preflight 丢进一个名为 vibe-preflight 的守护线程,并且吞掉所有异常——它的定位是欢迎横幅画完之后的预热,注释里明说了「旧路径会在任何 agent 调用前再跑一次」。而 agent/cli/_legacy.py 里的 cmd_runcmd_interactive 才是会真正拦人的:致命项没过,cmd_run 返回失败退出码,cmd_interactive 直接 return。另外 cmd_run 在 JSON 模式下会整段跳过自检。所以你用 JSON 模式跑出来的失败,和交互模式下的失败不是同一个现象,别拿一个的结论去解释另一个。

服务端还有一条独立路径:agent/api_server.py 启动时跑 _run_startup_preflight();而 agent/src/api/system_routes.py 里的 _provider_readiness() 是专门给探针用的,它复刻了 LLM 检查的配置校验部分,但刻意省掉了出站 ping——注释给的理由是就绪探针会被高频命中,绝不能卡在网络上或者产生模型开销。排查线上问题时,别把这两个的结论混着用。

三、第二层:限额造成的假性故障

agent/src/config/limits.py 是个很短的叶子模块,它的文件 docstring 把存在理由说清楚了:这个上限原本写在 agent 主循环里,又被当成裸字面量抄进了 swarm 的 worker,两处会漂移,于是抽成一个不依赖任何东西的叶子模块,让主循环、swarm worker 和各个工具都能读。

真正关键的是 truncate_tool_result() 的设计取向。它的 docstring 第一句就是判断:静默的截断和一个完整答案在模型看来无法区分,于是模型会把前缀当成全部。所以这里的处理不是简单切一刀,而是留一段告知:

_TRUNCATION_NOTICE = (
    "\n\n[TRUNCATED: {shown} of {total} characters delivered. The remainder was "
    "not sent. Do not treat this as the complete result — narrow the request, "
    "or call the tool again with its paging parameter if it has one.]"
)

注意这段文字是写给模型看的,它同时给了两条出路:缩小请求范围,或者用工具自带的分页参数再取一次。函数还处理了一个边角情况——如果给的上限小到连告知本身都放不下,它宁可只发告知,也不发一个无意义的碎片。

这一层往上还有两个配套件。agent/src/tools/_result_paging.pyfit_records() 解决的是 JSON 信封被拦腰切断的问题:字符级截断落在结构中间,模型收到的是坏掉的片段,而幸存的那几条记录会被当成完整答案。它的做法是整条整条地装页,并在信封里带一个 paging 块,说明总数、本次返回了多少、从哪里续。agent/src/tools/load_skill_tool.py 则是给技能文档用的:它的模块 docstring 里写着,88 个内置技能中有 31 个的文档超过这个上限,所以 load_skill 提供了 offset 参数让模型翻页;文件里还留了 _PAGE_CHARS 给信封的 JSON 键名腾空间,以及一个 _MIN_PAGE_CHARS 下限,防止转义符密集的文档退化成一次翻一个字符。

对你的意义:当模型给出的结论明显只覆盖了一部分数据,先别怀疑工具坏了,去看返回里有没有那段 TRUNCATED 告知或者 paging 块。有,就是限额;没有,才往数据源方向查。

四、第三层:数据源降级路径

A 股那几个资金面工具是这个仓库里降级逻辑最完整的地方。agent/src/tools/tushare_fallbacks.py 的模块 docstring 把定位写死了:公开的东方财富接口是免费的、也是这些工具的主源;当它不可用时,一个配好的 Tushare token 可以用另一家提供方、以兼容的信封结构把同一套研究流程恢复出来。

它对外暴露四个函数:fetch_fund_flowfetch_dragon_tigerfetch_northbound_flowfetch_margin_trading,分别对应资金流、龙虎榜、北向资金和融资融券。四个调用方是 agent/src/tools/ 下的 fund_flow_tool.pydragon_tiger_tool.pynorthbound_tool.pymargin_trading_tool.py

降级不是在一个地方触发的,而是三种情况:符号解析不出来(resolve_secid 返回 None)、主源请求抛异常、主源返回了空行。以资金流工具为例,符号解析失败时会直接尝试备源,成功就在结果上挂一条 warning,写明是解析失败后用了备源;请求异常时同理,warning 里会把原始异常一起带上。两边都失败时,返回里同时有 errorfallback_error 两个字段——排查时一定要把这两个都读了,只看 error 你会以为是主源的问题,而实际拦住你的可能是备源那句 token 没配。

备源不可用有它自己的异常类型 TushareFallbackUnavailable,整个文件里抛它的地方对应四种原因:token 是空值或占位符 your-tushare-tokenimport tushare 失败、日期串规范化不过(_compact_date() 要求去掉连字符后正好是八位数字,龙虎榜那条路会先过这一关)、符号不符合规范。最后一种最容易踩,因为 _ts_code() 的规范化规则是硬编码的:带后缀时只认 SH/SZ/BJ 且主体必须是 6 位数字;不带后缀时按首位数字推断,5/6/9 归 SH,0/2/3 归 SZ,4/8 归 BJ,其余直接抛异常。

还有两处细节会影响你对返回值的判读。一是时间窗:_date_window() 用的是 max(days * 3, 10) 个自然日,注释解释了原因——节假日和周末让自然日必须留出余量,才能凑够请求的交易日行数。二是单位换算:Tushare 的资金流金额字段是万元单位,而东方财富那条路输出的是元,所以代码里乘了 10000 来对齐;北向那边的沪股通、深股通与合计字段乘的是 100,返回信封里则直接写了 "unit": "10k CNY"。你如果绕过工具直接读这两条路的原始数据做对比,量纲会对不上。

顺带说清一件事:_check_tushare 这个自检项只看配置,不发请求。它检查 token 非空且不是占位符、tushare 包能不能导入,然后就返回 ready。所以自检表上 Tushare 显示 OK,不代表这个 token 有效或者你的账户有对应接口的权限——那要到真正调用时才知道。这是第一层和第三层之间最典型的断层。

五、边界与代价:这套设计明确不管什么

这套排查体系有清晰的取舍,看懂它放弃了什么,比记住它检查了什么更省时间。

自检不验证凭据的有效性。 前面说过,LLM 那一项只做 TCP 加 SSL 探活,数据源那几项大多只查包在不在、变量填没填。这是刻意的:真发一次鉴权请求就意味着每次启动都产生一次调用开销。代价就是「表上全绿但一跑就报鉴权失败」这种情况完全可能发生,而且自检帮不了你。

自检是快照,不是监控。 它只在启动时跑一次。运行到一半某个源挂了,表上不会有任何变化。运行期的降级得靠工具返回里的 warning 字段去发现。

降级只覆盖那四个 A 股资金面工具。 tushare_fallbacks.py 里就那四个函数,别指望别的工具也有同样的兜底路径。备源的字段映射也是逐个手写的,它保证信封兼容,不保证两家数据在口径上完全一致。

回测生成代码的执行边界要自己守。 仓库根目录 SECURITY.md 里写得很清楚:回测运行可能在本地执行生成的 Python 策略代码,应当把生成的策略当作你会先审阅的本地代码来对待。运行器会校验运行目录,并使用一个窄化的子进程环境——保留操作系统与 Python 基础项、代理与证书设置、允许的运行根目录配置,以及加载器需要的只读行情凭据;默认不透传 LLM 提供方密钥、API 服务的 bearer token、shell 工具开关、券商交易密钥,以及实盘与投顾类开关。但同一份文档也提醒:这个子进程仍然是能联网的,因为加载器要去取公开或用户授权的行情数据。所以不要在暴露敏感文件、带密钥的代理变量或你不愿意让本地代码访问的网络服务的环境里,运行不受信任的生成策略。

涉及实盘的部分风险性质完全不同。 agent/src/trading/connectors/ 下有 12 个连接器子目录(README 也自述 12 家券商)。一旦走到这一步,你面对的就不再是「跑不起来」这类可以重试的问题:凭据的暴露面变大了,下错的单不可撤销,而程序化交易的合规义务因司法辖区而异。仓库里那些默认关闭的开关是有道理的,别为了图省事全打开。SECURITY.md 还专门写了一条与代码无关的风险:这是一个开源金融研究工具,项目方不会要求你连接或用加密钱包签名来加入社区、领取空投或解锁功能,任何这样的提示都是诈骗;文档里点名了唯一的官方 Discord 地址,并让读者去 Discussions 看置顶的安全公告。这条和排查无关,但和你的资产安全有关。

因子库不是这个项目原创的。 讲到这里必须说清出处,因为它影响你能不能用。仓库根目录的 NOTICE 声明:qlib158 那部分是 Microsoft Qlib 的特征定义,走 Apache 2.0 许可;alpha101 来自 Kakushadze 2015 年的 101 Formulaic Alphas(arXiv:1601.00991);gtja191 来自国泰君安 2014 年的 191 短周期交易 alpha 因子研究报告;academic 那组对应 Fama-French 五因子、Carhart 动量与 Hou-Xue-Zhang q-factor。NOTICE 明确写了这些数学公式是作为事实性数学内容重新实现的,源论文与研报的正文、表格和图并未复制进仓库,各因子库子目录下另有各自的 LICENSE.md。本文不提供法律意见,能不能商用一律以许可证原文为准。也要说明:这些是公开公式的工程化重实现,历史表现不代表未来,本文只讨论工程实现,不评价任何因子或策略的效果。

六、上手与避坑清单

别把非致命项当成阻塞项。 会踩是因为终端上一片红字看着就像启动失败了。避法:只看 critical 那一项,也就是 LLM 提供方那行;其余每行末尾括号里的 impact 才是你要读的东西,对照你这次要跑的任务筛一遍。

别把 LLM 那项的 OK 当成密钥可用。 会踩是因为它显示的是 ready,人本能地就认为这条链路通了。避法:记住那次请求只是去掉 /v1 后的一次探活;密钥问题要在第一次真实模型调用时看错误。

别在 JSON 模式下排查启动问题。 会踩是因为 cmd_run 在 JSON 模式下整段跳过自检,你看不到任何诊断表。避法:排查时用交互模式或非 JSON 的单次运行,拿到那张表再说。

别忽略 _ensure_dotenv 之后那次重置。 会踩是因为你改了 .env 却发现读到的还是旧值,然后开始怀疑文件路径。避法:先确认你走的是会调 reset_env_config() 的自检路径;仓库注释已经点明了单例可能在 .env 加载前就被缓存。

别只读返回里的 error 字段。 会踩是因为降级路径失败时是两个字段并存的,fallback_error 才是备源那边的真实原因。避法:拿到失败返回先 grep 一遍 fallback_errorwarning

别把 A 股代码随手写。 会踩是因为备源的符号规范化是硬规则,后缀只认 SH/SZ/BJ,主体必须 6 位数字,不合规直接抛异常,而这个异常会被包成「备源不可用」,看起来像是 token 的问题。避法:不确定时按带后缀的完整写法给。

别跨路径对比数量级。 会踩是因为主源和备源的金额单位不同,代码里做了乘 10000 和乘 100 的对齐。避法:只信工具返回的信封,别拿两条路的原始数据直接比。

别在敏感环境里跑生成的策略代码。 会踩是因为回测子进程虽然被窄化过,但它仍然可以联网。避法:按 SECURITY.md 的说法,把生成的策略当作要先审阅的本地代码,并选一个你愿意让本地代码访问的环境。


把顺序再收一遍:进程起不来看 agent/src/preflight.py 那张表,只认致命那一项;模型答得不全看 agent/src/config/limits.py 这条线,找返回里的截断告知和 paging 块;某类数据取不到看 agent/src/tools/tushare_fallbacks.py,把 errorfallback_error 一起读。

接下来该读哪个文件,取决于你卡在哪一层:卡在配置就打开 agent/.env.example,它是所有环境变量的参考;卡在服务端健康检查就读 agent/src/api/system_routes.py 里那个免出网的就绪探针,弄清它和启动自检的差别;准备碰实盘相关能力,就先把 SECURITY.md 从头读一遍,再去看 agent/src/trading/connectors/ 下那 12 个连接器目录里对应的那一个。至于能不能把这套东西真的接到你的账户上,一律以你所在司法辖区的监管要求与券商协议为准。

推荐延伸阅读:Agent 自连线检查怎么做多模型 fallback 的设计取舍,这两篇讲的是通用做法,可以和本文里的具体实现对着看。

本篇属于一个把开源个人交易 Agent 项目 Vibe-Trading逐层拆开讲的系列,整体地图见 Vibe-Trading 是什么:HKUDS 这个开源交易 Agent 项目的工程全景与边界;沿着这条线往下,还可以看 开源交易 Agent Vibe-Trading 的四层安全边界拆解Vibe-Trading 开源交易 Agent:三个入口与第一次配置

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。