实战场景

在 SaaS 后台按登录用户嵌入

按登录用户签发令牌并传入用户信息,用浮窗做客服、用侧栏做课程助教,再让智能体在服务端按用户鉴权查订单。

本页带你把服务型智能体放进一个有登录的 SaaS 后台,并做到三件事:智能体知道当前是谁、知道他正在看什么、只能查他自己的数据。参照实现是仓库里「好物集」演示站(apps/kit-demo)的两个场景:右下角浮窗客服,和课程页右侧的课程助教。

你需要:一个 Praxis API Key、一个有登录态的后端、一个前端页面,以及(想让智能体查订单时)一个你自己的 MCP 服务。

工作方式

登录用户 ──▶ 你的后端 /api/praxis-token
              │  用登录态里的用户 ID + 用户信息
              ▼
        POST /agent/v1/embed_tokens
        { agent, external_user_id, system_message }
              │
              ◀── { token, expires_at }
浏览器(Kit)──(token)──▶ Praxis API ──▶ 智能体 ──MCP──▶ 你的订单服务
                                                 └ 带 X-Praxis-External-User-Id

三类信息各走各的渠道,可信度不同,完整说明见传入上下文:

信息渠道可信
当前用户的姓名、会员等级签发令牌时的 system_message是,来自你的后端
用户正在看哪一课、播到哪里页面上下文与前端工具否,来自浏览器
用户的订单智能体调用你的 MCP 工具取决于你的服务端鉴权

1. 创建智能体

在 Console「服务型智能体」点「新建服务型智能体」,然后在「接入与配置 · 配置」里:

  1. 指令:写角色与边界。演示站客服的要点是:只回答本店商品、订单、退货与会员问题;查订单和退货必须使用订单工具,不编造订单;退货前先用一句话向用户确认订单与原因。
  2. MCP 与凭据:添加 MCP 服务器,名称填 orders,类型选 HTTP,URL 填 https://你的域名/mcp。再到下方「凭据库」勾选存放该服务访问令牌的凭据库,并保存凭据库选择。凭据的「允许的主机」要包含 MCP 服务的域名,会话按它自动匹配凭据。
  3. 工具与授权:在「授权方式」里,查询类工具选「自动执行」,申请退货这类写入工具选「每次询问」。
  4. 安全:选运行环境,在来源白名单添加你的后台域名,保存安全设置。要允许用 system_message 之外的方式覆盖指令才打开「允许宿主覆盖指令」,这里不需要。

期望:在「试用调试」里用「模拟」页签填入 external_user_id 与 system_message,点「重新签发」后能和智能体对话,「上下文」页签的 host_context 显示你填的内容。

2. 后端:按登录用户签发,并带上用户信息

const API = "https://api-develop.deepraxis.com"; // 正式地址上线后替换

export async function handlePraxisToken(request: Request, user: { id: string; name: string; level: string } | null) {
  if (!user) return Response.json({ error: "请先登录" }, { status: 401 });
  const response = await fetch(`${API}/agent/v1/embed_tokens`, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.PRAXIS_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      agent: process.env.PRAXIS_AGENT_SUPPORT,
      external_user_id: user.id, // 稳定且唯一,来自登录态
      system_message: [{ type: "text", text: `当前用户 ${user.name},会员等级 ${user.level}` }],
    }),
    signal: AbortSignal.timeout(15_000),
  });
  if (!response.ok) return Response.json({ error: "客服暂时不可用,请稍后重试" }, { status: 502 });
  const { token, expires_at } = await response.json();
  return Response.json({ token, expires_at });
}
  • external_user_id 只能来自你的登录态,不接受请求体里的值;它还是下面 MCP 鉴权用的身份。
  • system_message 是文本块数组,合计不超过 8192 字节。它不是秘密,模型可能被要求复述。
  • 课程助教是另一个智能体:同一个接口,换 agent,system_message 里换成学员信息。演示站用一个 agent 参数在后端选择,并且只接受预先登记的几个值。

期望:HTTP 200 与 { token, expires_at };未登录 401。

3. 前端:浮窗客服

import "/praxis-kit.js"; // Kit 的构建产物,由你的站点提供

const agent = document.createElement("praxis-agent");
agent.setOptions({
  api: {
    url: "https://api-develop.deepraxis.com",
    getToken: async () => {
      const response = await fetch("/api/praxis-token", { method: "POST", credentials: "same-origin" });
      if (!response.ok) throw new Error("客服暂时不可用,请稍后重试");
      return (await response.json()).token;
    },
  },
  agent: { name: "好物集客服" },
  layout: "popup",
  launcher: { type: "bubble", position: "bottom-right", label: "咨询客服" },
  header: { title: "好物集客服", subtitle: "通常几秒内回复" },
  startScreen: {
    greeting: "你好!有问题随时问我。",
    prompts: ["我的订单到哪了", "怎么退货"].map(text => ({ label: text, prompt: text })),
  },
});
document.body.appendChild(agent);

