0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Xのリプ運用にreview gateを入れる:高インプレッション投稿へ自然に参加するための最小設計

0
Posted at

Xリプ運用のreview gate - 高インプレッション投稿へ自然に参加する

X engagement review gate - candidate、risk check、tone check、human approve、post log

はじめに

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つです。

  1. アカウントが @agent_memories
  2. 相手投稿と話題が一致しているか
  3. 炎上、攻撃、誤情報に乗っていないか
  4. リプが宣伝文になっていないか
  5. 当日の上限を超えていないか

実装では、判定結果を構造化しておきます。

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.proposedReplydraftReply に対して行います。候補投稿のリスク判定と、生成リプ本文の口調判定は別の入力として扱うのが安全です。

実行ログに残す

投稿したら、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です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?