Vibe-Trading 假设注册表:开源交易 Agent 怎样拦住越研究越自信

2026-08-05

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

假设注册表真正的价值不是「把想法记下来」,而是它逼你在动手查资料之前,先把这条假设写成一个日后能被判定为 rejected 的对象。 一旦这个对象存在,“我当初到底想验证什么”就不再由对话记忆决定,它变成磁盘上一条带 id、带状态、带作废备注的 JSON。Vibe-Trading(HKUDS 放出的开源个人交易 Agent,MIT 许可,LICENSE 里写的是 Copyright (c) 2026 Vibe-Trading Contributors)把这件事做成了三层:一个纯代码的注册表模块、四个交给模型调用的工具、外加一份在研究开始前先读的偏差自查技能。这三层拼起来,拦的是同一种病:越研究越自信。

本文只讨论这套 Agent 工程怎么实现,不讨论怎么投资,也不评价任何标的与策略。

一、它想拦的是哪种失效

做研究的 Agent 有一个很隐蔽的失效模式:它不会明着说谎,但它会在几十轮工具调用之后,把最初那个「我猜 A 可能导致 B」悄悄换成「已经确认 A 导致 B」。中间没有任何一步是错的,错的是那句原始假设从来没有被写在一个固定的地方,于是没人能回头核对措辞漂移了多少。

这跟站内几篇讲验证的文章处理的是不同层面。Agent 的对手验证怎么做 讲的是让另一个角色去挑当前结论的毛病,是结论层的对抗;完成前验证 讲的是交付之前必须跑一遍证据,是收尾层的门禁;输出约束怎么落地 讲的是让模型的输出格式守住契约,是格式层的约束。而 Vibe-Trading 这套假设注册表管的是起点层:你还没开始查之前,先把待验证的东西固化下来,后面所有产物都挂在它上面。三者不冲突,是同一条链上的不同卡点。

顺着这个思路看,注册表的设计目标就很清楚了:它不需要聪明,它需要稳。仓库里 agent/src/hypotheses/registry.py 的模块 docstring 自己写得很直白,说这个注册表刻意做得很小,只有本地 JSON 存储、确定性读取,不依赖 LLM,也不依赖任何实盘交易服务。这句设计声明是后面所有取舍的源头。

二、一条假设被存成了什么

打开 agent/src/hypotheses/registry.py,核心是一个 Hypothesis 数据类。它的字段清单值得逐个看,因为字段选择本身就是这套方法论的骨架:

  • hypothesis_id:稳定标识符,生成规则是把 title 转小写加上创建时间戳拼成 seed,取 SHA-256 前 12 位,前缀 hyp_。撞了就在后面追加 _2_3
  • title / thesis:短标题与研究论点,这两个是 create 时唯一必填的,空白直接抛 ValueError
  • status:生命周期状态,取值被 HYPOTHESIS_STATUSES 这个元组锁死为 exploringtestingvalidatedrejectedmonitoring 五个,默认 exploring
  • universe / signal_definition:目标范围与信号逻辑,都是纯文本。
  • data_sources / skills:字符串列表,记这条假设预期用到的数据源与相关技能。
  • run_cards:字典列表,用来挂回测产物。
  • invalidation_notes:作废说明。这个字段的存在,是整个设计里最关键的一处——它给”这条假设死了”预留了正式的书写位置。
  • created_at / updated_at:UTC 时间戳,格式由 _utc_now() 统一成去掉微秒、尾部为 Z 的 ISO 串。

存储上,HypothesisRegistry.__init__ 默认路径来自 default_hypotheses_path():先读环境变量 VIBE_TRADING_HYPOTHESES_PATH(经由 get_env_config().paths 拿到),没配就落在 ~/.vibe-trading/hypotheses.json。写盘走的是 _save,先按 created_at 排序,json.dumps 时带 ensure_ascii=Falseindent=2sort_keys=True,写到同名加 .tmp 的临时文件,再 tmp_path.replace(self.path) 原子替换。这几个参数凑在一起有个很实际的好处:这份 JSON 是人可读、diff 友好的,你可以直接把它扔进 git 看两次研究之间改了什么。想更系统地理解这种「让中间产物落盘」的取向,可以对照 Agent 中间产物落盘怎么设计 一起读。

