Agent 工程体系 · 第 61/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。

客服 Agent:意图、知识、工单、升级、质量和会话记忆

客服 Agent 不是“接入一个大模型,再连接几个客服接口”。它是一个以会话为入口、以业务状态为约束、以工具调用为执行手段、以人工责任边界为安全阀的决策系统。

一个完整的客服 Agent 至少要回答六个问题:

  1. 用户现在想完成什么,即意图是什么?
  2. 哪些事实可以回答,即知识来自哪里?
  3. 哪些动作需要在业务系统中留下记录,即是否创建或更新工单
  4. 什么时候继续自动处理,什么时候升级
  5. 如何证明回答正确、过程合规、用户问题真正被解决,即如何衡量质量
  6. 当前对话和历史偏好应该保存什么、检索什么、何时遗忘,即如何管理会话记忆

这些问题不是并列模块。意图决定知识范围和工具权限,知识决定能否回答,工单决定是否产生外部副作用,升级决定责任是否转移,质量决定系统是否可持续迭代,而记忆决定 Agent 能否在多轮和跨会话中保持一致。


一、先定义客服 Agent 的边界

客服 Agent是一个能够根据用户输入和业务上下文,进行判断、检索、调用工具、生成回复,并在必要时暂停、升级或交给人工处理的应用程序。

这里的“Agent”至少包含四个部分:

  • 推理器:判断用户要什么、下一步做什么;
  • 工具集合:查询订单、检索知识、创建工单、修改地址等;
  • 状态:当前会话、业务实体、已完成动作和待处理事项;
  • 控制器:权限、审批、超时、重试、升级和审计。

OpenAI 对 Agent 的定义强调,Agent 可以规划、调用工具、协作并保留完成多步任务所需的状态;其运行时会循环执行模型调用、工具调用、交接和最终回复,而不是只进行一次文本生成。(developers.openai.com)

客服 Agent 的“完成”也不能定义为“模型生成了一段文字”。更准确的完成条件是:

Done=AnswerablePolicySatisfiedSideEffectsCommittedUserInformed\text{Done} = \text{Answerable} \land \text{PolicySatisfied} \land \text{SideEffectsCommitted} \land \text{UserInformed}

其中:

  • Answerable:问题已被回答,或已明确说明无法回答;
  • PolicySatisfied:没有越过权限、合规和业务规则;
  • SideEffectsCommitted:需要写入订单、退款、工单等系统的动作已经成功提交;
  • UserInformed:用户知道结果、待办事项和下一步。

例如,Agent 说“退款已经提交”,但退款接口实际超时,这不算完成。生成的文本看起来正确,业务状态却是错误的。


二、客服 Agent 的总体数据流

客服系统通常由以下组件组成:

  • 渠道适配器:接收网页、App、电话转写、企业微信等渠道的消息;
  • 会话编排器:维护一次运行的生命周期;
  • 意图识别器:判断问题类型、实体和风险;
  • 知识检索器:从结构化数据、知识库和实时系统获取事实;
  • 工具层:封装订单、账户、退款、物流和工单系统;
  • 策略层:决定是否自动回答、追问、执行、升级或人工接管;
  • 质量与观测层:记录轨迹、工具结果、评价和失败原因;
  • 记忆层:保存当前会话状态与经过筛选的长期信息。

可以用下面的时序表示一条典型路径:

sequenceDiagram
    participant U as 用户
    participant C as 渠道适配器
    participant A as 编排器
    participant I as 意图与风险识别
    participant K as 知识检索
    participant T as 业务工具
    participant H as 人工队列
    participant Q as 质量与审计
    participant M as 会话/长期记忆

    U->>C: 发送消息
    C->>A: user_message + conversation_id
    A->>M: 读取会话状态
    A->>I: 分类意图、提取实体、评估风险
    I-->>A: intent + slots + confidence + risk

    alt 需要澄清
        A-->>C: 询问缺失信息
        C-->>U: 澄清问题
    else 可由知识回答
        A->>K: 检索适用知识
        K-->>A: 证据、版本、有效期
        A-->>C: 基于证据的答复
        C-->>U: 返回答案
    else 需要业务动作
        A->>T: 查询或执行工具
        T-->>A: 结构化结果
        alt 高风险或人工策略命中
            A->>H: 创建升级任务
            H-->>A: 接管或等待人工
            A-->>C: 告知用户升级状态
        else 自动完成
            A-->>C: 返回执行结果
            C-->>U: 返回结果
        end
    end

    A->>M: 更新会话状态/候选记忆
    A->>Q: 写入轨迹、指标和审计记录

