Agent发消息前,审批为什么必须绑定收件人?
“发送消息”看起来只是一个普通 Tool。
真正接进 Agent 以后,它和 search_docs 完全不是一个风险等级,因为它会在外部世界留下不可逆结果:收件人会看到消息,群聊可能触发后续讨论,错误发送甚至可能泄露客户信息。
OpenAI 8 月 20 日上线 Apple Messages 插件时,默认行为就很值得研究:ChatGPT Work 和 Codex 可以读取、搜索 Messages 对话,也可以准备或发送 iMessage、SMS 和 RCS;默认发送前需要用户批准消息内容和收件人。Business 管理员还可以通过 Computer Use 控制禁用这项能力。
这里最关键的不是“多了一个 Messages 插件”,而是一个非常具体的安全边界:
用户批准的不是:
send_message
而应该是:
把这段确定内容
发送给这组确定收件人
如果审批只绑定 Tool Name,Agent 在审批后修改收件人或正文,整个批准机制就失去意义。
一个很危险的错误实现
很多 Agent Framework 的审批对象只有:
public record Approval(
String toolName,
boolean approved) {
}
运行流程:
Agent:我要调用 send_message
↓
用户:批准
↓
Agent 继续生成参数
↓
send_message(
recipients = ?,
body = ?
)
问题就在这里。
用户点批准时根本不知道最终参数。
如果模型下一步把:
recipient = 张三
变成:
recipient = 全公司群
系统仍然认为:
approval = true
从审计上看“有审批”,业务上却完全不是同一件事。
Approval必须绑定Action Snapshot
我更建议先让 Agent 生成完整 Proposed Action:
public record MessageAction(
List recipients,
String body,
List attachmentIds,
MessageChannel channel) {
}
然后做标准化:
public String canonicalize(
MessageAction action) {
return JsonCanonicalizer.write(
Map.of(
"recipients",
action.recipients()
.stream()
.sorted()
.toList(),
"body",
action.body(),
"attachments",
action.attachmentIds()
.stream()
.sorted()
.toList(),
"channel",
action.channel()
));
}
计算 Hash:
String actionHash =
sha256(
canonicalize(action));
审批记录:
public record ApprovalRecord(
String approvalId,
String runId,
String stepId,
String actionHash,
String approvedBy,
Instant approvedAt,
Instant expiresAt) {
}
真正执行前再次计算:
current_action_hash
必须完全等于批准时的:
approved_action_hash
否则:
重新审批
这一步能挡住大量“审批后参数漂移”。
收件人为什么要单独高亮
消息内容很长,用户很容易只扫一眼正文。
真正最危险的是:
发送给谁
所以审批 UI 我不会简单放一个 JSON:
{
"recipients": ["..."],
"body": "..."
}
而是强制拆成:
发送渠道:
Apple Messages
收件人:
张三 +86xxxxxxxxxxx
消息:
……
附件:
customer-report.pdf
风险:
外部发送
如果收件人数 > 1:
3 位收件人
继续展开全部名单。
如果超过阈值,例如:
> 10
直接升级风险等级:
BULK_SEND
需要更强审批。
收件人必须经过Identity Resolution
模型说:
发给老王
不能直接让 Tool 自己猜。
应该先解析:
“老王”
↓
Google Contacts / 企业通讯录
↓
候选联系人
如果有多个:
王强
王伟
王磊
必须要求澄清。
不能让 LLM 自己挑一个“最像的”。
可以定义:
public record RecipientResolution(
String query,
List candidates,
ResolutionStatus status) {
}
只有:
EXACT
或者用户明确选择以后才能进入审批。
手机号和联系人ID要冻结
审批时如果显示:
张三
但执行时 Tool 又重新查询通讯录,联系人信息可能已经变化。
更稳的是审批对象保存:
contact_id
+
resolved_address
例如:
{
"contact_id": "contact-912",
"display_name": "张三",
"address": "+86138xxxx1234"
}
真正执行使用批准时冻结的地址。
群聊比单聊更危险
对于已有群聊,Agent 最好引用:
conversation_id
而不是重新根据一批号码创建新会话。
否则这两件事:
向“项目A群”回复
和:
重新创建一个包含项目A成员的新群
用户体验和副作用都不同。
Message Action 应该区分:
public sealed interface MessageDestination
permits DirectRecipient,
ExistingConversation,
NewGroup {
}
NewGroup 默认进入更高风险等级。
Persistent Approval为什么危险
OpenAI 的发布说明特别提醒 Apple Messages 插件存在 persistent-approval 风险,并提供撤销说明。
这个问题在任何 Tool 都一样。
如果用户第一次点:
“以后都允许发送”
平台很容易把一次具体授权升级成:
永久 send_message 权限
但消息发送高度依赖上下文。
我更建议持久授权只覆盖:
低风险准备动作
例如:
read_messages
search_messages
draft_message
真正的:
send_message
仍然按 Action Snapshot 做 Just-in-time Approval。
如果一定要做免审批发送,必须把边界写死
例如自动通知 Agent:
每天17:30
向固定内部群
发送固定模板日报
可以创建 Standing Delegation:
agent: daily-report-agent
capability: messages.send
conversation_id: team-report-group
schedule: "17:30"
template: daily-report-v4
max_messages_per_day: 1
expires_at: 2026-09-30
只允许:
固定群
固定用途
固定频率
固定模板族
而不是:
以后这个 Agent 发消息都不用问我
一个必须区分的状态:PREPARED和SENT
Agent 很容易出现:
模型生成了正文
然后 UI 显示:
消息完成
实际上还没有发送。
我会把状态明确拆成:
public enum MessageExecutionStatus {
DRAFTED,
WAITING_APPROVAL,
APPROVED,
SENDING,
SENT,
FAILED,
UNKNOWN
}
尤其是:
UNKNOWN
非常重要。
为什么发送消息也会出现UNKNOWN
请求:
Agent
→ Apple Messages Tool
→ 系统实际发送成功
但返回途中网络断了。
Agent 只看到:
timeout
如果直接 Retry:
用户收到两条完全一样的消息
这就是典型的“副作用已成功,调用方不知道”。
所以发送 Tool 应该带:
idempotency_key
如果底层不支持幂等,就在 Gateway 层维护发送 Ledger。
public record MessageSendLedger(
String sendId,
String actionHash,
String idempotencyKey,
MessageExecutionStatus status,
String providerMessageId,
Instant createdAt) {
}
Retry 前先查:
这次 Action 是否已经有 SENT 记录?
Approval和Idempotency要绑定同一个Action Hash
这样:
用户批准的是 A
发送记录也是 A
审计链可以完整闭环。
Proposed Action
↓
Action Hash
↓
Approval
↓
Execution
↓
Provider Message ID
↓
Receipt
发送前还要做一次DLP检查
如果正文包含:
身份证
客户手机号
内部Token
财务数字
即使用户点了批准,也未必允许发送到外部号码。
所以 Policy Engine 应该在 Approval 和 Execution 两个阶段都检查:
Data Classification
Destination Trust
例如:
if (payload.dataClass()
== DataClass.RESTRICTED
&& destination.isExternal()) {
return Decision.DENY;
}
不能把所有安全责任推给审批用户。
附件必须独立校验
正文批准不代表附件批准。
如果 Agent 在审批后新增:
customer-list.xlsx
Action Hash 应变化,审批自动失效。
附件最好记录:
artifact_id
content_hash
classification
而不是文件路径。
一个完整消息执行链
User Intent
↓
Recipient Resolution
↓
Draft
↓
DLP / Policy
↓
Action Snapshot
↓
Action Hash
↓
Approval
↓
Execution Grant
↓
Send
↓
Provider Receipt
↓
Ledger
这里真正不可省的不是 LLM。
而是:
Action Snapshot
Approval Binding
Idempotency
Audit
最少应该测试这8种情况
1. 审批后正文变化
2. 审批后收件人变化
3. 同名联系人歧义
4. 群聊和新建群混淆
5. 发送成功但响应超时
6. 附件审批后被替换
7. Restricted数据发送到外部
8. Persistent Approval被撤销
这8个 Case 如果没测,send_message 还不适合交给自主 Agent。
消息发送这类 Tool 很容易让人误以为只是“多一个插件”。
实际上它代表 Agent 从:
生成内容
跨到了:
代表用户影响别人
所以审批不能只绑定 Tool Name。
必须绑定:
收件人
正文
附件
渠道
动作版本
OpenAI 这次 Apple Messages 默认在发送前确认消息和收件人,我认为这个边界是对的。
生产 Agent 再往前走一步,就应该把这个默认交互固化成真正可审计的:
Action-bound Approval
否则“用户批准过”很容易成为一个看似安全、实际上无法证明用户批准了什么的假保障。
更多企业级 AI 应用、Agent、RAG 与模型工程化内容,我会继续整理在 智元界:
https://www.zyentor.com/