返回博客

Git 当共享记忆|13 个 Agent 12 天不重复搜索

人工智能6416
Git 当共享记忆|13 个 Agent 12 天不重复搜索

让 13 个语言模型 worker 在同一个难题上并行跑近 12 天,没有任务分配,也没有中央规划器,靠的只是一本写在 Git 里的公共账。这篇把这本账拆开:commit 即主张的数据模型、派生索引怎么算前沿与冷门分支、领取任务的去重策略,以及把 API 预算从重复搜索挪到新搜索上的完整做法——worker 的推理调用统一走 4sapi(https://4sapi.com)的 OpenAI 兼容接口。

一、开篇痛点:多 Agent 并行,最贵的是重复劳动

自主研究循环的诱惑很直接:一个 coding agent 可以无人值守地改进一个课题,一轮跑完回来拿到一份更好的配置。于是直觉就是多开几个,并行度翻倍,产出也翻倍。实际观察到的图景要难看得多:多个自主研究智能体各自开跑,每个会话都从零开始——重读同样的资料、重新推导同样的结论、把别的会话已经证伪的路再走一遍。更多智能体往往只是更多重复,而不是更多发现。

对按 token 计费的接入方,这是最冤的一类消耗:钱花出去了,信息增量是零。并行度越高,浪费的绝对值越大,而且这种浪费在账单上毫无标记——每一笔调用单独看都合理,加总起来却是一次昂贵的原地踏步。解法不是把更长的上下文塞给每个 worker,让每一家把全世界背在身上,而是给群体一本共同的账:可引用、可复现、可审计的公共记录。这本账放在哪?一次持续近 12 天、13 个智能体协作的运行给出了一个朴素到近乎固执的答案:Git。

二、一次近 12 天的运行,13 个 worker 干成了什么

先摆事实。那次运行里,13 个语言模型 worker 同时攻关一个"权重迁移"问题:给定 141 个预训练 donor 模型,目标是一个 119.6M 参数、维度与任何 donor 都不匹配的 attention-SSM 混合模型;约束是全流程不许用训练数据、不许做梯度更新,worker 只能靠"搬运"donor 里的统计信息,为目标模型找到可用的初始化。

整个过程没有任务分配,没有中央规划器。最终发布 1,703 项贡献,评估指标从 3.39 压到 1.899 bits/byte,关闭了与训练版 GPT-2 124M 差距的 62%。胜出配方的思路也清晰:把 donor 的 next-token 统计压缩进目标模型的 embedding 与 output head,再通过对 attention、feed-forward 与 state-space 块的稀疏编辑补上短程上下文信号。

比结果本身更值得看的是两个细节。其一,胜出配方的 145 个 commit 谱系横跨 15 个账号——最优解不是某一个 worker 的独角戏,而是一场跨账号接力。其二,这份配方后来收到 165 次独立复现,无一失败。没有人指挥谁做什么,重复劳动却没吞掉这场运行,靠的就是下一节的机制。

三、原理速览:Git 当共享记忆,只增不改的 DAG

机制的核心只有一句话:把研究过程本身写进一个 Git 仓库。每个主张(claim)——一次结果、一条洞察、一个假设、一次验证、一份报告——都是仓库里一个不可变的 commit,任何人可以检出它、复跑它;commit 之间的父边说明这条主张建立在什么之上。所有主张连起来,构成一棵只增不改的 DAG:历史不能改写,错误不删除,只能被后续的验证或反驳压下去。

架在这个底座上,worker 的每一轮工作循环长这样:

text
worker 领取前沿
    │  从派生索引挑方向:优先已验证的前沿,兼顾冷门分支
    ▼
本地试验
    │  checkout 父主张 → 复跑基线 → 在其上做增量尝试
    ▼
commit 主张
    │  把结果/洞察/假设/验证/报告追加进 Git,声明父边
    ▼
索引更新
    │  重算前沿、冷门分支与每个主张的验证状态
    ▼
多样性选择
    │  与现有主干过近的方向降权,防止单一路线坍缩
    ▼
下一轮

"只增不改"四个字有分量。Git 的不可变 commit 天然给了主张"发布后不可篡改"的性质,DAG 的父边天然给了主张"来源可追溯"的性质,这两条恰好是公共记录最需要的东西:可引用(commit 哈希就是引用),可复现(检出即可重跑),可审计(父边连成证据链)。

四、commit 即主张:五类主张的数据模型

主张的类型决定了它的载荷和父边接法。一次多 Agent 协作至少需要五类主张:

主张类型载荷内容父边指向初始验证状态
结果 result试验产出、复跑命令、环境说明所基于的方法或配置主张未验证
洞察 insight规律摘要、支撑它的结果清单一组结果主张讨论性
假设 hypothesis预期、可检验的判据相关洞察与结果未验证
验证 verification复跑记录、与原结果的差异被验证的那条主张已复现/存疑
报告 report阶段总结、引用的主张清单一组结果与验证汇总

设计上有三条纪律。第一,载荷必须自带复跑所需的最小信息:命令、依赖、环境说明,否则"可复现"只是口号。第二,验证状态是主张的一部分,发布时如实标注——没被独立复跑过的结果就是"未验证",不许美化。第三,失败结果也要作为主张入库:一条被证伪的路,价值不亚于一条被证实的路,它的作用是让后来的 worker 不必再走一遍。

五、父边:谱系即上下文

父边是这套数据模型里最便宜也最值钱的部分。结果主张的父边指向它所基于的配置,洞察的父边指向支撑它的结果,假设的父边指向触发它的洞察,验证的父边指向被验证的对象,报告的父边是它引用的全部主张。顺着父边往回走,任何 worker 都能精确还原"发布这条主张时,群体已经知道什么"——这正是并行 worker 最缺的共享上下文。它不靠把整段历史塞进 prompt 实现,worker 只需要检出自己关心的那条谱系。

谱系同时是功劳簿。胜出配方那 145 个 commit 横跨 15 个账号,每一笔贡献在谱系上都有自己的位置:谁开的分支、谁做的关键洞察、谁完成的验证,不需要额外的信用系统事后追认。当"贡献"天然落在一个不可变的图上,协作的激励问题就省掉了一大半。

六、派生索引:前沿、冷门分支与验证状态怎么算

Git 本身不回答"现在该干什么",派生索引补上这一层。它是从 commit 图推导出来的三个视图。

前沿:每条谱系最深处的叶子主张,按验证状态排序。被独立复现过的叶子是可以直接站立的地基,未验证的叶子是待办;后者的优先级取决于它离已有验证主干有多近。

被忽视的分支:长期没有后继、也没有验证挂上去的叶子。冷门不等于错误,更多时候只是没人往那边走。多样性红利往往藏在这些分支里——一次协作能否持续产出,很大程度上取决于冷门分支有没有被定期捞出来。

验证状态:每个主张被独立复跑的记录累积。165 次独立复现无一失败,说明验证在这套机制里不是仪式——每个 worker 都真的在检出别人的 commit 重跑。

实现上只有一条铁律:索引必须可重建。它是缓存,不是事实源,任何时候都要能从 git log 一键重算。索引一旦和图不一致,整个群体会被引向歧路,这比没有索引更糟。

七、多样性感知的选择:领取任务的去重策略

没有任务分配、没有中央规划器,"领任务"这个动作就变成 worker 从索引里自选方向。自选最大的风险是坍缩:一个方向暂时领先,所有 worker 涌过去,群体退化成单一化(monoculture),并行的多样性优势归零。那次运行中途就出现过群体挤在单一路线的时刻,最后靠一次干预才拉出来——这个细节说明,多样性不会自然出现,必须有规则守着。

可行的选择规则有四条。一,前沿优先但不满额:留出一部分名额给冷门分支和未验证叶子,捡漏和补验证同样是贡献。二,相似度去重:领取前把候选方向与近期主干的方向描述比对,过近的直接放弃——这一条就是"不重复搜索"的执行点。三,重复方向转验证:同方向已有人发布未验证结果时,优先做独立复现,而不是再开一条新枝。四,负结果占位:被证伪的方向同样占据"已搜索"名额,防止别的 worker 再撞上去。

八、方案:用 Python 搭一个最小共享记忆层

下面把整套机制压缩成一个可跑的骨架。模型调用是 OpenAI SDK 风格的标准写法,主张提交与索引刷新用 subprocess 调 git 完成。仓库先 git init 并配好提交者信息;骨架把所有主张线性追加在同一条主线上,真正的 DAG 结构由主张里的 parents 字段表达,Git 层面单分支追加即可:

python
import json
import os
import subprocess
import uuid
from pathlib import Path

from openai import OpenAI

# 模型调用统一走中转的 OpenAI 兼容入口;Key 从环境变量取,不要写死
client = OpenAI(
    api_key=os.environ["MODEL_API_KEY"],
    base_url="https://4sapi.com/v1",
)
MODEL = "deepseek-chat"            # 按后台模型列表替换
REPO = Path("./claims-repo")       # 共享记忆本体:一个普通 Git 仓库


def think(system: str, user: str) -> str:
    """worker 的唯一推理入口,所有模型预算都从这里过,便于按轮记账"""
    resp = client.chat.completions.create(
        model=MODEL,
        messages=[{"role": "system", "content": system},
                  {"role": "user", "content": user}],
    )
    return resp.choices[0].message.content


def git(*args: str) -> str:
    """Git 操作统一收口;仓库只增不改,禁止任何改写历史的命令"""
    return subprocess.run(["git", "-C", str(REPO), *args],
                          check=True, capture_output=True, text=True).stdout


def load_claims() -> dict:
    """从工作副本读取全部主张;事实源仍是 Git 历史,副本损坏可重新检出"""
    return {c["id"]: c for c in
            (json.loads(p.read_text(encoding="utf-8"))
             for p in sorted((REPO / "claims").glob("*.json")))}


def commit_claim(claim: dict) -> str:
    """主张即 commit:parents 字段就是父边,说明这条主张建立在什么之上"""
    claim = {
        "id": uuid.uuid4().hex,
        "type": claim["type"],         # result / insight / hypothesis / verification / report
        "parents": claim["parents"],   # 父主张 id 列表
        "body": claim["body"],         # 结论 + 可复跑命令 + 环境说明
        "status": "未验证",            # 验证状态如实标注,独立复现后才改"已复现"
        "author": claim["author"],     # worker 标识
    }
    rel = f"claims/{claim['id']}.json"
    path = REPO / rel
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(claim, ensure_ascii=False, indent=2), encoding="utf-8")
    git("add", rel)
    git("commit", "-m", f"claim({claim['type']}): {claim['id']}")
    return claim["id"]