关键点是:模型不应该直接拥有所有系统权限。模型输出的是意图、工具选择和参数候选;真正的权限校验、幂等控制和状态变更由工具服务完成。


三、意图:不是分类标签,而是可执行任务契约

3.1 意图的定义

意图是用户希望系统完成的目标,不等于用户说出的关键词。

例如:

  • “我的包怎么还没到?”
    意图可能是 查询物流状态
  • “昨天买的东西不要了,帮我退掉。”
    意图可能是 申请退款
  • “我想把收货地址改成公司。”
    意图是 修改收货地址,但还缺少订单号和新地址。
  • “你们这个扣款我完全不认可。”
    可能是 争议扣款,风险级别高于普通账单咨询。

一个适合工程实现的意图对象,不应只有字符串:

{
  "name": "refund_request",
  "confidence": 0.91,
  "entities": {
    "order_id": "A10086",
    "reason": "商品不需要了"
  },
  "missing_slots": [],
  "risk": "medium",
  "allowed_actions": [
    "check_refund_policy",
    "create_refund_request"
  ],
  "needs_confirmation": true
}

这里:

  • name 是业务目标;
  • confidence 是分类信心,不是执行许可;
  • entities 是从用户输入中提取的业务实体;
  • missing_slots 表示执行任务所缺的字段;
  • risk 决定是否需要人工或二次确认;
  • allowed_actions 是策略层允许的动作;
  • needs_confirmation 表示是否需要用户确认外部副作用。

3.2 意图识别的三个阶段

不要把意图识别压缩成一次分类。较可靠的流程是:

第一步:识别候选意图

设意图集合为:

I={i1,i2,,in}\mathcal{I}=\{i_1,i_2,\dots,i_n\}

给定用户输入 xx 和上下文 cc,模型产生:

P(ix,c)P(i \mid x,c)

例如:

用户:昨天扣了我两次钱,其中一笔我不认识。
候选:
- duplicate_charge:0.46
- unauthorized_charge:0.43
- invoice_question:0.11

此时不应直接选概率最高的 duplicate_charge,因为前两个意图都可能改变处理路径。

第二步:检查可区分性

定义前两名概率差:

Δ=P(i1x,c)P(i2x,c)\Delta = P(i_1\mid x,c)-P(i_2\mid x,c)

Δ\Delta 较小时,直接执行会产生较高误路由风险。更合理的策略是澄清:

“你是不认识这笔扣款,还是确认同一订单被重复扣款?”

这比让 Agent 自行猜测更安全,因为澄清问题直接针对两个候选意图的区分点。

第三步:检查执行前置条件

即使意图确定,也不代表可以执行。例如:

意图:修改收货地址
已知:用户身份、订单号
缺失:新地址
状态:需要澄清

再如:

意图:退款
已知:订单号、退款原因
业务状态:订单已签收超过售后期限
状态:不能自动执行,需要解释政策或升级

因此路由条件应同时考虑意图、字段、策略和业务状态:

Route(x)=f(intent,slots,policy,risk,business state)\text{Route}(x)= f(\text{intent},\text{slots},\text{policy},\text{risk},\text{business state})

3.3 反例:只按关键词路由

if "退款" in message:
    return "refund"

这段逻辑会把以下消息全部路由到退款流程:

  • “退款多久到账?”
  • “我不想退款,只想换货。”
  • “退款按钮在哪里?”
  • “你们为什么拒绝我的退款?”
  • “退款后发票怎么办?”

它们的后续动作分别可能是查询、换货、操作指导、申诉和发票咨询。

关键词可以作为特征,但不能作为最终意图。生产系统至少要把以下信息分离:

任务目标:用户想完成什么
对象实体:针对哪个订单、账户或商品
动作类型:查询、申请、修改、取消、申诉
状态:已发生、希望发生、正在失败
风险:是否涉及资金、身份、隐私或争议

四、知识:回答的依据,不是提示词里的百科全书

4.1 知识的三种来源

客服 Agent 需要区分三类知识。

稳定规则知识

例如:

  • 退货政策;
  • 账户安全流程;
  • 服务条款;
  • 常见故障排查步骤。

这类知识适合进入版本化知识库,并在检索结果中携带生效时间和适用范围。

实时业务知识

例如:

  • 订单当前状态;
  • 库存;
  • 物流节点;
  • 账户余额;
  • 工单处理进度。

