用户登录

技术文章

外部链接将在新标签页打开。

Octoverse 2026 解读:Agent 提交与推送指标怎么读

一句话回答

Octoverse 2026 显示提交与推送创新高,但这已不是人力产出的刻度。把提交按来源、切分、推送批次与存活率四个口径拆开,才能分清哪些是 agent 产出、哪些是真交付。

核心要点 TL;DR
  • commit 数受提交切分方式影响,不能直接当产出用
  • push 频次取决于远端类型,换成本地 remote 后下降 11 个百分点
  • 来源标注、pre-receive 限流、14 天存活率三件套一起读

Octoverse 2026 里 commit 和 push 都创了新高,但这两个数字已经不是人力产出的刻度了。把提交按来源、切分、推送批次、存活率四个口径拆开,才读得懂 agent 参与之后仓库里到底发生了什么。

先说个大白话:Octoverse 是 GitHub 每年发布的开发者年度数据报告,commit 是一次本地提交记录,push 是把本地提交同步到远端仓库的动作。commit 数就像家里的电表读数,agent 一接进来,电表转得飞快,可你分不清是空调在制冷、还是线路在漏电。

今年报告的原话是提交与推送双双创下新高,开发者已经在用 agent 做构建(GitHub Blog)。这句话本身没错,问题出在很多人把它直接当成"人效提升"的证据。我在几个团队里看过同一份数据的三种解读,结论能差出两倍。

为什么 agent 介入后 commit 与 push 这两个指标会失真?#

失真不是数据错了,是分母和计量单位变了。人写代码,一次提交通常对应一个可描述的意图;agent 写代码,一次提交可能只是"跑通了一行测试"。

我观察到的三个机制,按影响大小排序:

  1. 提交切分变细。agent 的循环是"改—跑测试—提交",为了便于回滚,它会频繁落小提交。同一个功能,人可能 1 个提交、800 行;agent 可能 12 个提交、平均 60 行。commit 数涨 10 倍,代码量没变。
  2. 推送策略由环境决定。微软在 agent 评测里做过一次对照:把远端从 GitHub 换成一台本地 git remote,rebase 和按需推送都正常,但 agent 自己发起推送的频率低了 11 个百分点(Microsoft for Developers)。模型能力没变,远端类型一变,push 数就变。这说明 push 频次更像环境参数,不像工作量。
  3. 强推与 rebase 造成重复计数。agent 习惯 rebase 后 force push,同一个逻辑改动会产出多个 sha。按 sha 计数,等于把同一份工作算了两三遍。

对比表:传统口径与 agent 参与后的建议口径

指标传统含义agent 参与后的偏差建议替代口径
commit 总数开发工作量被切分方式放大 3~10 倍按 patch-id 去重后的独立变更数
push 次数交付节奏受远端类型与触发策略影响推送批次(batch size)分布 + P95
人均 commit个人产出agent 提交挂在人的账号下按 trailer 拆分 human / agent
合并率质量信号agent 提交评审前移,合并快但返工也多14 天提交存活率

Martin Fowler 站点上有一篇讨论,说资深开发者对 agentic development 的迟疑,本质是清楚"跑起来了"和"能在生产里跑"是两回事(martinfowler.com)。这个判断放到指标上同样成立:commit 数证明代码被写出来了,证明不了它能活过两周。

怎么用 git 原生数据把提交拆成人和 agent 写的?#

不要靠作者邮箱猜。同一个工程师,上午手写、下午跑 agent,邮箱一模一样。可靠的做法是让 agent 侧自己在提交里留痕,用 git 的 trailer(提交信息末尾的键值行,类似 Signed-off-by)标注来源。

规范模板:agent 提交的 trailer 约定

feat(api): 为 IP 查询加 60 秒本地缓存

Generated-by: API.PLUS-Agent/1.4.2
Assisted-by: deepseek-v3.2
Reviewed-by: zhangwei

写进去用 git 2.32+ 的 --trailer 参数,比手工拼接提交信息稳:

git commit -m "feat(api): 为 IP 查询加 60 秒本地缓存" \
  --trailer "Generated-by=API.PLUS-Agent/1.4.2" \
  --trailer "Assisted-by=deepseek-v3.2"

可执行脚本:按 trailer 拆分提交来源

#!/usr/bin/env python3
# 依赖:Python 3.11+、git 2.40+
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
import json
import subprocess
import sys
from collections import Counter

GIT_TIMEOUT = 20                                  # 单次 git 调用上限,秒
RETRIES = 2                                       # 超时或非零退出的重试次数
AGENT_KEYS = ("generated-by:", "assisted-by:")    # 视为 agent 参与的 trailer

