开源项目 Vibe-Trading:30 份 yaml 定义的多智能体团队编制

2026-08-05

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

这里说的 Vibe-Trading 不是「凭感觉交易」这类泛指,而是 HKUDS 放在 GitHub 上的那个同名开源项目(仓库地址见上方标注),本文只读它的代码与配置。

这个项目里最值得抄的不是多智能体调度器,而是它把「一个团队怎么编制」整个搬进了 yaml——角色身份、工具白名单、可加载的技能、迭代与超时上限、谁在谁之后跑、谁读谁的结论,全部是声明式字段,Python 侧只剩一个通用的 DAG 执行器。 换句话说,新增一个五人研究团队不需要动一行代码,只需要往 agent/src/swarm/presets/ 里放一个文件。这个抽象边界画在哪里,比它用了什么框架有意思得多。

先说本篇和站内几篇相邻文章的分工:任务分解粒度讨论的是一个任务该切多细,多 Agent 框架选型讨论的是该不该上多智能体、上哪家,工作流编排讲的是另一套体系里编排层的写法。本篇不重复这三件事,只做一件:把 Vibe-Trading 这一套编制文件的字段逐个摊开,看它把哪些决策固化成了配置、又把哪些留给了运行时。

一、这 30 份文件到底在解决什么问题

打开 agent/src/swarm/presets/,里面躺着 30 份 yaml,文件名从 quant_strategy_desk.yamlmacro_strategy_forum.yaml 一直到 sentiment_intelligence_team.yamltechnical_analysis_panel.yaml。每一份就是一个「团队」。

用 yaml 里的 max_iterations 键数一遍,30 份文件一共声明了 118 个 agent;用 prompt_template 键数一遍,一共 118 个 task。两个数字相等不是巧合——这套编制里目前是严格的一角色一任务,没有出现同一个 agent 承接两个 task 的写法。

问题的起点很朴素。一个金融研究流程里,有些活天然可以同时干(看全球宏观的和看政策的互不依赖),有些活必须等(写策略的得先拿到筛选结果和因子结果)。如果把这套先后关系写进 Python,每加一种团队就要改一次调度代码;写进配置,调度器就只需要认识「依赖」这一个概念。

agent/src/swarm/presets.py 里的 build_run_from_preset() 做的就是这个翻译:把 yaml 的 agents 段解析成 SwarmAgentSpec 列表,tasks 段解析成 SwarmTask 列表,然后交给 agent/src/swarm/runtime.py 里的 SwarmRuntime 去跑。运行时对「这是量化团队还是宏观论坛」一无所知,它只认拓扑层。

二、一份编制文件里写了哪些字段

顶层三个描述性键:nametitledescription。下面三大段:agentstasksvariables

agents 段里每个条目的键,可以在 agent/src/swarm/models.pySwarmAgentSpec 里找到完整定义:

  • id:角色唯一标识,task 靠 agent_id 引它。
  • role:一句话角色名,会被拼进系统提示的开头。
  • system_prompt:多行字符串,实际是这套设计里最重的部分——quant_strategy_desk.yamlfactor_miner 那一段就写满了任务、要交付的五类内容、该调哪个工具。
  • tools:工具白名单,是个字符串数组。
  • skills:允许加载的技能名数组,对应 agent/src/skills/ 下的技能目录。
  • max_iterationstimeout_secondsmax_retries:单个 worker 的 ReAct 循环上限、超时秒数、失败重试次数。
  • model_name:可选的模型覆盖,为空则走全局配置。

有意思的是这几个数值键的实际用法。SwarmAgentSpec 里写的默认值是 max_iterations=25timeout_seconds=300max_retries=2;但把 30 份 yaml 里这三个键全部 grep 出来去重,得到的是清一色的 5018001,118 个 agent 无一例外。也就是说这套编制统一把默认值推翻了一遍:允许跑得更久、循环更多,但只给一次重试。而 model_name 这个键,30 份文件里一次都没出现过——分层调模型的能力留在了模型层,编制层没用。

tasks 段的字段对应 SwarmTaskidagent_idprompt_templatedepends_oninput_fromvariables 段则声明这份编制需要用户填哪些变量,每项有 namedescriptionrequired