这类信息不能只依赖向量检索。它应来自权威业务 API,因为数据会变化,并且需要权限控制。

会话事实

例如:

  • 用户已经提供过订单号;
  • 用户拒绝过某种处理方案;
  • 当前正在等待人工;
  • 上一步工具调用失败。

这类信息属于会话状态,而不是公共知识。

4.2 检索结果必须携带证据元数据

一条知识结果至少应包括:

{
  "content": "普通商品签收后7日内可申请无理由退货。",
  "source": "after_sale_policy",
  "version": "2026-08-15",
  "effective_from": "2026-08-15T00:00:00+08:00",
  "effective_to": null,
  "scope": {
    "region": "CN",
    "product_type": "普通商品"
  },
  "authority": "policy-service",
  "retrieved_at": "2026-09-01T10:00:00+08:00"
}

原因是同一句话在不同地区、商品类型和时间范围下可能不成立。若只把文本片段交给模型,模型无法判断它是否适用。

4.3 检索不是回答,证据也不是结论

一次检索应经过四步:

  1. 根据意图生成查询;
  2. 过滤权限、地区、产品和时间范围;
  3. 对候选结果排序;
  4. 判断证据是否足以支持结论。

设检索候选为 d1,,dmd_1,\dots,d_m,可以用一个简化评分表示:

S(d)=αR(d)+βA(d)+γV(d)δE(d)S(d)= \alpha R(d)+ \beta A(d)+ \gamma V(d)- \delta E(d)

其中:

  • R(d)R(d):与问题的相关性;
  • A(d)A(d):来源权威性;
  • V(d)V(d):版本和有效期匹配度;
  • E(d)E(d):矛盾或过期风险。

高相关但已过期的文档,不应超过低相关但当前有效的政策文档。

4.4 知识回答的边界

当没有足够证据时,Agent 应该输出以下三种结果之一:

  • 有证据,可以明确回答;
  • 证据不足,需要向用户澄清;
  • 该问题涉及业务判断或争议,需要升级人工。

反例是:

“根据政策,您的退款一定会在三个工作日到账。”

如果知识库只说明“通常在三个工作日内处理”,而实际到账还依赖支付渠道,Agent 就把处理时限错误扩展成到账承诺。


五、工单:把对话转化为可追踪的业务状态

5.1 工单的定义

工单是对一个待处理业务事项的持久化记录。它不是聊天摘要,也不是“给人工发一条消息”。

一个可用的工单至少包含:

{
  "ticket_id": "T202609010001",
  "conversation_id": "C7788",
  "customer_id": "U1001",
  "intent": "refund_request",
  "status": "pending_human_review",
  "priority": "high",
  "summary": "用户认为商品存在质量问题,要求退款",
  "facts": {
    "order_id": "A10086",
    "received_at": "2026-08-30",
    "reason": "质量问题"
  },
  "evidence": [
    "用户上传的商品照片",
    "订单查询结果"
  ],
  "next_action": "人工审核商品质量",
  "owner_queue": "after_sales",
  "created_at": "2026-09-01T10:00:00+08:00"
}

工单的核心不是“记录说了什么”,而是记录:

  • 谁的问题;
  • 当前状态;
  • 谁负责;
  • 下一步是什么;
  • 何时需要完成;
  • 哪些事实已经确认;
  • 哪些事实仍然不确定。

5.2 工单状态机

工单应有显式状态,而不是依赖自然语言描述:

stateDiagram-v2
    [*] --> Draft
    Draft --> Open: 创建成功
    Open --> WaitingCustomer: 缺少用户信息
    WaitingCustomer --> Open: 用户补充信息
    Open --> InProgress: 分配处理人
    InProgress --> PendingApproval: 需要审批
    PendingApproval --> InProgress: 审批通过
    PendingApproval --> Rejected: 审批拒绝
    InProgress --> Resolved: 已解决
    Resolved --> Reopened: 用户继续反馈
    Reopened --> InProgress
    InProgress --> Escalated: 超时/高风险/争议
    Escalated --> HumanOwned: 人工接管
    HumanOwned --> Resolved: 人工解决
    Resolved --> Closed: 用户确认或超时关闭

状态转换必须由服务端校验。例如,模型不能直接把 Draft 改成 Closed,也不能把未验证身份的请求标记为 Resolved

5.3 创建工单的幂等性

