智能体嵌入

前端工具与页面上下文

让智能体读取你页面里的数据、调用页面上的函数:前端工具(custom tool)和页面上下文(readable)。

智能体在对话里常常需要「此刻页面上的东西」:视频播到了哪里、表单填了什么、选中了哪一行。这些数据只在浏览器里,你的后端看不到。Kit 给了两个入口:

入口谁触发数据怎么到智能体适合
前端工具智能体决定调用智能体调用,页面里的函数执行并返回结果按需读取、需要计算或查询的数据,以及页面上的动作
页面上下文页面主动提供随每条用户消息附带几乎每轮都用得上的小事实,例如「正在看第几课」

先读这一条

前端工具和页面上下文都来自浏览器,用户可以改。智能体拿到的是不可信输入,不要据此做授权、计费这类决定。需要可信事实时用 system.message,需要鉴权的数据放在工具服务端。

前端工具

分三步:在智能体里声明工具,在页面里实现它,智能体调用时 Kit 把结果送回去。

1. 在智能体定义里声明

在智能体的 tools 里加一项 type: "custom"。智能体只看得到名字、描述和输入结构,执行在你的页面里。

{
  "type": "custom",
  "name": "read_playback_progress",
  "description": "读取用户当前视频的播放进度,单位秒。用户问「我看到哪了」时调用。",
  "input_schema": { "type": "object", "properties": {} }
}
  • name 必须匹配 ^[a-zA-Z0-9_-]{1,64}$,不能与其他自定义工具或内置工具重名。
  • description 决定智能体何时调用它,写清楚用途和何时用。
  • input_schema 是 JSON Schema,type 必须是 object。

2. 在页面里实现(Web Component)

把处理函数赋给元素的 onClientTool 属性。它是 JavaScript 属性,不是 HTML 属性,也不是 setOptions 的选项。

<video id="lesson-video" src="/lessons/3.mp4" controls></video>
<praxis-agent style="display:block;height:600px"></praxis-agent>

<script type="module">
  import "/praxis-kit.js";

  const video = document.querySelector("#lesson-video");
  const agent = document.querySelector("praxis-agent");

  agent.setOptions({
    api: {
      url: "https://api-develop.deepraxis.com",
      getToken: async () => {
        const response = await fetch("/api/praxis-token", { method: "POST" });
        if (!response.ok) throw new Error("暂时无法使用");
        return (await response.json()).token;
      },
    },
    agent: { name: "课程助手" },
  });

  // 前端工具:智能体调用时执行
  agent.onClientTool = async ({ id, name, input }) => {
    if (name !== "read_playback_progress") throw new Error(`未知工具:${name}`);
    return { positionSeconds: video.currentTime, durationSeconds: video.duration };
  };

  // 页面上下文:随每条用户消息附带
  agent.setReadable("lesson", "正在看第 3 课");
  agent.setReadable("playback", { positionSeconds: 83 });
  // 之后可以移除
  agent.removeReadable("playback");

  // 可选:观察调用与日志
  agent.addEventListener("praxis.tool.change", e => console.log(e.detail));
  agent.addEventListener("praxis.log", e => console.warn(e.detail));
</script>

处理函数收到 { id, name, input }:id 是这次调用的编号,name 是工具名,input 是智能体按 input_schema 传来的参数。Kit 不维护工具注册表,所有自定义工具都进这一个函数,所以对不认识的名字必须抛错。

返回值的处理:

你返回智能体收到
字符串原样
其他可序列化的值JSON.stringify 后的文本
undefined空字符串
抛出错误、Promise 被拒绝、结果无法序列化错误文本,并标记为错误(is_error)

2'. 在页面里实现(React)

React 绑定提供两个 hook:usePraxisTool 注册工具,usePraxisReadable 提供页面上下文。它们要在 <PraxisAgent> 的子树里,或在外层 <PraxisProvider> 里使用。

import { useMemo, useRef } from "react";
import {
  PraxisAgent, usePraxisTool, usePraxisReadable,
} from "@deepractice-ai/praxis-kit-react";

function LessonBridge({ lesson, videoRef }) {
  // 页面上下文:lesson 变化时自动更新,卸载时自动移除
  usePraxisReadable("lesson", `正在看第 ${lesson.number} 课:${lesson.title}`);

  // 前端工具:卸载或改名时自动取消注册
  usePraxisTool("read_playback_progress", () => ({
    positionSeconds: videoRef.current?.currentTime ?? 0,
    durationSeconds: videoRef.current?.duration ?? 0,
  }));
  return null;
}