def refresh_index() -> dict:
    """派生索引:前沿、冷门分支、验证状态全部从仓库重算,可随时重建"""
    claims = load_claims()
    parented = {p for c in claims.values() for p in c["parents"]}
    frontier = [cid for cid, c in claims.items()
                if cid not in parented and c["type"] in ("result", "verification")]
    verified = {p for c in claims.values()
                if c["type"] == "verification" and c["status"] == "已复现"}
    neglected = [cid for cid in frontier if cid not in verified]
    return {"frontier": frontier, "neglected": neglected, "size": len(claims)}


def similar(a: str, b: str) -> float:
    """字符 bigram Jaccard 相似度:骨架够用,不引外部依赖"""
    A = {a[i:i + 2] for i in range(len(a) - 1)}
    B = {b[i:i + 2] for i in range(len(b) - 1)}
    return len(A & B) / max(len(A | B), 1)


DUPLICATE_THRESHOLD = 0.6           # 演示值:候选与近期主干过近即视为重复搜索


def pick_direction(index: dict, claims: dict, recent: list) -> str:
    """多样性感知的领取:前沿优先,冷门分支保底,近邻方向降权"""
    pool = [(cid, 0.0) for cid in index["frontier"]]
    pool += [(cid, 1.0) for cid in index["neglected"]]   # 冷门分支带保底权重进候选
    best = None
    for cid, lane_bias in pool:
        dup = max((similar(claims[cid]["body"], r) for r in recent), default=0.0)
        if dup > DUPLICATE_THRESHOLD:
            continue                                     # 过近即放弃,把预算留给新搜索
        score = lane_bias + dup
        if best is None or score < best[0]:
            best = (score, cid)
    return best[1] if best else index["frontier"][0]