客服对话容易重试。网络超时后,Agent 可能再次调用“创建工单”。如果接口没有幂等键,就可能生成两个工单。

推荐使用:

idempotency_key=H(conversation_id,intent,business_object,action)\text{idempotency\_key} = H(\text{conversation\_id},\text{intent},\text{business\_object},\text{action})

例如:

C7788 + refund_request + A10086 + create

服务端应保证同一个幂等键重复提交时返回同一个工单,而不是重复创建。

5.4 工具调用的两阶段语义

涉及副作用时,工具调用最好区分:

  1. 预检查:确认身份、资格、金额、库存或政策;
  2. 执行提交:真正改变系统状态。

例如退款:

check_refund_eligibility(order_id)
    -> eligible=true, max_amount=199.00

confirm_refund(order_id, amount=199.00, idempotency_key=...)
    -> refund_id=R9001, status=submitted

如果 Agent 跳过预检查,直接生成“退款已提交”,它就把推理结果冒充成业务事实。


六、升级:不是“模型答不上来”这么简单

6.1 升级的定义

升级是将当前问题的处理责任、上下文和后续动作转移给更高权限、更高专业性或更适合承担责任的处理者。

升级对象可以是:

  • 专业 Agent;
  • 人工客服;
  • 风险审核队列;
  • 订单、支付或物流系统;
  • 紧急安全流程。

升级不是简单地发送一条“请稍等”。它必须产生一个可恢复的状态:

{
  "handoff_id": "H5001",
  "from": "general_support_agent",
  "to": "payment_risk_queue",
  "reason": "unauthorized_charge",
  "context_snapshot": "...",
  "customer_message": "我不认识这笔扣款",
  "ownership": "human_pending",
  "sla_deadline": "2026-09-01T10:30:00+08:00"
}

6.2 四类人工介入

确认

用户决定是否执行一个有副作用的动作。

“将取消订单并放弃当前优惠,是否继续?”

确认必须发生在动作执行前,而不是执行后补问。

澄清

系统缺少完成任务所需的信息。

“请提供需要修改地址的订单号。”

澄清不应重复询问已经确认过的字段。

升级

当前 Agent 仍然可以结束自己的处理,但后续责任转给人工或其他队列。

“这涉及扣款争议,我已提交支付风险团队审核。”

接管

人工成为当前会话的实际回复者。此时自动 Agent 不应继续并行回复,否则会出现“双重回复”。

6.3 升级条件

可以将升级条件表示为:

Escalate=RUFTC\text{Escalate} = R \lor U \lor F \lor T \lor C

其中:

  • RR:风险条件命中,例如身份、资金、隐私或安全问题;
  • UU:用户明确要求人工;
  • FF:连续失败,例如多次工具失败或多轮误解;
  • TT:超时或 SLA 风险;
  • CC:置信度不足或证据冲突。

反例是只用“模型置信度低于 0.5”作为升级条件。模型可能对错误结论很自信,也可能对一个简单问题给出低置信度。置信度只能是一个信号,不能替代业务规则和工具结果。

6.4 升级后的责任边界

升级时必须传递:

  • 用户原始诉求;
  • 已确认事实;
  • 未确认事实;
  • 已调用工具及结果;
  • 已向用户作出的承诺;
  • 当前工单和会话状态;
  • 禁止重复执行的动作;
  • 人工需要做出的决策。

否则人工接管后会重新询问用户,造成“转人工后从头开始”。


七、质量:从“说得像客服”转向可验证的完成度

质量不是单一的满意度分数。客服 Agent 至少有五个质量维度:

  1. 意图正确率:是否理解了用户目标;
  2. 事实正确率:答案是否由有效证据支持;
  3. 动作正确率:工具参数和业务变更是否正确;
  4. 过程质量:是否正确澄清、确认、升级和恢复;
  5. 结果质量:问题是否真正解决,是否减少重复联系。

可以定义一个简化评分:

Q=wiQi+wfQf+waQa+wpQp+wrQrQ = w_i Q_i+ w_f Q_f+ w_a Q_a+ w_p Q_p+ w_r Q_r

其中权重 wi++wr=1w_i+\dots+w_r=1。对于退款、支付争议等高风险场景,QaQ_aQpQ_p 应比语言自然度更重要。

7.1 不要只评估最终文本

一次客服运行应记录事件轨迹:

