Skip to content

fix(chat): channel chat 工具审批 Yield 后无续传无入口——死信审批 + middleware 谎言文案诱导 LLM "假装等审批" 幻觉 #2004

Description

@eanzhao

摘要

channel chat(Lark DM)路径上,任何 RequiresApproval=true 的工具(当前主要是 code_execute)触发审批后是永久死信:没有审批卡、没有任何批准入口、没有 pending 持久化、没有超时,turn 照常 Completed。同时 ToolApprovalMiddleware 喂给 LLM 的工具结果写死 "An approval request has been sent."——系统先对 LLM 撒谎,LLM 再向用户编造"你批准后会继续执行"。用户视角 = 审批幻觉。

生产实证(2026-06-11 17:53 CST,Lark DM)

用户:"三分钟后给我发一下我今天的日报"

bot 回复(verbatim):

已发起 3 分钟后发送日报的执行请求,但当前环境对延时执行触发了人工审批:
code_executeapproval_required
你批准后,它会继续等待 3 分钟再执行发送;如果审批没有通过,这次延时发送不会真正执行。
[tool receipt] Approval pending: code_execute (local_request=85ad2b7dbee64220834a32308b0495f4)

console-backend 同窗口日志(15 分钟 15062 行):

  • 85ad2b7dbee64220834a32308b0495f4 零命中;approval / code_execute 关键词零命中——"发起审批请求"动作根本不存在
  • 该 turn 出站仅 im/v1/messages/.../reactions + channel-relay/reply未发任何审批卡
  • ConversationTurnCompletedEvent(v2742)正常落盘——服务端认为本轮圆满结束,无任何挂起态

半真半假的结构(为什么极难分辨)

内容 真伪
工具调用 LLM 真调了 code_execute(想跑代码延时 3 分钟)
审批判定 NyxIdCodeExecuteTool.RequiresApproval => true 恒审批,middleware 真实挂起
receipt 行 [tool receipt] Approval pending: ... 由服务端 AgentRunGAgent.cs:1115 渲染
工具结果文案 "An approval request has been sent." (此路径从未发送审批请求)
LLM 转述 "你批准后,它会继续等待 3 分钟再执行发送" (无批准入口,无续传消费者)

根因(两层)

1. Yield 协议的隐含契约无人履行

ToolApprovalMiddleware.cs:155-176ToolApprovalDecision.Yield → 产出 TerminationKind=ApprovalPending + context.PendingApproval,注释明言"Actor 层检测此 result 后持久化 pending state 并走事件化续传"。

实际消费者盘点(grep PendingApproval,排除 obj/Test):

  • src/Aevatar.AI.Core/RoleGAgent.cs唯一实现PendingToolApprovalPersistedEvent 持久化 + RemoteApprovalId + 状态检查 + 超时对账(workflow llm_call 路径)
  • agents/Aevatar.GAgents.NyxidChat/AgentRunGAgent.cs零命中。只有 PendingToolCalls/PendingDropNotification;Yield 的 ToolApprovalPendingContext 直接丢弃

2. 装配层让谎言成为默认

src/Aevatar.Mainnet.Host.Api/Hosting/MainnetHostBuilderExtensions.cs:247(另 Bootstrap.Extensions.AI:107):

builder.Services.TryAddSingleton<IToolApprovalHandler, YieldApprovalHandler>();

YieldApprovalHandler 全局单例注册——所有路径都拿到"我会续传"的承诺,但只有 RoleGAgent 真的履行。channel chat 拿到 Yield 却无续传实现 → 死信。

ToolApprovalMiddleware.cs:200-209 BuildApprovalPendingResult 进一步写死:

message = "This tool requires user approval before execution. An approval request has been sent."

违反 CLAUDE.md「ACK 诚实:同步返回只承诺已达到阶段」与「抽象一旦能被滥用即设计未完成」。

对比:workflow 路径的正确姿势(#1904 / d5ef0c3

workflow tool_call 链路对 ApprovalPending fail-closed(throw → step 失败),测试 WorkflowTool_WhenApprovalPending_ShouldFailClosedWithoutReturningPendingPayload 明示设计意图。channel chat 是唯一"假装等审批"的路径,比 fail-closed 更糟。

修复方案(分层)

  1. 止血(文案诚实)BuildApprovalPendingResult 不再无条件声称 "approval request has been sent";改为"执行已暂停等待审批;若当前通道无审批入口,此工具视为不可用,请如实告知用户并建议替代路径"。
  2. 路径级 fail-closed(建议立即):channel chat 装配 fail-closed handler(对齐 [hardening] workflow tool_call 绕过 ToolApprovalMiddleware:审批按路径而非按工具强制 #1904 哲学):审批类工具直接 Denied("当前频道无审批入口")。LLM 拿到诚实失败,receipt 渲染为 Denied 而非悬空 pending。
  3. 架构级治理(彻底杜绝)YieldApprovalHandler 不得全局 TryAddSingleton。Yield 是带契约的协议("actor 必须实现续传"),handler 跟随 actor 能力装配:只有实现了 pending 持久化+事件化续传的 actor(当前仅 RoleGAgent)注入 Yield;其余默认 MissingApprovalHandler(Denied)。补 CI guard:使用 ToolCallMiddlewareChainFactory.ForAgentRuntime 且传入 Yield 型 handler 的装配点,必须能指认 PendingApproval 消费者。
  4. 功能级(按需后做):若产品上需要 chat 内审批,AgentRunGAgent 实现真续传:持久化 pending state → 发 Lark 审批卡(FeishuCard 链路现成)→ 卡片回调事件化续传 → durable timeout,对齐 RoleGAgent 模式。在此之前 fail-closed 是诚实的。

验收建议

  • 单测:channel chat 链路中 RequiresApproval=true 工具调用 → 工具结果为明确 Denied/不可用语义,不含 "approval request has been sent" 类承诺;receipt 状态非 ApprovalRequired。
  • 回归:Lark DM 实测同句式请求,bot 回复不再出现"等你批准"叙事。
  • guard:扫描禁止 Yield handler 注入到无 PendingApproval 消费者的执行环境。

关联

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions