X Ads MCP 上线:广告 Agent 真正难点不是接入,而是预算审批与审计

X Ads MCP 把广告操作搬进对话之后,变化到底在哪里

八月二十四日,X Business 发布了 X Ads MCP 的介绍。公告把广告创建、管理、优化和衡量放进兼容 MCP 的 AI 工具。运营人员可以用自然语言发起投放、查看表现、暂停低效广告或调整预算。对后端来说,变化不是少写几行请求,而是写操作离模型更近了。因此权限边界不能只靠聊天约定。这条底线先固化。

X 的公告明确提到,兼容的 AI 工具可以读取实时广告数据。代理由此能根据活动表现生成分析,也能提出停用或扩量建议。它甚至可能继续调用写工具,把建议变成真实修改。读取和执行在同一个会话里出现时,用户很容易把“建议”误认为“已经完成”。这正是产品便利变成工程风险的地方。

从使用者视角看,一句“把低效活动预算翻倍”很自然。对系统而言,这句话缺少活动编号、币种、时间窗口和上限。代理需要先补齐参数,再判断是否拥有写权限。任何一个字段理解错误,都可能让正确的意图变成错误的扣费动作。网关必须把自然语言停在边界外,把结构化请求留在边界内。

这次变化还会改变故障排查方式。以前可以从接口日志里找到调用方、参数和响应状态。接入代理后,还要追踪用户原话、模型选择、工具决策和重试过程。若只保留最后一次 API 请求,审计人员无法知道预算为何被改成那个数。新的日志也需要同时保存意图、决策、审批和执行四条线索。

广告场景尤其不适合把“能调用”当成“能放权”。广告预算的影响会跨越账户、活动、受众和转化目标。一次激活可能马上产生花费,停用也可能让关键时段失去流量。模型可以帮助人更快理解数据,却不该自动获得同等强度的资金权限。能力和授权必须拆成两张表来分别管理。

读懂二十三个工具,先把读权限和写权限分开

MediaPost 在八月二十五日的报道中提到,X Ads MCP 面向广告数据、分析和活动管理提供二十三个工具。这个数字可以帮助团队估算治理范围,却不能直接当成授权方案。工具数量少,不代表每个动作风险相同。列出活动和激活活动只差几个字,造成的后果却完全不同。权限模型应当看状态变化,而不是看工具名字长短。

读工具只返回信息,写工具会改变广告账户状态。列出账户、读取活动详情、查询统计属于观察动作。创建活动、更新预算、修改定向和激活广告属于变更动作。两者都可能需要账号凭证,但不应共享同一条放行路径。最小权限的第一步,就是在网关里建立不可由模型修改的分类表。

工具分类不能只靠开发者手工记忆。平台更新后,新增工具可能默认落入未知状态。未知状态必须拒绝,而不是自动归入低风险。每次更新都应重新审查工具的输入字段、影响对象和可逆程度。审查结果写入版本化配置,发布时再由网关加载。边界必须能被程序直接判断。

工具类别典型动作默认状态放行条件
读取查询活动和统计允许账号与范围校验
建议生成预算方案允许但不执行保留数据依据
写入修改预算与定向待审批凭证、上限、幂等键
高风险激活或批量变更默认拒绝人工复核与灰度

工具的风险还取决于参数范围。同一个更新接口,改一项素材和改十个活动不是同一类操作。同一个预算字段,增加百分之五和增加到无限大也不能同等处理。网关要把工具名、目标对象、修改字段和数量一起计算。只检查“这是写工具”还不够,还要检查“这次写入有多大”。数量和修改幅度都必须写入网关规则。

授权范围也要分层保存。账号级令牌只能放在服务端,不能被模型直接读取。用户身份、代理身份和审批人身份要分别记录。一个用户有权查看某账户,不等于代理有权改变这个账户。一个审批人能批准小额修改,也不等于能批准批量激活。新工具上线前必须经过安全复核。

团队可以先画出一张工具矩阵,再开始写代码。横轴放读取、建议、更新和激活动作,纵轴放账户、活动、广告组和受众对象。每个格子写清允许的人、允许的时间和允许的金额。矩阵缺项时,网关返回拒绝并记录原因。这样比把全部工具交给模型后再观察它是否听话可靠。也可复用。