[
  {"type": "user_message", "text": "..."},
  {"type": "intent_detected", "intent": "refund_request"},
  {"type": "retrieval", "source": "after_sale_policy", "version": "2026-08-15"},
  {"type": "tool_call", "name": "check_refund_eligibility"},
  {"type": "tool_result", "status": "eligible"},
  {"type": "confirmation_requested"},
  {"type": "user_confirmed"},
  {"type": "tool_call", "name": "submit_refund"},
  {"type": "tool_result", "refund_id": "R9001"},
  {"type": "assistant_message", "claim": "退款申请已提交"}
]

这样才能区分:

  • 模型选错了意图;
  • 检索返回了过期政策;
  • 工具返回错误;
  • 工具成功但回复误报;
  • 用户没有确认,Agent 却执行了动作。

OpenAI Agents SDK 的结果对象不仅包含最终输出,也提供运行历史、最后接管 Agent、延续 ID,以及人工审批暂停时的中断信息和可恢复状态;这些信息适合用于审计、恢复和调试。(developers.openai.com)

7.2 质量评估的完整算例

假设一次退款会话得到以下结果:

意图识别:正确
政策检索:使用了当前版本
工具查询:成功
用户确认:缺失
退款执行:未发生
最终回复:声称“退款已提交”

可以这样判定:

意图质量:通过
知识质量:通过
工具质量:通过
流程质量:失败
结果质量:失败

即使语言流畅、用户暂时满意,仍然是严重缺陷,因为最终声明与业务状态不一致。

7.3 生产指标要按失败类型拆分

有用的指标包括:

  • 意图混淆率;
  • 澄清后成功率;
  • 检索证据覆盖率;
  • 工具调用成功率;
  • 工具重试率;
  • 未授权动作拦截率;
  • 人工升级准确率;
  • 升级后重复询问率;
  • 工单重复创建率;
  • 会话恢复成功率;
  • 回复与业务状态不一致率;
  • 用户二次联系率。

“自动解决率”不能单独作为目标。若 Agent 通过错误承诺避免升级,自动解决率上升,实际质量却下降。


八、会话记忆:上下文、状态和长期记忆必须分层

8.1 会话记忆的定义

会话记忆是支撑当前对话连续性的状态集合。它不等同于把全部聊天记录拼接进提示词。

至少要区分三层:

原始消息历史

保存用户和 Agent 的完整消息、工具调用和结果,主要用于审计、回放和必要时重建上下文。

工作记忆

保存当前任务所需的结构化状态:

{
  "intent": "refund_request",
  "order_id": "A10086",
  "confirmed_facts": ["订单属于当前用户"],
  "missing_slots": [],
  "pending_action": "awaiting_user_confirmation",
  "last_tool_result": {
    "eligible": true,
    "amount": 199
  }
}

长期记忆

保存跨会话仍然有价值、且适合长期保留的信息:

{
  "user_id": "U1001",
  "memory": "用户偏好通过短信接收物流通知",
  "source_conversation": "C7001",
  "confidence": 0.86,
  "created_at": "2026-08-20",
  "expires_at": "2027-02-20",
  "status": "active"
}

长对话历史不应直接等同于长期记忆。历史是事实记录,记忆是经过筛选、压缩和授权后的可复用信息。

8.2 会话状态的四种延续方式

在 SDK 层面,常见策略包括:

  • 应用自己保存完整历史;
  • 使用 Session 保存和加载会话;
  • 使用服务端会话 ID;
  • 使用上一次响应 ID进行轻量延续。

OpenAI Agents SDK 文档明确区分了这些策略,并提醒不要在没有明确协调的情况下混用本地回放历史和服务端状态,否则可能重复注入上下文。(developers.openai.com)

工程上应为每个会话选择一个主状态源:

浏览器/网关:只传 conversation_id 和新消息
会话服务:加载结构化状态和必要历史
Agent:读取当前任务上下文
工具服务:以业务数据库为最终事实来源

8.3 长期记忆的写入策略

记忆写入不能由模型自由决定。可以使用一个写入判定函数:

WriteMemory(m)=S(m)P(m)R(m)D(m)\text{WriteMemory}(m)= S(m)\land P(m)\land R(m)\land D(m)

其中:

  • SS:稳定性,未来仍可能成立;
  • PP:对未来服务有用;
  • RR:来源可靠;
  • DD:符合数据权限和保留政策。

例如:

内容 是否写入长期记忆 原因
本次订单号 任务事实,应保存在工单或会话
用户喜欢短信通知 可以 稳定且有服务价值
用户说“我现在很生气” 临时情绪,不应人格化保存
用户上传的身份证号码 通常否 敏感数据,需严格受控
用户多次要求英文回复 可以 重复出现的稳定偏好

