AIエージェントで自動化したのに効果がない原因と対処|「動いた」と「効いた」を分ける測り方、UTMで流入元を数える方法、見直す日を先に決める判定スクリプト(Python)

手を振る小型ロボットを見ながら、手持ちカウンターを押すななみ。

こんにちは、おうどんです🍜

おうどん

ななみさん、うちの呼び込みロボット、今月も1日も休まず働いたよ

ななみ

それで、お店に入ったお客さんは何人ですか?

おうどん

……数えてない

(会話と呼び込みロボットは説明用に作ったものです。数字とコードの結果は本物です)

うどん屋の前に、呼び込みロボットの「よびこくん」が立っています。
毎朝きっちり起動して、「いらっしゃいませ!」を言い続ける。故障なし。皆勤賞。

でも、のれんをくぐったお客さんを、誰も数えていない。

これ、笑い話ではなく、おうどんの話です。
おうどんは X(旧 Twitter)に、Buffer という予約投稿のサービスを使って毎日自動で投稿しています。
そして note のダッシュボードで流入元(読者がどこから来たか)を見ると、X から来た PV(ページが読まれた回数)は、過去28日で 3 でした(2026-10-23 時点)。

毎日動いて、28日で3。
よびこくん、皆勤賞なのに、記事への流入は28日で3PVでした。

この記事では、AIエージェント(指示を受けて、調べる・書く・投稿するところまで自分で進める AI)や自動化で「ちゃんと動いているのに効果がない」とき、何をどう測ればいいかを、Python で動かしながら整理します。

まずやるのは、この3つです

  • 「動いた回数」ではなく「効いたかを表す数」を1つ決める
  • その数を見る場所を決める(投稿するリンクに UTM を付ける)
  • 見直す日を決めて、その日までは判定しない
目次

「動いた」と「効いた」は、別の帳簿につける

自動化の結果は、3段に分けて見ると迷子になりません。

段意味呼び込みロボットならX の自動投稿なら
動いた決めた作業が実行された「いらっしゃいませ」を言った回数投稿された本数
届いた相手の目に入った足を止めた人の数表示された回数
効いた目的の行動が起きたのれんをくぐった人の数記事が読まれた数、スキ、フォロー

自動化のツールやエージェントが自分から教えてくれるのは、たいてい1段目だけです。
「投稿しました」「完了しました」。うそではありません。動いたのは本当です。

おうどん

よびこくんの日報、毎日『本日も元気に呼び込みました』の一行だけ

日報としては満点です。経営の資料としては、0点です。

試しに、おうどんの数字(28日で X から 3PV)を、1日1本投稿したと仮定して割り算してみます。

posts = 28   # 28日間、1日1本投稿したと仮定
pv_from_x = 3

print(f"動いた: {posts}/28 日 投稿 ({posts / 28:.0%})")
print(f"効いた: X からの PV {pv_from_x}")
print(f"1投稿あたり: {pv_from_x / posts:.2f} PV")
print(f"1PV に必要な投稿: {posts / pv_from_x:.1f} 本")
動いた: 28/28 日 投稿 (100%)
効いた: X からの PV 3
1投稿あたり: 0.11 PV
1PV に必要な投稿: 9.3 本

(今回の検証環境:Windows の Python 3.13.17 で実行しました)

稼働率は 100%。1PV に 9.3 本。
同じ28日の数字なのに、上の2行と下の2行で、受ける印象がまるで違います。

新人

稼働率 100% なら、上司には褒められそうです

おうどん

褒められるよ。で、来月も同じ報告をして、また褒められる

それを1年続けると、365本投稿して、約39PV。
もちろん、この割り算は「1日1本」と「この28日のペースが続く」を仮定した、ざっくりした見積もりです。

AIエージェントの「完了しました」は、動いたほうの報告

なぜ、自動化は1段目の報告ばかりになるのか。
理由はシンプルで、指示が「行動」で書かれているからです。

「毎日 X に投稿して」と頼めば、エージェントのゴールは投稿です。
投稿できたら完了。そこに「読まれたか」は入っていません。頼んでいないので。

おうどん

よびこくんに『お客さんを連れてきて』って頼めばよかったのか

新人

それを頼むと、通行人を担いで店に入れてきそうです

言い方は考えます。

Anthropic の成功基準と評価の作り方のドキュメントでは、成功基準は Specific(具体的)・Measurable(測れる)・Achievable(届く)・Relevant(目的に合っている)であるべき、と説明されています。
悪い例として「安全な出力」のような書き方、良い例として「1万回の試行のうち、フィルターに引っかかる出力が 0.1% 未満」のような、数と条件のある書き方が並んでいました。

このドキュメントは LLM(大規模言語モデル)を使ったアプリの評価の話ですが、自動化の効果を測るときにも、この4つはそのまま使えます。

観点ダメな例直した例
SpecificX をがんばるX の投稿から記事に来てもらう
Measurable反応がよくなるutm_source=x の PV を28日ごとに数える
Achievable1日100PV28日で 3 → 10
Relevant投稿本数を増やす記事が読まれた数を見る

いちばん効くのは Relevant の行です。
投稿本数は測りやすいけれど、目的(読んでもらう)とつながっていない。測りやすい数ほど、目的からずれやすいんです。

おうどん

ダイエットも、体重より体重計に乗った回数のほうが測りやすい

毎日乗れば皆勤賞です。体重のほうは、誰も知りません。

なお、成果物がそもそもできているか(空ファイルではないか)の確かめ方は、AIエージェントの定期実行が「完了しました」なのに成果物がない原因の記事に書きました。今日は、その先。ちゃんとできているのに効いていない話です。

測る場所を先に決める:投稿するリンクに UTM を付ける

「効いたか」を数えるには、X から来た人を見分ける必要があります。
いちばん確実なのは、投稿するリンクに UTM パラメータ(URL の ? のうしろに付ける、どこから来たかの札)を付けることです。

Google アナリティクスのヘルプでは、リンク先の URL に UTM パラメータを付けると、どのキャンペーンから人が来たかを見られる、と説明されています。よく使うのはこの3つです。

パラメータ意味例
utm_sourceどこから来たかx
utm_mediumどんな手段で来たかsocial
utm_campaignどの施策かdaily_post

読者「札を付けるだけなら、URL の最後に ?utm_source=x を足せばいいのでは?」

それで大丈夫な URL もあります。大丈夫じゃない URL もあります。
文字列の足し算と、Python で URL を分解してから付ける方法を比べます。

from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode


def naive(url, tags):
    return url + "?" + urlencode(tags)


def add_utm(url, tags):
    parts = urlsplit(url)
    query = [(k, v) for k, v in parse_qsl(parts.query, keep_blank_values=True)
             if not k.startswith("utm_")]
    query += list(tags.items())
    return urlunsplit(parts._replace(query=urlencode(query)))


tags = {"utm_source": "x", "utm_medium": "social", "utm_campaign": "daily_post"}

urls = [
    "https://example.com/udon/",
    "https://example.com/udon/?page=2",
    "https://example.com/udon/#dashi",
]

for u in urls:
    print("元    :", u)
    print("足し算:", naive(u, tags))
    print("分解  :", add_utm(u, tags))
    print()
元    : https://example.com/udon/
足し算: https://example.com/udon/?utm_source=x&utm_medium=social&utm_campaign=daily_post
分解  : https://example.com/udon/?utm_source=x&utm_medium=social&utm_campaign=daily_post

元    : https://example.com/udon/?page=2
足し算: https://example.com/udon/?page=2?utm_source=x&utm_medium=social&utm_campaign=daily_post
分解  : https://example.com/udon/?page=2&utm_source=x&utm_medium=social&utm_campaign=daily_post

元    : https://example.com/udon/#dashi
足し算: https://example.com/udon/#dashi?utm_source=x&utm_medium=social&utm_campaign=daily_post
分解  : https://example.com/udon/?utm_source=x&utm_medium=social&utm_campaign=daily_post#dashi

1つ目は、どちらも同じです。
2つ目の足し算は ? が2回出てきます。この URL を Python の parse_qsl で読み直すと、page の値が「2?utm_source=x」になり、utm_source という札は1枚も見つかりませんでした(utm_medium と utm_campaign は見つかる)。
3つ目の足し算は、札が # のうしろ(ページの中の位置を表すフラグメント)に入ってしまい、パラメータになっていません。

