Agent业务设计:应用钉钉发布实现问题对话点赞 & 数据收集实现

文章目录

前言

博主介绍:✌目前全网粉丝4W+,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质作者、专注于Java后端技术领域。

涵盖技术内容:Java后端、大数据、算法、分布式微服务、中间件、前端、运维等。

博主所有博客文件目录索引:博客目录索引(持续更新)

CSDN搜索:长路

视频平台:b站-Coder长路

Agent业务设计:应用钉钉发布实现问题对话点赞 & 数据收集实现

在开发钉钉渠道Agent发布能力时,我们遇到了一个具体问题:用户通过钉钉与Agent对话后,如何高效收集问答数据、获取用户点赞反馈、并自动完成问题分类与归因分析?本文将基于实际落地方案,完整实现这套钉钉渠道的数据监控闭环。


一、背景:钉钉渠道的数据孤岛问题

1.1 业务场景

当我们将Agent发布到钉钉工作台后,面临三个核心痛点:

痛点 描述 影响
数据黑盒 不知道用户问了什么、回答是否满意 无法优化Agent
反馈缺失 钉钉没有原生评价机制 不知道回答质量
问题堆积 回答不好的问题无法追踪处理 重复踩坑

具体流程

复制代码
用户钉钉提问 → Agent回答 → ❌ 数据丢失 → 无法改进
                    ↓
            无法评价/反馈

本文的方案就是要解决这个"数据黑盒"问题。

1.2 目标架构

复制代码
用户钉钉提问 → Agent回答 → 数据收集入库 → AI自动分类/归因 → 监控大屏展示
                    ↓
            点赞/点踩(跳转H5) → 反馈记录 → 人工处理优化

核心价值:每次钉钉对话都成为优化素材,形成"数据收集 → 分析 → 优化 → 验证"的闭环。


二、核心概念:钉钉渠道的数据收集设计

2.1 关键数据维度

维度 钉钉端特殊处理 用途
用户标识 钉钉userId 区分不同用户
会话标识 每个用户独立sessionId 多轮对话跟踪
群组会话 一个群组一个sessionId 群聊场景分析
反馈收集 H5跳转页面 替代钉钉原生评价

2.2 钉钉与非流式响应的时间计算

钉钉渠道使用的是非流式响应(一次性返回完整回答),时间计算逻辑:

java 复制代码
// 非流式响应时机
ask_start_time = 用户点击发送的时间
ttft_start_time = AI开始处理的时间(可近似为请求到达时间)
end_answer_time = 完成回答的时间

ttft_count_time = ttft_start_time - ask_start_time
answer_count_time = end_answer_time - ask_start_time

重点:钉钉端无法像官网那样精确获取首字响应时间,我们采用请求开始到AI返回的时间作为TTFT。


三、表结构设计:支撑钉钉数据收集

3.1 核心表结构

