Skip to content

当两个 Agent 开始直接对话:有界多空协作的 Runtime 设计 ​

多 Agent 投研协作 · 第二篇|为什么需要反方,以及如何让讨论真正发生

投资者往往不缺看好某个行业的理由。行情上涨时,研究报告、社交媒体和产品介绍都可以解释它为什么“长期有空间”。即使让 AI 再分析一遍,它也可能总结出更多支持材料。

但决定是否调整组合时,有时候更值得问的是:我最相信的那条理由,有没有可能是错的?谁能认真检查,而不是顺着我说?

这让我们想给 ETF Agent 加入多空研究。不过这里的“多空”不是让 AI 真正做多、做空或自动交易,而是让一位研究者先提出支持理由,另一位专门检查可以推翻它的证据。

真正困难的部分,随后才出现:如果双方要持续问答,应用必须知道谁说过什么、谁收到过什么;即使模型调用中断、服务器重启或某一方退出,也不能凭空补出一场并不存在的辩论。

这篇先讲为什么投资者需要这样的讨论,再进入我们做过的协议设计和踩过的恢复问题。

一、一份“正反观点俱全”的报告,可能仍没有回答问题 ​

沿用一个具体场景:用户准备从原有组合里拿出总资产的 10 个百分点,配置一只科技 ETF。他的理由是:“多持有一只基金,组合应该更分散。”

普通研究可以列出两组信息:支持方讨论行业成长与产品特点,反对方提醒估值、波动和集中度。这样当然比只谈利好全面,但仍可能错过最关键的事实——新买的基金和原来持有的基金,也许有大面积重叠。

如果没有人要求支持方正面回答“基金数量增加为什么就等于风险更分散”,两段意见只是在各讲各的。

我们想增加的是有针对性的反证:针对一个已提出的前提,找出足以改变这个前提的资料,再要求原论证者回应。

对投资者而言,这种能力可能有三种实际用途:在买入前检验最有把握的理由;在定期复盘时挑战几个月前的原假设;在多人参与研究时保留分歧,而不是只留最终那句“综合考虑”。

这些都是研究质量的目标,不等于我们已经证明多空 Agent 能提高收益或减少损失。

二、真正的讨论,与“让两个模型各写一份”有什么区别 ​

我们比较过三种方式。

方式投资者会看到什么适合何时使用
并列研究一份支持意见,一份反对意见只是希望更全面地了解问题
主代理转述主代理读完双方意见,再分别要求补充需要少量追问,系统尚无直接通信机制
同伴直接对话反方引用正方的某条真实观点,正方收到后继续回应原假设很重要,值得围绕具体证据持续质询

这里的“直接对话”并不是两个模型私下连线。应用程序仍然负责转交消息,但不会由主代理逐轮改写双方原话。这样可以减少转述误差,也避免每一轮都额外调用 Main 充当传话人。

不过这不意味着第三种永远更好。一个简单的费用比较,可能一位 Agent 就够了。多空对话要消耗时间、模型调用和研究预算;如果找不到实质争议,继续辩论只会制造噪音。

所以我们没有采用“投资问题一律开多空会”的产品设计。是否启动,由主代理根据问题决定,也尊重用户明确要求或排除;是否继续,要看有没有新增价值。

三、反方应该敢于承认:我没有找到有力反证 ​

把 Bull 理解为支持论证的职责,把 Bear 理解为寻找反证的职责,会比“永远看涨”和“永远唱空”更准确。

Bear 应该找的是足以影响原假设的高质量证据,而不是为了凑齐观点数而罗列负面新闻。我们把关键挑战限制在最多三项,但没有要求每次必须提出三项;确实没有找到,就应说明检查范围与遗漏。

Bull 也不需要赢。面对合格的反例,它可以说:“这条理由不成立,我撤回它;其他需要核查的判断仍然开放。”

下面是一段合成教学对话,并非真实模型运行摘录:

Bull: 新增一只科技 ETF 可能带来新的投资机会,但是否改善分散,需要核对原组合。

