封面信息图

上周二上午,财务主管气冲冲地推开技术部的大门。起因是前一天晚上,售后客服 Agent 碰到了一个极其难缠的恶意买家。买家在聊天框里疯狂复制“再不退款立刻投诉到 12315”、“工商局见”等极端施压话术,结果大模型在连续多轮对抗后产生对齐破损,竟然跳过了“核对退货物流单号”的刚性逻辑,直接调起底层退款工具给买家放行了 4,200 元的退款,而货物根本就没退回仓库。

事后复盘,很多人把责任推给大模型的 Prompt 写得不够严密。但在真正的企业级生产环境里,靠修改系统提示词来保障资金安全无异于在沙滩上盖高楼。大模型天生具备不确定性,无论是被提示词注入攻击,还是单纯的模型幻觉,只要它拥有无限制调用敏感写操作(如退款、封号、删库、改价)的直连权限,资金穿透就是迟早的必然事件。

涉及真金白银的操作,必须在底层架构上实现“人机协同(Human-in-the-Loop, HITL)”。Agent 可以作为高效率的申请发起方与材料整理方,但在真正扣款执行前,Agent 状态机必须原地异步挂起(Suspension),将决策权移交给企业微信群内的真人审核员。只有收到合法的审批通过回调后,状态机才被重新唤醒(Resumption)继续向下走。

异步挂起与唤醒模型:解耦等待与执行

传统的同步阻塞调用在人机协同场景下完全行不通。审核员可能在 30 秒内点击同意,也可能在开会半小时后才处理。如果让后端协程或者 HTTP 连接一直挂起等待,连接池和系统内存分分钟被耗尽。

必须让 Agent 状态机具备两项核心能力:

  1. 就地冻结与持久化:当大模型输出的 Tool Call 命中高危操作名单(如 refund_execute)时,拦截器直接打断执行链,提取当前的会话上下文、已推理步骤、参数载荷以及待执行函数名,打包序列化为状态快照,存入数据库,并将状态标记为 SUSPENDED。
  2. 事件驱动唤醒:向企业微信群推送带交互按钮的审批卡片。真人点击后,企微服务器向我们的回调网关推送事件。网关校验签名与幂等性后,根据唯一的 workflow_id 重建 Agent 内存栈,将人工审批意见以标准的 Tool Execution Result 形式喂给模型,状态机平滑恢复运行。
[Agent 决策发起退款] ──> 触发高危工具拦截器
                             │
                             ▼
                    ┌─────────────────┐
                    │ 状态机序列化快照 │
                    │ 写入存储: SUSPEND│
                    └────────┬────────┘
                             │
                             ▼
                  向企业微信群推送审批卡片
                             │
            ┌────────────────┴────────────────┐
      [审批员点击: 同意]               [审批员点击: 驳回]
            │                                 │
            ▼                                 ▼
      企微 Webhook 回调                企微 Webhook 回调
            │                                 │
            ▼                                 ▼
   加载快照,唤醒状态机              加载快照,注入拒绝结果
   执行退款工具并通知用户            终止退款,模型组织委婉话术

状态机调度器:Go 1.27.1 实现

我们基于 Go 1.27.1 实现了一套轻量级工作流状态机。通过方法级通用泛型支持多样化的业务上下文,结合 Go 1.26 的 new(expr) 指针初始化,将快照生成和事件恢复做到了极致的轻量化。

package workflow

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"sync"
	"time"
)

type StepStatus string

const (
	StatusRunning   StepStatus = "RUNNING"
	StatusSuspended StepStatus = "SUSPENDED"
	StatusApproved  StepStatus = "APPROVED"
	StatusRejected  StepStatus = "REJECTED"
)

// WorkflowSnapshot 记录挂起时的全量运行态
type WorkflowSnapshot struct {
	WorkflowID   string         `json:"workflow_id"`
	OrderID      string         `json:"order_id"`
	RefundAmount float64        `json:"refund_amount"`
	AIRationale  string         `json:"ai_rationale"`
	ToolName     string         `json:"tool_name"`
	ToolArgs     map[string]any `json:"tool_args"`
	Status       StepStatus     `json:"status"`
	CreatedAt    time.Time      `json:"created_at"`
}

// Storage 抽象快照持久化层
type Storage interface {
	Save(ctx context.Context, snapshot *WorkflowSnapshot) error
	Load(ctx context.Context, workflowID string) (*WorkflowSnapshot, error)
	UpdateStatus(ctx context.Context, workflowID string, status StepStatus, operator string) error
}

// WeComNotifier 负责向企业微信机器人推送交互卡片
type WeComNotifier interface {
	SendApprovalCard(ctx context.Context, snapshot *WorkflowSnapshot) error
}