一个安全的流程是:

候选记忆生成
    -> 敏感信息检测
    -> 与已有记忆比对
    -> 稳定性和来源评分
    -> 用户授权/策略检查
    -> 写入或丢弃

8.4 记忆检索和冲突

长期记忆检索不应全量注入。应按当前意图过滤:

当前意图:查询物流
相关记忆:通知渠道=短信
无关记忆:上次购买的商品类别、历史投诉情绪

当新旧记忆冲突时,不能简单覆盖。应保存来源和时间:

[
  {
    "key": "notification_channel",
    "value": "短信",
    "observed_at": "2026-08-01",
    "source": "explicit_user_preference",
    "status": "active"
  },
  {
    "key": "notification_channel",
    "value": "邮件",
    "observed_at": "2026-08-30",
    "source": "explicit_user_preference",
    "status": "active"
  }
]

如果用户明确说“以后改用邮件”,新记忆可以使旧记忆失效;如果只是某次订单选择了邮件,则可能只是任务级事实,不能覆盖长期偏好。

8.5 记忆的遗忘

遗忘不是简单删除数据库行。至少要考虑:

  • 过期时间;
  • 用户主动删除;
  • 业务目的结束;
  • 敏感信息清理;
  • 旧版本记忆失效;
  • 工单和审计记录的保留要求。

因此长期记忆应有生命周期:

candidate -> active -> superseded -> expired -> deleted

对话摘要可以帮助降低上下文长度,但摘要不是事实数据库。摘要丢失细节或产生偏差时,应能够回溯到原始事件和工具结果。


九、把人工介入和 Agent 状态连接起来

人工介入最容易出错的地方,是把“暂停运行”误认为“返回一条文本”。

一次暂停至少需要保存:

{
  "run_id": "R100",
  "conversation_id": "C7788",
  "status": "interrupted",
  "reason": "approval_required",
  "pending_action": {
    "tool": "submit_refund",
    "arguments": {
      "order_id": "A10086",
      "amount": 199
    }
  },
  "state_snapshot": "...",
  "expires_at": "2026-09-01T12:00:00+08:00"
}

恢复时必须检查:

  1. 原始业务状态是否仍然有效;
  2. 用户确认是否仍在有效期内;
  3. 工具参数是否被修改;
  4. 是否已经被人工或其他重试流程执行;
  5. 是否需要重新确认。

OpenAI 文档将人工审批描述为一种可暂停、可恢复的运行:暂停时可能没有最终输出,而是返回待处理的中断项和可恢复状态;审批或拒绝后,再把状态交回运行时继续处理。(developers.openai.com)

因此,恢复不是:

run(agent, "继续")

而应接近:

加载暂停状态
-> 重新读取订单/工单状态
-> 检查幂等键
-> 应用人工决定
-> 继续或终止运行
-> 向用户发送与实际状态一致的结果

十、一个可运行的最小状态机示例

下面代码不依赖模型,展示客服 Agent 最核心的业务控制逻辑。它可以直接用 Python 运行,用来验证意图、澄清、确认和提交之间的边界。

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional


class Status(Enum):
    NEW = "new"
    NEEDS_CLARIFICATION = "needs_clarification"
    WAITING_CONFIRMATION = "waiting_confirmation"
    COMPLETED = "completed"
    ESCALATED = "escalated"


@dataclass
class Session:
    user_id: str
    intent: Optional[str] = None
    order_id: Optional[str] = None
    refund_amount: Optional[float] = None
    status: Status = Status.NEW
    events: list[str] = field(default_factory=list)


def check_refund_eligibility(order_id: str) -> dict:
    # 示例工具:真实系统应查询订单服务,而不是写死结果。
    if order_id == "A10086":
        return {"eligible": True, "amount": 199.0}
    return {"eligible": False, "reason": "order_not_found_or_not_eligible"}