Bear: 我检查了同一口径下的底层行业占比。按当前给定数据,科技暴露从 15% 升到 23%。因此“多一只基金便更分散”不能成立;但这还不足以判断整个组合的波动率。

Bull: 我撤回用基金数量论证分散改善的说法。若用户想主动提高科技配置,需要另起一个理由,检查估值、现金和风险承受条件。

这段对话并没有决定“该买还是不该买”。它只是让一条原来可能混在长篇报告里的错误理由,变得可以明确修正。

对我们而言,这比安排第三个 AI 投票裁定胜负更接近金融研究。证据不会因为被两位 Agent 重复说出,就变成两份独立证据。

四、用户真正该得到什么:可见的分歧,而不是一场表演 ​

在产品上,我们希望用户最后看到三件事:原来支持或反对的是什么;某条重要意见是否被真正挑战、回应和修正;哪些问题还没有答案。

例如,计算证明科技行业占比提高了,却不能据此自动推出组合总波动一定提高。资产间相关性、资金来源、投资期限等仍需资料。结论应清晰保留这种范围,而不是机械写“双方各有道理”。

我们还要避免一种虚假的透明:页面显示两个气泡来回出现,但实际上它们只是同一个 Main 一次性编写的“模拟辩论”。如果声称是实际 Agent 对话,就必须能追溯双方独立产生的内容和真实交接。

这时金融产品需求变成了一项工程问题:怎样证明这场讨论真的发生过,又不让它拖住正常研究?

五、技术机制从这里开始:先独立发言,再建立有界交流 ​

原来的 ETF Agent 已有 Main(主代理)与 Child(子代理):Main 负责最终回答,Child 可以在自己的上下文里查资料、计算并交回成果。我们没有为 Bull 和 Bear 各建一套新执行器,而是让它们复用原 Child 机制。

每位 Child 首先基于同一问题、自己获准的原始资料独立研究,提交一份可公开的初始立场,称为 opening。初始立场不是隐藏推理内容,也不是原始工具日志。双方有了可用 opening,才允许正式交叉提问。

配对分成两个步骤:第一个 Child 提出配对意向,停在安全点;第二个 Child 使用原任务身份加入。应用核对同一个根研究任务、互补角色及授权条件,再冻结这组配对身份。否则,第一个 Child 可能早在“对手”进入前结束,事后却被包装成参与过对话。

消息传递由原 Child 生命周期管理模块负责,而不是新增一个独立的“辩论平台”。简化后的真实处理路径是:

消息传递由原 Child 生命周期管理模块负责,而不是新增一个独立的“辩论平台”。简化后的真实处理路径是:

sequenceDiagram
    participant Bear as Bear 子代理
    participant Host as 执行与授权模块
    participant Store as 持久记录
    participant Bull as Bull 子代理
    Bear->>Host: 提交质询与证据引用
    Host->>Host: 核对配对、轮次及来源权限
    alt 不合法
        Host-->>Bear: 拒绝本次发送,解释缺口
    else 合法
        Host->>Store: 保存唯一消息及原回执
        Store-->>Host: 返回消息身份
        Host-->>Bear: 发送已接纳,可安全等待
        Host-->>Bull: 发出可去重的唤醒提示
        Bull->>Host: 请求继续执行
        Host->>Store: 读取真实消息和消费记录
        alt 根任务仍可继续
            Host->>Bull: 下一次模型请求携带原消息
            Bull->>Host: 提交对应回复
        else 已停止或到期
            Host-->>Bull: 结束局部讨论,保留成果
        end
    end
架构关系图

这个设计刻意没有让 Main 逐轮重新措辞,也没有把已有的交易、研究来源或预算权力交给 Bull/Bear。

六、为什么“消息发出去了”不能只用一个布尔值 ​

我们遇到的第一个关键概念,是区分三个不同事实。

第一,模型提出了消息。 Bear 想发一段质疑。

第二,应用接纳了发送。 系统核对身份、权限和轮次,并持久保存这条消息及回执。