读取侧同样保守。list() 在文件不存在时返回空列表,JSON 解析失败会抛出带路径的 ValueError,顶层不是 list 也直接报错。Hypothesis.from_dict 里有一处兼容处理:run_cards 取不到时会回落读 backtests 键——这说明字段被改过名,旧数据仍能读。

search() 的实现比想象中土,也比想象中够用:它把每条记录整个 json.dumps 成一个字符串当作检索面,用 _TOKEN_RE(正则是两个及以上的字母数字,或单个 CJK 汉字)切词,算查询词与检索面的 token 交集大小当分数,再按分数、updated_at 倒序排。没给查询词时每条记 1 分,等价于列全部。limitmax(1, min(int(limit), 100)) 夹住,既防 0 也防超大值。没有向量、没有嵌入模型——对一个个人级的假设本子来说,这个复杂度是对的。

三、四个工具,和它们的错误契约

注册表本身不认识模型。把它接到 Agent 上的是 agent/src/tools/hypothesis_tool.py,里面四个 BaseTool 子类:

组成部分它负责什么对应仓库位置你什么时候会碰到它
HypothesisRegistry / Hypothesis纯代码的存取层:建、改、挂产物、检索,落地为本地 JSONagent/src/hypotheses/registry.py想改字段、改存储路径、写单测时
create_hypothesis / update_hypothesis / link_backtest / search_hypotheses把注册表包成模型可调用的四个工具,统一返回 JSON 字符串agent/src/tools/hypothesis_tool.py想让模型自己登记与检索假设时
vibe-trading hypothesis list / show / invalidate人用的命令行入口,带 Rich 表格与详情面板agent/src/hypotheses/cli_handlers.py你自己想翻本子、手动作废一条假设时
research-discipline 技能研究开始前的偏差自查表,只改思考方式,不产生数据agent/src/skills/research-discipline/SKILL.md派研究任务、写系统提示词时
run_research_autopilot从一条已存假设起手,把 thesis 灌成研究目标的 objectiveagent/src/tools/autopilot_tool.py想把假设接进后续回测流水线时

工具层有三处值得抄走的做法。

第一,错误不抛给模型,包成结构化结果。 文件顶部两个小函数,_ok(payload) 返回 {"status": "ok", ...}_error(exc) 返回 {"status": "error", "error": str(exc)},四个工具的 execute 全是 try/except Exception 包住再走这两个出口。模型拿到的永远是能解析的 JSON,而不是一个把整轮调用炸掉的异常栈。

第二,参数 schema 与后端校验双保险。 四个工具的 parameters 里,凡涉及状态的地方都写死了同一份 enum,与 registry.pyHYPOTHESIS_STATUSES 一致;UpdateHypothesisTool 更进一步,parameters 上标了 "additionalProperties": Falseexecute 里还有一份 allowed_fields 白名单,先 pophypothesis_id,再把不在白名单里的、值为 None 的键统统过滤掉才转交给 registry.update。schema 拦一层,代码再拦一层。

第三,工具描述里写清能力边界。 CreateHypothesisTool.description 的原文后半句是 “Research-only: does not place trades or call live trading APIs.”——这句话是写给模型看的。它不只是免责声明,它同时是在压缩模型对这个工具的想象空间,防止模型把「登记假设」误当成「执行动作」。

SearchHypothesesTool 是四个里唯一没有把 is_readonly 设为 False 的,另外三个都显式写了 is_readonly = False,四个都标了 repeatable = True。这个差别对上层的权限与审批逻辑是有意义的信号。

人这一侧走 agent/src/hypotheses/cli_handlers.pylist 支持 --status 过滤、--limit(默认 50,传 0 表示不设上限)、--json 直出数组;show 打一个 Rich 面板,把 thesis、signal、作废备注和已挂的 run card 逐条列出来;invalidate 则是把 status 直接置成 rejected 并写入 --note。注意它对输出宽度做了处理:sys.stdout.isatty() 为真才用交互式 console,否则新建一个 Console(width=200, force_terminal=False),为的是管道或捕获输出时每行不被折断。这种细节决定了你能不能把 CLI 输出直接喂给别的脚本。

四、研究纪律技能读的是另一层

