客服知识 Agent
本页说明客服知识 Agent 如何在客户级委托下查询公开文档和授权可见的工单系统,结合该客户的历史上下文生成带来源的答案草稿。读完本页,你能理解这个场景为什么三个模块都是核心、标准答案为什么以知识库为准而不是记忆,以及客户数据的裁剪边界。
适用场景
客服团队需要 Agent 回答产品问题、排查常见故障,或根据客户历史记录给出更贴合的处理建议。答案素材分散在产品文档、状态页、社区帖和工单系统中,人工逐个翻找拖慢响应;直接把客服账号交给脚本,则工单系统里客户数据的访问范围完全失控。Agent 的产出是给支持团队复核的答案草稿,不直接回复客户。
典型触发时机:
- 高频问题涌入(例如版本发布后),需要快速产出带来源的标准答案草稿。
- 疑难工单需要汇总历史相似工单、文档和社区讨论后再答复。
- 产品公告或已知问题更新后,需要核对现有答案是否已经过期。
工程挑战
- 答案素材新旧混杂:产品文档、状态页、社区帖各自的更新节奏不同,一条公告发布,散落各处的旧答案就过期了。草稿不带来源链接和时间,客服无法判断每条结论还可不可信。
- 两类知识容易混淆:标准答案和产品知识是团队维护、按版本更新的知识库(canonical)内容;该客户的环境事实和历史处理记录才是客户上下文。把知识库内容抄进客户记忆,产品一更新就变成一批过期私货,且无法集中修正。
- 客户数据的裁剪边界:客服账号能看到的工单远多于本次问题需要的。传给网页任务的上下文必须裁剪到本次问题所需,无关客户隐私一旦进入任务就无法收回。
模块组合
| 模块 | 角色 | 说明 |
|---|---|---|
| GenAuth | 核心 | 交互式委托限定在当前客户上下文;撤销与审计链覆盖全部访问尝试。 |
| Web Agent | 核心 | 查询公开文档、状态页和社区帖(WebSearch 定位来源),并在授权范围内检索相似工单。 |
| GUMem | 核心 | 只承载该客户的历史问题、处理记录要点和已确认环境事实;标准答案与产品知识属于知识库,不进 Memory。 |
权限与委托边界
Agent 本身不持有任何固有权限。每次任务的实际权限是三个集合的交集:客服人员真实权限 ∩ 本次显式委托范围 ∩ 企业批准边界。落到这个场景:
- 委托范围只覆盖"读取当前客户相关的工单、历史记录和公开知识来源",不包含回复客户、关闭工单或修改客户数据。
- 委托凭证短时效,单次答疑任务建议分钟级有效期,过期后需重新委托。
- 客服人员或管理员可随时撤销授权;撤销后新的工单读取请求立即失败。
- 越权尝试(例如读取与本次问题无关的其他客户工单)会被拒绝并留痕——审计链覆盖全部尝试,不只是成功行为。
注意:SDK 示例申请的是产品级 scope(如 webagent.do_anything:read)。工单系统的域名清单和客户范围这类细粒度边界由 GenAuth 的 Agent Profile 或策略层配置强制执行,不由任务 prompt 承担;本页示例未展示该配置。完整语义见 Delegate Token 与缩权。
工作流程
客服人员打开客户会话并触发答疑任务。
GenAuth 通过交互式委托让客服人员确认授权,签发限定在该客户上下文的委托凭证。
GUMem 召回该客户的历史问题、处理记录要点和已确认环境事实——不召回知识库内容。
Web Agent 查询最新产品文档、状态页和社区讨论,并在授权范围内检索相似工单。
检查点:传给 Web Agent 的任务上下文必须经过裁剪,只含本次问题所需信息,不携带无关客户隐私。
Agent 汇总各来源信息,生成答案草稿,每条关键结论附来源链接和时间。
客服人员复核草稿,修改后回复客户;Agent 不直接触达客户。
检查点:草稿中每条结论都应能回溯到具体来源;引用的文档如与产品公告冲突,标注待核实而不是擅自取舍。
复核通过的标准答案更新到知识库(由团队维护);该客户的环境事实与处理要点写回 GUMem。
示例代码
下面的示例使用官方 Qoni SDK(@qoniai/qoni)把这个场景接到你的服务端:交互式委托(完整回调)→ 召回客户历史 → 一次 doAnything.run() 汇总文档与工单证据 → 应用侧校验答案草稿 → 支持团队复核后写回客户事实。
import { Qoni, QoniScopes } from '@qoniai/qoni'
const qoni = new Qoni({
accessKey: process.env.QONI_ACCESS_KEY!,
secretKey: process.env.QONI_SECRET_KEY!,
})
interface AnswerDraftItem {
conclusion: string
sourceRef: string
capturedAt: string
}
// 应用侧校验:解析 prompt 约定的 JSON 契约,
// 缺来源引用或采集时间的结论直接丢弃
function parseAnswerDraft(output: string): AnswerDraftItem[] {
const items = JSON.parse(output) as AnswerDraftItem[]
return items.filter((item) => item.conclusion && item.sourceRef && item.capturedAt)
}
export async function startAnswerDraft(supportUserId: string, ticketId: string) {
// 1. 交互式委托:涉及工单系统登录,让客服人员在 Qoni Console 确认授权
const { data: authorization } = await qoni.delegateToken({
mode: 'interactive',
agent: 'support-knowledge',
scopes: [
QoniScopes.DO_ANYTHING_READ,
QoniScopes.DO_ANYTHING_MANAGE,
QoniScopes.GUMEM_MEMORY_READ,
QoniScopes.GUMEM_MEMORY_WRITE,
],
redirectUri: 'https://app.example.com/qoni/callback',
state: `ticket-${ticketId}`,
user: { id: supportUserId },
expiresIn: 900, // 单次答疑任务给分钟级有效期
})
redirectUserTo(authorization.authorizationUrl)
}
// 客服人员同意后,Qoni 回调你的服务端路由,在这里兑换 grant
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 ticket = await loadTicket(parseTicketId(query.get('state')!))
return draftAnswer(grant, ticket)
}
async function draftAnswer(
grant: { token: string; auditId: string; grantedScopes: string[] },
ticket: { id: string; customerId: string; question: string },
) {
// 2. 任务前:召回该客户的历史问题与已确认环境事实——不是知识库内容
const { data: customerContext } = await qoni.gumem.recall({
token: grant.token,
sessionId: `customer-${ticket.customerId}`,
query: 'historical issues, confirmed environment facts, handling notes',
})
// 3. 一次调用完成汇总:查公开文档、检索授权可见的相似工单;
// prompt 明确 JSON 输出契约
const run = await qoni.doAnything.run({
token: grant.token,
prompt: `
Draft an answer for this support question: ${ticket.question}.
Search the latest product docs, status pages and community posts,
and retrieve similar tickets within the authorized scope only.
Return ONLY a JSON array of draft items, each shaped as
{ "conclusion": string, "sourceRef": string, "capturedAt": string };
if a doc conflicts with a product announcement, prefix the
conclusion with "[to verify]" instead of picking a side.
Draft only: do not reply to the customer, close tickets,
or modify any customer data.
Customer context from Memory: ${JSON.stringify(customerContext)}
`,
capture: { screenshots: true },
})
const result = await run.wait({
// 工单系统登录态失效等交互:转发给客服人员处理
onInteraction: (interaction) => notifySupportUserActionRequired(interaction),
})
// 4. 应用侧校验:每条结论必须带来源引用与采集时间,缺 sourceRef 或 capturedAt 的直接丢弃;
// 写回 Memory 不在这里发生,由复核后的 afterReview 单独完成
const draft = parseAnswerDraft(result.output)
return {
draft,
artifacts: result.artifacts,
audit: { auditId: grant.auditId, permissionBoundary: grant.grantedScopes },
}
}
// 5. 支持团队复核工单后调用:只写该客户的环境事实与偏好,
// 不写知识库内容——复核通过的标准答案由团队更新到知识库
export async function afterReview(
grantToken: string,
sessionId: string,
reviewedFacts: string[],
) {
await qoni.gumem.addMessages({
token: grantToken,
sessionId,
messages: [{ role: 'user', content: reviewedFacts.join('\n') }],
})
}答案草稿的结构由 prompt 中的 JSON 契约约定,parseAnswerDraft 在应用侧强制执行:缺来源引用(sourceRef)或采集时间(capturedAt)的结论直接丢弃。写回 Memory 由独立的 afterReview 在支持团队复核工单后调用,只接受复核确认的客户环境事实。SDK 顶层只返回通用的 RunResult(runId、status、output、artifacts 等)。
记忆策略
- 进 Memory:该客户的历史问题、处理记录要点和已确认环境事实(带来源与时间)——只属于这个客户的上下文。
- 不进 Memory:标准答案、产品知识和故障排查手册。它们是知识库(canonical)内容,由团队按版本维护、任务时注入;抄进 Memory 会在产品更新后变成无法集中修正的过期副本。
- 更正:客户环境变化导致旧事实失效时(例如客户升级了版本),把旧事实标记为已失效并指向新记忆,而不是物理删除,保证历史答复可追溯。
失败处理
| 情况 | 推荐处理 |
|---|---|
| 工单系统登录态失效 | 任务挂起,通知客服人员重新登录,从断点继续。 |
| 各来源答案互相矛盾 | 如实呈现分歧并标注置信度,交客服人员判断,不硬猜。 |
| 委托范围外的客户数据请求 | 直接拒绝并记录,事后可在审计链中查到未遂访问。 |
| 召回的客户事实与最新工单冲突 | 以本次工单确认的事实为准,把旧事实标记为已失效并写回 GUMem。 |
生产注意点
标准答案与产品知识以知识库为准:复核通过的答案更新到知识库供全团队复用,不写入单个客户的 Memory。客户隐私数据不要传给不需要的网页任务,Web Agent 只应接收经过裁剪的任务上下文。Agent 的产出始终是给支持团队的草稿,不应配置为直接回复客户;对外答复的最终责任在复核的客服人员。
下一步
- 阅读 授权与浏览器沙盒 了解受控会话的安全边界。
- 阅读 快速开始 跑通 Agent 身份与委托的最短路径。
- 继续查看 客户 Onboarding Agent 了解相邻场景。