Vibe-Trading 假设注册表:开源交易 Agent 怎样拦住越研究越自信
本文基于 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这个元组锁死为exploring、testing、validated、rejected、monitoring五个,默认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=False、indent=2、sort_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 分,等价于列全部。limit 被 max(1, min(int(limit), 100)) 夹住,既防 0 也防超大值。没有向量、没有嵌入模型——对一个个人级的假设本子来说,这个复杂度是对的。
三、四个工具,和它们的错误契约
注册表本身不认识模型。把它接到 Agent 上的是 agent/src/tools/hypothesis_tool.py,里面四个 BaseTool 子类:
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
HypothesisRegistry / Hypothesis | 纯代码的存取层:建、改、挂产物、检索,落地为本地 JSON | agent/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 灌成研究目标的 objective | agent/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.py 的 HYPOTHESIS_STATUSES 一致;UpdateHypothesisTool 更进一步,parameters 上标了 "additionalProperties": False,execute 里还有一份 allowed_fields 白名单,先 pop 掉 hypothesis_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.py。list 支持 --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_rigor 与 cross_validate 验的是数字(数据层),report_audit 验的是最终报告(输出层),而这份技能验的是推理过程(思考层)。这个三分法本身就是一份可迁移的资产——你在别的领域搭 Agent 时,同样可以问自己:数据层、输出层、思考层,我各有什么在把关。
把两块拼起来看就明白了:注册表提供了「原始论点」这个不可篡改的锚,研究纪律技能提供了「别只往一个方向查」的动作矫正。少了前者,反证据无处可挂;少了后者,你会给一条假设堆满同向证据然后把它标成 validated。
五、边界与代价:它明确不管的事
这个设计放弃的东西相当多,而且是有意放弃的。
它不做并发保护。 create、update、link_backtest 的写法都是先 list() 读全量、改内存对象、再 _save() 写全量。原子替换保证了不会写出半个文件,但两个进程同时改,后写的会覆盖先写的。单人单机没问题,多进程共享一份 hypotheses.json 就要自己加锁。
它不做真正的语义检索。 前面说的 token 交集打分,遇到同义不同词就搜不到。假设标题起得随意,三个月后你大概率搜不着自己写过的那条。
它不校验假设内容。 status 有 enum 兜着,但 thesis 与 signal_definition 都是自由文本,注册表不会检查一条假设是否可证伪、invalidation_notes 是否真的写了作废条件。create 时 invalidation_notes 甚至有默认空串——也就是说,你完全可以登记一条永远无法被证伪的假设,代码不会拦你。这块的纪律得靠提示词与技能补,工具本身不承担。
它不管状态迁移的合法性。 五个状态是个集合,不是状态机。从 exploring 直接跳 validated 是允许的,中间那步 testing 可以整个跳过。CLI 的 invalidate 也只是一个便捷入口,不是唯一通路。
它更不碰钱。 注册表模块 docstring 明写不依赖实盘交易服务,create_hypothesis 的工具描述也重申了 research-only。这条边界要认真对待:仓库里另有 agent/src/trading/connectors/ 下 12 家券商与交易所连接器子目录(README 亦自述 12 brokers),一旦走到那一侧,代价的性质就变了——凭据一旦落到本地配置或环境变量里就多了一个暴露面,下错的单不可撤销,程序化交易还有申报与合规义务,这些义务因司法辖区而异。假设注册表是纯本地 JSON,这两类风险它一个都不承担,也正因如此,别把它的安全性直觉套到连接器那一侧去。
关于回测与因子这一侧还有一句必须说清:link_backtest 的 metrics 参数在代码里就是个原样透传的 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。查询词与记录用词不重合就是零分,零分不进结果。避法是登记时把关键词直接写进 title 或 thesis,别指望检索器替你联想。
第四,update 传 None 等于不改,传空串会报错。 工具层过滤掉了值为 None 的键,而 registry.update 对 title 与 thesis 的处理是:传了但 strip 后为空,直接抛 ValueError。想清空一个字段和想跳过一个字段,在这套 API 里是两件事。踩坑通常发生在模型自动拼参数时把空串当成”不填”。
第五,link_backtest 至少要给一个路径。 run_card_path 与 backtest_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.py 与 agent/src/skills/strategy-dev-manager/SKILL.md——后者的 Phase 3 把 create_hypothesis、generate_backtest_config、scaffold_signal_engine、backtest、link_autopilot_backtest 串成了一条完整链路,你能看清一条假设是怎么一路挂到产物上的。仓库 agent/SKILL.md 里项目自己描述了引擎数、alpha 数与数据源数量,作为全貌参考可以看,但那是项目自述,不是本文实测的结论。
搬这套设计到你自己的 Agent 上时,有四个问题值得先自问一遍:我的假设对象有没有一个专门写作废理由的字段;状态集合是不是被代码锁死而不是靠提示词自觉;工具的错误有没有被包成模型能解析的结构;以及最容易被跳过的那条——研究开始之前,有没有一份东西先被读进去矫正检索方向。至于这套东西能不能用在你自己的实际场景里,涉及资金与交易的部分,一律以你所在司法辖区的监管要求与券商协议为准。
本篇属于一个把开源个人交易 Agent 项目 Vibe-Trading逐层拆开讲的系列,整体地图见 Vibe-Trading 是什么:HKUDS 这个开源交易 Agent 项目的工程全景与边界;沿着这条线往下,还可以看 Vibe-Trading 的目标账本:开源交易 Agent 如何防跑偏 和 Vibe-Trading 仓库的多智能体运行时:任务怎么派,崩了怎么办。