预算审批不能交给一句自然语言,策略与执行要拆成两层

自然语言适合表达目标,不适合充当资金变更凭证。运营说“把预算提高一点”,代理需要知道活动、当前值、目标值和有效时间。若审批只发生在聊天气泡里,批准内容就无法和最终参数绑定。执行服务必须收到一份固定结构的决策单。没有决策单,只有一句“同意”,请求就应当停住。

策略层的工作是理解数据和提出方案。它可以读取近段时间的花费、点击、转化和投产表现。它也可以说明为什么建议调整,以及调整可能影响哪些目标。策略层不能拥有直达 X Ads MCP 写工具的凭证。它输出的是候选决策,不是已经生效的指令。策略层不应直接触碰任何外部接口。

执行层的工作反而要更笨。它只接受已经编号、已经审批、已经过参数校验的决策单。它不重新解释“增加一点”,也不根据当前对话补充字段。执行层发现参数摘要和审批摘要不一致时,必须拒绝。让执行代码少一点智能,反而更容易把责任边界说清。审批结果必须可验证和复用。

审批页面应展示修改前后的值。只写“预算调整申请”太模糊,审批人看不出变化幅度。页面至少要显示目标活动、币种、旧预算、新预算、数据时间和提议原因。若请求来自代理,还要显示代理身份和原始会话编号。审批人的点击动作再绑定这些字段,才能形成有效证据。要可复核。

预算上限要在服务端做数值判断。页面上的输入框可以限制小数位,模型也可以被提示不要超限,但这些都不是安全边界。网关收到请求后,应把字符串金额转成 Decimal 再比较。金额格式异常、币种缺失或字段同时出现时,都要返回明确错误。审批不是给错误参数开绿灯。必须留痕。

审批凭证必须绑定原始参数摘要。常见做法是把工具名和排序后的 JSON 参数拼接,再计算 SHA 二百五十六摘要。执行前重新计算一次,两个摘要不一致就拒绝。这样即使有人修改了队列里的目标活动或预算,旧的批准也不会继续生效。审批批准的是那一组参数,不是一个模糊意图。

审批动作还要有有效期。预算机会可能只在某个时间窗口成立,隔天再执行就不一定合理。决策单可以记录创建时间、过期时间和允许的执行窗口。执行层超过窗口时先回到只读分析,不自动延长授权。过期不是失败,而是提醒团队重新看数据。审批结果必须可验证和复用。

把一次投放请求变成可审计的策略决策

一条完整的决策单要回答五个问题。谁发起了请求,代理依据什么数据提出方案。目标对象当前是什么状态,准备改成什么状态。谁在什么时间批准,最终由哪个服务执行。缺少其中任何一项,复盘时都会出现“大家都以为别人确认过”的空档。它不应直接触碰任何外部接口。

请求编号应该在进入网关时生成,而不是在执行完成后补写。编号要贯穿代理会话、审批队列、外部工具请求和审计日志。运营看到编号,就能追踪当前是待审核、已拒绝还是已执行。工程排障也可以用编号聚合分散在不同服务里的事件。编号不是展示字段,而是整条链路的主键。

操作者和审批人要使用不同身份字段。操作者代表谁提出了变化,审批人代表谁承担了放行责任。代理身份说明是哪一个模型或自动化服务发起调用。三种身份混在一个 user 字段里,后面很难判断责任。审计记录宁可多几个字段,也不要用一个万能字段偷懒。身份要单独留痕。

数据依据要记录时间范围和来源类型。比如建议来自某段统计窗口,读取的是活动级数据还是广告组级数据。不要只保存“模型认为表现下降”,而要保存它看到的关键数值摘要。原始报表可以放在受控存储,日志只保存摘要和引用编号。这样既能复核,又不会把整份敏感数据到处复制。

修改前后的状态都要落库。预算变更至少记录旧预算、新预算、币种和生效时间。定向变更还要记录增加或删除的条件。激活动作要记录激活前的状态,避免把本来暂停的对象误认为已经运行。只有结果没有前值,审计人员无法判断动作是否符合批准内容。原因应被保留。

