通过固定intent路由+frozen dataclass契约+白名单字段暴露,彻底切断模型自由发挥的能力。resolve后的Resolution只承载结构化事实,不给模型编造空间。
大多数"AI 客服"演示都以同样的方式崩塌。你问了一个模型无法从数据中回答的问题,它不会停下来,而是生成一些听起来合理的内容。在聊天玩具里,这只是个好奇。在售后支持里,这是你的公司现在必须兑现的承诺——一笔从未被批准的退款、一个根本不存在的交付日期、一个并非你政策的退货期限。
我为电商构建售后 Agent,几乎所有的工程投入都用在解决一个问题:让 Agent 的诚实成为架构的属性,而不是 prompt 的属性。以下是这件事实际上是如何实现的。
朴素的设计是一个模型、一个巨大的 prompt 和一堆存放在向量库里的文档。问"我的订单 41822 在哪里?",检索层会返回三个看起来最像这个问题的文本块。如果其中没有一个包含订单 41822——因为它是一条实时数据库记录,而不是一个文档——模型仍然会得到满满一上下文窗口的订单相关内容。它会回答,而且会是错的。
解决方案不是更好的 prompt。是完全剥夺模型回答这类问题的能力。
Agent 处理的每个请求都被路由到一组固定意图中的恰好一个——订单状态、交付延迟、退货、退款状态、换货、发票、产品问题、取消。该集合是封闭的。没有一个后备意图意味着"还是回答吧"。
每个意图映射到一个带有明确契约的 typed action:
@dataclass(frozen=True)
class OrderStatus:
"""Reads the order system of record. Never generates a status."""
order_ref: str
def resolve(self, ctx: Ctx) -> Resolution:
order = ctx.commerce.get_order(self.order_ref) # Shopify / WooCommerce / API
if order is None:
return Resolution.escalate(
reason=Reason.NOT_FOUND,
say="I can't find that order number on this account.",
)
return Resolution.answer(
template="order_status",
facts=order.public_facts(), # only whitelisted fields
)
这里有两件事很重要。
facts 是一个白名单,而不是整行数据。public_facts() 返回承运商、追踪号、发货时间戳、当前状态。它不返回利润率、内部备注、客户的其他订单或欺诈评分。模型在物理上无法泄露一个从未被给予的字段。
自然语言层只负责措辞。模型收到已解析的事实和一个模板意图,它的工作是用店铺的语气写一两句话。它不会被问及状态是什么。它被告知了状态,然后被要求友善地说出来。幻觉没有可以附着的外表面,因为在生成时已经没有任何开放性问题留给它了。
Resolution.escalate 不是错误路径。它是一个正常的、预期的结果,有着自己的质量标准——可以说它是最重要的那个。
class Reason(Enum):
NOT_FOUND = auto() # no matching record
OUT_OF_SCOPE = auto() # intent not in the closed set
POLICY_UNCLEAR = auto() # rule exists but doesn't cover this case
LOW_CONFIDENCE = auto() # intent classification below threshold
HUMAN_REQUESTED = auto() # customer asked for a person
EMOTIONAL = auto() # anger / distress detected
每个 reason 产生不同的转接:给客户的不同消息、人类队列中的不同优先级,以及附加到工单上的不同摘要,这样接手的人类 Agent 不需要从零开始。
EMOTIONAL 分支值得特别提出。一个愤怒的客户不是检索失败——系统可能拥有它需要的所有事实。它仍然会被路由给人类,因为"技术层面上可解决"和"应该由机器解决"是两个不同的问题。搞混这个区别就是自动化项目失去它们本应建立的信任的方式。
LOW_CONFIDENCE 需要一个真实的阈值,每个店铺单独校准,而不是硬编码的 0.7。而且阈值应该是不对称的:错误退款的代价不等于不必要升级的代价。
读取订单是安全的。发起退款不是。这两者位于不同的门后面,而门是商家拥有的配置,而不是 prompt:
actions:
order_status: { mode: auto }
return_label: { mode: auto, max_value_eur: 80 }
refund: { mode: propose } # drafts it, a human clicks send
cancel_order: { mode: propose }
address_change: { mode: auto, before_dispatch_only: true }
propose 模式让部署的第一个月能够存活。Agent 做工作并产生行动;人类批准它。你观察批准率。当某个行动被批准且未经修改的次数足够多时,这就是将它切换到 auto 的证据——不是供应商的声称,也不是幻灯片里的一个数字。
我在法国的法国模型(Mistral)上为每个店铺运行独立的实例。这听起来像是一句营销话术,所以以下是工程原因。
支持对话是店铺持有的最敏感的数据之一。不是因为订单号——而是因为客户围绕它们写下的内容。地址。退货的健康原因。财务困难。关于一个人的投诉。一旦这些通过另一个司法管辖区的共享多租户管道传输,"我的数据在哪里"就不再是一个你能回答的问题,而变成了你转发给供应商的问题。
专用实例也让可逆性成为现实而不是合同条款。如果商家离开,导出的是一个数据库和一个配置文件,而不是一个请求数据回去的支持工单。
欧盟 AI 法案的透明度义务(第 50 条)也推动了同样的方向:客户必须知道他们在和机器说话。当披露在你自己的消息编写层中时,这更容易保证,而不是在别人仪表板的一个设置里。
这个设计比自由发挥的模型解决的对话更少。封闭的意图集无法处理长尾。有界动作无法即兴发挥。propose 模式需要人类在循环中停留数周。
这是我每次都会做的权衡。宽松设计的失败模式不是"略微更差的答案"——而是一个真正的被信任的错误陈述,在店铺在法律上需要为之负责的渠道里。
一个说"我会让人来处理"的 Agent 是一次轻微令人失望的体验。一个捏造退款政策的 Agent 是一次事故。
我是 Amine,Bynevo Labs 的创始人——我们为法国电商构建主权售后 AI Agent,在开源技术栈上托管于法国境内。很高兴在评论中讨论架构。