Skip to content

活动 Follow-up Agent

本页说明活动 Follow-up Agent 如何在用户委托下读取参会名单、互动记录和 CRM notes,结合公开公司资料生成分组 follow-up 草稿,并把草稿以 draft 记录写回 CRM 等待销售确认。读完本页,你能理解这个场景需要哪些权限边界、为什么 CRM 是联系人事实的 canonical source、以及 GUMem 为什么只装销售确认过的措辞偏好。

适用场景

Marketing 和销售团队需要在 webinar、展会或线下活动结束后快速整理参会者,并生成个性化跟进内容。参会名单散落在活动平台和 CRM 里,逐个查公司背景再写跟进邮件往往要拖到活动热度消退之后;而把整个 CRM 的读写权限交给脚本,风险远超这项工作本身的需要。

典型触发时机:

  • webinar 结束后 24 小时内,需要按参会互动情况分组发出第一轮跟进。
  • 展会收集到一批名片和扫码记录,需要补齐公司上下文再交给销售。
  • 季度活动复盘时,需要核对哪些参会者已有 CRM 记录、哪些是新联系人。

工程挑战

  • 时效窗口:活动热度在 24–48 小时内衰减,人工逐个查公司背景赶不上这个窗口;但为了快而跳过事实核验,个性化内容就会出错并伤害客户关系。
  • 个性化的事实边界:草稿里"贵司刚完成 B 轮"这类话必须有公开来源支撑;没有来源的推测一旦发出,比模板邮件更糟。
  • CRM 数据主权:分组、历史活动和联系人事实的唯一真相在 CRM。把这些复制进第二套存储会立刻产生漂移——Agent 的产出必须以 draft 记录写回 CRM,由销售确认后才转正式数据。

模块组合

模块角色说明
GenAuth核心活动平台与 CRM 的授权读取:交互式委托、只读名单与 notes、短时凭证、可撤销,不授予发送或改写正式字段的能力。
Web Agent核心登录活动平台或 CRM 读取名单与互动(Profiles 复用登录态),WebSearch 补公开公司资料,逐条保留来源。
GUMem可选仅保存销售确认过的长期措辞偏好(例如"跟进邮件避免感叹号")。分组、历史活动和联系人事实以 CRM 为 canonical source,不在 Memory 建第二份真相。

活动 Follow-up Agent 场景架构

权限与委托边界

Agent 本身不持有任何固有权限。每次跟进任务的实际权限是三个集合的交集:用户真实权限 ∩ 显式委托范围 ∩ 企业批准边界。落到这个场景:

  • 委托范围只覆盖"读取当前活动的参会名单、互动记录和相关 CRM notes",不包含发送邮件、改写 CRM 正式阶段或修改联系人字段。
  • 活动平台与 CRM 登录属于高风险操作,必须走 mode: 'interactive':用户在 Qoni Console 确认授权,凭证在服务端回调中兑换。
  • 参会者个人信息只使用公开可见部分;委托范围不覆盖名单之外的客户隐私记录。
  • 越权尝试(例如读取其他活动的名单或改写正式 CRM 记录)会被拒绝并留痕——审计链覆盖全部尝试。

注意:SDK 示例申请的是产品级 scope(如 webagent.do_anything:read)。平台域名、活动范围这类细粒度边界由 GenAuth 的 Agent Profile 或策略层配置强制执行,不由任务 prompt 承担;本页示例未展示该配置。完整语义见 Delegate Token 与缩权

工作流程

活动 Follow-up Agent 工作流程

  1. 用户选择活动和跟进目标(例如首轮感谢、销售线索移交)。

  2. 应用发起交互式委托;用户在 Qoni Console 确认授权,服务端回调兑换凭证。

  3. 应用从规则库读取当前版本的跟进手册(分组标准、品牌语气、跟进规则),注入任务描述。

  4. Web Agent 登录活动平台与 CRM,读取参会名单、互动记录和相关 notes——CRM 是这些事实的 canonical source。

    检查点:登录墙、验证码或风控页出现时,Web Agent 应升级给人处理,而不是静默绕过。

  5. Web Agent 通过 WebSearch 查询参会者所在公司的公开资料,只采集公开可见部分并保留来源 URL。

  6. Agent 按互动程度分组,为每组生成 follow-up 草稿,每条公司事实附来源。

  7. 应用侧校验草稿(无来源事实剔除),把草稿以 draft 记录写回 CRM,附来源清单和 audit id,等待销售确认。

    检查点:草稿中的每条公司事实都应能回溯到公开来源;无来源支撑的个性化内容不应进入草稿。

示例代码

下面的示例使用官方 Qoni SDK@qoniai/qoni)接入这个场景:交互式委托(完整 callback)→ 从规则库读取带版本的跟进手册 → 一次 doAnything.run() 完成名单整理与草稿生成 → 应用侧校验后以 draft 记录写回 CRM。

ts
import { Qoni, QoniScopes } from '@qoniai/qoni'

const qoni = new Qoni({
  accessKey: process.env.QONI_ACCESS_KEY!,
  secretKey: process.env.QONI_SECRET_KEY!,
})

