这篇能做出什么
做完之后,你的飞书项目群里会多一个机器人。同事在群里 @ 它,它会:
- 接得住问题:基于你上传的项目文档、排期表、规范手册回答,答不上来会直说"不确定",而不是编。
- 分得清任务:有人说"这活儿谁接",它输出一张"任务 / 负责人 / 建议时间 / 验收标准"的表格,并 @ 到具体的人。
- 记得住上下文:同一群里前 20 条对话它都看得见,@ 它说"就按刚才说的改",它知道"刚才"指什么。
- 扛得住多人并发:5 个人同时跟它说话不会串台,A 问的问题不会回到 B 头上。
- 收得到口子:每天固定时间在群里汇总当天结论、待办和风险。
整条链路是这样的:
```
飞书群消息 → 飞书事件回调 → 你的中转服务 → 豆包工作智能体 → 回到飞书群
```
路径有两种。如果豆包工作后台提供了飞书渠道/连接器,可以直接授权绑定,零代码;如果没有或你想自己控上下文、控权限,就走下面的中转服务方案。具体是否提供飞书渠道、叫什么名字,以豆包工作官方页面为准。
---
前置条件清单
开工前把这 6 项对齐,能省掉后面 80% 的排查时间:
1. 飞书侧权限:能创建"企业自建应用"的账号,或者能找到能帮你审批的管理员。机器人发版、加权限通常需要管理员过一遍。
2. 豆包工作侧:能创建智能体的账号,以及调用它所需的凭据(API Key / 应用 ID 之类,名称以官方页面为准)。
3. 一台能跑服务的地方:Node.js 18 以上的环境即可,本地电脑 + 内网穿透也能先跑通。
4. 一个公网可访问地址(走 webhook 模式时需要);飞书也有长连接模式,可以免公网 IP,推荐先用长连接把链路跑通。
5. 一张"能力清单":这个 Agent 服务的群里,谁是负责人、要有哪些技能、哪些知识库能读、哪些数据不能碰。
6. 一份输出规范:群聊消息和文档不一样,超过 200 字就没人看了。先把格式规则定下来。
---
第 0 步:先想清楚"群里的 Agent 到底干什么"
这一步不写代码,但决定后面好不好用。把 Agent 的职责收敛成三类,别贪多:
| 职责 | 触发场景 | 输出形态 |
|---|---|---|
| 应答 | 有人问项目背景、规范、排期 | 一句话结论 + 引用来源 |
| 分派 | 有人问"谁来做" | 表格 + @ 具体人 |
| 汇总 | 定时触发或有人说"总结一下" | 结论 / 待办 / 风险 三段 |
为什么必须收敛? 一个什么都干的 Agent 最后什么都干不好。群里最常见的失败不是模型不行,是它每次回 800 字,两轮之后没人再 @ 它了。
---
第 1 步:在豆包工作里创建并调试智能体
1.1 建智能体、写人设
在豆包工作的智能体创建入口新建一个,把人设和指令写清楚。下面这段可以直接改成你的版本:
```markdown
你是「项目小助」,服务对象是 XX 项目群的 8 位同事。
【职责】
1. 应答:回答项目背景、排期、规范类问题,优先引用知识库;知识库里没有的,明说"我查不到"。
2. 分派:当有人问"这个谁来做",输出表格:任务 | 负责人 | 建议时间 | 验收标准,并 @ 对应的人。
3. 汇总:收到"总结一下"时,输出三段:今天结论 / 待办 / 风险。
【硬性规则】
- 结论先行:第一句话就是答案,理由放后面。
- 群消息不超过 200 字,需要展开时输出"回复:详情"。
- 不确定就说不确定。禁止编造人名、日期、数字、链接。
- 只给建议时间,不承诺交付时间。
- 与项目无关的闲聊,回一句"这块我不熟"就结束。
```
1.2 挂知识库
把项目文档、排期表、规范手册传进知识库。两个小技巧:
- 给文档起人话名字。叫"XX项目-2024Q3排期"比叫"文档1"更容易被正确召回。
- 删掉过期文档。知识库里躺着三份不同版本的排期,Agent 就会开始"编"。
1.3 在调试台自测四种场景
- 问一个知识库里明确有的问题 → 看它引不引用来源。
- 问一个完全不相关的问题 → 看它拒答还是硬编。
- 说"这个谁来做" → 看它输出表格还是输出散文。
- 故意问一个模糊问题 → 看它反问还是瞎猜。
1.4 拿到调用凭据
在平台的应用管理或 API 页面拿到 Key 和调用端点。字段名、鉴权方式、是否有会话(session)参数,一律以豆包工作官方文档为准。 拿到后先存进环境变量,别写在代码里。
---
第 2 步:在飞书开放平台创建机器人
2.1 创建企业自建应用
在飞书开放平台创建"企业自建应用",进去后开启机器人能力。开启后你会得到 App ID 和 App Secret,这两个是用来换 tenant_access_token 的。
2.2 配权限
需要几类权限,名称以官方文档为准,大致覆盖:
- 接收群聊中 @ 机器人的消息
- 以机器人身份发送消息
- 读取群信息(知道群里有哪些人)
关键点:默认情况下机器人只能收到 @ 它的消息。想读全部群消息要单独申请,且通常需要审核。绝大多数团队协作场景,只收 @ 消息就够了,也更省心。
2.3 订事件
订阅消息接收事件(消息事件名大致是 im.message.receive_v1 这类,以官方文档为准)。接收方式选长连接或 webhook 均可。
2.4 发布版本,把机器人拉进群
改完权限一定要重新发布版本并等审核通过——权限变更不重新发版是不生效的,这是新手最常踩的坑。
发完版,在群里"设置 → 群机器人 → 添加机器人",把它拉进来。想让它认识群成员,可以提前把成员的 open_id 和姓名建一张映射表。
---
第 3 步:写一个最小可用的中转服务
先跑通"收到 @ → 回一句话"这条最短链路,别急着做卡片和定时任务。
```javascript
// server.js —— 最小可用的飞书中转服务
const express = require('express');
const app = express();
app.use(express.json());
// 已处理的事件 id,防止飞书重试导致重复回复
const handledEvents = new Set();
// 简易会话记忆:chatId -> [{ role, content }]
const sessions = new Map();
app.post('/feishu/events', (req, res) => {
const body = req.body || {};
// 1) 事件订阅地址校验:按官方文档要求把 challenge 原样返回
if (body.type === 'url_verification') {
return res.json({ challenge: body.challenge });
}
const eventId = body.header && body.header.event_id;
// 2) 幂等:同一个事件只处理一次
if (!eventId || handledEvents.has(eventId)) {
return res.json({ code: 0 });
}
handledEvents.add(eventId);
// 3) 先 ACK 再异步处理,避免回调超时被重试
res.json({ code: 0 });
handleMessage(body.event).catch((err) =>
console.error('handleMessage failed', err)
);
});
app.listen(process.env.PORT || 3000, () => {
console.log('listening');
});
```
```javascript
// agent.js —— 把群消息转给豆包工作智能体,再把回答发回群里
async function handleMessage(event) {
const msg = event && event.message;
if (!msg || msg.message_type !== 'text') return;
const chatId = msg.chat_id;
const openId = event.sender && event.sender.sender_id
? event.sender.sender_id.open_id
: '';
// 去掉 @机器人 的占位符,拿到真正的问题
const rawText = JSON.parse(msg.content || '{}').text || '';
const cleanText = rawText.replace(/@\S+/g, '').trim();
if (!cleanText) return;
// 把 open_id 换成人名,Agent 才知道"谁在说话"
const speaker = nameByOpenId[openId] || '某位同事';
const reply = await callDoubaoAgent({
sessionId: feishu:${chatId}, // 同群共用一段上下文
userText: 【${speaker}】${cleanText},
history: sessions.get(chatId) || [],
});
// 把本轮对话写回记忆,供下一轮使用
const history = sessions.get(chatId) || [];
history.push({ role: 'user', content: cleanText });
history.push({ role: 'assistant', content: reply });
sessions.set(chatId, history.slice(-20)); // 只保留最近 20 条
await sendToFeishu(chatId, reply);
}
```
```javascript
// doubao.js —— 调用豆包工作智能体
// 端点、字段名、鉴权头请以豆包工作官方文档为准
async function callDoubaoAgent({ sessionId, userText, history }) {
const resp = await fetch(${process.env.DOUBAO_BASE_URL}/your-agent-endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: Bearer ${process.env.DOUBAO_API_KEY},
},
body: JSON.stringify({
session_id: sessionId,
input: userText,
history: history, // 若平台自带会话记忆,这个字段可以去掉
}),
});
if (!resp.ok) {
console.error('agent error', resp.status, await resp.text());
return '我这边暂时连不上,稍后再试一次。';
}
const data = await resp.json();
return data.output_text || '我没拿到结果,换个说法再问我一次。';
}
```
```javascript
// feishu.js —— 拿 token、发消息
async function getTenantAI 词典:Token">Token() {
const resp = await fetch(
'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_id: process.env.FEISHU_APP_ID,
app_secret: process.env.FEISHU_APP_SECRET,
}),
}
);
return resp.json(); // { tenant_access_token, expire }
}
// 生产环境把 token 缓存起来,别每条消息都换一次
async function sendToFeishu(chatId, text) {
const { tenant_access_token } = await getTenantToken();
await fetch(
'https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id',
{
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
Authorization: Bearer ${tenant_access_token},
},
body: JSON.stringify({
receive_id: chatId,
msg_type: 'text',
content: JSON.stringify({ text }),
}),
}
);
}
```
跑起来,在群里 @ 一下,收到回复就算通了。
---
第 4 步:做出"分派任务"和"多人多轮"
4.1 分派任务:用卡片而不是纯文本
纯文本的 @ 很容易被刷过去。用交互卡片,把任务摊开,还带确认按钮:
```javascript
// 用卡片消息发任务,比纯文本更好确认
const card = {
config: { wide_screen_mode: true },
header: { title: { tag: 'plain_text', content: '任务分派' } },
elements: [
{
tag: 'div',
fields: [
{ is_short: true, text: { tag: 'lark_md', content: '任务\n接口联调' } },
{ is_short: true, text: { tag: 'lark_md', content: '负责人\n<at id=ou_xxx></at>' } },
{ is_short: true, text: { tag: 'lark_md', content: '建议时间\n本周五前' } },
{ is_short: true, text: { tag: 'lark_md', content: '验收标准\n联调环境跑通全流程' } },
],
},
{
tag: 'action',
actions: [
{
tag: 'button',
text: { tag: 'plain_text', content: '我来接' },
type: 'primary',
value: { action: 'accept', task_id: 't_001' },
},
],
},
],
};
// 卡片结构以飞书官方文档为准,字段大小写敏感
```
按钮点击会走卡片回调,你在回调里把 task_id 和点击人写进多维表格,任务闭环就成了。
4.2 多人多轮:三个必须做对的设计
会话键(session key)。用 chat_id 做键,群内共享上下文;如果希望每个人有独立记忆,用 chat_id + open_id。想清楚再选,中途换会丢历史。
上下文裁剪。别把 200 条消息全塞进去。只保留最近 20 条,同时把超过 3 天的历史丢弃。因为群聊里 90% 的消息对当前问题毫无价值。
并发隔离。同一个人连发三条消息时,加一个排队:
```javascript
// 同一个会话串行处理,避免"三连问"被并行回复乱序
const queues = new Map();
function enqueue(key, task) {
const prev = queues.get(key) || Promise.resolve();
const next = prev.then(task, task);
queues.set(key, next);
return next;
}
```
---
第 5 步:上线前的加固清单
- 幂等:事件 id 去重(第 3 步已做),否则网络抖动会重复回复。
- 超时:回调必须"秒回",重活异步干。具体超时阈值以官方文档为准。
- 限流:Agent 调用加并发上限,超了排队而不是直连。
- 密钥:
App Secret、API Key全部走环境变量,不进 Git。 - 审计:每条请求记录
chat_id / open_id / 输入 / 输出 / 耗时,出问题时这是唯一的证据。 - 兜底:Agent 调用失败时,回一句固定话术,而不是静默失败让人干等。
---
常见坑与排错
1. 群里 @ 了没反应。
九成是版本没发布,或者机器人没被拉进群。先去开放平台看"应用发布"状态,再看群成员列表里有没有它。
2. 不 @ 它就不理人。
这是正常行为。机器人默认只收 @ 消息,想收全部消息需要额外申请权限并走审核。
3. 同一条消息被回复了两次。
事件推送会重试。检查幂等逻辑是不是在 res.json() 之前就写好了。
4. 回调一直报超时。
你在回调里同步等 Agent 回答了。改成"先返回 200,再异步处理"。
5. 改了权限还是不生效。
权限变更必须重新发布版本,且通常要管理员审核通过。
6. Agent 把 A 的问题答到 B 头上。
会话键设计问题。先确认你要的是"群人共享记忆"还是"每人独立记忆",再定 key。
7. 长回答刷屏。
在指令里写死字数上限,超过就输出"回复:详情",用户主动要再看。这比事后调格式有效得多。
8. 回调验签失败。
加密 key、verification token 配置对不上。改完记得两边同步。
9. Agent 承诺了做不到的时间。
在指令里明确"只给建议时间,不承诺交付"。这类幻觉靠提示词能压掉大半。
10. 知识库里答案过期。
删掉旧文档,或者给文档加生效日期字段,让 Agent 优先引用最新的。
---
下一步建议
先扩场景,别急着扩人数。 跑通一个群之后,最容易想到的是"再拉 10 个群"。更好的做法是先在这个群里多跑两个场景:把日报汇总接进来、把任务卡片写进多维表格。场景密度比群数量更能体现价值。
把人的反馈变成数据。 在 Agent 的回复下面挂"有用 / 没用"两个按钮,攒够一两百条,你就有了自己的评测集。换模型、改指令之后跑一遍,才知道是真变好了还是碰巧。
开放"人纠正"的入口。 群里允许大家回一句"这条答错了,正确是 XXX",你定期把这些回灌进知识库或指令。这是让群聊 Agent 越用越准的唯一可靠办法。
最后再考虑多群多租户。 那时候要处理的是数据隔离、配额和权限模型,是另一篇教程的内容了。先让一个群真的离不开它。