def handle_message(session: Session, message: str) -> str:
    session.events.append(f"user:{message}")

    if session.status == Status.WAITING_CONFIRMATION:
        if message.strip() in {"确认", "是", "继续"}:
            # 幂等键应由会话 ID、订单号和动作共同生成。
            idempotency_key = f"{session.user_id}:{session.order_id}:refund"
            session.status = Status.COMPLETED
            session.events.append(
                f"refund_submitted:{idempotency_key}:{session.refund_amount}"
            )
            return f"退款申请已提交,金额为 {session.refund_amount:.2f} 元。"

        session.status = Status.NEW
        session.events.append("confirmation_rejected")
        return "好的,我不会提交退款申请。"

    if "退款" in message and session.intent is None:
        session.intent = "refund_request"

    if session.intent == "refund_request" and session.order_id is None:
        # 这里只是示例解析,生产系统应使用结构化意图识别。
        if "A10086" in message:
            session.order_id = "A10086"
        else:
            session.status = Status.NEEDS_CLARIFICATION
            return "请提供需要申请退款的订单号。"

    if session.intent == "refund_request" and session.order_id:
        result = check_refund_eligibility(session.order_id)

        if not result["eligible"]:
            session.status = Status.ESCALATED
            session.events.append("refund_not_eligible")
            return "该订单暂时不满足自动退款条件,我已为你转人工客服进一步处理。"

        session.refund_amount = result["amount"]
        session.status = Status.WAITING_CONFIRMATION
        session.events.append(f"eligibility_checked:{result}")
        return (
            f"该订单可申请退款,金额为 {result['amount']:.2f} 元。"
            "退款提交后将进入处理流程,是否确认?"
        )

    return "我可以帮你查询订单、处理退款或转人工,请说明你希望完成什么操作。"


if __name__ == "__main__":
    s = Session(user_id="U1001")

    print(handle_message(s, "我要退款"))
    print(handle_message(s, "订单 A10086"))
    print(handle_message(s, "确认"))
    print(s.status.value)
    print(s.events)

预期输出类似:

请提供需要申请退款的订单号。
该订单可申请退款,金额为 199.00 元。退款提交后将进入处理流程,是否确认?
退款申请已提交,金额为 199.00 元。
completed
[
  "user:我要退款",
  "user:订单 A10086",
  "eligibility_checked:{'eligible': True, 'amount': 199.0}",
  "user:确认",
  "refund_submitted:U1001:A10086:refund:199.0"
]

这个示例刻意没有让语言模型直接修改 status。模型可以提出“用户可能要退款”,但只有编排器和工具服务才能决定状态是否迁移。

代码中的几个边界很重要:

  • 第一次消息缺订单号,因此进入澄清;
  • 查询资格成功后,只能生成确认请求;
  • 用户确认后才允许提交;
  • 提交动作带幂等键,避免超时重试造成重复退款;
  • 工具返回不满足条件时,不能编造失败原因,也不能继续执行,而是进入升级路径。

十一、如何映射到 Agents SDK

OpenAI Agents SDK 的基本使用方式是定义 Agent、运行 Agent,然后逐步加入工具、专业 Agent、交接、守卫和状态管理;官方示例同时提供 JavaScript 和 Python 用法。(developers.openai.com)

一个最小 Python 结构如下:

import asyncio
from agents import Agent, Runner, function_tool


@function_tool
def get_order_status(order_id: str) -> str:
    """查询订单状态。真实实现应调用订单服务。"""
    if order_id == "A10086":
        return "订单已签收,签收时间为 2026-08-30。"
    return "未找到订单"


support_agent = Agent(
    name="Customer support",
    instructions=(
        "你是客服 Agent。"
        "先识别用户意图,再决定是否需要查询工具。"
        "不得在工具未返回成功结果时声称业务动作已完成。"
        "退款、取消、修改等有副作用的动作必须先请求用户确认。"
    ),
    tools=[get_order_status],
)


async def main() -> None:
    result = await Runner.run(
        support_agent,
        "请查询订单 A10086 的状态。",
    )
    print(result.final_output)


if __name__ == "__main__":
    asyncio.run(main())

运行前提是:

python -m venv .venv
source .venv/bin/activate
pip install openai-agents
export OPENAI_API_KEY='你的密钥'
python app.py

这个示例适合验证 Agent 循环和工具调用,不适合直接作为生产退款流程。生产版本仍需补充:

  • 工具参数校验;
  • 用户身份和权限校验;
  • 业务服务端的幂等;
  • 工具超时和重试策略;
  • 审批或人工介入;
  • 工具结果和回复之间的一致性检查;
  • 运行轨迹和审计记录。

多 Agent 设计时,还需要区分两种模式:

  • handoff:专业 Agent 接管该分支,并负责后续用户回复;
  • agents as tools:主 Agent 仍然拥有回复权,只把专业 Agent 当作受限工具调用。