幂等键用来阻止重复执行。代理重试、网络超时和人工重复点击,都可能让同一请求再次抵达执行层。幂等键应由业务对象、操作类型和请求意图共同生成。执行成功后还保留键的状态,重复请求返回原结果或明确冲突。不要每次重试都生成一个新审批编号来掩盖重复。

审计日志不能只写成功事件。被拒绝的未知工具、超预算请求、摘要不一致和过期批准都同样值得保留。它们能告诉团队代理在哪些边界上反复试探。日志应包含拒绝原因、规则版本和调用来源。这样治理团队看到的是行为趋势,而不是一串无意义的四百或五百状态码。

一个能跑起来的最小安全代理

下面的网关示例只依赖 FastAPI 和 Uvicorn。它把读取工具直接返回模拟结果,把写工具放进内存审批队列。代码不会真的修改 X Ads 账户,执行位置用日志标出。读者可以先在本机验证审批状态机,再把日志位置替换为真实 MCP 客户端。这个顺序能避免一上来就拿生产凭证做实验。先把安全边界跑通。

保存文件后,可以执行 pip install fastapi uvicorn 安装依赖,再用 uvicorn gateway:app --reload --port 8080 启动服务。调用读工具会得到 success,调用写工具会得到 pending_approval。拿到 approval_id 后,审批人调用第二个接口提交 approve 或 reject。示例使用内存字典,进程重启会清空队列,生产环境必须换成持久化存储。读工具请求可以用一个固定 JSON 直接发送。写工具请求要带活动编号、预算字段和幂等键。

代码里的预算上限只是演示参数,不代表任何平台的官方限制。团队应按账户、币种和业务风险配置真实规则。参数摘要绑定的是当前请求,不是用户后来补充的聊天内容。审批接口会再次检查摘要和状态,重复处理会被拒绝。即使没有接入外部广告 API,这几个边界也可以先测试。

运行测试时,先发一个读取活动的请求,确认网关不会创建审批记录。再发一个更新预算的请求,检查响应里有审批编号。用批准接口通过它,观察日志是否记录审批人和执行时间。把同一个幂等键重新发送,服务应返回冲突而不是再次执行。这样一轮就能验证读写分流、审批、摘要和幂等四个核心路径。

from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
from hashlib import sha256
from json import dumps
from typing import Any
from uuid import uuid4

from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title="ads-approval-gateway")

READ_TOOLS = {
    "list_campaigns",
    "get_campaign_stats",
    "list_ad_groups",
}
WRITE_TOOLS = {
    "update_campaign_budget",
    "update_bid",
    "activate_campaign",
}
BUDGET_CAP = Decimal("100000.00")
APPROVALS: dict[str, "ApprovalRecord"] = {}
EXECUTED_KEYS: set[str] = set()


class ToolCall(BaseModel):
    tool_name: str = Field(min_length=1)
    arguments: dict[str, Any]
    agent_id: str = Field(min_length=1)
    session_id: str = Field(min_length=1)
    idempotency_key: str | None = None


class ApprovalRecord(BaseModel):
    approval_id: str
    tool_name: str
    arguments: dict[str, Any]
    requester_id: str
    agent_id: str
    session_id: str
    idempotency_key: str | None
    argument_digest: str
    status: str
    created_at: datetime
    approved_by: str | None = None
    approved_at: datetime | None = None


class ApprovalAction(BaseModel):
    action: str = Field(pattern="^(approve|reject)$")


def argument_digest(tool_name: str, arguments: dict[str, Any]) -> str:
    payload = dumps(
        {"tool_name": tool_name, "arguments": arguments},
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    )
    return sha256(payload.encode("utf-8")).hexdigest()


def check_budget(arguments: dict[str, Any]) -> None:
    for name in ("budget", "daily_budget", "lifetime_budget"):
        if name not in arguments:
            continue
        try:
            amount = Decimal(str(arguments[name]))
        except (InvalidOperation, TypeError, ValueError) as exc:
            raise HTTPException(status_code=422, detail=f"{name} 不是有效金额") from exc
        if amount < 0 or amount > BUDGET_CAP:
            raise HTTPException(status_code=422, detail=f"{name} 超过网关上限")