组成部分它负责什么对应仓库位置你什么时候会碰到它
编制 yaml声明角色、任务、依赖、变量agent/src/swarm/presets/*.yaml想新增或改造一个团队时
数据模型定义 yaml 能写哪些键、默认值是多少agent/src/swarm/models.py拿不准某个键是否存在时
加载与校验解析 yaml、查重复 id、查环、算拓扑层agent/src/swarm/presets.py自己写的 yaml 报错时
DAG 执行器按层调度、层内并行、层间串行agent/src/swarm/runtime.py想搞清楚并发与失败传播时
单个 worker拼系统提示、渲染用户提示、跑 ReAct 循环agent/src/swarm/worker.py想知道变量到底在哪一步被替换时
技能目录skills 字段引用的实际内容agent/src/skills/skills 数组时要对着目录名填

三、顺序与依赖:两个键各管一半

角色之间的关系,这套设计拆成了两个正交的键,这是它最值得记的一处。

depends_on执行顺序。它是一个 task id 数组,声明当前任务必须等这些任务完成。agent/src/swarm/task_store.py 里的 validate_dag() 先做 DFS 环检测,topological_layers() 再用 Kahn 算法把任务切成若干层。SwarmRuntime._execute_run() 按层推进:同一层丢进 ThreadPoolExecutor 并行跑,层与层之间严格串行。

input_from上下文流向。它是一个字典,键是上游产出在下游提示里的名字,值是上游 task id。看 quant_strategy_desk.yaml 的 tasks 段:

  - id: task-backtest
    agent_id: backtester
    prompt_template: "Build the strategy from screening and factor output and backtest."
    depends_on: [task-screen, task-factor]
    input_from:
      screener_result: task-screen
      factors: task-factor

两个键分开的好处,是「我要等你」和「我要读你」不再被迫绑定。你可以只等不读(纯粹的时序约束),理论上也可以只读不等——后者是个坑,presets.pyinspect_preset() 专门为它留了一条 warning:如果 input_from 指向的任务在 DAG 里并非上游,就报「which is not upstream in the DAG」。

上游内容是怎么进到下游提示里的?worker.pybuild_worker_prompt()input_from 收集到的每条摘要拼成 ### <键名> 小节,套上 ## Upstream Context (from previous agents) 标题,然后把整块字符串替换掉 system_prompt 里的 {upstream_context} 占位符。所以你会看到 macro_strategy_forum.yaml 里三个并行的经济学家角色都没有这个占位符,而汇总角色 chief_strategistsystem_prompt 里明明白白写着一行 {upstream_context}。占位符写不写、写在哪一段,直接决定了上游结论出现在系统提示的什么位置。

把 30 份编制横过来统计,形状高度收敛:118 个 task 里有 78 个 depends_on: [],剩下 40 个有依赖的 task 恰好对应 40 个 input_from 块。按拓扑层算,21 份是两层、8 份是三层、只有 equity_research_team.yaml 是四层。也就是说这套编制的主流范式是「N 个专家并行 → 1 个汇总角色收口」,三层的那几份多加了一道「先并行,再合成,再审查」。sentiment_intelligence_team.yaml 是最典型的两层:三个分析角色各跑各的,signal_synthesizer 在第二层把三份结论合起来。

四、失败怎么传播,这一层配置管不到

顺序声明在 yaml 里,但顺序坏掉之后怎么办,是运行时的事,读编制文件看不出来。

runtime.py_execute_layer() 在派发每个任务前会重新加载它的所有上游 task,只要有一个上游状态不是 completed,就把当前任务标成 TaskStatus.blocked、发一个 task_blocked 事件、直接跳过派发。代码注释里给的理由是具体的:investment_committee 这份编制里投资组合经理依赖风控任务,如果风控失败了还让下游跑,下游就会在完全没有风控输入的情况下产出一个「决定」。

失败与「没干活」也被区分开了。models.pyWorkerStatus 有五个值:completedfailedtimeouttoken_limitincomplete。文档字符串里特别强调 incomplete 不能被折进 completed——它指的是 worker 没抛异常但也没产出实质交付物,比如只写了个计划、编了假数字、工具标记没解析出来,或者一个数据角色一次工具都没调、报告也没写。

顺带一提,编制文件里那些「要求模型产出哪些字段」的写法,是配置对输出格式的约束,跟本文对读者的任何判断无关。比如 quant_strategy_desk.yamlbacktester 角色,system_prompt 里列了五项必交内容:策略逻辑、策略代码、回测指标、净值曲线评述、改进方向,末尾还硬性写了一句必须真跑而不许编数字。这里只讨论「配置怎么约束模型输出」这件工程事,历史表现不代表未来,本文不评价任何策略或因子的好坏。

五、边界与代价:这个设计放弃了什么

把组织写成配置,代价是实打实的。

放弃了动态编队。 拓扑层在 start_run 时就由 topological_layers() 一次算完,之后不会变。你没法写「如果第一层发现分歧就临时加一个仲裁角色」,也没法让某个角色自己决定要不要拉人。这套设计里所有的自由度都在单个 worker 的 ReAct 循环内部,团队结构本身是冻死的。需要运行时动态分叉的场景,这个抽象不适用。

放弃了循环。 validate_dag() 是显式的环检测,检出来直接抛 ValueError 并把环路径打进错误消息。所以「A 出结论 → B 挑刺 → A 修订」这种多轮往返写不出来。investment_committee.yaml 里多空双方的「辩论」,实际是多方并行各写一份,再由下游角色一次性读完两份,不是真的来回过招。

放弃了跨角色共享状态。 角色之间唯一的信息通道就是 input_from 拉过来的上游摘要文本,粒度是一整段字符串。想让两个角色共享一个结构化对象,配置层没有这个概念。

它明确不管的事: 编制文件不管资金、不管下单、不管账户。tools 里那些名字(bashread_filewrite_fileload_skillread_urlget_market_datafactor_analysisbacktest 等)决定的是这个角色能碰什么工具,跟券商连接是两套东西。真要往实盘方向接,代价必须自己算清楚:凭据一旦落到能被 agent 读到的地方,暴露面就等于整个工具链的暴露面;下错的单不像文件那样能回滚;程序化交易本身的合规义务因司法辖区而异。这些事情该不该做、能不能这么做,以你所在司法辖区的监管要求与券商协议为准,本文不提供任何这方面的意见。

关于因子库的来源,别写错。 仓库根目录的 NOTICE 写得很清楚:Microsoft Qlib 的特征定义按 Apache 2.0 引入,对应 agent/src/factors/zoo/qlib158/;另有几组公式分别来自 Kakushadze 的「101 Formulaic Alphas」、国泰君安 2014 年的 191 短周期因子研报,以及 Fama-French 五因子、Carhart 动量、Hou-Xue-Zhang q-factor 这些学术模型,仓库把公式当作数学事实重新实现,对应目录下各有 LICENSE.mdNOTICE 同时声明原文的正文、表格、图表没有被复制。所以描述这部分时不要说成「项目自研的因子库」——它是公开公式的工程化重实现。能不能商用,以许可证原文为准,本文不提供法律意见。

六、上手与避坑清单

第一件事:先跑校验,别直接跑。 presets.py 里有个 inspect_preset(),不启动 worker、不调模型,只做静态检查并返回拓扑层。它会查重复 agent id、重复 task id、task 指向不存在的 agent、input_from 指向不存在的 task、DAG 成环,还会对比 variables 声明与 prompt_template 里实际用到的变量,双向报「用了但没声明」和「声明了但没用」。为什么会踩:一个 yaml 拼写错误在运行时表现为某个角色拿不到上下文、静默产出低质量结果,而不是报错。怎么避:改完 yaml 先走这条静态路径,把 errors 和 warnings 清干净再跑。

第二件事:system_prompt 里的变量占位符不会被替换。 这个坑我是顺着代码路径核出来的。worker.py 里对 agent_spec.system_prompt 只做了一次 .replace("{upstream_context}", upstream_block);真正用 format_map 做变量渲染的是 task.prompt_template,渲染结果进的是用户消息。所以 macro_strategy_forum.yamlsystem_prompt 里那些 {market}{horizon},是以字面形式进到系统提示里的,靠模型从用户消息里去对应。为什么会踩:你照着现成编制文件抄,会以为系统提示里的占位符也会被填。怎么避:所有必须被精确替换的变量,写进 prompt_templatesystem_prompt 里的占位符当作给模型看的提示语,别当作模板引擎。

第三件事:变量缺失不会报错,会被塞进一句提示。 run_worker() 里定义了一个 _FallbackDict__missing__ 返回的是一句「让模型自己根据目标判断」的英文提示串。而 variables 段里的 required: true,在我读到的加载与执行路径(presets.pyworker.pyruntime.py)里没有任何地方做校验。为什么会踩:少传一个变量,整个 run 会照常跑完,只是所有产出都建立在模型的臆测上,事后翻 run.json 也看不出异常。怎么避:调用方自己按 list_presets() 返回的 variables 做一次必填校验,别指望配置里的 required 生效。

第四件事:tools 白名单里的名字不一定真的可用。 30 份编制里所有 118 个 agent 的 tools 都写了 bash,但 agent/src/tools/__init__.py_SHELL_TOOL_NAMESbashbackground_runcancel_background 归为 shell 工具,是否注册取决于调用链上传下来的 include_shell_toolsSwarmRuntime.start_run() 这个参数默认是 False。更要命的是 _filter_registry() 对拿不到的工具只打一条 warning 就丢掉,worker 照跑不误。为什么会踩:编制文件的 system_prompt 里明明写着「用 write_file 加 bash 跑脚本」,实际注册表里没有 bash,模型只能绕路,产出质量掉下来但没有任何报错。怎么避:确认入口传的 include_shell_tools,并把这条 warning 级日志纳入排查清单。

第五件事:改编制不用改包,也不该改包。 presets.py 里除了包内的 PRESETS_DIR,还有一个 USER_PRESETS_DIR,指向 ~/.vibe-trading/swarm/presets/,并且用户目录被排在搜索顺序的第一位——同名文件用户版覆盖内置版,list_presets() 返回的每条还带一个 source 字段标明是 user 还是 bundled。为什么会踩:直接改 site-packages 里的 yaml,下次升级包就被抹掉。怎么避:自己的编制一律放用户目录。另外 _validate_preset_name() 会拒绝空名、... 以及带路径分隔符的名字,所以别指望用名字做路径拼接。

收束:把这套抽象搬走时该问自己什么

如果你打算把「组织即配置」这个做法搬到自己的项目里,对着下面几条过一遍:

  1. 你的角色之间到底是时序依赖还是信息依赖?两者分成两个键,比合成一个 parent 字段清晰得多。
  2. 你的流程能不能被无环地表达?如果必须有往返修订,DAG 这层抽象会立刻不够用,得换别的模型。
  3. 配置里的每一个键,是真的被执行路径读了,还是只是给界面看的?required 这种「看起来会生效」的键最容易骗人。
  4. 工具白名单是「声明意图」还是「保证可用」?两者的差别,会在产出质量上而不是错误日志里体现。
  5. 单个角色的迭代上限、超时、重试次数,你打算写进编制还是留给全局?这套编制把前三个统一写死、把模型选择完全留给全局,是一个明确的取舍。

想继续往下读,顺序建议是:先 agent/src/swarm/models.py 把字段清单认全,再挑一份 presets/ 下的 yaml 对着看,然后 presets.py 看解析与校验,最后 runtime.py 看调度与失败传播、worker.py 看提示是怎么拼出来的。关于单个角色该配多少工具、白名单该收到多紧,可以接着看工具设计最小权限设计

本篇属于一个把开源个人交易 Agent 项目 Vibe-Trading逐层拆开讲的系列,整体地图见 Vibe-Trading 是什么:HKUDS 这个开源交易 Agent 项目的工程全景与边界;沿着这条线往下,还可以看 Vibe-Trading 仓库的多智能体运行时:任务怎么派,崩了怎么办技能文档该写多长:Vibe-Trading 88 份文档量出的长度预算

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