def run_git(args: list[str]) -> str:
    last_err = ""
    for attempt in range(RETRIES + 1):
        try:
            p = subprocess.run(["git", *args], capture_output=True,
                               text=True, timeout=GIT_TIMEOUT, check=False)
            if p.returncode == 0:
                return p.stdout
            last_err = (p.stderr or "").strip()[:200]
        except subprocess.TimeoutExpired:
            last_err = f"timeout>{GIT_TIMEOUT}s"
        except FileNotFoundError:
            raise SystemExit("PATH 里找不到 git")
    sys.stderr.write(f"git {' '.join(args)} 失败:{last_err}\n")
    return ""                                     # 边界:失败返回空串,调用方自行判空

def classify(repo: str, since: str) -> dict:
    raw = run_git(["-C", repo, "log", f"--since={since}", "--no-merges",
                   "--pretty=format:%H%x1f%B%x1e"])
    stats = Counter()
    for chunk in raw.split("\x1e"):
        chunk = chunk.strip("\n")
        if not chunk:
            continue
        sha, _, body = chunk.partition("\x1f")
        if len(sha) != 40:                        # 边界:格式异常不污染分母
            continue
        stats["total"] += 1
        low = body.lower()
        if any(k in low for k in AGENT_KEYS):
            stats["agent"] += 1
        else:
            stats["human"] += 1
    total = stats["total"]
    return {"since": since, "total": total, "agent": stats["agent"],
            "human": stats["human"],
            "agent_ratio": round(stats["agent"] / total, 3) if total else 0.0}

if __name__ == "__main__":
    repo = sys.argv[1] if len(sys.argv) > 1 else "."
    since = sys.argv[2] if len(sys.argv) > 2 else "2026-10-01"
    print(json.dumps(classify(repo, since), ensure_ascii=False))

参数说明:第一个位置参数是仓库路径,第二个是时间下界,传给 git log --since,接受 ISO 日期。脚本用 %x1f(0x1F)分隔 sha 和提交正文、%x1e(0x1E)分隔不同提交,避免正文里的换行把记录切碎。

在 1842 条提交的样本仓库上跑,预期输出:

{"since":"2026-10-01","total":1842,"agent":1103,"human":739,"agent_ratio":0.599}

耗时约 0.6 秒,内存占用可以忽略。踩过的坑:浅克隆(shallow clone)里 git log --since 只会扫到已有历史,统计值会偏低,CI 上跑之前先确认 git rev-parse --is-shallow-repository 返回 false。

怎么算 push 才不会被批量推送刷量?#

push 是传输层事件,不是逻辑事件。一次 push 可以只带 1 个提交,也可以带 200 个。按次数统计,等于把"打包方式"当成了"工作节奏"。

我建议换成推送批次(push batch,一次 push 包含的提交数)来看,并配一个服务端钩子(pre-receive hook)在入口处把异常批次挡掉。为什么用钩子而不是事后分析:事后只能看到结果,钩子能在写入前拦住,顺带把数据落到自己的日志里。

可执行脚本:pre-receive 钩子限制推送批次

#!/usr/bin/env php
<?php
// 依赖:PHP 8.2 CLI、git 2.40+、timeout(coreutils)
// 放到裸仓库的 hooks/pre-receive 并 chmod +x
// 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
const MAX_COMMITS_PER_PUSH = 40;    // 单次推送最多提交数
const MAX_LINES_PER_COMMIT = 800;   // 单个提交最多增删行数
const GIT_TIMEOUT_SEC = 15;

function git(array $args): string
{
    $cmd = 'timeout ' . GIT_TIMEOUT_SEC . ' git '
         . implode(' ', array_map('escapeshellarg', $args));
    $out = [];
    $code = 0;
    exec($cmd . ' 2>&1', $out, $code);
    if ($code === 124) {
        throw new RuntimeException('git 调用超时');
    }
    if ($code !== 0) {
        throw new RuntimeException(implode("\n", $out));
    }
    return implode("\n", $out);
}

$zero = str_repeat('0', 40);
$input = stream_get_contents(STDIN);
if ($input === false || trim($input) === '') {
    fwrite(STDERR, "pre-receive: 未收到引用更新,放行\n");
    exit(0);
}