@app.post("/agent/tool/call")
async def tool_call(
    request: ToolCall,
    x_user_id: str = Header(..., alias="X-User-Id"),
) -> dict[str, Any]:
    if request.tool_name in READ_TOOLS:
        return {
            "status": "success",
            "tool_name": request.tool_name,
            "data": {"mode": "read_only_simulation"},
        }
    if request.tool_name not in WRITE_TOOLS:
        raise HTTPException(status_code=403, detail="未知工具默认拒绝")
    check_budget(request.arguments)
    if request.idempotency_key in EXECUTED_KEYS:
        raise HTTPException(status_code=409, detail="幂等键已经执行")

    approval_id = str(uuid4())
    record = ApprovalRecord(
        approval_id=approval_id,
        tool_name=request.tool_name,
        arguments=request.arguments,
        requester_id=x_user_id,
        agent_id=request.agent_id,
        session_id=request.session_id,
        idempotency_key=request.idempotency_key,
        argument_digest=argument_digest(request.tool_name, request.arguments),
        status="pending",
        created_at=datetime.now(timezone.utc),
    )
    APPROVALS[approval_id] = record
    return {
        "status": "pending_approval",
        "approval_id": approval_id,
        "argument_digest": record.argument_digest,
    }


@app.get("/approval/{approval_id}")
async def get_approval(approval_id: str) -> ApprovalRecord:
    record = APPROVALS.get(approval_id)
    if record is None:
        raise HTTPException(status_code=404, detail="审批请求不存在")
    return record


@app.post("/approval/{approval_id}")
async def decide_approval(
    approval_id: str,
    action: ApprovalAction,
    x_approver_id: str = Header(..., alias="X-Approver-Id"),
) -> dict[str, Any]:
    record = APPROVALS.get(approval_id)
    if record is None:
        raise HTTPException(status_code=404, detail="审批请求不存在")
    if record.status != "pending":
        raise HTTPException(status_code=409, detail="审批请求已经处理")
    if argument_digest(record.tool_name, record.arguments) != record.argument_digest:
        raise HTTPException(status_code=409, detail="参数摘要不一致")

    record.approved_by = x_approver_id
    record.approved_at = datetime.now(timezone.utc)
    if action.action == "reject":
        record.status = "rejected"
        return {"status": "rejected", "approval_id": approval_id}

    record.status = "approved"
    if record.idempotency_key:
        EXECUTED_KEYS.add(record.idempotency_key)
    # 生产环境在这里调用 X Ads MCP,并传入 approval_id 作为追踪字段。
    record.status = "executed"
    return {
        "status": "executed",
        "approval_id": approval_id,
        "approved_by": x_approver_id,
        "executed_at": record.approved_at.isoformat(),
    }

这段代码有意把真实平台调用留在一个明确位置。接入 X Ads MCP 时,执行函数要使用审批记录里的工具名和参数。它不能重新从模型消息里取值,也不能接受浏览器前端传来的新预算。外部调用成功后,再把响应摘要写入审计日志。外部调用失败时,状态应进入可重试但不可重复扣费的状态。

生产网关还需要把内存队列换成数据库或可靠消息系统。审批创建、状态变更和幂等键写入要使用事务。多个执行器同时处理同一编号时,必须有行锁或条件更新。外部调用无法回滚时,要保存请求和响应,后续用查询接口确认结果。代码跑通只是起点,持久化和并发控制决定它能否上生产。

身份校验也不能停在请求头。示例用请求头表达操作者和审批人,真实服务应从经过验证的会话中取得身份。代理令牌需要限制账户和工具范围,审批人还要经过组织权限判断。跨账户操作必须再次校验目标账户归属。身份字段如果可以由调用方随便填写,整条审计链就失去可信度。

为什么只读试运行比全自动投放更适合第一阶段

只读试运行的价值,是让团队观察代理会如何使用工具。它可以查询活动、分析表现和整理建议,但任何写工具都只生成模拟结果。团队能看到代理是否选对活动,是否读懂时间窗口,是否把相关性当成因果关系。错误会出现在日志里,而不是出现在广告账单里。这段观察期值得保留。

MCP 规范强调用户必须明确同意数据访问和工具调用。只读模式正好把这条原则落实成工程开关。用户可以允许代理读取指定账户,却不允许它改变账户状态。网关还可以把允许的工具目录返回给代理,减少它对未知能力的猜测。授权范围越清楚,后面的审批体验越简单。

