实战场景

给文档站加问答助手

按 ⌘K 弹出问答窗口,由服务型智能体回答帮助中心的问题:后端签发令牌,前端按需加载 Kit。

本页带你给一个帮助中心或文档站加上「⌘K 问答」:访客按下 ⌘K(Windows / Linux 为 Ctrl+K)或点击搜索框,弹出一个居中的对话窗口,由服务型智能体依据你写好的内容回答。参照实现是仓库里「好物集」演示站(apps/kit-demo)的帮助中心页,路径 /help。

你需要:一个 Praxis API Key、一个能放后端接口的站点、一个前端页面。

未登录访客即将支持

这个场景最常见的需求是「访客不用登录就能问」。令牌签发需要你的后端带上终端用户 ID,所以现在的做法是:只对已登录的用户开放,或由后端自行决定用什么 ID 签发(见下文「未登录的访客」)。面向匿名访客的正式方案「可公开 key」还在开发中,上线后本页会补充,现在请不要依赖。

工作方式

访客按 ⌘K ──▶ 页面加载 Kit,调用 getToken
                         │
                         ▼
        你的后端 POST /api/praxis-token ──(API Key)──▶ POST /agent/v1/embed_tokens
                         │
                         ◀── { token, expires_at }
Kit(modal)──(token)──▶ Praxis API ──▶ 服务型智能体

浏览器里没有 API Key。Kit 只在访客第一次打开时才加载,之后令牌由 getToken 按需取得。

1. 创建智能体并写入答案范围

  1. 在 Console 打开「服务型智能体」,点「新建服务型智能体」。
  2. 进入「接入与配置 · 配置 · 指令」,写明角色、回答范围和越界时怎么办,并把要回答的内容放进去。演示站的帮助中心智能体就是这样做的:把 FAQ 全文放进指令,要求「依据下面的内容回答,不编造政策、优惠或时效;范围外的问题请用户联系客服」,并且不配置任何工具。
  3. 在「配置 · 安全」选择运行环境,在来源白名单里点「添加来源」,填你的站点来源(例如 https://help.example.com)。本地调试可打开「允许 localhost」。点「保存安全设置」。
  4. 记下页面标题下方的智能体短号(旁边有复制按钮)。

期望:保存安全设置后出现「已保存,不产生新版本」;「接入与配置 · 试用调试」里能直接和智能体对话。

指令不是秘密,模型可能被用户要求复述,不要放不想公开的内容,见传入上下文。

2. 后端签发令牌

在 Console 的 API Keys 里创建一个专门用来签发令牌的 Key,放进后端环境变量。下面是一个与框架无关的处理函数,参照演示站的 /api/praxis-token:

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

export async function handlePraxisToken(request: Request, currentUserId: string | null): Promise<Response> {
  if (!currentUserId) 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,   // 智能体 id 或短号
      external_user_id: currentUserId,   // 来自你的登录态,不接受前端传入
    }),
    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 });
}

期望:已登录请求返回 HTTP 200 和 { token, expires_at };未登录返回 401。令牌字段与错误见嵌入令牌与终端用户。

3. 前端:⌘K 打开 modal

Kit 的 layout: "modal" 是居中的原生对话框,自带遮罩、焦点管理和 Esc 关闭。launcher: { type: "none" } 表示不显示启动按钮,由你的页面调用 open()。⌘K 快捷键由页面自己监听,Kit 不内置。

<button id="help-search">搜索或提问(⌘K)</button>

<script type="module">
  let pending;
  function loadAssistant() {
    pending ??= (async () => {
      await import("/praxis-kit.js"); // Kit 的构建产物,由你的站点提供
      await customElements.whenDefined("praxis-agent");
      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: "modal",
        launcher: { type: "none" },
        header: { title: "帮助中心问答", icon: "help-circle" },
        startScreen: {
          greeting: "有什么可以帮你?",
          prompts: ["如何申请退货?", "在哪里查看配送进度?"].map(text => ({ label: text, prompt: text })),
        },
        composer: { placeholder: "输入你的问题…" },
      });
      document.body.appendChild(agent);
      return agent;
    })().catch(error => { pending = undefined; throw error; }); // 加载失败后允许再次尝试
    return pending;
  }

  async function openAssistant() {
    try { (await loadAssistant()).open(); }
    catch { alert("问答加载失败,请重新打开搜索。"); }
  }

  document.querySelector("#help-search").addEventListener("click", openAssistant);
  document.addEventListener("keydown", event => {
    if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
      event.preventDefault();
      openAssistant();
    }
  });
</script>
  • open() / close() 是元素方法;窗口关闭时元素触发 praxis.close 事件,可以在里面把焦点还给搜索框。
  • 想用 CDN 加载而不是自托管,见安全里的「子资源完整性」:浮动路径 https://kit-develop.praxisusercontent.com/v1/praxis-kit.js,或不可变路径 /v0.1.0/praxis-kit.js 配 integrity。
  • 用 React 时换成 @deepractice-ai/praxis-kit-react 的 <PraxisAgent> 与 usePraxisAgent(),control.open() 打开,见嵌入方式。

期望:按 ⌘K 或点击按钮,弹出带遮罩的对话窗口并出现欢迎语与两个推荐问题;点推荐问题收到回复;按 Esc 关闭。

4. 验证

不打开浏览器也可以验证后端和令牌:

curl -X POST https://api-develop.deepraxis.com/agent/v1/embed_tokens \
  -H "Authorization: Bearer $PRAXIS_API_KEY" -H "Content-Type: application/json" \
  -d '{"agent":"<智能体 id 或短号>","external_user_id":"smoke-test"}'

期望:HTTP 200,返回 id、token、expires_at。随后在 Console 的「运营与管理 · 签发记录」能看到这条记录,见运营与管理。

未登录的访客

演示站为了能让任何人试用,对帮助中心智能体在未登录时用固定的 external_user_id: "guest" 签发。这只适合演示:所有访客共用同一个终端用户身份,嵌入策略里「单个终端用户的活跃会话上限」「每分钟请求上限」按这个身份合计,封禁一个访客就等于封禁全部。正式站点请在「可公开 key」上线前只对已登录用户开放,或自行给每位访客分配稳定的匿名 ID。

常见问题

  • 签发返回 INVALID_PARAMS「embed policy not configured」:智能体还没有嵌入策略,回到「配置 · 安全」保存一次。
  • 签发返回 EMBED_USER_BLOCKED:这个 external_user_id 在封禁名单里,在「运营与管理 · 终端用户」解封。
  • 弹窗打开但无法对话:确认页面来源在来源白名单里,api.url 与签发令牌用的是同一个环境。
  • 页面设置了 CSP 后脚本被拦截:见安全里的内容安全策略。

下一步:在 SaaS 后台按登录用户嵌入 · 组件参考 · 限制与错误

导出

本页内容