Agent 评测用本地裸仓替代 GitHub 的边界
可以用容器内的本地裸仓替代 GitHub 做 Agent 评测的远端,延迟与配额成本显著下降且 commit 可复现,但 Agent 自发推送比例会下降约 11 个百分点,涉及 PR、评审、CI 语义的指标不能直接替换。
- 本地裸仓把远端延迟从数百毫秒压到几十毫秒,配额成本归零
- 固定 author/committer 时间戳后,同输入可产出同 commit SHA
- 按需 rebase 与 push 不受影响,自发 push 下降约 11 个百分点
- 含 PR、评审、CI 触发语义的评测任务必须保留 GitHub 影子仓
能替代,但只替代传输层。容器内起一个本地裸仓(Bare Repository)当远端,评测的远端延迟从数百毫秒降到几十毫秒,配额成本归零,同一输入能产出同一 commit SHA;代价是 Agent 自发推送的比例会掉约 11 个百分点。
先把术语说白:裸仓就是一个只有 .git 目录、没有工作区的文件夹。Git 通过 file:// 协议访问它,握手、对象协商、ref 更新的流程和访问 github.com 走的是同一套代码,只是不吃网络。所以它能替你省掉的是传输与配额,替不掉的是平台语义(PR、评审、CI 触发)。
为什么 Agent 评测里值得把 GitHub 换掉?#
评测不是 CI。CI 一天跑几十次,评测一轮几百个 task,回归一次就是几十轮。Git 操作密度完全不是一个量级。Octoverse 2026 的数据也印证了这点:agent 参与后提交与推送量创下新高,单个工作流里的 git 调用次数明显变多。
我按自己的一个评测集算过账:240 个 task,每个 task 平均 1.6 次 push、0.3 次 PR 创建、2 次拉取 diff。一轮下来大约 1100 次 GitHub REST 调用。GitHub 对 PAT 认证请求的主限流是 5000 次/小时,4 个 worker 并行跑同一账号,一小时就能顶到 403/429。限流一旦发生,评测框架如果没区分“环境错误”和“任务失败”,指标就被污染了——这是最隐蔽的一类脏数据。
还有状态污染。跨 run 的分支残留、issue 编号漂移、Actions 被意外触发扣掉分钟数,这些在长周期回归里都会累积成噪声。
对比表:GitHub 远端与本地裸仓在评测场景下的差异
| 对比项 | GitHub 远端 | 本地裸仓(file://) |
|---|---|---|
| 单次 push P95 耗时 | 600~1500 ms(跨境) | 40~120 ms(同盘) |
| 调用配额 | PAT 认证 5000 次/小时 | 无 |
| 跨 run 状态隔离 | 需显式删分支、清 issue | 按 RUN_ID 建目录,删目录即隔离 |
| commit 可复现性 | 受时间戳、签名影响 | 完全可控,同输入同 SHA |
| PR / 评审 / CI 语义 | 原生支持 | 不存在 |
| 审计留痕 | 平台侧完整 | 需自行归档 refs 与 reflog |
本地裸仓怎么搭,才能让两次 run 的 commit SHA 一致?#
核心是三件事:固定作者与提交者时间戳、固定身份、关闭一切隐式副作用(hooks、GPG 签名、autocrlf)。tree、parent、message、身份、时间全部相同,SHA 就一定相同。
环境基线:Ubuntu 24.04、git 2.43.0、bash 5.2、目录挂载在 /srv/evals。
可执行脚本:创建裸仓并做一次确定性提交
#!/usr/bin/env bash
# 环境:Ubuntu 24.04 / git 2.43.0 / bash 5.2
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
set -euo pipefail
RUN_ID="${RUN_ID:?need RUN_ID}"
ROOT="/srv/evals/${RUN_ID}"
REMOTE="${ROOT}/remote.git"
WORK="${ROOT}/work"
mkdir -p "${ROOT}"
# 1) 裸仓:只有对象库与 refs,没有工作区
git init --bare --initial-branch=main "${REMOTE}" >/dev/null
# 2) 服务端收敛参数
git -C "${REMOTE}" config receive.maxInputSize 64m # 单次 push 上限 64MB,越界直接拒绝
git -C "${REMOTE}" config receive.denyNonFastForwards false
git -C "${REMOTE}" config gc.auto 0 # 评测期关掉后台 gc,避免尾延迟抖动
# 3) 客户端:Git 2.38.1 起 file:// 在部分场景默认禁用,必须显式打开
git config --global protocol.file.allow always
git config --global core.autocrlf false
git config --global user.name "eval-agent"
git config --global user.email "agent@eval.local"
git config --global commit.gpgsign false
git config --global core.hooksPath /dev/null # 沙箱里不允许任何 hook 改状态
# 4) 确定性提交
git clone --quiet "${REMOTE}" "${WORK}"
cd "${WORK}"
printf 'seed\n' > README.md
git add README.md
GIT_AUTHOR_DATE="2026-01-01T00:00:00 +0000" \
GIT_COMMITTER_DATE="2026-01-01T00:00:00 +0000" \
git commit -q -m "seed"
timeout 15 git push --quiet origin main # 15s 硬超时,卡住就判环境错误
git rev-parse HEAD
参数含义与预期输出:receive.maxInputSize 是服务端侧保护,防止 agent 一次性推上来几百 MB 的构建产物把评测机磁盘打满。gc.auto 0 关掉自动回收,是因为 gc 会在 push 尾部触发,把 P95 从 80ms 拉到 2s,评测里这种抖动会被误判成模型行为变化。timeout 15 是关键,裸仓在正常盘上不会超 1s,超了基本是 IO 卡死或路径挂载掉了。
两次独立 run 的 git rev-parse HEAD 输出应当完全相同的 40 位 SHA,例如 3f9c1a7e0b4d8f2a6c1e5b9d0a7f3c2e8b6d4a1c。如果两次不同,先查 GIT_COMMITTER_DATE 是否漏传,再查 user.email 是否被镜像里的全局配置覆盖。
风险清单:搭建阶段最常见的三个报错
fatal: transport 'file' not allowed:Git ≥ 2.38.1 的安全收紧,加protocol.file.allow always即可,但只应在沙箱内开,宿主机不要开。fatal: could not read from remote repository:路径不存在或容器内没挂到/srv/evals,先ls -ld验证。error: remote failure: max input size exceeded:agent 推了超限产物,属于任务行为异常,应回滚该 task 而不是放宽阈值。
评测 runner 里如何封装 push,兼顾超时、重试与边界保护?#
直接在评测脚本里裸调 git push 是危险的,因为远端地址是 agent 可以改的。git remote set-url origin https://… 之后,评测机的数据就出网了。封装层必须做白名单。
可执行脚本:带超时、重试与路径白名单的 push 封装
# 环境:Python 3.12.3 / git 2.43.0
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
import subprocess, time, re
REMOTE_ALLOWLIST = ("/srv/evals/",)
GIT_TIMEOUT = 20
SHA_RE = re.compile(r"^[0-9a-f]{40}$")
class PushError(RuntimeError):
pass
def push(workdir: str, remote: str, branch: str = "main", retries: int = 3) -> str:
# 边界保护 1:远端必须落在白名单前缀内
if not any(remote.startswith(p) for p in REMOTE_ALLOWLIST):
raise PushError(f"remote out of allowlist: {remote}")
last = None
for attempt in range(1, retries + 1):
try:
subprocess.run(
["git", "-C", workdir, "push", "--quiet", remote, f"HEAD:refs/heads/{branch}"],
check=True, capture_output=True, timeout=GIT_TIMEOUT,
env={"GIT_TERMINAL_PROMPT": "0", "PATH": "/usr/bin:/bin"},
)
sha = subprocess.run(
["git", "-C", workdir, "rev-parse", "HEAD"],
check=True, capture_output=True, timeout=5,
).stdout.decode().strip()
if not SHA_RE.match(sha):
raise PushError(f"bad sha: {sha}")
return sha
except subprocess.TimeoutExpired as e:
last = e # 超时:环境问题,重试
except subprocess.CalledProcessError as e:
stderr = e.stderr.decode(errors="replace")
if "max input size exceeded" in stderr:
raise PushError("payload too large") from e # 行为异常,不重试
last = e # 其余错误:退避后重试
time.sleep(0.2 * (2 ** (attempt - 1))) # 0.2s / 0.4s / 0.8s
raise PushError(f"push failed after {retries} attempts: {last}")
几个刻意的取舍。env 里只保留最小 PATH 并置 GIT_TERMINAL_PROMPT=0,避免 git 卡在凭据输入上把整个评测挂住。CalledProcessError 分支里区分了“体积超限”和“其它错误”,前者重试三次也没用,纯粹浪费 60 秒。指数退避基线取 0.2s,是因为本地裸仓的失败几乎都是瞬时争用(并发 clone 同一个目录),不值得等太久。
预期效果:一次 120 个文件、2.1MB 的推送,debug 日志里形如 pushed 3f9c1a7e -> /srv/evals/run-0f3a/remote.git main in 87 ms, attempt 1。同规模推送打到 GitHub 通常是 600~1500ms,一轮 240 个 task 累计能省 3~6 分钟,更重要的是没有限流导致的整批重跑。
丢了 11 个百分点,这个成本边界卡在哪?#
Microsoft DevBlogs 的对照实验给了最关键的一个数:把 GitHub 换成本地 git remote 后,按要求执行的 rebase 与 push 完全不受影响,但 Agent 自发推送(pushed on its own)的比例下降了约 11 个百分点。
我的判断是远端地址本身就是提示词的一部分。模型在语料里见过海量的 git@github.com: 与 PR 工作流,file:///srv/evals/... 这种地址不在那套分布里,模型更少走到“推上去看看”这一步。所以这个差距属于对训练语料分布敏感的任务指标,不是环境 bug。
对比表:按任务类型决定换不换
| 评测任务类型 | 能否用本地裸仓 | 理由 |
|---|---|---|
| 仓库内改代码、跑测试、看 diff | 可以 | 只依赖协议层,不依赖平台语义 |
| 按要求 rebase、按请求 push | 可以 | 实验已证实无差异 |
| 测量“Agent 是否主动提交/推送” | 不可以 | 指标本身会偏 11 个百分点 |
| 创建 PR、写评审意见、等待 CI | 不可以 | 裸仓没有这些对象 |
| 多分支协作冲突复现 | 可以 | 分支冲突逻辑完全在本地 |
落地做法是双 remote。评测容器里 origin 指向本地裸仓,负责日常高频操作;另配一个 shadow 指向真实 GitHub 私有仓,只在需要平台语义的子集任务上异步推送。git push shadow 放到后台队列,失败不影响本轮判分,下一轮补推。
评测机出网自检怎么做?#
即便主链路走本地,评测机仍要出网拉依赖、拉模型。出口 IP 漂移会让影子仓那一侧的限流表现不可对照,所以每个 run 我都在 manifest 里记一笔出口指纹。
可执行脚本:带超时与重试的出口 IP 探测
# Linux / macOS 通用,Windows 用 Git Bash 或 WSL
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
curl -sS --max-time 5 --retry 3 --retry-delay 1 --retry-connrefused \
"https://www.api.plus/api/get?apikey=${API_PLUS_KEY}&ip=8.8.8.8" \
-o /tmp/ip_probe.json \
-w 'http=%{http_code} total=%{time_total}s\n'
参数说明:--max-time 5 是整次请求的总时限,包含 DNS 与 TLS 握手;--retry 3 --retry-delay 1 对 5xx 与瞬时超时自动重试三次;--retry-connrefused 把连接被拒也纳入重试;-w 输出状态码与总耗时,方便直接写进 manifest。预期输出一行 http=200 total=0.18s。
返回片段(字段以实际响应为准,解析时按 key 取值并容错):
{"code":200,"data":{"ip":"8.8.8.8","country":"美国","isp":"Google LLC"}}
完整字段说明见 GET 示例接口 文档。我在 runner 里只取 code 和 data.ip 两个字段:code 不等于 200 就标记环境降级,data.ip 与上一个 run 不同则记一条告警。同理,如果需要一个不落盘的外部资源来验证沙箱出网是否真的通,可以用 随机动漫壁纸接口 的 type=json 返回,只校验 HTTP 200 与 JSON 结构,不下载图片本身。
监控与告警清单#
裸仓替代 GitHub 之后,原来由平台兜底的那部分可观测性要自己补。下面这张表是我线上实际在用的配置。
监控与告警清单:本地裸仓评测链路
| 指标 | 建议阈值 | 告警动作 |
|---|---|---|
| push 成功率(含重试后) | < 99.5% 持续 10 分钟 | 暂停调度器,检查磁盘 inode 与挂载点 |
| push P95 耗时 | > 300 ms 持续 5 分钟 | 看 iostat -x 1 的 %util,排查是否误开 gc |
| 单次 push 体积 | > 64 MB | 拒绝该 task,标记 agent 行为异常,人工复核 |
| 非白名单 remote 出现次数 | > 0 | 立即中断整轮 run,审计 agent 脚本与镜像 |
| 磁盘剩余 | < 15% | 停止新 run,清理 7 天前的 /srv/evals/* |
| commit SHA 与基线不一致 | 出现即告警 | 判定本轮不可复现,阻断结果合入 |
| 出口 IP 变化 | 与上轮不同 | 记录到 manifest,影子仓限流数据单独标注 |
风险清单#
换掉 GitHub 引入的不是性能风险,是语义风险。下面几条我都实际踩过或见团队踩过。
风险清单:本地裸仓评测
| 风险 | 影响 | 规避方式 |
|---|---|---|
| 缺失 PR / 评审 / CI 语义 | 相关指标虚高,结果不可对外发布 | 双 remote,平台类任务保留 GitHub 影子仓 |
为跑通 file:// 放宽全局协议限制 | 沙箱可能读到宿主机其它目录 | 只在容器内开,宿主机不开;其余路径只读挂载 |
| 固定时间戳掩盖真实时间行为 | 依赖时间差的 task 被误判 | 这类 task 单独打标,改用两次独立提交 |
| 裸仓无限增长 | 磁盘打满导致整轮 run 失败 | gc.auto 0 后由外部 cron 按 run 目录整体删除 |
| 本地 push 不触发 CI | 漏掉集成类缺陷 | 影子仓异步跑一次完整 CI,结果不回灌判分 |
评测产物的归档策略也值得同步调整。裸仓删掉之后 refs 就没了,我会把每轮的 git for-each-ref 输出和 reflog 一起落盘,字段设计可参考 API 文章中心 里统计口径那类文章的做法,保证事后能追溯。
总结#
本地裸仓是 Agent 评测里性价比很高的一次替换:延迟降一个数量级、配额成本归零、commit 可复现。但它替换的只是传输层,替换不掉 GitHub 作为平台的行为提示。判定标准很干脆——指标里只要出现“是否主动推送”“是否开 PR”“是否等 CI”,就不能换。
三个落地要点:固定 author/committer 时间戳保证 SHA 可复现;push 封装必须带远端白名单,否则沙箱数据会出网;按任务类型做双 remote 分流,而不是一刀切。
适用范围
- 高频回归的代码修改类评测,单轮 task 数在 50 以上
- 需要严格复现同一 commit SHA 的对照实验
- 评测机与存储同机房、同盘挂载,
file://延迟在百毫秒以内
不适用范围
- 指标依赖 Agent 自发推送频率、PR 创建率、评审互动次数
- 需要真实触发 CI、等待 Actions 结果的任务
- 多人协作、需要保留平台侧完整审计留痕的正式发布流程
- 沙箱与宿主机共享文件系统且未做命名空间隔离的环境
本站实测数据(近 24 小时)#
以下数据来自 API.PLUS 线上调用日志(统计时间 2026-10-30 13:07,口径:仅统计已上线接口,不含扫描与无效路径),可作为容量规划与稳定性判断的现实参考。
- 接口调用总数:140 次;成功返回 137 次,鉴权/参数校验拦截 3 次
- 最活跃接口:随机动漫壁纸(93 次)、Menhera 动漫图片(41 次)、IP 地址查询(4 次)
- 近 7 天累计调用:896 次
常见问题(FAQ)
本地裸仓能完全替代 GitHub 做 Agent 评测吗
为什么换成裸仓后 Agent 主动推送变少了
本地裸仓怎么做才能保证两次 run 的 commit 一致
参考来源
- Can a local git remote replace GitHub in your agent eval?
- Octoverse: Commits, pushes, reach new highs in 2026 as developers build with agents
- GitHub Universe 2026: Evolving every layer of the stack
- Publish Visual Studio Marketplace extensions securely with Trusted Publishing
- How we detect frontier model misalignment
- A JSON schema Is not an AI governance layer
- JetBrains Academy – October Digest
以上为本文写作时的参考资料,引用观点以原文为准。