// 1. 入口:发起交互式委托——涉及活动平台与 CRM 登录,让用户在 Qoni Console 确认授权
export async function startFollowUp(userId: string, eventId: string) {
  const { data: authorization } = await qoni.delegateToken({
    mode: 'interactive',
    agent: 'event-follow-up',
    scopes: [QoniScopes.DO_ANYTHING_READ, QoniScopes.DO_ANYTHING_MANAGE],
    redirectUri: 'https://app.example.com/qoni/callback',
    state: eventId, // 任务参数通过 state 带到回调;参数较多时可存应用存储,state 只放任务 ID
    user: { id: userId },
    expiresIn: 900, // 单次整理任务给分钟级有效期
  })
  redirectUserTo(authorization.authorizationUrl)
}

// 2. 用户同意后,在服务端回调中兑换委托令牌,并继续执行任务
export async function handleQoniCallback(request: Request) {
  const query = new URL(request.url).searchParams
  const { data: grant } = await qoni.completeDelegateToken({
    grantId: query.get('grantId')!,
    code: query.get('code')!,
    state: query.get('state')!,
  })
  const eventId = query.get('state')! // startFollowUp 通过 state 传入的 eventId
  return draftEventFollowUps(grant, eventId) // grant.token 只保存在服务端
}

export async function draftEventFollowUps(
  grant: { token: string; auditId: string; grantedScopes: string[] },
  eventId: string,
) {
  // 3. 任务前:从你的规则库读取当前版本的跟进手册(不是 Memory)
  const playbook = await loadFollowUpPlaybook() // 例如 { version: '2026-08', segments: [...], voice: ... }

  // 4. 一次调用完成整理:读名单与 CRM notes、补公开资料、分组起草
  const run = await qoni.doAnything.run({
    token: grant.token,
    prompt: `
      Read the attendee list and interaction records for event ${eventId},
      plus related CRM notes — the CRM is the canonical source for contact
      facts. Add public company context for each attendee, keeping the
      source URL for every fact and using only publicly visible personal
      information. Segment attendees by engagement and draft one follow-up
      per segment. Return drafts as a JSON array of
      { segment, attendeeIds, draft, facts: [{ claim, sourceUrl }] } objects.
      Drafts only: never send email or messages, and never change official
      CRM stages or contact fields.

      Follow-up playbook (version ${playbook.version}):
      ${JSON.stringify(playbook)}
    `,
    capture: { screenshots: true },
  })

  const result = await run.wait({
    // 登录墙 / MFA / 风控页:转发给用户,由人完成
    onInteraction: (interaction) => notifyUserActionRequired(interaction),
  })

  // 5. 应用侧校验:无来源的公司事实剔除,不进入草稿
  const drafts = parseDrafts(result.output).map((d) => ({
    ...d,
    facts: d.facts.filter((f) => f.sourceUrl),
  }))

  // 6. 草稿以 draft 记录写回 CRM(你的应用直接调用 CRM API),等待销售确认
  await crm.createDraftRecords(eventId, drafts, { playbookVersion: playbook.version })

  return {
    drafts,
    artifacts: result.artifacts,
    playbookVersion: playbook.version,
    audit: { auditId: grant.auditId, permissionBoundary: grant.grantedScopes },
  }
}

输出结构由任务描述约定:这里约定返回 { segment, attendeeIds, draft, facts } 数组,应用侧 parseDrafts 负责解析与校验,缺少 sourceUrl 的事实被剔除。SDK 顶层只返回通用的 RunResultrunIdstatusoutputartifacts 等)。若销售在确认草稿时沉淀了长期措辞偏好,由你的应用确认后再写入 GUMem;本示例默认不读写 Memory。

数据与记忆边界

这个场景涉及四类数据,只有最后一类可选地属于 GUMem:

  • 版本化规则:分组标准、品牌语气、跟进规则——放在跟进手册(playbook)里按版本管理,每批草稿引用手册版本号。
  • 业务状态:分组结果、跟进草稿、公司事实与来源——以 draft 记录写回 CRM;分组、历史活动和联系人事实以 CRM 为 canonical source,不建第二份真相。
  • 审计记录:交互式授权、每次读取与越权拒绝构成的行为链——由 GenAuth 维护。
  • 用户 Memory(可选):销售确认过的长期措辞偏好(例如"跟进邮件避免感叹号")——这才是 GUMem 的位置;单次活动整理默认不召回也不写回。

失败处理

情况推荐处理
活动平台或 CRM 登录态失效任务挂起,通知用户重新登录,从断点继续整理。
参会者公开资料查不到或来源不可靠该联系人只保留名单内信息,不填充推测的公司背景。
委托范围外的 CRM 记录请求直接拒绝并记录,事后可在审计链中查到未遂访问。
草稿中的事实缺少来源应用侧校验剔除该事实,并在交付物中标注剔除数量。

生产注意点

不要自动发送邮件或改写 CRM 正式字段:草稿以 draft 记录写回 CRM,由销售确认后才转正式数据,发送动作始终属于人的职责。参会者个人信息只使用公开可见部分,超出名单范围的个人数据不应进入草稿、CRM draft 记录或 Memory。

下一步