只在用户登录之后才创建这个元素;退出登录时整页刷新,这样上一位用户的令牌不会留在内存里。

期望:右下角出现气泡,点开出现欢迎语;问「我的订单到哪了」时智能体调用订单工具并给出该用户的订单。

4. 前端:侧栏课程助教,带页面上下文和前端工具

课程页用 layout: "sidebar"(右侧 400px 非模态面板,需要你为它在页面上留出位置)。助教要知道「学员正在看哪一课、播到哪里」,用两个入口:页面上下文放每轮都用得上的小事实,前端工具放需要时才读的数据。

先在智能体定义的 tools 里声明前端工具(类型 custom,写法见前端工具与页面上下文):

{
  "type": "custom",
  "name": "get_playback_progress",
  "description": "读取学员当前的视频播放进度",
  "input_schema": { "type": "object", "properties": {}, "additionalProperties": false }
}

再在页面里实现:

const tutor = document.createElement("praxis-agent");
tutor.setOptions({
  api: { url: "https://api-develop.deepraxis.com", getToken: fetchTokenForTutor },
  agent: { name: "课程助教" },
  layout: "sidebar",
  launcher: { type: "none" },
  header: { title: "课程助教", icon: "message-circle" },
});

// 页面上下文:随每条用户消息附带,标记为不可信
tutor.setReadable("course", "降噪耳机入门");
tutor.setReadable("lesson", "第 1 课:认识主动降噪");

// 前端工具:智能体调用时在页面里执行,返回值交还给智能体
tutor.onClientTool = ({ name }) => {
  if (name !== "get_playback_progress") throw new Error("unknown tool");
  return { lesson: "第 1 课:认识主动降噪", position_seconds: 83, duration_seconds: 300, playing: true };
};

document.body.appendChild(tutor);
tutor.addEventListener("praxis.open", () => document.body.classList.add("tutor-open"));
tutor.addEventListener("praxis.close", () => document.body.classList.remove("tutor-open"));
document.querySelector("#open-tutor").addEventListener("click", () => tutor.open());

换课时再调用一次 setReadable("lesson", …),之后每条消息自动带最新值。onClientTool 是元素的 JavaScript 属性,不是 setOptions 的选项;对不认识的工具名必须抛错。限制、超时与 React 写法见前端工具与页面上下文。

期望:打开侧栏后问「我现在看到哪了」,智能体调用 get_playback_progress 并据返回值回答,页面上能看到它没有编造进度。

5. 服务端按用户鉴权的 MCP 工具

平台调用你的 MCP 服务时会带上 X-Praxis-External-User-Id(令牌里的 external_user_id)、X-Praxis-Agent-Id、X-Praxis-Session-Id。工具必须按这个头限定数据范围,不要用模型传来的参数确定用户。

// MCP 请求入口:先校验平台出示的访问令牌,再取用户身份
export async function handleMcp(request: Request, expectedToken: string) {
  if (request.headers.get("authorization") !== `Bearer ${expectedToken}`) {
    return Response.json({ error: "未授权" }, { status: 401 });
  }
  const userId = request.headers.get("X-Praxis-External-User-Id"); // 每个请求单独读取
  // list_orders: 只返回 userId 名下的订单
  // request_return: 先确认订单属于 userId,再受理
  // userId 缺失或不认识 → 以工具错误返回,不返回任何数据
}

演示站的 MCP 服务有两个工具:list_orders(readOnlyHint: true)和 request_return(destructiveHint: true,参数 order_id、reason,订单不属于当前用户时返回「订单不存在或不属于当前用户」)。每个请求单独创建服务与闭包,并发请求之间不会串用户。

期望:用 alice 登录问订单只看到 alice 的订单;换 bob 登录看到 bob 的;让智能体「查 alice 的订单」时 bob 拿不到任何数据。询问确认只是帮用户确认意图,不是安全手段,详见工具授权。

常见问题

  • 智能体说查不了订单:在「配置 · MCP 与凭据」确认 MCP 服务器地址可访问,凭据库已勾选,且凭据的允许主机包含该域名。
  • 工具总是返回「缺少用户身份」:只有会话属于某位终端用户时才带身份头;确认令牌签发时传了 external_user_id。
  • 侧栏遮住了页面内容:侧栏是浮在页面上的面板,要用 praxis.open / praxis.close 事件给页面留出空位。
  • 前端工具总是「not available」:onClientTool 要在智能体调用之前赋到元素上。

下一步:给文档站加问答助手 · 令牌与终端用户 · 安全

导出

本页内容