foreach (explode("\n", trim($input)) as $line) {
    $parts = preg_split('/\s+/', trim($line));
    if (count($parts) < 3) {
        continue;
    }
    [$old, $new, $ref] = $parts;
    if ($new === $zero) {
        continue;                                  // 删除分支,不计数
    }
    $range = ($old === $zero) ? $new : $old . '..' . $new;
    try {
        $revs = array_values(array_filter(
            explode("\n", git(['rev-list', '--no-merges', $range]))
        ));
    } catch (RuntimeException $e) {
        fwrite(STDERR, "pre-receive: 解析 {$ref} 失败:{$e->getMessage()}\n");
        exit(1);
    }
    if (count($revs) > MAX_COMMITS_PER_PUSH) {
        fwrite(STDERR, sprintf("拒绝 %s:一次推送 %d 个提交,上限 %d\n",
            $ref, count($revs), MAX_COMMITS_PER_PUSH));
        exit(1);
    }
    foreach ($revs as $sha) {
        try {
            $numstat = git(['show', '--numstat', '--format=', $sha]);
        } catch (RuntimeException $e) {
            fwrite(STDERR, "pre-receive: 读取 {$sha} 失败\n");
            exit(1);
        }
        $lines = 0;
        foreach (explode("\n", trim($numstat)) as $row) {
            $cols = explode("\t", $row);
            if (count($cols) < 2) {
                continue;
            }
            if ($cols[0] === '-' || $cols[1] === '-') {
                continue;                          // 二进制文件,跳过
            }
            $lines += (int)$cols[0] + (int)$cols[1];
        }
        if ($lines > MAX_LINES_PER_COMMIT) {
            fwrite(STDERR, sprintf("拒绝 %s:提交 %s 改动 %d 行,上限 %d\n",
                $ref, substr($sha, 0, 8), $lines, MAX_LINES_PER_COMMIT));
            exit(1);
        }
    }
    fwrite(STDERR, sprintf("通过 %s:%d 个提交\n", $ref, count($revs)));
}
exit(0);

预取输入是 git 通过 STDIN 传进来的三列文本:旧 sha、新 sha、引用名。全零 sha 表示新建或删除,脚本对新建按单点处理、对删除直接跳过。阈值我取 40 提交 / 800 行,是按团队 P95 反推的——先把阈值设成只告警不拦截跑一周,拿到真实分布再收紧。正常推送时 stderr 会打印 通过 refs/heads/main:7 个提交,超限时退出码 1,git 客户端会看到拒绝原因。

顺带提一个去重细节:按 sha 数提交会被 rebase 干扰,要更准的话用 git log | git patch-id --stable 算补丁指纹,同一改动无论 sha 怎么变,指纹不变。

怎么用 API.PLUS 接口交叉校验提交来源?#

trailer 是自报的,能伪造,也能漏写。加一层外部信号会更稳:agent 通常在 CI runner 上跑,runner 的出口 IP 有登记。把 IP 归属地和提交里的 runner 标识对比,能抓出"标了 agent 但不是从 runner 来的"这类异常。

可执行脚本:带超时与重试的 IP 归属查询

# 依赖:curl 8.x
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
curl -sS \
  --max-time 5 \
  --retry 2 --retry-delay 1 --retry-connrefused \
  -G 'https://www.api.plus/api/get' \
  --data-urlencode 'apikey=YOUR_KEY' \
  --data-urlencode 'ip=8.8.8.8' \
  -H 'Accept: application/json' \
  -o /tmp/apiplus_ip.json \
  -w '\nHTTP %{http_code} in %{time_total}s\n'

参数逐个说:--max-time 5 是整个请求的硬超时,防止钩子里卡死;--retry 2 配 --retry-connrefused 只对连接类错误重试两次,不对 4xx 重试,避免把鉴权失败刷成限流;-G 让 --data-urlencode 拼成查询串;YOUR_KEY 换成控制台里的实际密钥,ip 是要查的地址,这里用 8.8.8.8 做连通性验证。

返回片段(字段名以 GET 示例接口文档 为准,这里只演示取值位置):

{"code":200,"msg":"ok","data":{"ip":"8.8.8.8","country":"美国","region":"","city":"","isp":"Google LLC"}}

HTTP 200 in 0.182s

PHP 侧解析时注意两点:code 不等于 200 要按失败处理而不是当空数据;data 可能为 null,取字段前先判空。Python 侧的封装可以写成这样,超时和退避都显式写死,不做无限重试:

# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
import json
import time
import urllib.error
import urllib.parse
import urllib.request

def fetch_ip_owner(apikey: str, ip: str, timeout: float = 5.0, retries: int = 2) -> dict:
    query = urllib.parse.urlencode({"apikey": apikey, "ip": ip})
    url = f"https://www.api.plus/api/get?{query}"
    last = None
    for i in range(retries + 1):
        try:
            req = urllib.request.Request(url, headers={"Accept": "application/json"})
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                payload = json.loads(resp.read().decode("utf-8"))
            if payload.get("code") != 200:
                raise RuntimeError(f"接口返回 code={payload.get('code')}")
            return payload.get("data") or {}
        except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, RuntimeError) as e:
            last = e
            time.sleep(0.5 * (2 ** i))          # 0.5s、1s、2s 退避
    # 降级:查不到就返回空字典,由调用方决定是否拦,不直接判失败
    return {"_error": str(last)}