第三,Bull 的下一次真实模型请求携带了这条消息。 只有到了这里,才能说明这条质疑确实进入过对方的输入。

第三步仍然不能证明模型理解了,更不能证明质疑正确。金融研究中的“证据被引用”“模型看见了引用”“引用支持结论”也是三个不同命题。

一条消息记录至少要关联谁发、发给谁、属于哪对配对、哪轮质询、回复的是哪条原消息,以及它依赖什么来源。下方是示意结构,正式系统还需要原封存模型请求、Tool 回执和授权关联:

ts
type PeerMessage = {
  messageId: string;
  pairId: string;
  senderExecutionId: string;
  recipientExecutionId: string;
  round: number;
  replyTo?: string;
  text: string;
  evidenceRefs: string[];
};

接收者由已冻结的合法配对推导,模型不能随意填写另一个 Agent 的地址。replyTo 关联实际消息身份,而不是根据“你刚刚说过”这样的文字猜测。

我们把这些操作都留在同一套原执行回执里,避免再维护另一份真假不明的聊天账本。因为用户真正关心的是“那条质疑是否被处理”,不是系统拥有多少消息表。

七、让对话有终点:轮次、等待和预算 ​

我们没有设计“争到双方一致”为止的机制。

一个对话轮次,是一项主张或质询,以及至多一次对应回应。独立 opening 不计轮数;半轮已经占用了位置,就不能在恢复时免费重来。首轮由 Bear 开始,Bull 回应;继续与否取决于实际发现,而不是必须凑满轮数。

最大轮次由合法的用户要求和平台可信上限共同决定,随本次配对固定。轮数限制不等于模型预算增加:研究仍受原 Run 的模型调用、工具、截止时间和取消规则约束。缺预算、对方退出或意见已经足够,都可以提前停止,并保留已有成果。

最大轮次由合法的用户要求和平台可信上限共同决定,随本次配对固定。轮数限制不等于模型预算增加:研究仍受原 Run 的模型调用、工具、截止时间和取消规则约束。缺预算、对方退出或意见已经足够,都可以提前停止,并保留已有成果。

flowchart TD
    A[双方形成独立初始立场] --> B{还值得继续且仍有许可吗}
    B -- 否 --> H[结束讨论并交回现有成果]
    B -- 是 --> C[提交一项真实质询]
    C --> D[保存状态并释放执行槽]
    D --> E{收到合法回复或结束事件吗}
    E -- 回复 --> F[沿原身份继续处理]
    F --> B
    E -- 结束事件 --> H
    H --> I[Main 综合实质分歧与缺口]
架构关系图

这里的等待不是让服务器始终占用一个执行线程,更不是无限轮询。Child 在安全位置 park(保存状态并让出执行槽),有新消息或截止事件再 wake(在原身份上重新调度)。保留多少资源预留,继续由原预算模块裁决。

一个容易漏掉的竞态是:对方回复恰好出现在“检查没有消息”和“登记等待”之间。因此等待登记与消息检查需要共享一致的记录约定;收到唤醒后,也要再核对持久事实。通知只是提醒,不是消息本身。

八、最深的坑在恢复,而不是模型能否说出漂亮的话 ​

真正验证一段对话是否可靠,可以在最不方便的时刻停止进程:消息已经保存,但对方还没真正消费它。

进程中断在……可以确认的事实恢复原则
发送尚未提交有提议,无成功回执不补造已发送事实
保存后、通知前唯一消息回执存在继续提示原消息,不再新增一条
接收方请求封存后、派发结果未知请求身份存在,外部结果不明保留未知,不冒充安全重试
接收方请求确已提交消息进入过真实请求保留消费关系,不重复扣轮次

这叫作幂等性:重复处理同一次操作,不重复产生业务效果。消息身份要绑定真实发送动作,不能只算正文哈希;两个轮次即使碰巧有相同文本,也可能是两次不同发言。外部模型提供商未必支持“恰好一次”的派发保证,进程故障后必须诚实保留未知。[1]

真实问题一:通知已到,恢复却失败 ​

