know-engine

钉钉机器人接入做客服答疑

如果你的RAG系统,要实现企业内部答疑,其实不需要单独弄一个网页,直接用办公软件集成进来就行了,比如说钉钉。(就像现在很火的openclaw一样,也是对接到各个IM上) 我们可以借助企业钉钉机器人来实现这样的功能,我们需要对话的时候…

TL;DR

如果你的RAG系统,要实现企业内部答疑,其实不需要单独弄一个网页,直接用办公软件集成进来就行了,比如说钉钉。(就像现在很火的openclaw一样,也是对接到各个IM上) 我们可以借助企业钉钉机器人来实现这样的功能,我们需要对话的时候…

如果你的RAG系统,要实现企业内部答疑,其实不需要单独弄一个网页,直接用办公软件集成进来就行了,比如说钉钉。(就像现在很火的openclaw一样,也是对接到各个IM上) 我们可以借助企业钉钉机器人来实现这样的功能,我们需要对话的时候,直接找到这个机器人,问他就行了。如: (图片来自钉钉官网公开案例:https://open.dingtalk.com/document/development/single-chat-bot-overview ) 我们的实现效果如下:

创建钉钉机器人

https://open.dingtalk.com/?spm=a2q3p.21071111.0.0.37d365eeU4YUj9 在钉钉开发者平台上,用你的钉钉的账号登录,登陆之后选择一个企业,然后就可以进到开发者后台了。 接着,进入【应用开发】tab中,创建一个钉钉应用,先输出【应用名】和【应用描述】下一步之后,进入应用页面。 在这里【添加应用能力】,添加一个【机器人】,这在里填好机器人的基本信息,主要是名称、描述、头像等等,然后最下面的【消息接收模式】选择Stream模式。 Stream 模式是钉钉开放平台提供的一种集成方式,它可以监听机器人回调、事件订阅回调和注册卡片回调。使用 Stream 模式接入,钉钉开放平台将通过 Websocket 连接与应用程序通讯,Stream 模式将极大降低接入门槛和资源依赖,不需要公网服务器、IP、域名等资源,只需集成钉钉开放平台 SDK 即可。 之后点击发布,将机器人发布。

申请权限

如果想让你的钉钉机器人具备单聊的功能,需要申请一个【企业内机器人发送消息权限】 申请路径如下:

应用发布

之后,在【版本管理与发布】这里创建一个新版本,然后发布。 可见范围可以自己调整。之后发布即可。

代码开发

接着就是代码开发了,我们可以参考钉钉官方给的demo:https://github.com/open-dingtalk/dingtalk-stream-sdk-java-quick-start 这里面是java中接入钉钉机器人的代码,把全部代码copy到我们的应用中。然后就可以启动应用了。 记得增加和修改配置:

dingtalk:
  appKey: ${DINGTALK_APP_KEY}
  appSecret: ${DINGTALK_APP_SECRET}
  robotCode: ${DINGTALK_APP_KEY}  # 企业内部应用机器人的 robotCode 通常与 appKey 相同

(也可以直接看我们项目中的代码,在cn.hollis.llm.mentor.know.engine.dingtalk这个目录下,我真是demo的代码也做了修改和精简) 如果直接复制完代码后,运行时,如果是群聊,则能正常回复,但是如果是和机器人的单聊,则会报错: 这是因为demo中给的代码默认是群聊模式的代码,单聊模式的并没有给出,于是我们需要做些改造。先定义一个RobotPrivateMessageService:

package cn.hollis.llm.mentor.rag.dingtalk.service;

import com.alibaba.fastjson.JSONObject;
import com.aliyun.dingtalkrobot_1_0.Client;
import com.aliyun.dingtalkrobot_1_0.models.*;
import com.aliyun.tea.TeaException;
import com.aliyun.teautil.models.RuntimeOptions;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Objects;

@Slf4j
@Service
public class RobotPrivateMessageService {
    private Client robotClient;
    private final AccessTokenService accessTokenService;

    @Value("${robot.code}")
    private String robotCode;

    @Autowired
    public RobotPrivateMessageService(AccessTokenService accessTokenService) {
        this.accessTokenService = accessTokenService;
    }

    @PostConstruct
    public void init() throws Exception {
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
        config.protocol = "https";
        config.regionId = "central";
        robotClient = new Client(config);
    }

    public String send(String text,String userId) throws Exception {
        BatchSendOTOHeaders batchSendOTOHeaders = new BatchSendOTOHeaders();
        batchSendOTOHeaders.setXAcsDingtalkAccessToken(accessTokenService.getAccessToken());
        BatchSendOTORequest batchSendOTORequest = new BatchSendOTORequest();
        batchSendOTORequest.setMsgKey("sampleText");
        batchSendOTORequest.setRobotCode(robotCode);
        batchSendOTORequest.setUserIds(java.util.Arrays.asList(userId));

        JSONObject msgParam = new JSONObject();
        msgParam.put("content", "java-getting-start say : " + text);
        batchSendOTORequest.setMsgParam(msgParam.toJSONString());


        try {
            BatchSendOTOResponse batchSendOTOResponse = robotClient.batchSendOTOWithOptions(batchSendOTORequest, batchSendOTOHeaders, new RuntimeOptions());
            if (Objects.isNull(batchSendOTOResponse) || Objects.isNull(batchSendOTOResponse.getBody())) {
                log.error("RobotPrivateMessages_send batchSendOTOResponse return error, response={}",
                        batchSendOTOResponse);
                return null;
            }
            return batchSendOTOResponse.getBody().getProcessQueryKey();
        } catch (TeaException e) {
            log.error("RobotPrivateMessages_send batchSendOTOResponse throw TeaException, errCode={}, " +
                    "errorMessage={}", e.getCode(), e.getMessage(), e);
            throw e;
        } catch (Exception e) {
            log.error("RobotPrivateMessages_send batchSendOTOResponse throw Exception", e);
            throw e;
        }
    }

}

紧接着在ChatBotCallbackListener中改造代码, 处理流程: 1. 从 ChatbotMessage 中取出文本内容、会话类型、发送者 StaffId。 2. 给发送方回一条"已接到您的请求,正在思考中...",降低等待焦虑。 3. 拼接 dingtalk_ 前缀作为内部 userId,调用 ChatApplicationService.chat(userId, content, null, ChatSource.STAFF_DING)。 4. 由于钉钉机器人为请求-响应模型,无法直接消费 SSE 流,通过 aggregateChatResult(...) 把 Flux 阻塞聚合为结构化结果。 5. 根据聚合结果选择消息模板,调用群聊或单聊发送服务。 改造后代码如下:

package cn.hollis.llm.mentor.know.engine.dingtalk.callback.chatbot;

import cn.hollis.llm.mentor.know.engine.business.vo.CarInfoVO;
import cn.hollis.llm.mentor.know.engine.business.vo.MyCarVO;
import cn.hollis.llm.mentor.know.engine.chat.constant.ChatSource;
import cn.hollis.llm.mentor.know.engine.chat.entity.ChatMessage;
import cn.hollis.llm.mentor.know.engine.chat.service.ChatApplicationService;
import cn.hollis.llm.mentor.know.engine.dingtalk.service.RobotGroupMessagesService;
import cn.hollis.llm.mentor.know.engine.dingtalk.service.RobotPrivateMessageService;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.dingtalk.open.app.api.callback.OpenDingTalkCallbackListener;
import com.dingtalk.open.app.api.models.bot.ChatbotMessage;
import com.dingtalk.open.app.api.models.bot.MessageContent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 机器人消息回调(支持单聊和群聊)
 *
 * @author Hollis
 */
@Slf4j
@Component
public class ChatBotCallbackListener implements OpenDingTalkCallbackListener<ChatbotMessage, JSONObject> {

    private static final String DINGTALK_USER_PREFIX = "dingtalk_";

    @Autowired
    private RobotGroupMessagesService robotGroupMessagesService;

    @Autowired
    private RobotPrivateMessageService robotPrivateMessageService;

    @Autowired
    private ChatApplicationService chatApplicationService;

    @Override
    public JSONObject execute(ChatbotMessage message) {
        try {
            MessageContent text = message.getText();
            if (text != null) {
                String conversationType = message.getConversationType();

                String msg = text.getContent();
                String openConversationId = message.getConversationId();
                String senderUserId = message.getSenderStaffId();
                log.info("receive bot message,conversationType={}, msg={}", conversationType, msg);

                String userId = DINGTALK_USER_PREFIX + senderUserId;

                if (conversationType.equals("2")) {
                    log.info("receive bot message from group={}, msg={}", openConversationId, msg);
                    robotGroupMessagesService.send(openConversationId, "已接到您的请求,正在思考中...");
                } else if (conversationType.equals("1")) {
                    log.info("receive bot message from user={}, msg={}", userId, msg);
                    robotPrivateMessageService.send("已接到您的请求,正在思考中...", senderUserId);
                }

                // 复用 ChatApplicationService 的完整对话链路(会话创建、意图识别、RAG/兜底通用对话)
                Flux<String> chatFlux = chatApplicationService.chat(userId, msg, null, ChatSource.STAFF_DING);

                // 钉钉机器人为请求-响应模型,无法直接消费 SSE 流,阻塞聚合为完整结果
                ChatResult chatResult = aggregateChatResult(chatFlux);

                // 这里需要注意,官网中说0是单聊,1是群聊,我实测发现1是单聊,2是群聊。
                if (conversationType.equals("2")) {
                    sendGroupResult(openConversationId, chatResult);
                } else if (conversationType.equals("1")) {
                    sendPrivateResult(senderUserId, chatResult);
                }
            }
            return new JSONObject();
        } catch (Exception e) {
            log.error("receive bot message failed", e);
            return new JSONObject();
        }
    }

    /**
     * 将流式对话结果聚合为结构化结果。
     * <p>
     * 除了 LLM 答案 token,还会收集 [REFERENCE] 引用、[CARD] 提示以及 [CARD_CHOICE_*] 车辆选择卡片,
     * 便于后续根据内容类型选择钉钉消息模板(文本/Markdown/卡片)。
     * 设置 120 秒超时,避免 RAG 链路异常时无限阻塞钉钉回调线程。
     */
    private ChatResult aggregateChatResult(Flux<String> chatFlux) {
        ChatResult result = new ChatResult();
        chatFlux.doOnNext(event -> {
            if (event == null || event.isEmpty()) {
                return;
            }
            if (event.startsWith("[PROGRESS]") || event.startsWith("[DONE]")) {
                return;
            }
            if (event.startsWith("[WARN]:")) {
                result.warnMessage = event.substring("[WARN]:".length()).trim();
                return;
            }
            if (event.startsWith("[REFERENCE]:")) {
                parseReferences(event.substring("[REFERENCE]:".length()), result);
                return;
            }
            if (event.startsWith("[CARD]:")) {
                result.cardPrompt = event.substring("[CARD]:".length()).trim();
                return;
            }
            if (event.startsWith("[CARD_CHOICE_MYCAR]:")) {
                result.myCarChoices = parseMyCarChoices(event.substring("[CARD_CHOICE_MYCAR]:".length()));
                return;
            }
            if (event.startsWith("[CARD_CHOICE_CAR]:")) {
                result.carChoices = parseCarChoices(event.substring("[CARD_CHOICE_CAR]:".length()));
                return;
            }
            result.answerBuilder.append(event);
        }).then().block(Duration.ofSeconds(120));
        return result;
    }

    private void parseReferences(String json, ChatResult result) {
        try {
            List<ChatMessage.RagReference> refs = JSON.parseArray(json, ChatMessage.RagReference.class);
            if (!CollectionUtils.isEmpty(refs)) {
                result.references.addAll(refs);
            }
        } catch (Exception e) {
            log.warn("解析RAG引用失败: {}", json, e);
        }
    }

    private List<MyCarVO> parseMyCarChoices(String json) {
        try {
            return JSON.parseArray(json, MyCarVO.class);
        } catch (Exception e) {
            log.warn("解析我的车辆选择卡片失败: {}", json, e);
            return null;
        }
    }

    private List<CarInfoVO> parseCarChoices(String json) {
        try {
            return JSON.parseArray(json, CarInfoVO.class);
        } catch (Exception e) {
            log.warn("解析车型选择卡片失败: {}", json, e);
            return null;
        }
    }

    private void sendGroupResult(String openConversationId, ChatResult result) throws Exception {
        if (!CollectionUtils.isEmpty(result.myCarChoices)) {
            String markdown = buildMyCarChoicesMarkdown(result.cardPrompt, result.myCarChoices);
            robotGroupMessagesService.sendMarkdown(openConversationId, "请选择车辆", markdown);
            return;
        }
        if (!CollectionUtils.isEmpty(result.carChoices)) {
            String markdown = buildCarChoicesMarkdown(result.cardPrompt, result.carChoices);
            robotGroupMessagesService.sendMarkdown(openConversationId, "请选择车型", markdown);
            return;
        }
        String answer = resolveAnswer(result);
        if (!CollectionUtils.isEmpty(result.references)) {
            String markdown = buildAnswerWithReferences(answer, result.references);
            robotGroupMessagesService.sendMarkdown(openConversationId, "智能问答", markdown);
        } else {
            robotGroupMessagesService.send(openConversationId, answer);
        }
    }

    private void sendPrivateResult(String senderUserId, ChatResult result) throws Exception {
        if (!CollectionUtils.isEmpty(result.myCarChoices)) {
            String markdown = buildMyCarChoicesMarkdown(result.cardPrompt, result.myCarChoices);
            robotPrivateMessageService.sendMarkdown("请选择车辆", markdown, senderUserId);
            return;
        }
        if (!CollectionUtils.isEmpty(result.carChoices)) {
            String markdown = buildCarChoicesMarkdown(result.cardPrompt, result.carChoices);
            robotPrivateMessageService.sendMarkdown("请选择车型", markdown, senderUserId);
            return;
        }
        String answer = resolveAnswer(result);
        if (!CollectionUtils.isEmpty(result.references)) {
            String markdown = buildAnswerWithReferences(answer, result.references);
            robotPrivateMessageService.sendMarkdown("智能问答", markdown, senderUserId);
        } else {
            robotPrivateMessageService.send(answer, senderUserId);
        }
    }

    private String resolveAnswer(ChatResult result) {
        String answer = result.getAnswer();
        if (!answer.isBlank()) {
            return answer;
        }
        if (result.warnMessage != null && !result.warnMessage.isBlank()) {
            return result.warnMessage;
        }
        return "抱歉,我暂时无法回答您的问题,请稍后再试。";
    }

    /**
     * 构建带引用来源的 Markdown 回答。
     */
    private String buildAnswerWithReferences(String answer, List<ChatMessage.RagReference> references) {
        List<ChatMessage.RagReference> uniqueRefs = references.stream()
                .filter(ref -> ref.getDocumentTitle() != null || ref.getUrl() != null)
                .distinct()
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(uniqueRefs)) {
            return answer;
        }
        StringBuilder sb = new StringBuilder();
        sb.append(answer);
        sb.append("\n\n---\n**参考来源:**\n");
        int idx = 1;
        for (ChatMessage.RagReference ref : uniqueRefs) {
            String title = ref.getDocumentTitle() != null ? ref.getDocumentTitle() : "来源" + idx;
            String url = ref.getUrl();
            if (url != null && !url.isBlank()) {
                sb.append(idx).append(". [").append(escapeMarkdown(title)).append("](").append(url).append(")\n");
            } else {
                sb.append(idx).append(". ").append(escapeMarkdown(title)).append("\n");
            }
            idx++;
        }
        return sb.toString();
    }

    private String buildMyCarChoicesMarkdown(String prompt, List<MyCarVO> cars) {
        String header = (prompt != null && !prompt.isBlank()) ? prompt : "请选择您的车辆";
        StringBuilder sb = new StringBuilder();
        sb.append("**").append(escapeMarkdown(header)).append("**\n\n");
        for (int i = 0; i < cars.size(); i++) {
            MyCarVO car = cars.get(i);
            sb.append(i + 1).append(". ");
            if (car.getFullName() != null) {
                sb.append(escapeMarkdown(car.getFullName()));
            }
            if (car.getPlateNumber() != null) {
                sb.append(" (车牌:").append(escapeMarkdown(car.getPlateNumber())).append(")");
            }
            sb.append("\n");
        }
        sb.append("\n请回复\"选择第N个\"或直接回复车辆名称。");
        return sb.toString();
    }

    private String buildCarChoicesMarkdown(String prompt, List<CarInfoVO> cars) {
        String header = (prompt != null && !prompt.isBlank()) ? prompt : "请选择您要咨询的车型";
        StringBuilder sb = new StringBuilder();
        sb.append("**").append(escapeMarkdown(header)).append("**\n\n");
        for (int i = 0; i < cars.size(); i++) {
            CarInfoVO car = cars.get(i);
            sb.append(i + 1).append(". ");
            if (car.getFullName() != null) {
                sb.append(escapeMarkdown(car.getFullName()));
            } else if (car.getBrand() != null || car.getModelName() != null) {
                sb.append(escapeMarkdown((car.getBrand() != null ? car.getBrand() + " " : "") +
                        (car.getModelName() != null ? car.getModelName() : "")));
            }
            if (car.getGuidePrice() != null) {
                sb.append(" (指导价:").append(car.getGuidePrice()).append("万)");
            }
            sb.append("\n");
        }
        sb.append("\n请回复\"选择第N个\"或直接回复车型名称。");
        return sb.toString();
    }

    /**
     * 转义 Markdown 特殊字符,避免标题/链接等被破坏。
     */
    private String escapeMarkdown(String text) {
        if (text == null) {
            return "";
        }
        return text.replace("\\", "\\\\")
                .replace("*", "\\*")
                .replace("_", "\\_")
                .replace("[", "\\[")
                .replace("]", "\\]")
                .replace("(", "\\(")
                .replace(")", "\\)")
                .replace("`", "\\`");
    }

    private static class ChatResult {
        private final StringBuilder answerBuilder = new StringBuilder();
        private final List<ChatMessage.RagReference> references = new ArrayList<>();
        private String warnMessage;
        private String cardPrompt;
        private List<MyCarVO> myCarChoices;
        private List<CarInfoVO> carChoices;

        String getAnswer() {
            return answerBuilder.toString().trim();
        }
    }
}

