文章目录
- 前言
- [Agent业务设计:应用钉钉发布实现问题对话点赞 & 数据收集实现](#Agent业务设计:应用钉钉发布实现问题对话点赞 & 数据收集实现)
- 一、背景:钉钉渠道的数据孤岛问题
-
- [1.1 业务场景](#1.1 业务场景)
- [1.2 目标架构](#1.2 目标架构)
- 二、核心概念:钉钉渠道的数据收集设计
-
- [2.1 关键数据维度](#2.1 关键数据维度)
- [2.2 钉钉与非流式响应的时间计算](#2.2 钉钉与非流式响应的时间计算)
- 三、表结构设计:支撑钉钉数据收集
-
- [3.1 核心表结构](#3.1 核心表结构)
- [3.2 钉钉渠道特殊字段说明](#3.2 钉钉渠道特殊字段说明)
- 四、核心实现一:钉钉渠道数据收集Middleware
-
- [4.1 Middleware拦截设计](#4.1 Middleware拦截设计)
- [4.2 钉钉渠道与非流式响应的适配](#4.2 钉钉渠道与非流式响应的适配)
- 五、核心实现二:钉钉点赞/点踩H5反馈
-
- [5.1 整体流程](#5.1 整体流程)
- [5.2 钉钉卡片消息设计](#5.2 钉钉卡片消息设计)
- [5.3 反馈回调Controller实现](#5.3 反馈回调Controller实现)
- [5.4 钉钉内跳转说明](#5.4 钉钉内跳转说明)
- 六、监控大屏:钉钉渠道数据展示
-
- 6.1、钉钉监控大屏展示
- [6.2、钉钉机器人问答后的点赞 & 踩交互](#6.2、钉钉机器人问答后的点赞 & 踩交互)
- 资料获取

前言
博主介绍:✌目前全网粉丝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需要配置安全域名:
- 在钉钉开放平台配置应用安全域名
- 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、钉钉机器人问答后的点赞 & 踩交互
下面为钉钉问答后的回复消息内容如下,最后会带上回答是否有用的提示,可引导用户进行选择:

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

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


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

资料获取
大家点赞、收藏、关注、评论啦~
精彩专栏推荐订阅:在下方专栏👇🏻
- 长路-文章目录汇总(算法、后端Java、前端、运维技术导航):博主所有博客导航索引汇总
- 开源项目Studio-Vue---校园工作室管理系统(含前后台,SpringBoot+Vue):博主个人独立项目,包含详细部署上线视频,已开源
- 学习与生活-专栏:可以了解博主的学习历程
- 算法专栏:算法收录
更多博客与资料可查看👇🏻获取联系方式👇🏻,🍅文末获取开发资源及更多资源博客获取🍅