def run_experiment(plan: str) -> dict:
    """课题相关的试验逻辑占位:接入评测脚本、产物哈希与差异摘要"""
    raise NotImplementedError


def worker_loop(worker_id: str, rounds: int) -> None:
    """单 worker 主循环:领取 → 试验 → 提交 → 索引更新,直至预算耗尽"""
    for _ in range(rounds):
        index = refresh_index()
        claims = load_claims()
        recent = [c["body"] for c in claims.values() if c["type"] == "result"]
        parent = pick_direction(index, claims, recent)
        plan = think("检出父主张,复跑基线,再设计一个增量实验",
                     f"父主张:{parent};谱系见该主张的父边链")
        outcome = run_experiment(plan)   # 本地试验:跑在 worker 自己的环境里
        commit_claim({"type": "result", "parents": [parent],
                      "body": json.dumps(outcome, ensure_ascii=False),
                      "author": worker_id})
        refresh_index()                  # 索引更新,供全部 worker 的下一轮领取

骨架里有三个接缝值得说明。think() 是 worker 唯一的模型调用入口,所有推理预算都从这里过,方便按轮次记账和熔断;commit_claim() 把主张写成不可变 commit,parents 字段就是父边,验证状态在发布时如实标注;refresh_index() 从仓库重算前沿、冷门分支与验证状态,随时可重建。run_experiment() 留给具体课题——在权重迁移那次运行里,它装的就是"压缩 donor 统计信息、对 attention、feed-forward 与 state-space 块做稀疏编辑"这一类尝试。