这里通过一个if-else来判断消息是来自群聊还是单聊,然后调用不同的服务做回复。 这里需要注意的是,官网中说conversationType如果0是单聊,1是群聊,我实测发现1是单聊,2是群聊。

复用 RAG 对话链路

为了避免在钉钉回调里重复实现对话流程,完整链路已下沉到 ChatApplicationService: - ChatApplicationService.java - ChatController.java ChatController.send() 和 ChatBotCallbackListener 共用同一个 chat(...) 方法:

// HTTP 网页端
chatApplicationService

// 钉钉机器人
chatApplicationService

通过 ChatSource 参数,RAG 流程可以区分来源: - USER_WEB:需要车辆信息时会返回 [CARD_CHOICE_MYCAR] / [CARD_CHOICE_CAR] 交互卡片。 - STAFF_DING:跳过车辆选择卡片分支,直接走 RAG 检索或返回 [WARN] 提示。

消息类型与模板

钉钉企业内部应用机器人支持多种 msgKey/msgParam 组合。项目通过 DingTalkMessageBuilder 统一构造消息体:

package cn.hollis.llm.mentor.know.engine.dingtalk.util;

import com.alibaba.fastjson2.JSONObject;

/**
 * 钉钉机器人消息体构造工具。
 * <p>
 * 企业内部应用机器人(群聊/单聊)支持的 msgKey 与 msgParam 格式相同,
 * 本类统一封装文本、Markdown、链接、ActionCard 等常用消息模板。
 *
 * @author Hollis
 */
