我们组上线了个 AI 周报助手:平时把零碎进展随手记进去,周五点一下,自动汇总成一份结构化周报。用了两个月,组里没人再手写周报了。复盘一下这套东西前后端是怎么搭的,以及哪几个地方我返工过。
整体分三层
捋清楚边界后,架构其实很简单:
- 前端:记录录入 + 周报编辑器 + 流式预览。
- 薄后端(BFF) :鉴权、聚合一周的记录、调智能体、落库。这层我自己写,但很薄。
- 智能体后端:真正把一堆流水记录揉成周报的「大脑」。这层我没自己写模型,在一个零代码搭智能体的平台上配出来的。
最早我把汇总逻辑写在 BFF 里,用一堆 prompt 模板手工拼,代码又长又难调。后来整个搬到那个平台上,BFF 一下子瘦了一大半,只剩编排和数据。这是我做得最对的一个决定。
BFF:聚合 + 调用
BFF 的核心活儿是把一周的碎记录捞出来,组装成智能体能吃的输入,再把结果落库:
async function generateWeekly(userId: string, weekStart: string) {
const logs = await db.logs.findMany({
where: { userId, date: { gte: weekStart, lt: addDays(weekStart, 7) } },
orderBy: { date: 'asc' },
})
if (logs.length === 0) throw new AppError('NO_LOGS', '这周还没记录')
const input = logs.map(l => `[${l.date}] ${l.project}: ${l.content}`).join('\n')
const report = await callAgent({ template: 'weekly', input })
return db.reports.upsert({
where: { userId_weekStart: { userId, weekStart } },
create: { userId, weekStart, content: report, status: 'draft' },
update: { content: report, status: 'draft' }, // 重生成覆盖草稿
})
}
upsert 这里有讲究:周报默认存成 draft,用户可以重新生成覆盖,确认后才转 final。早期我直接存成终稿,结果用户每点一次生成就多一条记录,数据库里全是垃圾。
流式怎么穿过 BFF
智能体是流式输出的,BFF 不能等它全部生成完再返回,不然用户干等十几秒白屏。得把流透传到前端。难点是:既要边流给前端,又要在流结束后把完整内容落库。我用一个 tee 的思路,一边转发一边攒:
async function streamAgent(input: string, res: Response, onComplete: (full: string) => void) {
const upstream = await callAgentStream(input)
const reader = upstream.getReader()
const decoder = new TextDecoder()
let full = ''
while (true) {
const { value, done } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
full += chunk // 攒一份完整的,用于结束后落库
res.write(`data: ${JSON.stringify({ chunk })}\n\n`) // 同时实时转发
}
res.write('data: [DONE]\n\n')
res.end()
await onComplete(full) // 流结束才落库,中途断了不存半截
}
onComplete 放在最后,保证只有完整生成才入库。中途网络断了就不落,用户重试即可,不会存进半截脏数据。
前端:乐观更新 + 断流重连
前端记录录入做了乐观更新,点保存先上屏再发请求,失败回滚:
function addLog(draft: LogDraft) {
const temp = { ...draft, id: `temp-${Date.now()}`, pending: true }
logs.value.unshift(temp) // 先上屏
api.saveLog(draft)
.then(saved => replaceLog(temp.id, saved)) // 换成真 id
.catch(() => { removeLog(temp.id); toast('保存失败,已撤回') })
}
流式预览那块,我还做了断线重连——SSE 断了自动用已生成的部分重新接续。这块实现挺啰嗦,但 AI 生成耗时长,网络抖动概率不低,不做的话用户体验很脆。
一个一直没优化的点
周报生成对长度敏感。某个同事一周记了 200 多条,输入太长,生成出来的周报开始丢细节、把前面的项目漏掉。我现在的兜底是超过阈值就按项目分组、分批喂进去再合并,但合并出来的周报有时衔接生硬,读着像两段拼的。这块属于已知缺陷,排在待办里还没动。
整体回头看,这套东西能两周搭起来、BFF 还这么薄,关键就是把「大脑」这层外包掉了——前端管交互和流式,BFF 管编排和数据,模型那摊子完全没碰。职责切干净,每层都简单。
最后说下,那个智能体「大脑」我是在讯飞上配的:它是 MaaS 模式,现成模型直接出 API,我不用搭推理服务、不用养卡,BFF 才能瘦成这样。
你们的周报/日报是怎么自动化的?长输入丢细节这个问题有没有更优雅的解法,评论区等你支招。