Skip to content

实战案例(三):工单处理 Agent ​

1. 本节产出 ​

一个跨系统、有状态、带审批的工单处理 Agent:接单 → 分类 → 查资料 → 生成处理方案 → 执行(需审批)→ 通知。它把前面所有能力串起来了,是 03 篇的综合练习。

2. 前置依赖 ​

3. 为什么工单是「综合能力」的试金石 ​

对比三个案例:

案例主要能力
运维助手诊断(只读为主)
代码审查分析(生成型)
工单处理流程编排 + 多系统写操作 + 状态持久化 + 审批

工单场景同时具备:

  • 多步流程(分类 → 查 → 处理 → 通知)
  • 写操作(改工单状态、发通知、可能退款)
  • 长耗时(可能等待审批几小时)
  • 跨系统(工单系统、订单系统、用户系统、通知系统)

所以它能检验 Harness 的每一个部件:状态持久化撑住长耗时、审批网关管住写操作、循环检测防止卡单、审计记录全程可追溯。

4. 核心原理 ​

4.1 流程设计 ​

接单
  │
  ▼ 分类(结构化输出)
工单类型 / 紧急度 / 涉及系统
  │
  ▼ 查资料(RAG + 查询工具)
用户订单信息、历史工单、相关政策
  │
  ▼ 生成处理方案
  ├─ 可直接处理(退款 200 元以内) → 执行(需审批)
  ├─ 需人工判断(超出权限)       → 升级给人工,附分析
  └─ 信息不足                     → 回复用户询问补充信息
  │
  ▼ 通知
给用户发处理结果(需审批,对外通信)

「超出权限就升级人工」这个分支很重要。Agent 不该硬撑着处理它判断不了的事——明确升级比给一个错误的处理结果好得多。

4.2 权限边界的量化 ​

条件动作
退款金额 ≤ 200 且用户等级正常Agent 可执行(审批后)
退款金额 > 200必须人工处理
涉及账号封禁必须人工处理
用户投诉升级中必须人工处理

把权限边界写成明确的规则,而不是让模型自己判断。模型判断「这个要不要人工」是不可靠的,规则判断是确定的。

4.3 长耗时与状态 ​

工单处理可能持续几小时(等审批)
   │
   ▼ 状态必须持久化
审批回来后从断点继续
   │
   ▼ 如果超时(如 24 小时)→ 自动升级人工,不要无限等

审批等待不计入 Agent 总超时。这是个容易搞错的点:总超时应该只算「运行时间」,等待审批的时间要排除。

4.4 与人工坐席的协作 ​

Agent 输出的不只是结论,还有「给人工的处理建议」

升级时附上:
  - 已查到的信息(用户订单、历史工单)
  - 分类结果与依据
  - 建议的处理方案
  - 尝试过但失败的操作

这让人工接手时不用重做 Agent 已经做过的事。协作设计得好,Agent 的价值就体现在「人工处理时间缩短」这个可量化的指标上。

5. 代码走查 ​

5.1 权限规则(代码判定,不让模型判断) ​

java
// src/main/java/com/example/harness/caseticket/TicketPolicy.java
@Component
public class TicketPolicy {

    /** 返回处置方式:这是确定性规则,不由模型决定 */
    public Disposition decide(TicketClassified c) {
        if (c.amount() != null && c.amount().compareTo(REFUND_LIMIT) > 0) {
            return Disposition.ESCALATE("退款金额 %s 超出自动处理上限 %s"
                    .formatted(c.amount(), REFUND_LIMIT));
        }
        if (c.type() == TicketType.ACCOUNT_BAN) {
            return Disposition.ESCALATE("账号封禁需人工审核");
        }
        if (c.userComplaining()) {
            return Disposition.ESCALATE("用户已投诉,转人工");
        }
        if (c.missingInfo() != null && !c.missingInfo().isEmpty()) {
            return Disposition.ASK_USER(c.missingInfo());
        }
        return Disposition.AUTO_HANDLE();
    }

    public sealed interface Disposition {
        record AUTO_HANDLE() implements Disposition {}
        record ESCALATE(String reason) implements Disposition {}
        record ASK_USER(List<String> fields) implements Disposition {}
    }
}

用 sealed interface 表达处置方式,编译期就能保证所有分支被处理。

5.2 处理流程 ​

java
// src/main/java/com/example/harness/caseticket/TicketAgent.java
@Service
public class TicketAgent {