public final class DingTalkMessageBuilder {

    private DingTalkMessageBuilder() {
    }

    /**
     * 文本消息:msgKey = sampleText
     */
    public static JSONObject text(String content) {
        JSONObject msgParam = new JSONObject();
        msgParam.put("content", content);
        return msgParam;
    }

    /**
     * Markdown 消息:msgKey = sampleMarkdown
     */
    public static JSONObject markdown(String title, String text) {
        JSONObject msgParam = new JSONObject();
        msgParam.put("title", title);
        msgParam.put("text", text);
        return msgParam;
    }

    /**
     * 链接消息:msgKey = sampleLink
     */
    public static JSONObject link(String title, String text, String messageUrl, String picUrl) {
        JSONObject msgParam = new JSONObject();
        msgParam.put("title", title);
        msgParam.put("text", text);
        msgParam.put("messageUrl", messageUrl);
        if (picUrl != null && !picUrl.isBlank()) {
            msgParam.put("picUrl", picUrl);
        }
        return msgParam;
    }

    /**
     * ActionCard 卡片(单按钮):msgKey = sampleActionCard
     */
    public static JSONObject actionCard(String title, String text, String singleTitle, String singleUrl) {
        JSONObject msgParam = new JSONObject();
        msgParam.put("title", title);
        msgParam.put("text", text);
        msgParam.put("singleTitle", singleTitle);
        msgParam.put("singleURL", singleUrl);
        return msgParam;
    }
}