export function LessonPage({ lesson }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const options = useMemo(() => ({
    api: { url: "https://api-develop.deepraxis.com" },
    agent: { name: "课程助手" },
  }), []);

  return (
    <>
      <video ref={videoRef} src={lesson.src} controls />
      <PraxisAgent
        options={options}
        getToken={async () => {
          const response = await fetch("/api/praxis-token", { method: "POST" });
          if (!response.ok) throw new Error("暂时无法使用");
          return (await response.json()).token;
        }}
        onToolChange={e => console.log(e.detail)}
      >
        <LessonBridge lesson={lesson} videoRef={videoRef} />
      </PraxisAgent>
    </>
  );
}

要点:

  • usePraxisTool(name, handler):handler 收到 (input, call),可返回值或 Promise。handler 更换时自动使用最新的函数,不会重新注册。同名工具重复注册时,最后挂载的生效,开发模式下会警告;它卸载后恢复上一个。
  • usePraxisReadable(name, value):挂载时设置,JSON.stringify(value) 变化时更新,卸载时移除。值要能 JSON 序列化,变化要触发 React 渲染。
  • 没有用 hook 注册的工具名,会交给 <PraxisAgent onClientTool={…}>;两者都不处理时,调用以 unknown tool <name> 报错返回给智能体。
  • 想在 <PraxisAgent> 之外的组件里注册,把整棵树包在 <PraxisProvider> 里。
  • options 请保持引用稳定(useMemo),见组件参考。

页面上下文

setReadable(name, value)(React 里是 usePraxisReadable)登记一条页面事实。此后每一条用户消息发出时,Kit 都会附带当前所有登记项的快照,包括首条消息、输入框发送、sendUserMessage 和推荐问题。

  • name 匹配 [A-Za-z0-9_.:-]{1,64};值是字符串或可 JSON 序列化的值。非法的名字或值会抛出 TypeError,且不会覆盖原来的值。
  • 更新同名项保持它原来的位置;移除后重新登记会排到最后。
  • 没有登记项时不附带任何内容。

服务端把它渲染成附在本轮用户消息末尾的一段文本,并明确标记为不可信:

<page_context untrusted="true">
<item name="lesson">正在看第 3 课</item>
<item name="playback">{"positionSeconds":83}</item>
</page_context>

它不进系统提示,也不会延续到下一轮:下一条消息带的是那一刻的新快照。所以页面状态变了,智能体下一轮自然看到新值,你不需要清理旧值。

超时、错误与未注册的工具

情况结果
元素上没有 onClientTool(React 里没有对应 hook 和 onClientTool)立刻返回 tool <name> is not available on this page,标记为错误(React 中为 unknown tool <name>)
处理函数抛错或 Promise 被拒绝错误信息作为错误结果返回,智能体看得到,通常会向用户解释或换个办法
结果无法 JSON.stringify同上,作为错误返回
页面一直没有返回结果服务端等待最多 120 秒,到时记一条错误结果(client tool timed out),本轮继续
结果写回失败Kit 触发 praxis.log(level: "error"),不会重跑你的函数,也不会发出成功的 end 事件;服务端的 120 秒超时兜底

同一个调用编号只执行一次:Kit 在执行前先认领编号,并检查历史里是否已有结果。切换会话、断线重连、重新挂载都不会让已经回答过的调用重跑;还没有结果的历史调用在页面重新打开时可以被执行。这不是跨标签页的事务保证,同一会话在两个标签页打开时,不要让工具产生副作用。

事件

在元素上监听,都会冒泡并穿过 Shadow DOM。

事件detail说明
praxis.tool.change{ id, name, status: "start" | "end", isError? }start 在你的函数执行前;end 在结果被服务端确认之后
praxis.log{ level: "info" | "warn" | "error", message, data? }上下文被截断时为 warn,data.names 列出被丢弃的名字;结果写回失败时为 error

React 里对应 onToolChange 和 onLog。

限制

项限制
页面上下文条数每条消息最多 32 项
页面上下文体积序列化后合计最多 4096 字节(UTF-8)
超出时Kit 从最早登记的开始丢弃,直到满足两项限制,并触发 praxis.log 的 warn;登记本身不受影响。全部被丢弃时不附带该字段
名字[A-Za-z0-9_.:-]{1,64}
工具名^[a-zA-Z0-9_-]{1,64}$
等待结果最多 120 秒

给智能体看的数据要精简:只放这一轮可能用到的事实,大块数据用前端工具按需读取。

常见问题

  • 工具没被调用:确认智能体定义里有同名的 custom 工具,并且 description 说明了何时用。
  • 总是收到「not available」:onClientTool 要赋给元素的属性,且在智能体调用之前赋好;放在 setOptions 里无效。
  • 上下文没有出现:确认值已登记且没有超过限制,留意 praxis.log 的警告。

下一步:工具授权 · 安全 · 组件参考

导出

本页内容