试运行期间要记录建议,不要只记录调用。建议包括目标对象、建议动作、数值变化和引用的数据范围。运营可以每周抽查建议是否合理,工程师可以统计代理误判的类型。若只看成功的查询次数,团队会错过最重要的风险信号。模型的错误理解通常会先出现在建议里,之后才可能变成写入。

模拟执行要尽量接近真实流程。写工具可以走同一套网关、审批和摘要校验,只在最后一步不调用外部账户。审批人看到的页面、过期规则和拒绝原因全程都应与未来生产版本一致。这样切换到真实执行时,变化只在适配器,不在治理流程。演练越像真实情况,放权越有依据。

团队还要给只读阶段设定退出条件。比如连续一段观察期内,活动识别、字段引用和建议理由达到约定质量。退出条件应该记录为可检查的指标,而不是“感觉模型已经稳定”。如果某类建议经常超过预算上限,说明治理规则仍在发挥作用。此时应修规则或缩小工具目录,不应直接放开权限。

只读并不等于没有风险。广告报表可能包含客户业务、受众和转化信息,读取范围也要严格最小化。网关要屏蔽无关账户,日志要避免保存完整个人数据。代理需要什么数据就给什么数据,不要因为 MCP 能连接就把整个账号暴露出去。观察代理的同时,也要观察它接触了什么。

给团队的落地顺序:先让 Agent 看懂,再让它动手

落地可以从一个低风险账户开始。这个账户要有真实结构和历史数据,但预算规模不能影响核心业务。先只开放账户、活动和统计读取,关闭所有写工具。让代理完成固定问题集,检查它能否正确引用活动和时间范围。问题集稳定后,再进入审批演练。账户选择也要经过确认。

第二阶段开放写工具的“模拟执行”开关。代理可以提交预算和定向变更,但外部适配器只返回模拟响应。团队重点看决策单是否完整,审批人是否能看懂变化,拒绝后是否不会继续执行。任何摘要不一致、重复键或过期请求都要留下证据。这个阶段是在测流程,不是在追求自动化率。

第三阶段只选择一个低风险写动作。可以从单个活动的有限预算调整开始,不要同时开放激活、批量创建和定向删除。为目标对象设置白名单,为修改幅度设置上限,为执行时间设置窗口。真实调用前仍然需要审批人确认。每一次放权都应能单独撤回。写权限必须能随时撤回。

灰度期间要安排人工观察窗口。执行成功后马上读取外部状态,确认旧值和新值符合决策单。若接口返回超时,不要立刻重试写请求,而要先查询是否已经生效。系统要区分“未发送”“结果未知”和“已执行”。这三个状态混在一起,最容易造成重复扣费。超时先查询状态再决定是否重试。

异常处理要有一处紧急总开关。发现代理连续提交超预算、工具目录变化或外部响应异常时,网关可以一键关闭全部写工具。关闭后读取和审计仍然可用,团队能继续收集证据。恢复前要重新核对令牌、规则版本和待审批队列。紧急开关的价值,在于它不依赖模型是否配合。

每周复盘时,把数据质量和权限质量放在一起看。代理回答得准,不代表它适合执行写操作。审批点击很快,也不代表审批人真正看过修改前后的值。团队需要同时统计建议采纳率、拒绝原因、越权拦截和重复请求。指标的目的不是给自动化贴金,而是找出下一条该收紧的边界。

等治理链路稳定后,再考虑跨平台和多代理协作。不同广告平台的预算字段、状态含义和审批要求可能不同。不能因为都接入 MCP,就把一套写权限配置复制过去。每个平台要有自己的适配器、工具矩阵和回滚规则。统一的是审计格式,不是所有平台的业务语义。统一格式也要稳定。

X Ads MCP 让广告工作少一些复制粘贴,多一些实时协作。它真正考验的却是团队有没有把建议、审批和执行分开。Agent 可以替人读数据,但预算和发布权必须留在可追溯的审批链里。把这条边界写进代码,再谈自动化,才不会用效率换来失控。真正的效率来自清晰边界,而不是取消责任。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

deepseek23

你的鼓励,是我创作的最大动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值