    public TicketOutcome handle(Ticket ticket) {
        // 1. 分类(结构化输出,temperature=0)
        TicketClassified c = classify(ticket);

        // 2. 规则判定处置方式(确定性)
        Disposition d = policy.decide(c);

        if (d instanceof Disposition.ESCALATE e) {
            return escalate(ticket, c, e.reason());       // 升级人工
        }
        if (d instanceof Disposition.ASK_USER a) {
            return askUser(ticket, a.fields());           // 询问补充
        }

        // 3. 交给 Harness 执行(含审批、循环检测、审计)
        AgentRequest req = AgentRequest.builder()
                .goal("""
                        处理工单 %s。
                        用户信息:%s
                        分类结果:%s
                        可执行操作:退款(≤%s)、补发优惠券、修改订单状态
                        完成后给用户发送处理结果通知。
                        """.formatted(ticket.id(), ticket.userSummary(),
                                c, REFUND_LIMIT))
                .tenant(ticket.tenantId())
                .tools("ticket")
                .limits(Limits.of(12, 150_000, Duration.ofMinutes(5)))
                .build();

        AgentResult r = harness.run(req);

        return switch (r.status()) {
            case DONE           -> TicketOutcome.resolved(r.answer());
            case NEED_APPROVAL  -> TicketOutcome.waitingApproval(r.runId());
            default             -> escalate(ticket, c, "Agent 未能完成:" + r.status());
        };
    }
}

注意最后的 default -> escalate:Agent 失败时升级人工,而不是让工单卡住。这是兜底设计。

5.3 审批等待不计入超时 ​

java
// 恢复运行时重算 deadline,排除等待审批的时间
public AgentResult resumeAfterApproval(String runId, String observation) {
    AgentState s = repo.load(runId).orElseThrow();

    // 关键:deadline 从「当前时间」重新起算,而不是用创建时的
    s = s.withDeadline(Instant.now().plus(props.totalTimeout()));

    return runner.continueWith(s, observation);
}

5.4 升级给人工时的信息包 ​

java
// src/main/java/com/example/harness/caseticket/EscalationPack.java
public record EscalationPack(
        String ticketId,
        TicketClassified classification,
        List<String> factsFound,        // 已查到的信息
        String suggestedAction,         // 建议方案
        List<String> attemptedSteps,    // 已尝试(含失败)
        String reason                   // 升级原因
) {}

6. 跑起来 ​

bash
git checkout ch03-16-ticket-agent
docker compose up -d
mvn spring-boot:run

三类工单各跑一次:

bash
# 1. 可自动处理(小额退款)
curl -X POST http://localhost:8080/api/ticket/handle -d '{
  "id":"TK-001","content":"我买的商品少发了一件,要退款","amount":89}'
# 期望:执行退款(触发审批),审批通过后完成并通知

# 2. 超出权限(大额退款)
curl -X POST http://localhost:8080/api/ticket/handle -d '{
  "id":"TK-002","content":"订单要全额退款","amount":2999}'
# 期望:直接升级人工,不调用任何写工具

# 3. 信息不足
curl -X POST http://localhost:8080/api/ticket/handle -d '{
  "id":"TK-003","content":"我要退货"}'
# 期望:回复用户询问订单号
检查项通过标准
自动处理小额退款走完流程并生成审批单
超限升级大额退款不调用写工具,直接升级
信息不足询问用户而非猜测
审批继续批准后从断点继续,完成通知
失败兜底Agent 失败时升级人工而非卡住
审计完整全过程可回放

第二项是本节最重要的验收:验证规则判定(而非模型判定)确实拦住了越权操作。

7. 生产避坑 ​

  1. 权限边界必须写成确定性规则,不能让模型自己判断。模型判断「该不该升级人工」会因表述、上下文而变化,规则判断是确定的。金额、操作类型这类可量化的边界,一律用代码判定。
  2. 审批等待时间不计入总超时。把等待审批的时间算进超时,会导致「审批了两小时后回来,任务已被判超时」。恢复运行时要重算 deadline。
  3. Agent 失败必须有兜底(升级人工),不能让工单卡住。Agent 会因为各种原因失败(工具挂了、循环检测到、预算耗尽),每一种都要有对应的处置——默认兜底永远是「转人工」。

8. 延伸与锚点 ​

  • 思考题:三个案例都跑通了,怎么知道它们在生产上跑得好不好?要看哪些指标?(答案在下一课时:可观测面板)
  • 代码锚点:git checkout ch03-16-ticket-agent
  • 下一课时:03-17 Agent 可观测面板
  • 对应课件:L03-16 工单 Agent