一次隔离 PostgreSQL 恢复验证中,等待状态成功保存,唤醒通知也到达,但负责接管的第二个进程仍然失败。

排查发现两个不同问题:验证辅助程序没有传入原持久化唤醒输入;同时,对数据库往返后的授权对象使用了字符串比较。PostgreSQL 的 jsonb 不保证保留 JSON 对象键的原始顺序,同一逻辑身份可能仅因键顺序不同被误拒。[2]

我们修复了实际恢复链路,并对逻辑授权对象采用结构化比较,同时继续拒绝真实不同的身份。不能因此把所有东西都改成“忽略顺序”:已经封存的模型输入字节和签名材料,有自己的精确性要求。

这个问题很有代表性:消息通知成功,不等于任务真正恢复;数据看起来一样,也不代表校验方式用对了。

真实问题二:双方看到了文字,来源却成了“未知” ​

还有一次,公开质询确实进入了后续处理,但后面的计算无法继续使用它的原始来源身份。

根因是模型看到的精简内容,省略了部分仅供应用内部核验的 sourceIds;后续代码却用这份简化视图去比完整的内部记录。正确修复是先验证精简视图确实来自哪条原消息,再回原记录取得合法来源证明,而不是给空字段随意补一个“已验证”。

金融信息尤其经不起这种混淆。引用在界面上出现了,并不意味着其来源链可以被后续计算合法继承;正确的系统应当承认缺口,而不是为了顺畅叙事造一个来源。

九、为什么两个 Agent 不能共享所有资料,也不能投票下单 ​

在金融产品里,不同数据有不同访问资格。假如 Bear 有权阅读某份受保护的账户材料,而 Bull 没有,Bear 把材料里的数字改写成一句话,并不会自动使它成为可传播的公开信息。

我们检查的不仅是附带的引用 ID,还包括发送正文可能依赖的受保护来源。无法确认可共享时,拒绝该次发送,保留相关缺口;不会试图用几条正则把敏感字段删掉后假装安全。Bear 自己已完成的合法研究仍可交回 Main。

同样,来自同伴的一句“你必须执行交易”,仍然是模型生成的观点,不能升级成用户授权。多空角色不获得新工具、新预算或账户写入权限。

这揭示了我们非常在意的一项工程取舍:让观点能相互影响,但不让一个 Agent 的话改写另一个 Agent 的权限。

最终由 Main 结合可用证据回答用户。没有自动裁判、没有少数服从多数,也不把“买 10%”和“不买”折中成“买 5%”。如果真正缺的是现金安排或协方差数据,取中间值解决不了问题。

十、做成可选能力,而不是投研的必经仪式 ​

截至 2026 年 10 月 24 日,有界多空机制已经完成限定的工程验收:直接问答、真实回执、等待与冷恢复、原 Thesis 关联都有对应验证;也有有限真实模型对话观察。它们证明执行机制在对应条件下能够运作,不证明已获生产部署保证,更不证明多空一定提高金融判断质量。真实运行仍观察到成本和表达上的局限。

我们的最终选择不是“每次投资研究都用两个 Agent 交战”,而是把直接讨论留给确实存在重要假设、需要主动反证的问题。简单查询尽量简单,复杂争议允许深入;某位参与者失败时,Main 仍可以诚实交付已有的独立研究。

多空 Agent 真正值得保留的,不是把反对意见说得更激烈,而是让一个理由能够被质疑、被回应、被修正,并且在系统故障后仍能找到原来的依据。


延伸阅读 ​

[1] AWS Builders’ Library,Making retries safe with idempotent APIs。相关思想用于理解重试的操作身份,不表示模型供应商拥有同样的幂等保证。

[2] PostgreSQL,JSON Types。对象键的逻辑同一性与原始字符串同一性不能混淆。

相关基础:Subagent:从上下文隔离到专业化协作。下一篇:AI 投研不能只有支持意见。更多项目文章见 ETF Agent 文集,欢迎 Star 关注后续架构演进。

与个人博客并行:这里按问题连续阅读,博客保留时间线。