如果说注册表约束的是产物,那 agent/src/skills/research-discipline/SKILL.md 约束的是动作。这份技能很短,核心是一张五行表,列的是它认定会系统性扭曲 AI 研究的五种偏差,以及各自的纠正动作:

  • leader-bias:搜索结果被大市值主体主导,最后只分析了那几个最容易被搜到的名字。纠正方式是主动往小盘、中盘、供应链方向补查询词,并追问「本该在名单里、却没进前十的是谁」。
  • English-bias:英文语料对日韩台与欧洲主体覆盖不足,导致直接漏掉。技能要求对任何硬件或供应链论点,用当地语言显式检索 JP/KR/TW 市场。
  • narrative-bias:被概念标签牵着走,分析的是营销话术而不是生意本身。它的原话是,一个被打上 AI 标签的公司可能根本没有 AI 收入。
  • confirmation-bias:论点一旦成型,就只搜支持它的证据。纠正动作写得很具体——做一次芒格式反向思考,每个正面论点都去搜一遍反面,并且每个结论至少引用一个反证据。
  • recency-bias:因为某个旧数字在搜索里排得高就直接用了。要求是任何关键数字都要核日期,优先近 30 天,超过一年的标注为可能过期。

它的应用姿势也写死了顺序:先读表,再用一句话写下论点,然后对每种偏差自问一遍「我是不是正要掉进去」,接着有意识地扩展查询计划,最后在写结论之前回头复检——有没有引用反证据、有没有漏掉非英语来源、有没有哪个关键数字已经陈旧。

技能文件末尾还标了它在体系中的位置:financial_rigorcross_validate 验的是数字(数据层),report_audit 验的是最终报告(输出层),而这份技能验的是推理过程(思考层)。这个三分法本身就是一份可迁移的资产——你在别的领域搭 Agent 时,同样可以问自己:数据层、输出层、思考层,我各有什么在把关。

把两块拼起来看就明白了:注册表提供了「原始论点」这个不可篡改的锚,研究纪律技能提供了「别只往一个方向查」的动作矫正。少了前者,反证据无处可挂;少了后者,你会给一条假设堆满同向证据然后把它标成 validated

五、边界与代价:它明确不管的事

这个设计放弃的东西相当多,而且是有意放弃的。

它不做并发保护。 createupdatelink_backtest 的写法都是先 list() 读全量、改内存对象、再 _save() 写全量。原子替换保证了不会写出半个文件,但两个进程同时改,后写的会覆盖先写的。单人单机没问题,多进程共享一份 hypotheses.json 就要自己加锁。

它不做真正的语义检索。 前面说的 token 交集打分,遇到同义不同词就搜不到。假设标题起得随意,三个月后你大概率搜不着自己写过的那条。

它不校验假设内容。 status 有 enum 兜着,但 thesissignal_definition 都是自由文本,注册表不会检查一条假设是否可证伪、invalidation_notes 是否真的写了作废条件。createinvalidation_notes 甚至有默认空串——也就是说,你完全可以登记一条永远无法被证伪的假设,代码不会拦你。这块的纪律得靠提示词与技能补,工具本身不承担。

它不管状态迁移的合法性。 五个状态是个集合,不是状态机。从 exploring 直接跳 validated 是允许的,中间那步 testing 可以整个跳过。CLI 的 invalidate 也只是一个便捷入口,不是唯一通路。

它更不碰钱。 注册表模块 docstring 明写不依赖实盘交易服务,create_hypothesis 的工具描述也重申了 research-only。这条边界要认真对待:仓库里另有 agent/src/trading/connectors/ 下 12 家券商与交易所连接器子目录(README 亦自述 12 brokers),一旦走到那一侧,代价的性质就变了——凭据一旦落到本地配置或环境变量里就多了一个暴露面,下错的单不可撤销,程序化交易还有申报与合规义务,这些义务因司法辖区而异。假设注册表是纯本地 JSON,这两类风险它一个都不承担,也正因如此,别把它的安全性直觉套到连接器那一侧去。

关于回测与因子这一侧还有一句必须说清:link_backtestmetrics 参数在代码里就是个原样透传的 dict,注册表不解读、不比较、不排名。历史表现不代表未来,本文只讨论工程实现。 同样地,仓库 agent/src/factors/ 下那 482 个文件构成的因子库,按根目录 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。所以它不是「项目自研的因子库」,能不能商用请以许可证原文为准,本文不提供法律意见。