// ApprovalEngine 人机协同状态机引擎
type ApprovalEngine struct {
	storage  Storage
	notifier WeComNotifier
	mu       sync.Mutex
}

func NewApprovalEngine(s Storage, n WeComNotifier) *ApprovalEngine {
	return &ApprovalEngine{storage: s, notifier: n}
}

// SuspendForHumanApproval 利用 Go 1.27.1 泛型方法接收待审核上下文,挂起工作流
func (e *ApprovalEngine) SuspendForHumanApproval[T any](ctx context.Context, workflowID, orderID string, amount float64, rationale string, toolName string, args T) error {
	e.mu.Lock()
	defer e.mu.Unlock()

	rawArgs, err := json.Marshal(args)
	if err != nil {
		return fmt.Errorf("serialize args failed: %w", err)
	}

	var parsedArgs map[string]any
	_ = json.Unmarshal(rawArgs, &parsedArgs)

	// 使用 Go 1.26 简洁指针初始化
	snapshot := new(WorkflowSnapshot)
	*snapshot = WorkflowSnapshot{
		WorkflowID:   workflowID,
		OrderID:      orderID,
		RefundAmount: amount,
		AIRationale:  rationale,
		ToolName:     toolName,
		ToolArgs:     parsedArgs,
		Status:       StatusSuspended,
		CreatedAt:    time.Now(),
	}

	if err := e.storage.Save(ctx, snapshot); err != nil {
		return fmt.Errorf("save snapshot failed: %w", err)
	}

	// 异步向企业微信发送审批卡片
	go func() {
		_ = e.notifier.SendApprovalCard(context.Background(), snapshot)
	}()

	return nil
}

// ResumeAfterCallback 处理企微审批回调并唤醒流程
func (e *ApprovalEngine) ResumeAfterCallback(ctx context.Context, workflowID string, approved bool, operator string) (string, error) {
	snapshot, err := e.storage.Load(ctx, workflowID)
	if err != nil {
		return "", errors.New("workflow snapshot not found")
	}

	if snapshot.Status != StatusSuspended {
		return "", fmt.Errorf("workflow %s is already processed: %s", workflowID, snapshot.Status)
	}

	newStatus := StatusRejected
	if approved {
		newStatus = StatusApproved
	}

	if err := e.storage.UpdateStatus(ctx, workflowID, newStatus, operator); err != nil {
		return "", fmt.Errorf("update status failed: %w", err)
	}

	// 恢复状态机后,根据审批结论返回给 Agent 的真实结果
	if approved {
		// 此处执行真实微服务的退款 API
		return fmt.Sprintf("人工审批通过(审批人: %s),退款金额 %.2f 已成功划转原账户", operator, snapshot.RefundAmount), nil
	}

	return fmt.Sprintf("人工审批驳回(审批人: %s),退款请求已被拦截,请向买家说明驳回原因", operator), nil
}

生产实战:企微交互与防御细节

在企业微信端,很多团队一开始图简单,直接发一段文本链接让客服在浏览器里登录后台点审批,结果导致响应极慢。体验最好的方案是使用企业微信的“交互型任务卡片(Template Card)”:

  1. 结构化卡片呈现:
    卡片直接展示关键决策要素:
    • 【订单编号】:20261010098765
    • 【退款金额】:¥ 4,200.00
    • 【买家诉求】:商品损坏要求全额退款
    • 【AI 研判建议】:买家未提供有效货损图片,且系统无退货入库记录,建议驳回或要求补传凭据。
      下方设置两个带确认弹窗的按钮:“同意退款(高危)”与“驳回申请”。
  2. 防止二次点击与并发竞态:
    审核员在企微手机端点击按钮时,可能会因为网络卡顿连点两次。我们在回调网关处理端加了 Redis 分布式排他锁:SETNX workflow:lock:{id} 1 EX 10。一旦某次点击进入处理,后续请求直接丢弃,并且在企业微信卡片上调用 API 实时将卡片状态更新为“已处理”,杜绝多名主管重复审核。
  3. 超时保护机制(Dead Man's Switch):
    人工审批不能无限制卡住用户对话。如果超过 30 分钟没有真人点击企微卡片,后端的定时扫描定时器会自动触发超时熔断:将工作流状态标记为 ESCALATED_TO_MANUAL_TICKET,将当前对话平滑切给人工在线坐席,同时在用户前端提示:“您的问题涉及大额退款复核,资深客服专家正在接入为您处理”。

这套人机协同机制上线运行了整整两周,拦截了 3 起恶意的黑产刷单退款,挽回直接经济损失近两万元。把边界划在确定性的代码和真实的真人决策上,大模型才能真正从一个会说胡话的玩具,变成敢在生产环境扛核心业务的帮手。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