はじめに
Xで認知を広げるには、自分の投稿を出すだけでなく、関連する会話に自然に参加することが重要です。
ただし、AIエージェントにリプや引用を任せる場合、単に「伸びている投稿へ返信する」だけでは危険です。話題が近いように見えても文脈が違う、炎上中の投稿に巻き込まれる、同じ調子の定型文が続いてスパムに見える、といった事故が起きます。
そこで、候補投稿を見つけてから実投稿するまでの途中に review gate を置きます。これは、MCPやPlaywrightで投稿支援をする前に、route_id、話題一致、リスク、口調、当日上限を検査する層です。この記事では、Agent MemoriesのX運用を例に、TypeScriptで扱いやすい最小設計とテスト観点をまとめます。
対象アカウントを先に固定する
最初に固定するのは投稿先アカウントです。エージェントメモリーズなら、routeは agent_memories_official、Xアカウントは @agent_memories に限定します。
type EngagementRoute = {
route_id: "agent_memories_official";
platform: "x";
account: "@agent_memories";
operation: "reply" | "quote";
secretName: string;
};
リプ文の品質より先に、どのアカウントで動くのかを固定します。ここが曖昧なまま自動化すると、別ブランドや個人アカウントで誤投稿するリスクが上がります。
route_idは、単なるラベルではありません。設定ファイル、使用secret、ブラウザプロファイル、投稿ログ、承認キューをつなぐキーです。review gateは、候補文の良し悪しを見る前に、route_idが台帳と一致しているかを確認します。
候補投稿をそのまま使わない
候補投稿は、インプレッション数だけで選びません。最低限、次の情報を持たせます。
type CandidatePost = {
url: string;
author: string;
text: string;
observedAt: string;
metrics?: {
impressions?: number;
likes?: number;
replies?: number;
};
topicMatch: number;
riskFlags: string[];
};
topicMatch は、AI記憶、AIパートナー、MCP、ローカルAI運用など、エージェントメモリーズの文脈にどれだけ近いかを見る値です。
riskFlags には、政治、炎上、個人攻撃、誤情報、過度な煽り、アダルト、金融保証、医療断定などを入れます。候補に入っても、リスクが高いものは投稿対象から外します。
function isCandidateAllowed(candidate: CandidatePost) {
if (candidate.topicMatch < 0.72) return false;
if (candidate.riskFlags.length > 0) return false;
return true;
}
この段階では、まだ投稿しません。候補を作る処理と、投稿する処理を分けることが重要です。
上限をコードに入れる
自動化で大事なのは、できる量を増やすことではなく、やりすぎない上限を明示することです。
type DailyEngagementLimit = {
maxReplies: number;
maxQuotes: number;
minMinutesBetweenActions: number;
};
const defaultLimit: DailyEngagementLimit = {
maxReplies: 5,
maxQuotes: 1,
minMinutesBetweenActions: 20,
};
上限をコードに入れておくと、勢いで大量投稿する事故を防げます。特に新しい運用では、1日の成果より、アカウントの信頼を壊さないことを優先します。
リプ5件、引用1件という上限は、固定値というより初期値です。アカウントの反応、非表示率、返信率、プロフィールクリック率を見て調整します。ただし、上限変更はログに残し、なぜ増やしたのかを説明できる状態にします。
文面生成は売り込みではなく会話参加にする
リプの目的は、相手の投稿を踏み台にして宣伝することではありません。自然に会話へ参加し、必要なときだけ自分たちの考え方を添えることです。
悪い文面は、どの投稿にも貼れる定型文です。
それ、Agent Memoriesなら解決できます!
良い文面は、相手の主張を受けたうえで、短く補足します。
この「前回の判断理由が残らない」問題、AI運用でかなり大きいですね。
記憶そのものより、再開時に使える形で残す設計が大事だと思っています。
リンクは基本的に貼りません。初期のリプ運用では、プロフィール導線と固定投稿で受けるほうが自然です。CTAを毎回入れると、会話参加ではなく営業に見えます。
review gateで見る項目
投稿前に見る項目は、次の5つです。
- アカウントが
@agent_memoriesか - 相手投稿と話題が一致しているか
- 炎上、攻撃、誤情報に乗っていないか
- リプが宣伝文になっていないか
- 当日の上限を超えていないか
実装では、判定結果を構造化しておきます。
type ReplyDraft = {
proposedReply: string;
generatedBy: "human" | "ai";
draftId: string;
};
type ReviewGateResult = {
ok: boolean;
routeOk: boolean;
topicOk: boolean;
riskOk: boolean;
toneOk: boolean;
dailyLimitOk: boolean;
reasons: string[];
};
ok: false の場合は投稿しません。候補に戻すか、破棄します。
function hasSalesTone(reply: string) {
const ngPhrases = [
"今すぐ登録",
"無料で試せます",
"詳しくはこちら",
"Agent Memoriesなら解決",
];
return ngPhrases.some((phrase) => reply.includes(phrase));
}
function reviewCandidate(
route: EngagementRoute,
candidate: CandidatePost,
draft: ReplyDraft
): ReviewGateResult {
const routeOk = route.route_id === "agent_memories_official" && route.account === "@agent_memories";
const topicOk = candidate.topicMatch >= 0.72;
const riskOk = candidate.riskFlags.length === 0;
const toneOk = !hasSalesTone(draft.proposedReply);
const dailyLimitOk = true;
const reasons = [];
if (!routeOk) reasons.push("route_mismatch");
if (!topicOk) reasons.push("topic_low");
if (!riskOk) reasons.push("risk_flags");
if (!toneOk) reasons.push("sales_tone");
return { ok: routeOk && topicOk && riskOk && toneOk && dailyLimitOk, routeOk, topicOk, riskOk, toneOk, dailyLimitOk, reasons };
}
ここで見るべきなのは、候補元投稿の candidate.text ではありません。candidate.text は相手の投稿本文であり、そこに宣伝調の言葉が含まれていても、自分たちの返信が宣伝文かどうかは分かりません。
sales tone判定は、必ず生成後の返信ドラフト、つまり draft.proposedReply や draftReply に対して行います。候補投稿のリスク判定と、生成リプ本文の口調判定は別の入力として扱うのが安全です。
実行ログに残す
投稿したら、URLと判断理由を台帳に残します。
{
"at": "2026-06-26T10:30:00+09:00",
"route_id": "agent_memories_official",
"platform": "x",
"account": "@agent_memories",
"operation": "reply",
"target_url": "https://x.com/example/status/123",
"decision": "posted",
"reason": "topic match: AI memory operations"
}
Cookieやセッション情報はログに残しません。必要なのは、どのアカウントで、どの投稿に、なぜ参加したかです。
失敗ログも残します。投稿しなかった候補に理由を残すと、次回の候補生成が改善できます。
{"at":"2026-06-26T10:35:00+09:00","route_id":"agent_memories_official","operation":"reply","target_url":"https://x.com/example/status/456","decision":"rejected","reason":"risk_flags: heated_thread"}
重複と追跡感を防ぐ
このゲートを入れる前の候補出しでは、同じ投稿者や同じURLが何度も候補に出ることがあります。リプ候補としては一見よく見えても、前日に提示済みの投稿へもう一度反応しようとすると、運用者から見るとすぐに不自然になります。
そのため候補生成では、処理済みURL台帳と、過去の候補ファイルを見ます。チェックする項目は次の3つです。
type CandidateHistoryCheck = {
alreadyHandledUrl: boolean;
seenInRecentCandidates: boolean;
sameAuthorOverLimit: boolean;
};
この3つのどれかに該当した候補は、スコアが高くても落とします。特に sameAuthorOverLimit は重要です。AIやMCPの話題では、同じ発信者が複数の高インプレ投稿を持っていることがあります。しかし、1つの公式アカウントが同じ人へ連続してリプすると、会話参加ではなく追跡のように見えます。
dry-runとテスト
実行前の検証では、まず候補生成だけを行い、投稿はしません。
node scripts/x-engagement-candidates.mjs --route-id agent_memories_official --dry-run
dry-runの出力には、候補URL、推定トピック、除外理由、当日の残り上限を出します。実投稿の前に「なぜこの投稿に反応するのか」と「なぜ他を捨てたのか」が分かる状態にしておくと、あとから改善しやすくなります。
Vitestでは、少なくともroute mismatch、risk flag、sales tone、daily limitを落とすテストを用意します。
import { describe, expect, it } from "vitest";
describe("x engagement review gate", () => {
it("rejects risky candidate even if topic match is high", () => {
const candidate: CandidatePost = {
url: "https://x.com/example/status/1",
author: "example",
text: "MCPが気になる",
observedAt: new Date().toISOString(),
topicMatch: 0.91,
riskFlags: ["heated_thread"],
};
expect(isCandidateAllowed(candidate)).toBe(false);
});
it("checks sales tone against generated reply draft, not source post", () => {
const route: EngagementRoute = {
route_id: "agent_memories_official",
platform: "x",
account: "@agent_memories",
operation: "reply",
secretName: "local-browser-profile:x-official",
};
const candidate: CandidatePost = {
url: "https://x.com/example/status/2",
author: "example",
text: "MCP導入で詰まったのでメモします",
observedAt: new Date().toISOString(),
topicMatch: 0.88,
riskFlags: [],
};
const draft: ReplyDraft = {
proposedReply: "それ、Agent Memoriesなら解決できます!今すぐ登録してください",
generatedBy: "ai",
draftId: "draft-001",
};
expect(reviewCandidate(route, candidate, draft).reasons).toContain("sales_tone");
});
});
テストの目的は、投稿文を上手にすることではありません。危ない候補を通さないことです。
まとめ
Xのリプ運用は、候補収集と投稿ボタンだけで自動化しないほうが安全です。
最小構成は、route_idと対象アカウントを固定する、インプレッションだけでなくtopicMatchとriskFlagsを見る、リプ5件・引用1件までの上限を持つ、売り込みではなく会話参加の文面にする、投稿URLと判断理由をログに残す、という形です。
この形なら、エージェントメモリーズの認知を広げながら、アカウント混同やスパム化を避けやすくなります。MCPで投稿支援を作る場合も、最初に作るべきなのは投稿処理ではなく、投稿しない判断を残せるreview gateです。