六、上手与避坑清单

第一,路径先定,不然测试会污染你自己的本子。 默认路径是 ~/.vibe-trading/hypotheses.json,跑测试或试玩时很容易把实验数据混进真实记录。避法是用 VIBE_TRADING_HYPOTHESES_PATH 环境变量指到临时目录,或直接给 HypothesisRegistry(path=...) 传参;CLI 侧则用 --path。踩这个坑的原因是默认值太顺手,顺手到你不会想起来它是全局的。

第二,别用标题当主键。 hypothesis_id 的 seed 包含创建时间戳,同一个标题在不同时刻创建会得到不同 id,而重复标题在同一秒内创建才走 _2 后缀分支。这意味着你无法靠标题去重,只能靠 id。会踩是因为标题看着像自然主键,避法是在建之前先 search_hypotheses 查一遍。

第三,别把 search 当模糊匹配用。 分词规则是两个及以上的字母数字或单个汉字,纯符号、单个英文字母都进不了 token。查询词与记录用词不重合就是零分,零分不进结果。避法是登记时把关键词直接写进 titlethesis,别指望检索器替你联想。

第四,updateNone 等于不改,传空串会报错。 工具层过滤掉了值为 None 的键,而 registry.updatetitlethesis 的处理是:传了但 strip 后为空,直接抛 ValueError。想清空一个字段和想跳过一个字段,在这套 API 里是两件事。踩坑通常发生在模型自动拼参数时把空串当成”不填”。

第五,link_backtest 至少要给一个路径。 run_card_pathbacktest_run_dir 全空会抛 ValueError,只传 metrics 是不行的。这个约束是刻意的:注册表要的是能回溯的产物位置,不是一堆无处可查的数字。

第六,作废要写理由,别只改状态。 CLI 的 invalidate--note 参数,但不传也能执行,invalidation_notes 就保持原样。一条只有 rejected 没有理由的假设,三个月后跟没记过差不多。会偷懒是因为改状态一秒就完,写理由要想;避法是把「作废必须带 note」写进你的流程约定,代码不会替你把关。

第七,别指望它自己被读到。 research-discipline 只是 agent/src/skills/ 下 88 个技能目录(共 404 个文件)中的一个,什么时候加载取决于你的编排。它自己在文档里也强调要在研究任务的开始就读。放到中途读,前面那些已经跑偏的检索是补不回来的。

接下来读哪几个文件

按依赖顺序推荐:先 agent/src/hypotheses/registry.py 把数据模型吃透,再 agent/src/tools/hypothesis_tool.py 看工具层怎么包,然后 agent/src/hypotheses/cli_handlers.py 看人机两侧怎么共用同一份存储,最后 agent/src/tools/autopilot_tool.pyagent/src/skills/strategy-dev-manager/SKILL.md——后者的 Phase 3 把 create_hypothesisgenerate_backtest_configscaffold_signal_enginebacktestlink_autopilot_backtest 串成了一条完整链路,你能看清一条假设是怎么一路挂到产物上的。仓库 agent/SKILL.md 里项目自己描述了引擎数、alpha 数与数据源数量,作为全貌参考可以看,但那是项目自述,不是本文实测的结论。

搬这套设计到你自己的 Agent 上时,有四个问题值得先自问一遍:我的假设对象有没有一个专门写作废理由的字段;状态集合是不是被代码锁死而不是靠提示词自觉;工具的错误有没有被包成模型能解析的结构;以及最容易被跳过的那条——研究开始之前,有没有一份东西先被读进去矫正检索方向。至于这套东西能不能用在你自己的实际场景里,涉及资金与交易的部分,一律以你所在司法辖区的监管要求与券商协议为准。

本篇属于一个把开源个人交易 Agent 项目 Vibe-Trading逐层拆开讲的系列,整体地图见 Vibe-Trading 是什么:HKUDS 这个开源交易 Agent 项目的工程全景与边界;沿着这条线往下,还可以看 Vibe-Trading 的目标账本:开源交易 Agent 如何防跑偏Vibe-Trading 仓库的多智能体运行时:任务怎么派,崩了怎么办

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