langchain4j 学习系列(7)-文本分类

继续我们的langchain4j学习之旅,很多“智能客服”之类的AI应用,“问题分类”是非常重要的功能之一。比如:客人进来咨询问题,得判断出客人的问题是“订单相关”(比如:我要取消订单),还是“支付相关”(比如:我要退款),还是“投诉相关”(比如:你们的服务太差了,我要投诉到相关部门)。识别出对应分类后,就可以交给相应的流程(或细分的sub agent)做进一步处理。

langchain4j 提供了2种分类方法:

一、基于LLM的语义理解

1.1 定义分类枚举

    enum CustomerServiceCategory {
        PRODUCT("产品相关"),
        ORDER("订单相关"),
        ACCOUNT("账户相关"),
        MEMBER("会员相关"),
        PAYMENT("支付相关"),
        OTHERS("其它问题");

        @Getter
        private final String desc;

        CustomerServiceCategory(String desc) {
            this.desc = desc;
        }
    }

1.2 定义AIService的接口

    interface CustomerServiceCategoryClassifier {
        @UserMessage("将客人遇到的问题【{{text}}】归类")
        CustomerServiceCategory classify(String text);
    }

1.3 测试

    @GetMapping(value = "/classify", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> classify(@RequestParam String query) {
        try {
            CustomerServiceCategoryClassifier customerServiceClassifier = AiServices.create(CustomerServiceCategoryClassifier.class, ollamaChatModel);
            CustomerServiceCategory classify = customerServiceClassifier.classify(query);
            return ResponseEntity.ok(classify.getDesc());
        } catch (Exception e) {
            log.error("classify", e);
            return ResponseEntity.ok("{\"error\":\"classify error: " + e.getMessage() + "\"}");
        }
    }

效果:

image

image

观察日志的话,能看到与LLM的交互:

2025-12-09T21:00:57.280+08:00  INFO 5948 --- [langchain4j-study] [io-8080-exec-10] d.l.http.client.log.LoggingHttpClient    : HTTP request:
- method: POST
- url: http://localhost:11434/api/chat
- headers: [Content-Type: application/json]
- body: {
  "model" : "deepseek-v3.1:671b-cloud",
  "messages" : [ {
    "role" : "user",
    "content" : "将客人遇到的问题【我的退款为什么还没到账?】归类\nYou must answer strictly with one of these enums:\nPRODUCT\nORDER\nACCOUNT\nMEMBER\nPAYMENT\nOTHERS"
  } ],
  "options" : {
    "stop" : [ ]
  },
  "stream" : false,
  "tools" : [ ]
}

这种方法的准确性完全取决于模型能力的强弱,大多数情况下是OK的,但有些case可能会不太符合我们的预期,比如:

image

一些互联网平台,对于“积分”可能会归到【会员相关】(比如:积分累积到了一定程度,可以兑换成高等级会员),这时候就可以考虑第2种方法。

  

二、基于向量数据库的相似度计算

2.1 先梳理已知问题的归类

    Map<CustomerServiceCategory, List<String>> getExamples() {
        Map<CustomerServiceCategory, List<String>> examples = new HashMap<>();

        examples.put(PRODUCT, asList(
                "怎么没有产品说明书?",
                "产品的保修期过了怎么办?",
                "我新买的东西用了一周就坏了?",
                "产品无法使用?"
        ));

        examples.put(ORDER, asList(
                "我的订单现在到哪里了?",
                "能给我一个快递单号吗?",
                "我怎么知道我的订单已经发货了?",
                "我可以更改配送方式吗?",
                "你们提供次日达服务吗?",
                "可以选择到店自提吗?",
                "我的订单什么时候能到?",
                "为什么我的配送延迟了?",
                "我可以指定配送日期吗?",
                "已经过了预计送达日期,我的订单在哪里?",
                "如果出现延迟,我会收到通知吗?",
                "天气原因会导致配送延迟多久?",
                "我收到了订单,但少了一件商品。",
                "包裹送达时是空的。",
                "我收到的商品错了,该怎么办?",
                "我所有的商品会同时送达吗?",
                "为什么我只收到部分订单商品?",
                "剩下的商品能更快送达吗?"
        ));

        examples.put(PAYMENT, asList(
                "能用支付宝吗?",
                "微信支付可以吗?",
                "支持信用卡吗?",
                "付款时我遇到一个错误",
                "可以通过银行转账付款吗?",
                "为什么我的付款被拒绝了",
                "可以给我发送上一笔订单的发票吗?",
                "发票会自动发送到我的电子邮箱吗?",
                "如何申请退款?"
        ));

        examples.put(MEMBER, asList(
                "我的会员等级变低了?",
                "我的积分过期了?",
                "我的优惠券不能用了?",
                "能给我发个支付95折优惠券吗?",
                "满减优惠券当次消费就能用吗?",
                "能送100点优惠积分吗?"
        ));

        examples.put(ACCOUNT, asList(
                "如何注销账号?",
                "我的密码过期了?",
                "为什么无法登录",
                "登录时手机收不到验证码",
                "如果更换账号绑定的手机号"
        ));

        examples.put(OTHERS, asList(
                "厂商的联系电话是多少?",
                "如何加盟?",
                "你们公司还招人吗?"
        ));

        return examples;
    }

2.2 使用embedding模型分类

    @GetMapping(value = "/classify/embed", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> classifyEmbed(@RequestParam String query) {
        try {
            TextClassifier<CustomerServiceCategory> classifier = new EmbeddingModelTextClassifier<>(ollamaEmbeddingModel, getExamples());
            List<CustomerServiceCategory> categories = classifier.classify(query);
            String result = "";
            if (!CollectionUtils.isEmpty(categories)) {
                CustomerServiceCategory classify = categories.get(0);
                result += classify.getDesc() + ",";
            }
            return ResponseEntity.ok(StringUtils.trimTrailingCharacter(result, ','));
        } catch (Exception e) {
            log.error("classify", e);
            return ResponseEntity.ok("{\"error\":\"classify error: " + e.getMessage() + "\"}");
        }
    }

image

刚才的case符合预期了,但该方法有缺陷也十分明显,如果梳理的已知问题分类不够全面,会出现误判,比如:

image

这个问题不在事先准备好的问题列表中,计算出来的结果就差强人意了。实际应用中,如何取舍看业务场景,或者二者结合使用(比如:2个方法都跑一遍,看是否一致,然后再根据一定策略做处理。或者先用方法1,先做一轮分类,将结果人工复检后,用于完善方法2中的分类列表)

 

文中示例代码:GitHub - yjmyzz/langchain4j-study at day07

posted @ 2025-12-09 21:29  菩提树下的杨过  阅读(0)  评论(0)    收藏  举报