已封装的消息类型: | msgKey | 说明 | msgParam 示例 | | --- | --- | --- | | sampleText | 文本消息 | {"content":"..."} | | sampleMarkdown | Markdown 消息 | {"title":"...","text":"..."} | | sampleLink | 链接消息 | {"title":"...","text":"...","messageUrl":"...","picUrl":"..."} | | sampleActionCard | 单按钮卡片 | {"title":"...","text":"...","singleTitle":"...","singleURL":"..."} |

功能调试

以上改造之后,再启动应用,然后在一个群聊中把我们的机器人配置进去 : 添加机器人的时候,输入你刚刚创建的机器人的名字就可以了。然后就在群聊中了。只需要at他,就可以这几个他对话了,如: 单聊也一样,搜索框找到这个机器人, 和他对话即可。 因为我们默认返回的失败Hello World,所以机器人就会回复这个,至此,我们就实现了如何通过钉钉机器人接收用户消息,然后给用户做回复了。

群聊和单聊

单聊机器人方便查找,对话简单,不需要at他,找到对话框直接问就行了。适合做一个企业内部通用的AI助手。 群聊机器人需要在一个群聊中把他设置进去,每次对话的时候都需要at他。这种适合做一个单产品的AI客服助手,把和这个产品相关的客服、运营等人员都拉到一个答疑群,群内有问题可以直接问机器人的方式,避免大家需要记住这个机器人的名字,每次想要问问题,直接找到这个群,at机器人即可。

