做一个内部团队知识 Agent
团队聊天里的关键决策、技术选型、谁负责什么常常埋在 100K+ 消息历史里。 这个 Agent 帮你把”团队大脑”显式化 — 索引、检索、定期摘要。
用户故事
“我们 30 人团队的 Hashee 群每天上千条消息。两周前讨论的设计决策已经 没人记得在哪条。希望 Agent 能:(1) 答 ‘X 项目谁负责’ (2) 答 ‘我们决定 用什么数据库’ (3) 每周自动生成决策摘要。“
设计要点
- Agent 只响应被 @mention 的消息(避免群里刷屏)
- 用户主动
@Bot 索引最近 100 条才入库(避免静默存所有消息) - 摘要 cron 每周一早跑(覆盖上周)
- 所有索引数据只在 Agent 自己的 PG(不在 Hashee 后端)
完整代码
import { HasheeAgent } from "@hasheeai/agent-sdk-ts";import OpenAI from "openai";import { Pool } from "pg";
const openai = new OpenAI();const db = new Pool({ connectionString: process.env.DATABASE_URL });
const agent = await HasheeAgent.init({ agentId: process.env.HASHEE_AGENT_ID!, token: process.env.HASHEE_AGENT_TOKEN!, baseUrl: "https://api.hashee.ai", connectionMode: "websocket",});
const MY_ID = process.env.HASHEE_AGENT_ID!;
agent.addMessageHandler(async (msg) => { if (msg.conversation_type !== "group") return; if (!msg.mentions?.includes(MY_ID)) return;
const text = stripMention(msg.payload?.text ?? "", MY_ID); const intent = parseIntent(text);
switch (intent.type) { case "index": await indexLast(msg, intent.count); break; case "ask": await answer(msg, intent.question); break; case "weekly_summary": await sendWeeklySummary(msg.conversation_id); break; default: await agent.send(msg.conversation_id, { type: "text", text: "我能做:\n• `@Bot 索引最近 100 条` — 把最近消息入库\n• `@Bot 谁负责 X 项目` — 问问题\n• `@Bot 生成周报` — 上周决策摘要", mentions: [msg.sender_id], }); }});
// === 1. 索引消息(用户主动触发)===
async function indexLast(msg: any, count: number) { await agent.send(msg.conversation_id, { type: "text", text: `开始索引最近 ${count} 条消息...`, }); // 假设业务侧本地存了消息(通过监听所有群消息但不入库,只 in-memory 短期 cache) const recent = await fetchRecentFromLocalCache(msg.conversation_id, count); for (const m of recent) { const embed = await openai.embeddings.create({ model: "text-embedding-3-small", input: m.text, }); await db.query( `INSERT INTO team_msgs (conv_id, msg_id, sender_id, text, ts, embedding) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (msg_id) DO NOTHING`, [m.conv_id, m.msg_id, m.sender_id, m.text, m.ts, embed.data[0].embedding], ); } await agent.send(msg.conversation_id, { type: "text", text: `✓ 已索引 ${recent.length} 条。`, });}
// === 2. 问答 ===
async function answer(msg: any, question: string) { await agent.typing(msg.conversation_id); const qEmbed = await openai.embeddings.create({ model: "text-embedding-3-small", input: question, }); const r = await db.query( `SELECT sender_id, text, ts FROM team_msgs WHERE conv_id = $1 ORDER BY embedding <=> $2 LIMIT 8`, [msg.conversation_id, qEmbed.data[0].embedding], ); const context = r.rows.map((row) => `[${row.ts.toISOString()}] ${row.sender_id}: ${row.text}`, ).join("\n");
const c = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: `你是团队知识助理。基于以下群历史回答:\n\n${context}\n\n如果不足以回答,直说。` }, { role: "user", content: question }, ], }); await agent.send(msg.conversation_id, { type: "text", text: `@<U:${msg.sender_id}> ${c.choices[0]?.message?.content}`, mentions: [msg.sender_id], });}
// === 3. 周报(cron)===
async function sendWeeklySummary(convId: string) { const r = await db.query( `SELECT text FROM team_msgs WHERE conv_id = $1 AND ts > NOW() - INTERVAL '7 days' ORDER BY ts`, [convId], ); const c = await openai.chat.completions.create({ model: "gpt-4o", messages: [ { role: "system", content: "总结上周团队群里的关键讨论。按主题分组(决策 / 待办 / 问题)。" }, { role: "user", content: r.rows.map((x) => x.text).join("\n") }, ], }); await agent.sendArtifact(convId, { artifact: { subtype: "info", title: `上周团队摘要 (${r.rows.length} 条)`, payload: { body: c.choices[0]?.message?.content ?? "(空)" }, }, });}
// === 工具 ===
function stripMention(text: string, agentId: string): string { return text.replace(new RegExp(`@<U:${agentId}>\\s?`, "g"), "").trim();}
function parseIntent(text: string): any { const m1 = text.match(/^索引最近\s*(\d+)/); if (m1) return { type: "index", count: Number(m1[1]) }; if (text.includes("周报") || text.includes("摘要")) return { type: "weekly_summary" }; if (text.length > 3) return { type: "ask", question: text }; return { type: "help" };}
async function fetchRecentFromLocalCache(_convId: string, _count: number): Promise<any[]> { // 业务侧实现:监听所有群消息存 in-memory 24h cache 即可 return [];}
// === Cron:每周一 09:00 自动周报 ===
setInterval(async () => { const now = new Date(); if (now.getDay() === 1 && now.getHours() === 9 && now.getMinutes() === 0) { const convs = await db.query("SELECT DISTINCT conv_id FROM team_msgs"); for (const row of convs.rows) { await sendWeeklySummary(row.conv_id); } }}, 60_000);
agent.addStatusHandler((s) => console.log(`[status] ${s}`));console.log("[team-knowledge] up");DB schema
CREATE TABLE team_msgs ( msg_id TEXT PRIMARY KEY, conv_id TEXT NOT NULL, sender_id TEXT NOT NULL, text TEXT NOT NULL, ts TIMESTAMPTZ NOT NULL, embedding vector(1536) NOT NULL);CREATE INDEX ON team_msgs USING ivfflat (embedding vector_cosine_ops);CREATE INDEX ON team_msgs(conv_id, ts);隐私
- 用户显式触发”索引最近 N 条”才入库 — 不静默存
- 关系撤销 →
relation.revoked→ 清理该 user 发的所有消息 - 群解散 →
group.dismissed→ 清理整个 conv_id 的数据 - 周报推送到群里 — 所有成员可见(透明,不偷偷做)