sql 复制代码
-- 用户对话消息表(支撑钉钉渠道)
CREATE TABLE `ai_agent_user_message` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(128) NOT NULL COMMENT '钉钉用户ID',
  `session_id` varchar(255) NOT NULL COMMENT '会话ID,一个用户一个sessionId',
  `aid` varchar(64) NOT NULL COMMENT 'agent标识',
  `question` longtext COMMENT '用户提问内容',
  `ai_answer` longtext COMMENT 'AI回答内容',
  `channel_id` varchar(64) DEFAULT NULL COMMENT '频道ID',
  `channel_code` varchar(50) DEFAULT '2' COMMENT '2-钉钉渠道',
  `ask_start_time` datetime COMMENT '钉钉用户提问时间',
  `ttft_start_time` datetime COMMENT 'AI开始响应时间',
  `end_answer_time` datetime COMMENT 'AI回答结束时间',
  `ttft_count_time` bigint(20) COMMENT 'TTFT耗时(毫秒)',
  `answer_count_time` bigint(20) COMMENT '回答总耗时(毫秒)',
  `status` tinyint(4) DEFAULT '0' COMMENT '0进行中、1成功、2失败',
  PRIMARY KEY (`id`),
  KEY `idx_aid` (`aid`),
  KEY `idx_channel_code` (`channel_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户对话消息表(钉钉渠道)';

-- 用户反馈表(钉钉点赞/点踩)
CREATE TABLE `ai_agent_user_feedback` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `message_id` bigint(20) NOT NULL COMMENT '关联对话消息ID',
  `user_id` varchar(128) NOT NULL COMMENT '钉钉用户ID',
  `session_id` varchar(255) NOT NULL COMMENT '会话ID',
  `aid` varchar(64) NOT NULL COMMENT 'agent标识',
  `feedback_type` tinyint(4) NOT NULL COMMENT '1-点赞(有用),2-点踩(无用)',
  `feedback_reason` varchar(1024) DEFAULT NULL COMMENT '点踩原因描述',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_message_id` (`message_id`),
  KEY `idx_aid` (`aid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='钉钉用户反馈评价表';

3.2 钉钉渠道特殊字段说明

字段 说明 示例值
channel_code 固定为'2'表示钉钉渠道 2
channel_id 可选的频道/群组ID dingtalk_group_123
user_id 钉钉开放平台userId manager1234
session_id 每个用户独立 user_123_session

四、核心实现一:钉钉渠道数据收集Middleware

4.1 Middleware拦截设计

java 复制代码
/**
 * Agent对话监控拦截器
 * @author changlu
 * @date 2026-05-02
 */
@Component
public class AgentMonitorMiddleware {
    
    // 对话开始:插入消息记录
    public void onInitComplete(ChatContext context) {
        AiAgentUserMessagePojo message = AiAgentUserMessagePojo.builder()
            .userId(context.getUserId())        // 钉钉userId
            .sessionId(context.getMemoryId())   // 会话ID
            .aid(context.getAid())
            .channelCode("2")                    // 钉钉渠道固定2
            .askStartTime(new Date())            // 记录开始时间
            .status(0)                           // 进行中
            .build();
        userMessageMapper.insertUserMessage(message);
        context.setAttribute("messageId", message.getId());
    }
    
    // 非流式响应:记录TTFT和结束时间
    public void beforeModelCall(ChatContext context) {
        Long messageId = (Long) context.getAttribute("messageId");
        // 更新TTFT开始时间
        userMessageMapper.updateTtftTime(messageId, new Date());
    }
    
    // 对话结束:记录完整结果
    public void onStop(ChatContext context, StopResult result) {
        Long messageId = (Long) context.getAttribute("messageId");
        AiAgentUserMessagePojo message = userMessageMapper.selectById(messageId);
        
        message.setAiAnswer(context.getFinalAnswer());
        message.setEndAnswerTime(new Date());
        message.setStatus(result.isSuccess() ? 1 : 2);
        
        // 计算耗时
        long ttft = message.getTtftStartTime().getTime() - message.getAskStartTime().getTime();
        long total = message.getEndAnswerTime().getTime() - message.getAskStartTime().getTime();
        message.setTtftCountTime(ttft);
        message.setAnswerCountTime(total);
        
        userMessageMapper.updateUserMessage(message);
        
        // 触发AI自动分析
        triggerAutoAnalysis(messageId, context.getAid());
    }
}

4.2 钉钉渠道与非流式响应的适配

java 复制代码
// 钉钉渠道调用入口
@PostMapping("/dingtalk/webhook")
public Response<String> handleDingTalkMessage(@RequestBody DingTalkRequest request) {
    ChatContext context = ChatContext.builder()
        .userId(request.getUserId())           // 钉钉userId
        .memoryId(generateSessionId(request))  // 每个用户独立session
        .aid(request.getAgentId())
        .channelCode("2")                      // 标记钉钉渠道
        .build();
    
    // 执行对话
    return agentRunner.run(context, request.getQuestion());
}

关键点

  • 钉钉每个用户有独立sessionId,用于多轮对话
  • 群聊场景可用groupId作为sessionId,区分不同群

五、核心实现二:钉钉点赞/点踩H5反馈

5.1 整体流程

复制代码
钉钉消息卡片展示点赞/点踩按钮
        ↓
用户点击 → 跳转H5页面
        ↓
    ┌────┴────┐
点赞        点踩
  ↓           ↓
直接记录   展示填写表单
  ↓           ↓
展示"感谢"  用户提交原因
  ↓           ↓
自动关闭    记录+关闭

5.2 钉钉卡片消息设计

Agent回答时,在消息卡片底部增加反馈按钮:

java 复制代码
// 构建钉钉卡片消息
private DingTalkMessage buildFeedbackCard(String question, String answer, Long messageId) {
    String likeUrl = "https://your-domain/api/agent-feedback/callback?messageId=" + messageId 
                     + "&feedbackType=1&aid=" + aid;
    String dislikeUrl = "https://your-domain/api/agent-feedback/callback?messageId=" + messageId 
                        + "&feedbackType=2&aid=" + aid;
    
    return DingTalkMessage.builder()
        .msgType("action_card")
        .content(answer)
        .btns(Arrays.asList(
            new Button("👍 有用", likeUrl),
            new Button("👎 无用", dislikeUrl)
        ))
        .build();
}

5.3 反馈回调Controller实现

java 复制代码
@Controller
@RequestMapping("/api/agent-feedback")
public class AiAgentFeedbackController {
    
    private final AiAgentUserFeedbackMapper feedbackMapper;
    private final AiAgentUserMessageMapper messageMapper;
    
    /**
     * 钉钉用户点击后的回调入口
     */
    @GetMapping("/callback")
    @ResponseBody
    public String callback(@RequestParam Long messageId,
                           @RequestParam String aid,
                           @RequestParam String sessionId,
                           @RequestParam String userId,
                           @RequestParam Integer feedbackType) {
        
        // 1. 参数校验
        if (messageId == null || aid == null || feedbackType == null) {
            return buildNoticePage("参数错误", false);
        }
        
        // 2. 查询消息是否存在
        AiAgentUserMessagePojo message = messageMapper.selectById(messageId);
        if (message == null || !aid.equals(message.getAid())) {
            return buildNoticePage("消息不存在", false);
        }
        
        // 3. 幂等校验:避免重复评价
        AiAgentUserFeedbackPojo exist = feedbackMapper.selectByMessageId(messageId);
        if (exist != null) {
            return buildNoticePage("您已评价过该消息", true);
        }
        
        // 4. 点赞:直接入库
        if (feedbackType == 1) {
            feedbackMapper.insert(buildFeedback(messageId, userId, sessionId, aid, 1, null));
            return buildNoticePage("感谢您的赞赏!", true);
        }
        
        // 5. 点踩:返回填写表单页
        return buildDislikeFormPage(messageId, aid, sessionId, userId);
    }
    
    /**
     * 点踩原因异步提交接口
     */
    @PostMapping("/submit")
    @ResponseBody
    public String submit(@RequestParam Long messageId,
                         @RequestParam String aid,
                         @RequestParam String sessionId,
                         @RequestParam String userId,
                         @RequestParam String feedbackReason) {
        
        // 入库点踩记录和原因
        feedbackMapper.insert(buildFeedback(messageId, userId, sessionId, aid, 2, feedbackReason));
        return "ok";
    }
    
    /**
     * 构建点踩填写表单页面(移动端适配)
     */
    private String buildDislikeFormPage(Long messageId, String aid, String sessionId, String userId) {
        return """
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset='UTF-8'>
                <meta name='viewport' content='width=device-width,initial-scale=1.0'>
                <style>
                    body{background:#f5f5f5;padding:20px}
                    .card{background:#fff;border-radius:12px;padding:24px}
                    textarea{width:100%;height:120px;border:1px solid #ddd;border-radius:8px;padding:12px}
                    button{width:100%;padding:12px;background:#1677ff;color:#fff;border:none;border-radius:8px}
                    .tags{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}
                    .tag{padding:6px 14px;border:1px solid #ddd;border-radius:20px;cursor:pointer}
                    .tag.active{background:#1677ff;color:#fff;border-color:#1677ff}
                </style>
            </head>
            <body>
                <div class='card'>
                    <h3>哪里做得不够好?</h3>
                    <div class='tags'>
                        <div class='tag' onclick='toggle(this)'>回答不准确</div>
                        <div class='tag' onclick='toggle(this)'>太啰嗦</div>
                        <div class='tag' onclick='toggle(this)'>逻辑不通</div>
                        <div class='tag' onclick='toggle(this)'>没解决我的问题</div>
                    </div>
                    <textarea id='reason' placeholder='详细描述问题(选填)...'></textarea>
                    <button onclick='submit()'>提交反馈</button>
                </div>
                <script>
                    function toggle(el){ el.classList.toggle('active'); }
                    function submit(){
                        let tags = [...document.querySelectorAll('.tag.active')].map(t=>t.innerText);
                        let text = document.getElementById('reason').value;
                        let reason = [...tags, text].filter(Boolean).join(' | ');
                        fetch('/api/agent-feedback/submit', {
                            method: 'POST',
                            headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                            body: `messageId=${messageId}&aid=${aid}&sessionId=${sessionId}&userId=${userId}&feedbackReason=${encodeURIComponent(reason)}`
                        }).then(() => {
                            alert('反馈已提交,感谢您的建议!');
                            window.close();
                        });
                    }
                </script>
            </body>
            </html>
        """;
    }
    
    /**
     * 构建简单提示页面
     */
    private String buildNoticePage(String message, boolean autoClose) {
        String script = autoClose ? "setTimeout(()=>window.close(), 2000);" : "";
        return """
            <!DOCTYPE html>
            <html>
            <head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1.0'></head>
            <body style='text-align:center;padding:50px'>
                <div>${message}</div>
                <script>${script}</script>
            </body>
            </html>
        """.replace("${message}", message).replace("${script}", script);
    }
}

5.4 钉钉内跳转说明

关键点:钉钉内打开H5需要配置安全域名:

  1. 在钉钉开放平台配置应用安全域名
  2. H5页面需要引入钉钉JSAPI用于关闭页面:
html 复制代码
<script src="https://g.alicdn.com/dingding/dingtalk-jsapi/3.1.1/dingtalk.open.js"></script>
<script>
    function closePage() {
        if(window.dd && dd.biz) {
            dd.biz.navigation.close({});
        } else {
            window.close();
        }
    }
</script>

六、监控大屏:钉钉渠道数据展示

6.1、钉钉监控大屏展示

钉钉渠道数据大屏

  • 今日总问答数(钉钉渠道)
  • 平均TTFT响应时间
  • 点赞数 vs 点踩数分布

点赞反馈栏目为:

在这里即可看到点踩反馈原因

点击详情即可查看到对话以及点踩原因:

6.2、钉钉机器人问答后的点赞 & 踩交互

下面为钉钉问答后的回复消息内容如下,最后会带上回答是否有用的提示,可引导用户进行选择

点击有用后,即可跳转页面:

点击无用,跳转页面进行评价:

对于点赞、无用后,再次进行点赞的时候,即可出现无需重复操作:

资料获取

大家点赞、收藏、关注、评论啦~

精彩专栏推荐订阅:在下方专栏👇🏻

更多博客与资料可查看👇🏻获取联系方式👇🏻,🍅文末获取开发资源及更多资源博客获取🍅

相关推荐
青云交1 年前
大数据新视界 -- 大数据大厂之 Impala 性能优化:融合人工智能预测的资源预分配秘籍(上)(29 / 30)
大数据·impala·数据收集·模型构建·人工智能预测·资源预分配·查询性能优化
Amd7942 年前
深入理解 Nuxt.js 中的 app:error:cleared 钩子
生命周期·nuxt.js·错误处理·应用开发·钩子·用户反馈·状态恢复
chengbo_eva2 年前
用户反馈解决方案 —— 兔小巢构建反馈功能
前端·用户反馈
小信瑞3 年前
OpenText EnCase 客户案例——哥伦比亚总检察长办公室建立值得信赖的司法警察服务
大数据·电子取证·证据收集·数据收集·证据完整性·公共安全·证据分析软件