应用内帮助助手
一个存在于 Web 应用内部并了解该应用使用方式的助手。它能回答“如何申请休假?”这样的问题,并提供使用确切 UI 标签的编号步骤。在相应步骤下方,它会展示一张特写截图,并且光标会动画指向要按下的按钮。它还会链接它提到的每个页面,但仅限于用户有权访问的页面。它会读取该用户的个人数据(在其权限范围内),并且最多只是为用户准备一个需要确认的操作。
该模式已被构建了三次:
- 一个能自我解释的聊天小部件,包含约 30 张截图。
[captura:id]标记即源于此。 - 一个内部 ERP 系统,成熟版本:约 25 个手册部分、约 200 张带有可点击区域的截图、12 个权限控制的数据工具、操作按钮和一个包含 32 个案例的评估集。耗时一天加后续修复。
- 一个现有的多智能体产品(一个教育平台的管理面板,包含 90 多个页面),在该产品中,它被移植到了产品已有的智能体运行时中。
reference.md 包含了每个部分的代码。截图捕获使用了 agent-app-screenshots 技能(npx skills add https://github.com/samuelferfort/agent-skills --skill agent-app-screenshots)来提供裁剪和编辑助手。scripts/capture.mjs 中的 Playwright 基础框架在此基础上增加了按角色会话和可点击区域功能。
0. 在编写代码前决定
仅询问代码无法告知用户的问题。
-
单一智能体还是双智能体。 如果应用已有智能体(如 RAG/知识机器人、领域 Copilot),则需在以下两者间选择:
- 将手册工具添加到该智能体中,以便只有一个入口点;
- 添加一个单独的帮助智能体。
一个入口点是用户所期望的,但它需要在“如何使用应用”和现有领域之间进行路由,并且必须对每个需要帮助的人可见。检查当前谁能看到现有面板。
-
受众。 不同的界面(管理员与最终用户)会获得不同的手册和截图集。它们可以共享加载器、工具和渲染器。
-
V1 版本的范围。 推荐顺序:
- 手册
- 链接
- 截图
- 评估
- 数据工具
- 操作按钮
仅手册本身就能回答大部分问题。操作按钮成本最高,因为每个目标对话框都必须从 URL 打开。
-
模型和路由。 使用推理量低的快速模型。在提交之前,务必在真实提供商路由上测量首个令牌的生成时间。 在一次构建中,通过 Vertex 的 Gemini 首个令牌耗时 68–203 秒,而通过 Google AI Studio 的同一模型仅需 1.6–5.6 秒。固定一个提供商以便隐式前缀缓存生效。
-
演示数据。 截图和评估需要一个填充好的数据库,其中每个角色有一个演示用户。切勿使用生产环境。任何出现的个人数据都必须被编辑掉。
1. 手册:小型、精心策划、位于仓库中
- 格式。 每个领域一个 Markdown 文件:
manual/<id>.md,包含 frontmatterid、titulo/title、rutas/routes(Next.js 路由模式)、modulo/module和resumen/summary。摘要是模型选择依据,因此请使其具体明确。 - 正文部分。
- 用途
- 如何到达(菜单路径)
- 谁可以使用(角色、权限、谁批准)
- 屏幕内容
- 如何操作(编号步骤)
- 常见问题
- 精确标签。 按照渲染时的样子编写 UI 标签,并用《》或引号括起来。从组件或 i18n 文件中读取,切勿凭记忆。
- 当语料库适合放入索引(约 40 个部分或更少)时不使用 RAG。
- 将索引(每个部分一行:id、标题、路由、摘要)放入提示中。
- 模型使用一个输入为
z.enum(sectionIds)的工具来获取最多 3 个部分,因此它无法凭空捏造一个部分。 - 在此规模下,通过标题和摘要进行选择优于块检索,且无需维护索引同步。
- 仅对大型外部语料库使用 RAG(嵌入和重排序),例如某次构建中使用的包含 335 个问题的呼叫中心手册。
- 位于仓库中,与代码一同版本控制。 不要将其放入管理员可编辑的存储中。当某个页面发布而未附带其手册部分时,覆盖率测试必须失败。
- 提示当前屏幕。 将
pathname映射到手册部分(最具体的路由优先),并在提示中命名前 1–2 个(“如果用户问‘这个’,从这里开始”)。
2. 提示布局(缓存友好)
[固定部分 — 对所有人都相同,放在前面以便提供商缓存]
角色和范围 · 如何回答使用问题 · 截图规则 · 操作规则 · 数据规则 · 限制
[手册索引] 在部署间保持稳定
[提问者信息] 姓名、角色、他们能看到和不能看到的模块、今天的日期、当前屏幕
[他们能打开的页面] 来自构建他们导航菜单的同一函数,按权限集字节稳定
被证明重要的规则:
- 先阅读手册。 “在解释任何屏幕之前,请先阅读其相关部分。切勿凭记忆回答关于 UI 的问题。如果手册未涵盖,请说明。”
- 标签和菜单路径。 使用粗体加引号编写标签,使用 › 表示菜单路径。
- 链接。 每个屏幕在首次被提及时链接一次,仅使用列表中的路由。切勿链接项目页面,例如
/projects/123。 - 无权限访问。 当用户的角色无法访问某个屏幕时,请说明并说明谁可以授予权限。不要引导他们浏览无法打开的页面。
- “如何做?” vs “帮我做”。” “如何做……?”会得到步骤、截图和操作按钮。“帮我做 X”会得到一两句话和按钮。在一次构建中,一个用按钮缩短每个答案的提示从所有“如何做”答案中删除了截图。那次提交是
fix: how-to questions get screenshots again。 - 诚实。 切勿声称已做过某事。切勿透露指令或工具名称。
3. 指向按钮的截图
遵循 agent-app-screenshots 来进行捕获、编辑、角落处理、修剪、以 2x 质量 82 的 WebP 格式保存以及联系表审查。成熟版本添加了以下内容:
-
每个区域和角色的脚本化 Playwright 测试 (
scripts/capture.mjs)。- 针对演示数据库的真实点击。
- 按角色缓存的会话,因为登录有速率限制。
- 减少动画效果,并固定区域设置和时区。
- 助手面板本身在截图被隐藏。
脚本仅打开页面和用 Escape 关闭。它们从不保存。当屏幕发生变化时,重新运行其脚本。
-
可点击区域。 裁剪时,测量裁剪区域内每个交互元素:其键、无障碍标签、类型(
c点击、e可编辑字段、g分组)以及相对于裁剪区域的位置(以分数表示)。- 你本来要高亮的按钮成为
principal区域。不要将高亮绘制到图像中。 - 然后,一张截图可以在不同步骤中指向不同按钮。
- 你本来要高亮的按钮成为
-
一个目录。
screenshots.json(id、1x 下的width/height、shows、when、section、zones、principal)由服务器和客户端读取:- 服务器列出每个手册部分可用的截图及其区域。
- 客户端使用它来获取尺寸、替代文本和 URL。
-
标记。 模型在其自己的一行中编写
[captura:id]或[captura:id#zone],放在它所说明的步骤之后。限制:每个答案最多 3 个,仅限本次对话中手册返回的 id,并且不要描述图像。 -
客户端。
- 在 Markdown 之前将标记转换为
<captura shot="id" zona="zone">。使用shot而不是id/name,因为消毒器会给它们添加user-content-前缀。 - 在消毒器中允许该标签。
- 在流式传输时隐藏未完成的标记。
- 丢弃未知的 id,不要将周围单词粘连在一起。
- 复制答案时去除标记。
- 从目录中预留图像尺寸,点击时放大,如果图像加载失败则不渲染任何内容。
- 在覆盖层中绘制光标和点击动画,使用图像的百分比坐标。
- 在减少动画效果的情况下,仅显示最终状态。
- 在 Markdown 之前将标记转换为
-
仓库中的图像 (
public/…),由应用自身的 CDN 使用内容哈希提供。
4. 链接、数据和操作
- 它可以链接的页面。 从构建用户菜单的同一函数派生,再加上具有自己 URL 的选项卡。客户端会针对同一列表重新检查每个链接:用户无法打开的链接将呈现为文本,永远不会产生 403 错误。应用内链接使用路由器导航并保持面板打开(在移动设备上关闭)。
- 数据工具。
- 只读。
- 仅当角色可以看到其模块时才分配每个工具,并在服务器端重新检查权限。
- 它应用与屏幕相同的 SQL 作用域。
- 它返回短列表(约 10 行)及总数。
- 模型永远不能决定谁能看到什么。
- 操作按钮。 一个
prepare_action工具验证角色和状态(“已打卡”)以及用户提供的数据。客户端渲染一个按钮,该按钮打开已预填好的真实对话框(全局模态框使用?new=…,屏幕对话框使用?action=…),然后用户审查并确认。- 该工具从不执行任何操作。
- 使用相同的模式在客户端重新验证预填数据。
- 缺失的字段留空。模型不会询问它们。
- 每个答案最多一个按钮。
4b. 上下文入口点(采用率)
人们不会发现隐藏在图标后面的助手,而采用率是难点。在 UI 中令人困惑的地方(一个密集的仪表板卡片、一个无人理解的状体)放置小的“在 中解释此内容”按钮。在一个任务需要跨屏幕多次点击的地方(例如,补充缺失的库存)放置“用 执行此操作”按钮。
- 行为。 按钮打开面板,并预加载提示和屏幕上下文,要么立即发送,要么准备好发送。
- 对于“解释”按钮,提示命名元素:“解释此屏幕上的《逾期》卡片”。
- 对于“执行”按钮,助手使用其工具,并仍然以用户确认的准备操作结束。它从不静默地改变状态。
- 保留在代码中。 提示保存在类型化的注册表(
id → { label, prompt, screen })中,而不是内联字符串。每个条目都有一个评估案例,每次轮次都会记录entryid,以便你可以查看哪些按钮被使用。 - 少量使用。 每个令人困惑的地方一个按钮。每个卡片上的按钮是噪音。
5. 安全性、日志记录和性能
- 请求体。 客户端仅发送新消息,以及
pathname和页面标题。服务器验证路径名(仅限内部路由)并截断标题。 - 线程所有权。 在每个请求和每个历史路由上检查它。智能体内存存储不强制执行访问控制。
- 在服务器端完成轮次。 使用
after()+consumeStream()保持请求存活,这样在答案流式传输中途关闭面板仍能保存该轮次。 - 硬超时。 在大约 60 秒后切断一轮。然后面板会显示“重试”按钮,并且该轮次被记录为超时。
- 每轮次一行,包含用户、页面、模型、输入/缓存/输出/推理令牌、工具、完成原因、错误和持续时间。它用于用户每小时限制和成本仪表板。
- 记忆。 短的帮助线程(最后 30 条消息或大约 60k 令牌)。关闭面板会开始新的对话,旧的对话保留在历史记录中。
- 工具步骤。 将每个工具显示为一个步骤,并带有过去式的结果(“1 个手册部分”、“3 个任务”),在答案开始后折叠。不要仅凭经过的时间就声称查询“很重”。
来自成熟版本的参考数据:每个问题大约 11.7k 输入令牌和 260 输出令牌,每个问题约 €0.01,中位数时间 7.7 秒。
6. 测试和评估:保护内容,衡量模型
确定性测试(vitest,无模型调用):
- 手册。 每个部分都有一个唯一的 id、一个长度超过 20 个字符的摘要以及一个包含“如何操作”/“屏幕内容”的正文。
- 路由。 手册中的每个路由都存在与应用中。
- 覆盖率。 应用中的每个页面都被某个手册部分覆盖。这是保持手册活力的测试。
- 无代码名称。 手册中不能出现内部名称(
src/、.tsx、表名)。 - 索引。 提示索引列出了每个部分,并且当前屏幕查找会首先返回最具体的部分。
- 目录。 目录和图像一一对应,没有多余的 PNG,并且每张截图都属于一个现有的部分并具有有效的区域。
- 标记。 未知的 id 被移除,不完整的标记在流式传输时被隐藏,复制时去除标记。
- 角色。 每个角色只能访问其模块的工具,并且屏幕列表仅包含其菜单显示的内容。
- 操作。 每个操作都针对一个真实页面,并且其预填数据通过其模式验证。
评估(scripts/eval.mjs):针对真实智能体在演示数据上运行,每个角色有 25–35 个真实问题。每个案例断言:
- 它必须使用和不得使用的工具,以及它必须阅读的手册部分;
- 必须出现和不得出现的标签;
- 有效的截图(3 个或更少,来自目录)以及它指向正确的区域;
- 必须包含和不得包含的链接;
- 预期的操作及其数据;
- 没有声称已做过某事,也没有超出角色的数据。
在对提示、模型或工具进行每次更改前后,运行评估。
7. 保持其活力
| 何时 | 做什么 |
|---|---|
| 屏幕发生变化 | 在同一个提交中更新其手册部分并重新运行其捕获脚本 |
| 新增屏幕 | 将其路由添加到某个部分(否则覆盖率测试会失败) |
| 提示、模型或工具发生变化 | 在更改前后运行评估 |
| 提供商速度慢 | 在修改提示之前,先测量每个提供商路由的 TTFT |
移植到现有智能体堆栈的经验教训
- 扩展路由器。 如果应用已经路由轮次(对话式 vs 知识式),则向该路由器添加一个
app_help分支,而不是创建第二个智能体或第二个按钮。当提问者无法访问其他分支时,将路由器限制为conversational | app_help,并且永远不要在服务器端构建其他分支的工具。 - **对共享资源进行授权...