上个月领导给我派了个活儿:把公司的售后工单做智能化,让AI自动给工单分拣、打标签、写初步处理建议。
我第一反应是直接做个大Agent,把三个步骤全塞进去。结果试了一周,效果惨不忍睹——不是分拣错,就是标签贴得莫名其妙,建议写得驴唇不对马嘴。
后来我把这个"全能Agent"拆成了三个专职Agent:一个管分拣,一个管打标,一个管写建议。效果直接起飞,准确率从61%干到了89%。
这篇文章把我拆分的思路、踩的坑、还有代码结构全写出来。你要是也在做Agent,这个案例应该能帮到你。
为什么单个Agent做不好
一开始我的做法很朴素:一个Agent,system prompt里写上"你是售后工单处理助手,你需要做三件事:分拣、打标、写建议"。
看起来逻辑很顺,实际跑起来全是问题:
问题一:上下文互相污染。 分拣只需要工单的标题和内容,打标需要的是商品信息和客户历史,写建议需要的是售后政策库。全塞进一个上下文,模型分不清该用哪部分,经常拿售后政策去判断分拣结果,错得离谱。
问题二:意图混在一起,输出不稳定。 你让它分完拣顺手打个标签,它可能把标签写在分拣结果里,也可能把分拣结果写进建议里。JSON输出结构三天两头变,解析代码跟着改,改完还漏。
问题三:一个环节出错,全盘皆输。 分拣错了,后面打标和建议全错,而且你根本不知道错在哪一步。排查的时候像个黑盒,日志里全是一段对话,看不出哪步出的问题。
这个问题很典型,其实跟微服务拆分的逻辑一样:职责单一,边界清晰。 一个Agent只干一件事,上下文干净,输出稳定,出错了也好定位。
拆成三个Agent之后
我的结构是这样的:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 分拣Agent │ ──► │ 打标Agent │ ──► │ 建议Agent │
│ (分类判断) │ │ (打标签) │ │ (写建议) │
└─────────────┘ └─────────────┘ └─────────────┘
每个Agent各管一段,上游的输出作为下游的输入。
分拣Agent:只负责判断工单属于哪个类别(退换货、物流、发票、维修……)。输入是工单标题+内容,输出是一个类别+置信度。上下文里只有分类规则。
// 用Spring AI的ChatClient,每个Agent一个实例
@Service
public class SortingAgent {
private final ChatClient chatClient;
public SortingAgent(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("""
你是售后工单分拣员。你的唯一任务是判断工单所属类别。
类别只有:退货、换货、物流、发票、维修、其他。
根据工单标题和内容判断,返回JSON:{"category":"退货","confidence":0.95}
不要输出任何其他内容。
""")
.build();
}
public SortResult sort(String title, String content) {
String resp = chatClient.prompt()
.user("标题:" + title + "\n内容:" + content)
.call()
.content();
// 解析JSON,顺便校验类别是否合法
return parseAndValidate(resp);
}
}
打标Agent:只负责根据工单内容打标签,比如"加急""涉及退款""需要仓库介入"。输入是工单内容+分拣结果,输出是一组标签。上下文里只有标签规则。
建议Agent:只负责根据工单内容、分拣结果、标签,结合售后政策库,写初步处理建议。这个Agent挂了一个Function Calling,去查售后政策:
@Bean
@Description("根据类别查询售后处理政策")
public Function<PolicyQuery, PolicyInfo> queryPolicy(PolicyService policyService) {
return query -> policyService.getPolicy(query.category());
}
三个关键设计
拆分只是第一步,真正让准确率从61%到89%的,是下面这三个设计。
1. 上游输出要做校验,别直接喂给下游
分拣Agent输出"退货",但模型偶尔会抽风输出"退 货"或者"Retrun"。这种脏数据直接喂给打标Agent,整个链路就断了。
我在每个Agent输出之后加了校验层:
private SortResult parseAndValidate(String resp) {
// 解析JSON
SortResult result = jsonMapper.readValue(resp, SortResult.class);
// 校验类别是否在合法列表里
if (!VALID_CATEGORIES.contains(result.category())) {
// 不在列表里,走兜底:重新问一次,或者标记为"其他"
result = new SortResult("其他", 0.5);
}
return result;
}
这个兜底逻辑看着简单,但把链路的稳定性拉高了一大截。模型的输出再不可控,到我们手里之前已经被约束成合法数据了。
2. 置信度低的时候,别硬猜,转人工
分拣Agent输出置信度低于0.6的时候,我一开始是硬着头皮往下走,结果错得离谱。后来改成:置信度低直接转人工,不进自动化链路。
if (sortResult.confidence() < 0.6) {
// 转人工队列,人工处理后再进后续流程
manualQueue.enqueue(ticket);
return;
}
这个改动最直接的效果是:自动化处理的数量少了,但准确率涨了。 我们算过账,62%的工单能自动处理,其中89%是准的,剩下的38%人工兜底。整体效率反而比"硬自动化"高了。
做AI功能的时候要记住一个反直觉的结论:敢让AI"不干活",比让AI硬干活更重要。
3. 中间结果全部落库,出问题能回放
这是排查利器。每个Agent的输入输出我都存了一张表,字段就是:工单ID、Agent名称、输入摘要、输出全文、耗时、token数。
出问题的时候,一条工单沿着链路查下去,一眼就能看出是哪个Agent错的、错在哪。没有这个,多Agent系统就是个黑盒,排查全靠猜。
性能问题的处理
多Agent串行调用,每个Agent一次大模型调用,一条工单平均要调3次模型。高峰期工单多的时候,响应时间感人。
我的处理办法:
第一,能并行的环节并行。 打标和查政策其实不依赖彼此,我把它俩并行调用,链路时间从3次串行变成2次串行的量级。这里的调整对P99影响很明显。
第二,结果缓存。 同类型的工单,比如"退货+加急",处理建议有大量重复。我在建议Agent那层加了缓存,相同的输入组合直接命中,省一次模型调用。缓存命中率大概有30%左右,直接省了这部分成本。
第三,控制并发。 大模型API有QPS限制,高峰期我用了Semaphore限流,防止把API打爆:
// 用信号量控制并发调用数
private final Semaphore semaphore = new Semaphore(20);
public String callWithLimit(String prompt) {
semaphore.acquire();
try {
return chatClient.prompt().user(prompt).call().content();
} finally {
semaphore.release();
}
}
一个让我印象深刻的翻车案例
有一次线上突然出现一批工单,分拣结果全变成了"维修",但实际内容是"退货"。查了半天,最后发现是上游改了商品名称,新商品名里带"维修"两个字,分拣Agent的prompt里示例太少,被带偏了。
这个案例让我意识到:prompt里的示例要覆盖真实数据的多样性,而且要定期更新。 我们后来把示例扩充到覆盖所有历史类别的真实工单,分拣准确率又涨了一截。
模型的判断是会漂移的,数据变了,示例就得跟着变。这不是一次性的工程,是个持续维护的过程。
结尾
多Agent不是炫技,是被单个Agent的能力边界逼出来的。它的本质和微服务一样:把复杂问题拆成简单问题,每个简单问题用最合适的方式解决,再串起来。
这套架构的好处是:每个Agent可以单独优化、单独测试、单独替换模型。后面我想把分拣Agent换成一个更快的轻量模型,其他两个不动,只需要改一处配置。
如果这篇对你有帮助,点个关注。我后面会写多Agent系统的评测怎么做——怎么量化"准确率",怎么建评测集,这个坑更多。