OpenAI 文档将这种差异概括为“谁拥有最终面向用户的回复”。如果支付 Agent 应该直接负责支付争议,就使用交接;如果主客服 Agent 只需要支付 Agent 提供一份分析结果,则更适合使用 Agent 作为工具。(developers.openai.com)

Anthropic 对路由工作流的说明也指出,复杂客服问题适合将一般咨询、退款和技术支持分发到不同的下游流程、提示词和工具;但只有当类别确实需要不同能力,且分类足够可靠时,拆分才有价值。(anthropic.com)


十二、并发、故障和恢复路径

客服 Agent 的故障不只来自模型。

工具超时

处理方式:

第一次调用超时
-> 查询幂等状态
-> 如果已提交,读取已有结果
-> 如果未提交且允许重试,再次提交
-> 无法确认时,不向用户声称成功,进入待核查状态

用户重复发送

同一用户可能连续发送:

“帮我退款”
“确认退款”
“怎么还没退款”

消息处理应有顺序号或版本号:

conversation_id = C7788
message_seq = 1, 2, 3

状态更新采用乐观锁:

UPDATE conversation_state
SET status = 'completed',
    version = version + 1
WHERE conversation_id = 'C7788'
  AND version = 7;

如果影响行数为 0,说明另一个并发请求已经更新状态,当前请求必须重新读取状态,而不是覆盖结果。

人工接管与自动回复并发

人工接管后,自动回复任务必须检查:

conversation.owner == "human"

如果成立,自动 Agent 只能写内部事件,不能向用户发送消息。

知识版本变化

对话开始时读取的政策和提交动作时读取的政策可能不同。高风险动作应在提交前重新校验当前规则,而不是永久相信早先检索到的文档。

记忆写入失败

记忆不是业务主事务。即使长期记忆写入失败,也不应回滚已经成功的退款或工单创建。两者应分离:

业务事务:必须可靠提交
会话状态:需要可恢复
长期记忆:允许异步最终一致

十三、常见误解和失败表现

误解一:有知识库就能回答

失败表现:

  • 检索到了过期政策;
  • 没有过滤地区和商品类型;
  • 把订单实时状态当成静态文档;
  • 证据之间冲突却仍然给出确定答案。

修正方法是让知识结果携带来源、版本、有效期和适用范围,并让 Agent 在证据不足时澄清或升级。

误解二:有工单 API 就实现了升级

失败表现:

  • 工单没有负责人;
  • 没有 SLA;
  • 没有上下文快照;
  • 用户不知道是否已转人工;
  • 人工无法知道 Agent 做过什么。

升级必须同时完成责任转移、上下文传递和用户通知。

误解三:会话历史就是长期记忆

失败表现:

  • 每次把大量历史塞入提示词;
  • Agent 被无关投诉和旧订单干扰;
  • 用户更改偏好后旧信息仍然生效;
  • 敏感信息被长期保留。

应将原始历史、工作状态和长期记忆分开设计。

误解四:自动解决率越高越好

失败表现:

  • Agent 避免升级,反复套用模板;
  • 错误地声称已退款、已取消或已联系物流;
  • 用户被迫重复提问;
  • 后台工单数量暂时下降,但投诉增加。

客服系统的目标不是减少人工次数,而是在责任、正确性和成本之间取得可解释的平衡。


十四、落地时应先固定的契约

在开始调 Prompt 之前,先固定以下契约:

意图契约:
- 意图名称
- 必填实体
- 澄清问题
- 允许工具
- 风险等级
- 升级条件

知识契约:
- 来源
- 版本
- 有效期
- 适用范围
- 证据格式

工具契约:
- 输入 Schema
- 权限要求
- 是否有副作用
- 是否需要确认
- 幂等键
- 错误分类

工单契约:
- 状态机
- 负责人
- SLA
- 上下文快照
- 用户可见状态

记忆契约:
- 写入条件
- 检索范围
- 冲突规则
- 过期时间
- 删除方式

质量契约:
- 轨迹事件
- 失败分类
- 评估样本
- 业务结果指标

这些契约决定了 Agent 的可测试性。没有它们,系统只能依靠人工阅读对话来判断“感觉是否还可以”。

客服 Agent 的核心不是让模型表现得像人,而是让每一次判断都能落到明确的意图、有效证据、受控工具、可追踪工单、清晰升级、可验证质量和可恢复记忆上。只有这些边界同时成立,Agent 才真正具备处理客服业务的工程属性。


系列导航与关联阅读

官方资料

本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。