拿到结果后的判断逻辑,我倾向"不一致只标记、不阻断":把 isp 与登记的服务商比对,不匹配就在提交上打一个 Source-Unverified: true 的 trailer,让后续统计能把这批数据单独拉出来看。直接拒绝推送会把偶发的网络抖动变成发布事故。

监控与告警清单#

对比表:建议先落地的六个指标

指标建议阈值(示例)告警动作
agent 提交占比(7 日滚动)> 75%通知技术负责人,抽查 20 条提交的评审记录
单提交变更行数中位数连续 3 天 < 15 行检查提交是否被切碎,调 agent 的提交策略
单次推送提交数 P95> 40统计 pre-receive 拒绝次数,复核批处理逻辑
14 天提交存活率< 85%拉 revert / amend 清单,定位高返工模块
trailer 缺失率> 10%阻断合并,补标后再入
IP 校验不一致率> 2%暂停该 runner 的写权限,人工核对

存活率的算法要写死,别用"是否被 revert"这种单一口径:取提交在 14 天后仍被 HEAD 包含、且该文件未被同名逻辑整体替换,两条都满足才算存活。我见过用 revert 单指标统计,存活率虚高到 97%,把问题全盖住了。

风险清单#

风险清单:读指标时最容易踩的五个坑

风险影响规避方式
trailer 由 agent 自报,可伪造或漏写来源统计失真,agent 占比偏低叠一层 runner IP 或签名校验,缺失率单独监控
force push 改写 sha提交数重复计数,趋势线出现尖刺用 patch-id 去重,统计前先过滤受保护分支
小步提交刷量commit 数虚高,人效被高估用变更行数中位数做归一,不单看总数
pre-receive 阈值设得过紧误拦正常发布,阻塞交付灰度仓库先跑 7 天只告警,按真实 P95 定阈值
校验接口超时被当成失败正常提交被拒,排查成本高接口异常降级为"未知",只标记不阻断

总结#

Octoverse 2026 的提交与推送新高是真实的,但它衡量的东西变了。commit 记录的是"有多少次落盘",push 记录的是"用什么节奏同步",两者都不再等价于产出。想读准,就补三件事:让 agent 在提交里留 trailer、在服务端用钩子管住推送批次、用 14 天存活率兜底看质量。缺任何一件,剩下的数字都容易被解释成自己想要的结论。

适用范围

  • 有 agent 参与日常提交、且团队在 5 人以上的仓库
  • 使用 git 2.32+ 与能挂 pre-receive 钩子的自建或托管远端
  • 已经要求提交信息写 trailer 或愿意在两周内推行
  • 需要向管理层解释研发投入与产出的团队

不适用范围

  • 单人项目或提交量每周不足 20 条的仓库,统计噪声大于信号
  • 以 monorepo 大规模代码生成(build 产物入库)为主的仓库,行数指标会失效
  • 远端是只读镜像、没有钩子能力的场景
  • 把 commit 数直接当 KPI 发放绩效的组织,任何口径都会被反向优化

延伸阅读#

想直接动手的话,可以先看这些接口文档和相关文章:

常见问题(FAQ)

agent 提交占比多少算正常?
没有通用阈值。先跑 7 天基线,把 agent 占比和 14 天提交存活率放一起看。存活率低于 85% 时,占比再高也说明不了产能。
为什么不能用 push 次数衡量 agent 活跃度?
push 是传输层动作,不是逻辑工作量。微软在 agent 评测中把远端从 GitHub 换成本地 git remote,agent 主动 push 的频率低了 11 个百分点,被评估的能力并没有变化。
团队还没有 trailer 规范怎么办?
先在 pre-receive 里只统计缺失率、不拦截,跑两周拿到基线;再按 10% 阈值灰度拦截,同时给 agent 侧提交模板补上 --trailer 参数。

参考来源

  1. Octoverse: Commits, pushes, reach new highs in 2026 as developers build with agents
  2. AWS Weekly Roundup: Amazon Polly Generative engine, TorchNeuron for Trainium, and more (October 26, 2026)
  3. GitHub Universe 2026: Evolving every layer of the stack
  4. Can a local git remote replace GitHub in your agent eval?
  5. SEGGER Tools Gain Deeper Integration in CLion with AI-assisted Debugging
  6. JetBrains Academy – October Digest
  7. [$] LWN.net Weekly Edition for October 29, 2026
  8. A JSON schema Is not an AI governance layer

以上为本文写作时的参考资料,引用观点以原文为准。

作者:沐小悠

本文约 30 分钟读完 · 关键词:Octoverse 2026、commit 统计、push 指标、Agent 提交、git trailer

更多文章 GET 示例接口

「」标签下的其他文章