おうどん

だしの章に、札がめり込んでる

だしに札を入れても、味は変わりません。数も数えられません。

分解する方法なら、元のパラメータを残して & でつなぎ、# の前に札を入れてくれます。すでに付いている utm_ は付け直すので、2回付けても札が重なりません。
AIエージェントに投稿させるなら、文面を作る指示とは別に、「リンクはこの関数を通してから貼る」と決めておくと安心です。

🔰 ここまで読めば今日から困らない

  • 自動化の報告(投稿しました・完了しました)は「動いた」の報告。「効いた」は自分で数える
  • 効いたかを表す数を1つ決め、投稿するリンクに utm_source などの札を付けて見分ける
  • 札は URL の文字列に足さず、分解してから付ける(? が2回、# のうしろ、に注意)

ここから先は中級者向けです。札の数え方の落とし穴と、見直す日の決め方を見ます。

Referer だけで数えると、X から来た人が迷子になる

札を付けなくても、アクセス解析には「どこから来たか」が出ることがあります。
ブラウザが送る Referer(リファラー。どのページのリンクから来たかを伝えるヘッダー)を使っているからです。

ただし、MDN の Referer の説明によると、Referer に何を入れるかはリファラーポリシーで決まります。
MDN の Referrer-Policy の説明では、ポリシーが指定されていないときの既定は strict-origin-when-cross-origin で、HTTPS のまま別のサイトへ移るときはオリジン(https://example.com/ のような、サイトの名前の部分)だけを送り、HTTPS から HTTP へ移るときは送らない、と書かれています。no-referrer というポリシーなら、まったく送りません。

つまり Referer は、来ないこともあるし、来てもサイト名だけのことがある。
よびこくんでいうと、お客さんに「どこから来ました?」と聞いても、「あっち」としか言わない人と、無言の人がいるわけです。

新人

無言の人は、帳簿ではどこの人になるんですか

「(なし)」さんです。調べてみると、けっこうな大家族です。

説明用の架空のアクセス記録で、数え方による違いを見ます。

from collections import Counter
from urllib.parse import urlsplit, parse_qs

# 説明用の架空のアクセス記録(来たURL, Referer。Referer なしは "-")
visits = [
    ("/udon/?utm_source=x&utm_medium=social", "-"),
    ("/udon/?utm_source=X&utm_medium=social", "https://x.com/"),
    ("/udon/", "https://x.com/"),
    ("/udon/", "https://www.google.com/"),
    ("/udon/", "-"),
    ("/udon/", "-"),
    ("/udon/?utm_source=x&utm_medium=social", "https://x.com/"),
]


def by_referer(url, ref):
    return urlsplit(ref).hostname or "(なし)"


def by_utm_then_referer(url, ref):
    src = parse_qs(urlsplit(url).query).get("utm_source")
    if src:
        return "utm:" + src[0]
    return by_referer(url, ref)


def by_utm_lower(url, ref):
    src = parse_qs(urlsplit(url).query).get("utm_source")
    if src:
        return "utm:" + src[0].lower()
    return by_referer(url, ref)


for name, f in [("Referer だけ", by_referer),
                ("UTM→Referer", by_utm_then_referer),
                ("UTM(小文字)→Referer", by_utm_lower)]:
    c = Counter(f(u, r) for u, r in visits)
    print(name, dict(sorted(c.items())))
Referer だけ {'(なし)': 3, 'www.google.com': 1, 'x.com': 3}
UTM→Referer {'(なし)': 2, 'utm:X': 1, 'utm:x': 2, 'www.google.com': 1, 'x.com': 1}
UTM(小文字)→Referer {'(なし)': 2, 'utm:x': 3, 'www.google.com': 1, 'x.com': 1}

見るところは3つです。

  • Referer だけだと、1行目の札付きの訪問(Referer なし)が「(なし)」に入る。X から来たのに、出どころ不明の扱い
  • 札を先に見ると、x と X が別々に数えられる。Google アナリティクスのヘルプにも、パラメータの値は大文字と小文字を区別する(utm_source=google と utm_source=Google は別)と書かれています
  • 小文字にそろえると、札付きの3件がまとまる。札のない X の訪問(3行目)は、Referer で拾う
おうどん

X さんと x さん、別人として来店記録に載ってた

新人

同じ人ですよね?

同じ人です。でも帳簿は、名札の字で見分けます。
だから札の値は、最初から小文字で決めておきます(ヘルプでも、小文字で統一するのがふつう、とされています)。

ちなみに、おうどんの note のダッシュボード(2026-10-23 時点、過去28日)では、流入元が「no referrer」の割合が 23.5%、X が 0.9% でした。
no referrer の中に X から来た人がどれだけ混じっているかは、この画面からは分かりません。混じっている可能性はある、までです。
「X から 3PV」は、少なめに数えている可能性もある。だからこそ、札を付けて自分で見分けられるようにしておく価値があります。

なお、note のダッシュボードで UTM の札ごとに数を見られるかは、今回は確かめていません。札が役に立つのは、札を読むアクセス解析を自分のサイトに入れている場合です。

見直す日を先に決める:少ない数は、毎日ゆれる

札を付けたら、次は「いつ判定するか」です。

28日で3PV くらいの少ない数は、何もしなくても大きくゆれます。
人がばらばらに、たまたま来ると仮定して(ポアソン分布という、まれな出来事の回数の分布で計算します)、平均3回のとき、実際に何回になりやすいかを出してみます。

import math

lam = 3  # 28日で平均3PV が来ると仮定

print("k  確率")
total = 0.0
for k in range(0, 9):
    p = math.exp(-lam) * lam ** k / math.factorial(k)
    total += p
    print(f"{k}  {p:6.1%}")

p_le_5 = sum(math.exp(-lam) * lam ** k / math.factorial(k) for k in range(6))
print(f"0回の確率      : {math.exp(-lam):.1%}")
print(f"6回以上の確率  : {1 - p_le_5:.1%}")
k  確率
0    5.0%
1   14.9%
2   22.4%
3   22.4%
4   16.8%
5   10.1%
6    5.0%
7    2.2%
8    0.8%
0回の確率      : 5.0%
6回以上の確率  : 8.4%

何も変えていないのに、28日で6回以上になる確率が約 8%。
「投稿の時間を変えたら、X からの PV が倍になった!」は、この計算の前提の上では、何も変わっていなくても1割弱は起きることになります。

おうどん

よびこくんの声を高くしたら、お客さんが倍に

新人

その日、近くでお祭りがあっただけでは

ほかにも、ばらつきを大きくする理由はあります。投稿が誰かにシェアされて、まとめて人が来ることもある。実際はこの計算より、もっとゆれると思っておいたほうが安全です。

だから、見直す日を先に決めて、その日までは判定しない。
そして判定の基準(ここまで来たら続ける、ここを下回ったらやめる候補)も、数字を見る前に決めておきます。数字を見てから基準を決めると、たいてい自分の施策に甘くなるので。

計画を JSON に書いて、その日に判定するスクリプトにしました。

{
  "施策": "X への毎日の自動投稿",
  "測るもの": "X から来た PV(28日)",
  "測る場所": "アクセス解析の utm_source=x",
  "基準値": 3,
  "目標": 10,
  "やめる線": 5,
  "見直す日": "2026-12-08"
}
"""自動化の「効いたか」を、決めた日に決めた基準で判定する。

使い方: python t5_review.py plan.json 今日の日付 測った値
終了コード: 0=続ける/まだ判定しない 1=見直す 2=やめる候補 3=計画の不備
"""
import json
import sys
from datetime import date

REQUIRED = ["施策", "測るもの", "測る場所", "基準値", "目標", "やめる線", "見直す日"]


def main():
    plan_path, today_s, value_s = sys.argv[1:4]
    with open(plan_path, encoding="utf-8") as f:
        plan = json.load(f)

    missing = [k for k in REQUIRED if k not in plan]
    if missing:
        print("計画が足りません:", ", ".join(missing))
        return 3
    if plan["やめる線"] >= plan["目標"]:
        print("やめる線は目標より小さくしてください")
        return 3

    today = date.fromisoformat(today_s)
    review = date.fromisoformat(plan["見直す日"])
    value = float(value_s)

    print(f"施策: {plan['施策']} / {plan['測るもの']}({plan['測る場所']})")
    if today < review:
        print(f"まだ判定しません。見直す日まであと {(review - today).days} 日(今の値 {value:g})")
        return 0

    print(f"基準 {plan['基準値']:g} → 今回 {value:g}(目標 {plan['目標']:g}・やめる線 {plan['やめる線']:g})")
    if value >= plan["目標"]:
        print("判定: 続ける")
        return 0
    if value < plan["やめる線"]:
        print("判定: やめる候補(人が決める)")
        return 2
    print("判定: 見直す(やり方を1つだけ変えて、次の見直す日を決める)")
    return 1


if __name__ == "__main__":
    sys.exit(main())

見直す日の前に動かすと、判定しません。

python t5_review.py plan.json 2026-11-20 4
施策: X への毎日の自動投稿 / X から来た PV(28日)(アクセス解析の utm_source=x)
まだ判定しません。見直す日まであと 18 日(今の値 4)

見直す日に、3通りの値で動かした結果です。

python t5_review.py plan.json 2026-12-08 12
python t5_review.py plan.json 2026-12-08 7
python t5_review.py plan.json 2026-12-09 2
施策: X への毎日の自動投稿 / X から来た PV(28日)(アクセス解析の utm_source=x)
基準 3 → 今回 12(目標 10・やめる線 5)
判定: 続ける
施策: X への毎日の自動投稿 / X から来た PV(28日)(アクセス解析の utm_source=x)
基準 3 → 今回 7(目標 10・やめる線 5)
判定: 見直す(やり方を1つだけ変えて、次の見直す日を決める)
施策: X への毎日の自動投稿 / X から来た PV(28日)(アクセス解析の utm_source=x)
基準 3 → 今回 2(目標 10・やめる線 5)
判定: やめる候補(人が決める)

終了コードは、上から 0・1・2 でした(1回ずつ別に実行し、出力を並べています)。
「測る場所」と「見直す日」を書き忘れた計画を渡すと、判定の前に止まります。

計画が足りません: 測る場所, 見直す日

こちらの終了コードは 3 でした。
測る場所と見直す日のない計画は、よびこくんに「いい感じに頑張って」と言っているのと同じです。いい感じに、毎日頑張ってくれます。

目標・やめる線の 10 と 5 は、説明用に置いた数です。自分の施策では、前の期間の数(基準値)を見てから、数字を見る前に決めてください。

AIエージェントへの指示に、何を書くか

ここまでの話を、エージェントへの指示に落とすとこうなります(おうどんの考えた例で、この指示でエージェントを動かして確かめたものではありません)。

# X 投稿タスク
- 目的: X の投稿から、記事を読みに来てもらう
- 投稿するリンクには、必ず utm_source=x&utm_medium=social&utm_campaign=daily_post を付ける
  (URL の文字列に足さず、既存の ? と # を保ったまま付ける)
- 完了報告には「投稿した」とだけ書く。読まれた・効果があった、とは書かない
- 札の値・測る場所・判定の基準は変えない。変えたほうがよいと思ったら、提案として報告する
- 効果の判定は、計画ファイルの見直す日に人が行う

ポイントは2つです。

1つは、エージェントに「効いた」を名乗らせないこと。
エージェントが見られるのは、たいてい自分が投稿したところまでです。見ていないものを「効果がありました」と報告されるほうが、空の報告より困ります。

おうどん

よびこくんが『今日は大盛況でした!』って報告してきたら、うれしくて信じちゃう

新人

店の中、見てないのに

もう1つは、物差しを本人に触らせないこと。
札の値や判定の基準をエージェントが自分で直せると、数字が良く見える方向に、悪気なく寄っていく可能性があります。物差しを決めるのも、判定するのも人、が安全です(これもおうどんの考えです)。

おうどん

麺の長さを、麺を打った本人のメジャーで測ったら、全部1メートル

測るたびに、少しずつ伸びていきそうです。

上級者向け:効果を1つの数で見ないための設計

ここから先は上級者向け。読み飛ばしてもOKです。

物差しは複数、ただし主役は1つ

Anthropic のドキュメントには、ほとんどのユースケースでは、いくつかの成功基準にまたがる多次元の評価が必要、とあります。例として、精度・有害な出力の少なさ・応答時間などを同時に満たす基準が並んでいました。

自動投稿にあてはめると、主役の数(X から来た PV)に、守りの数を添える形です。

種類例役目
主役utm_source=x の PV(28日)効いたかを決める
守りフォロワーの増減、投稿への苦情数字を上げるために、評判を削っていないか
費用人が確認に使った時間、ツールの料金効いていても、割に合うか

主役を2つ以上にすると、片方が上がって片方が下がった日に、決められなくなります。
よびこくんの評価を「入店数」と「声の大きさ」の2本立てにすると、静かな日の大繁盛を、どう評価していいか分からなくなる。

おうどん

声の大きさで評価したら、よびこくん、近所から苦情

守りの数の出番です。

比べるなら、基準値と同じ条件で

同じドキュメントには、基準となるモデルや前のバージョンと比べる A/B テストも、評価の方法として挙がっています。良い例の成功基準にも、今の基準値から 5% の改善、という形で基準値が入っていました。

自動投稿でいうと、基準値は「前の28日の数」です。
ただし、前の28日と今の28日で、記事の本数・季節・投稿した時間が違えば、差がどれのせいかは分かりません。

おうどん

先月はお祭り、今月は台風。比べる条件が毎月ちがう

うどん屋の売り上げ会議は、だいたい天気の話で終わります。
変えるのは1回に1つだけ、と判定スクリプトの「見直す」の文に書いたのはそのためです。

UTM と Referer の細かい落とし穴

  • 札の値は大文字と小文字を区別する(Google アナリティクスのヘルプ)。x と X、social と Social は別の行になる
  • 札の一部を付け忘れると、レポートでは (not set) になる(同じヘルプ)。3つは必ずセットで付ける
  • 札付きの URL を読んだ人がそのままコピーして別の場所に貼ると、その先から来た人も utm_source=x として数えられます。札は「最初にどこで見つけたか」ではなく「どの URL で来たか」の印だ、と覚えておくと読み違えません(これは札の仕組みからの考えで、実測はしていません)
  • Referer は既定ではサイト名だけで、どの投稿から来たかまでは分からない(MDN の Referrer-Policy)。投稿ごとに比べたいなら、utm_content などで投稿を見分ける札を足す

ポアソンの計算は、楽観的な見積もり

さっきの計算は、人が1人ずつばらばらに来る、と仮定しています。
実際には、1つの投稿がシェアされて何人かがまとめて来ることがあり、そうなると回数のばらつきはこの計算より大きくなります。

「6回以上が 8.4%」は、人が独立に来ると仮定した、単純化した目安として使ってください。少ない数で決めたいなら、期間を長くするか、数の多い段(届いた=表示回数)を補助に見るのが現実的です。

おうどん

つまり、たまたまの可能性は、計算よりもっと大きい

新人

計算した意味は……

あります。少なくとも「倍になったから効いた」と言う前に、一度止まれます。

皆勤賞のよびこくんに、入店数の帳簿を

冒頭のよびこくんは、1日も休まず呼び込んで、お客さんの数は誰も数えていませんでした。

壊れていたのは、よびこくんではありません。
「呼び込んで」とだけ頼んで、何人入ったら成功かを決めていなかった、頼み方のほうです。

ななみ

数えてないと気づいて、自分の数字を記事に出せるのは、ちゃんとした人のやることですよ

おうどん

褒められたので、のれんの横にカウンターを置いてきます

自動化は、動いたことではなく、効いたかで評価する。
そして効いたかは、測る場所と見直す日を先に決めないと、いつまでたっても分かりません。

まずは、自動で貼っているリンクに utm_source の札を1枚付けるところから🍜

参考資料

2026年11月時点の内容です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

おうどん|癒しと創作をたのしむ雑食クリエイター
写経アプリやクレイセラピー、優しい和風デザインがすき。
ZARDと刀剣と文字に癒されて、今はアプリ作ってます。

コメント

コメントする

目次