实现原理

在 Stream 模式下,开发者的应用程序通过集成 SDK 的方式与钉钉开放平台建立一条 WebSocket 连接,建立连接过程中开放平台将对连接进行鉴权。当有回调发生时,开放平台将通过 WebSocket 连接将数据通知到开发者的应用程序。开发者的应用程序可以接收到这些数据并进行相应处理,从而实现与钉钉开放平台的实时通信 DingTalkStreamClientConfiguration中的代码,使用钉钉官方 SDK OpenDingTalkStreamClientBuilder 创建长连接客户端,配置应用的 appKey 和 appSecret 进行身份认证,再注册机器人消息回调监听器 ChatBotCallbackListener,然后通过 @Bean(initMethod = "start") 在应用启动时自动建立连接。 ChatBotCallbackListener是一个消息回调处理器。实现 OpenDingTalkCallbackListener 接口接收钉钉推送的消息,区分单聊(conversationType=1)和群聊(conversationType=2),目前回复固定内容 "Hello World",后面会扩展为为调用 RAG 服务生成智能回答。 一个消息的处理流程如下:

用户@机器人/私聊机器人
        │
        ▼
   钉钉服务器
        │
        ▼
  Stream长连接推送
        │
        ▼
ChatBotCallbackListener.execute()
        │
        ├──► 解析消息内容 (message.getText().getContent())
        │
        ├──► 判断会话类型 (conversationType)
        │       ├── 1: 单聊 → RobotPrivateMessageService.send()
        │       └── 2: 群聊 → RobotGroupMessagesService.send()
        │
        └──► 调用钉钉机器人API发送回复
                    │
                    ▼
              用户收到回复消息

消息扩展

上面我们只是发送了简单的文本内容(sampleText),钉钉还支持我们发送其他的类型,比如图片,markdown,卡片等等,发送方式如下: https://open.dingtalk.com/document/dingstart/types-of-messages-sent-by-robots

版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。