九、领取任务的去重与验收清单

上线前按这份清单过一遍,前半管去重,后半管验收:

领取前(去重)

提交时(验收)

十、成本与风险提示

十一、迁移到一般工程:多 Agent 共写代码库与文档库

这套机制不挑领域,凡是"多个 Agent 往同一个仓库里写东西"的场景都能套用:

研究协作里的概念多 Agent 共写代码库多 Agent 共写文档库
主张 commit变更/PR文档修订
验证主张CI 通过与评审记录事实核查记录
前沿通过 CI 未合并的最优分支最新定稿前的候选稿
被忽视的分支无人接手的重构方向长期未更新的章节
多样性选择避免全员挤同一个模块避免全部并行改同一篇

落地顺序建议三步走:先把记录层立起来(只增不改、父边完整),再把派生索引跑起来(前沿、冷门、验证状态),最后才上多样性选择。跳过前两步直接做调度,得到的只是又一个中心规划器,恰好是要避开的反模式。

验收标准也只有一条:任何一个 commit,检出后可复跑、可引用、可审计。做不到这一条,共享记忆就退化成了共享文件夹,重复搜索会从后门回来。

总结

这一期拆了"把 Git 当共享记忆"的完整方案:多 Agent 并行最贵的浪费是重复劳动,根源是每个 worker 都带着自己的上下文从零探索。解法是把研究过程写成 Git 里只增不改的 DAG——commit 即主张,父边即谱系,五类主张各有载荷与验证状态;派生索引从图上算出前沿、冷门分支与复现记录;多样性感知的选择规则守住群体的探索面。一次近 12 天、13 个 worker 无中央规划器的运行给出了量化注脚:1,703 项贡献,评估指标从 3.39 到 1.899 bits/byte,关闭与训练版 GPT-2 124M 差距的 62%,胜出配方 145 个 commit 横跨 15 个账号,165 次独立复现无一失败。这套机制同样适用于多 Agent 共写代码库与文档库的场景;worker 侧的模型调用统一走 4sapi(https://4sapi.com)的 OpenAI 兼容接口,预算按轮记账才控得住。

欢迎在评论区聊聊:多 Agent 并行时,重复搜索在账单里留下的最深的一道痕是什么?

标签:多AgentGit共享记忆重复搜索成本优化

推荐阅读

探索更多前沿洞察与行业干货。