docs(agent): prompt layering, skill authoring and eval runbook
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
425415b3ba
commit
9962494936
3 files changed
+141
-3
No files matched your search
@@ -0,0 +1,68 @@
|
||||
# AG-5 智能体提示评测:前后对比
|
||||
|
||||
对照样本:`docs/design/agent-report-sample-bad.md`(用户 2026-09-30 贴的反例与期望样子)。实现与跑法见 `docs/runbook/agent.md`「提示分层与 Skill」。
|
||||
|
||||
- **前**:main(0023872)的 `packages/agent/src/system-prompt.ts`,一段 15 条"必须遵守" + 6 条汇报格式 + 工作方式 + 约束里一行"可用能力:…"。
|
||||
- **后**:分层提示(角色与边界 / 当前环境 / 能力地图 / Skill / 行为规则 / 汇报格式 / 本次目标)+ 6 个 Skill + 新能力 `download.list`。
|
||||
|
||||
## 1. 反例的问题逐条对应
|
||||
|
||||
| 反例里的问题 | 前:提示里有什么 | 后:改了什么 |
|
||||
|---|---|---|
|
||||
| 不知道有下载器,汇报"系统里没有它们的登记对象" | 一条规则"下载器…用 downloader.list 查;站点列表里没有下载器不代表没配置",混在 15 条里 | 「当前环境」层直接写出"下载器 1 个:「家里的 qB」qBittorrent,正常(刚刚),本工具管理 3 个任务…""媒体服务器:还没有接入",并说明"还没有接入就是真的没有";能力地图第一条路线就是"问下载器、媒体服务器、站点是否正常:downloader.list → site.list → site.user_data → site.health_check(需要批准)";「巡检」Skill 第 1–2 步 |
|
||||
| 要用户给下载任务 ID / 种子 hash | 没有能列下载任务的能力,只有 `download.inspect {id|hash}` | 新增只读能力 `download.list`;能力地图"问下载 → download.list → download.inspect",提示"不要向用户要 ID 或种子 hash";行为规则"自己能查的不问用户" |
|
||||
| 原始字节数 `upload 17008345370067` | 汇报格式没提单位 | 汇报格式"大小写成 GB / TB,时间写相对时间;不写原始字节数和 ISO 时间";`download.list` 输出直接给 "18.6 GB""63%""3 小时前";环境层也全是人话 |
|
||||
| 表格列 ID / adapter | "不写函数名、工具名、字段名、参数、UUID 或 hash" | 加上 adapter、英文状态码;表格"只放名称和用户关心的 2–3 列" |
|
||||
| "无证据不等于故障"式说教 | 无 | 行为规则"不说教",点名这句 |
|
||||
| 授权缺失写成一段 | "工具返回 unauthorized 时如实报告缺少哪项授权" + 约束"被拒时说明缺少哪项授权"——等于鼓励展开 | "只写一句「没做:<动作>,需要你批准」,界面会给批准按钮;不解释授权机制";Skill 的「需要批准」节给出每个动作的一句话写法 |
|
||||
| 结论"无法确认"、开头"检查结果",整体太长 | "第一句写结论";"不超过 10 行" | "第一句就是结论…不写「检查结果」「结论:」「以下是…」这类开头";≤8 行;异常放前、正常合并一句;每个 Skill 带期望汇报模板(巡检模板就是用户给的期望样子) |
|
||||
|
||||
## 2. 提示体积
|
||||
|
||||
同一套能力(不含浏览器 / 网页搜索 / 子会话)、三条提示,字符数:
|
||||
|
||||
| 提示 | 前 | 后 | 载入的 Skill |
|
||||
|---|---|---|---|
|
||||
| 检查站点、下载器和媒体服务器是否正常 | 2057 | 5516 | 巡检 |
|
||||
| 把《沙丘 2》4K 收进来 | 2052 | 5336 | 收录一部作品 |
|
||||
| 为什么沙丘2没入库 | 2048 | 5400 | 入库 / 整理问题排查 |
|
||||
|
||||
后者多约 3.4k 字符(中文约 2k token),主要是一个 Skill 全文(约 1.5–2k 字符)和环境层。每次只载入命中的 1–2 个 Skill,其余只列一行索引;前者约束里那行"可用能力:(30 多个能力名)"去掉了(工具定义里已有)。
|
||||
|
||||
## 3. fake 模型评测(e16,CI)
|
||||
|
||||
`apps/server/test/e2e/e16-agent-prompt-eval.test.ts`,测试库数据仿反例现场:下载器「家里的 qB」(mock qB,3 个任务:下载中 / 文件缺失 / 已入库做种)、站点「憨憨」(上次检查正常,账号带原始用户数据)、没有媒体服务器、《沙丘2》(追更中订阅 + 1080p / 2160p 两个收录版本、一条 2160p 资源)、4 条已取消订阅、1 条待处理。
|
||||
|
||||
| # | 提示 | 载入的 Skill | 能力调用顺序 | 结果 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 检查站点、下载器和媒体服务器是否正常 | 巡检 | downloader.list → site.list → site.health_check(被拒)→ decision.list | 通过 |
|
||||
| 2 | 把《沙丘 2》4K 收进来 | 收录一部作品 | catalog.lookup → inventory.query → download.list → release.search → download.plan → download.submit(被拒) | 通过 |
|
||||
| 3 | 为什么沙丘2没入库 | 入库 / 整理问题排查 | inventory.query → download.list → download.inspect(ID 来自上一步) | 通过 |
|
||||
| 4 | 订阅追更情况 | 订阅追更检查 | subscription.list | 通过 |
|
||||
| 5 | 有没有失败的下载任务 | 下载排查 | download.list → download.inspect | 通过 |
|
||||
| 6 | 空间不够了,看看能清理什么 | 数据清理与存储 | downloader.list → download.list | 通过 |
|
||||
| 7 | 待处理里有什么要我处理的 | (不载入) | decision.list | 通过 |
|
||||
| 8 | 站点憨憨的登录是不是掉了 | 巡检 | site.list | 通过 |
|
||||
|
||||
每条都断言:系统提示里有「当前环境」的下载器 / 媒体服务器 / 站点事实且不含地址;调用顺序;只读调用全部成功、需要批准的被拒绝;合格汇报(只用工具结果写)过 `lintReport`(无 UUID / hash / 原始字节 / ISO 时间 / 内部字段 / 能力名,≤8 行,开头不是套话)。反例原文过 `lintReport` 报 7 类问题(太长、原始字节、ISO 时间、内部字段、开头不是结论、向用户要 ID、说教),期望样子 0 个问题。
|
||||
|
||||
fake 模型只能证明"按提示的路线走得通、数据够写出合格汇报",不能证明真实模型会这么走,所以还要真实模型对比。
|
||||
|
||||
## 4. 真实模型对比(DeepSeek V4.1 Flash,OpenCode Go)
|
||||
|
||||
**本次没有跑,真跑 0 次。** 任务说明里的凭据文件 `.local/admin-credentials` 只有管理员、TMDB、Brave 搜索与 qB 的条目,没有 OpenCode Go 的 key;用户栈里配置的模型服务按约束不能碰。评测脚手架已就绪:
|
||||
|
||||
- `apps/server/test/e2e/e16-agent-prompt-eval.real.test.ts`:同一套测试库数据,"前"用 main 的 `buildSystemPrompt`(`git show main:packages/agent/src/system-prompt.ts`)+ 旧约束 + 不含 `download.list` 的能力集,"后"用分层提示;缺省跑反例那条 + 收录 + 入库排查共 3 条 × 2 组 = 6 次会话(上限 12)。
|
||||
- 结果写 JSON:每次的能力调用序列(含是否成功)、汇报原文、`lintReport` 结果、行数、token / 费用。
|
||||
- 跑法见 runbook;拿到 key 后执行一次,把结果按下表补进本节:
|
||||
|
||||
| 提示 | 组 | 调用序列 | 行数 | lintReport | 摘要 |
|
||||
|---|---|---|---|---|---|
|
||||
| 检查站点、下载器和媒体服务器是否正常 | 前 | 待跑 | | | |
|
||||
| 同上 | 后 | 待跑 | | | |
|
||||
| 把《沙丘 2》4K 收进来 | 前 | 待跑 | | | |
|
||||
| 同上 | 后 | 待跑 | | | |
|
||||
| 为什么沙丘2没入库 | 前 | 待跑 | | | |
|
||||
| 同上 | 后 | 待跑 | | | |
|
||||
|
||||
看点:前组是否复现"没有下载器 / 要用户给 ID / 原始字节 / 长段授权说明";后组第一句是否是结论、是否 ≤8 行、是否先 `downloader.list`、下载任务 ID 是否来自 `download.list`、被拒是否只写一句。
|
||||
+62
-3
@@ -6,11 +6,11 @@
|
||||
|
||||
## 能力
|
||||
|
||||
启动时装配 39 个能力,注册数不等于 `CAPABILITY_IDS.length` 时进程启动失败。Web(`POST /capabilities/{id}/invoke`)、Pi 工具和后台任务都走 `ctx.capabilities.invoke` 这一条路径。描述符在 `packages/contracts/src/capability.ts`,每条描述都带一句"何时用 / 何时不用"。
|
||||
启动时装配 41 个能力,注册数不等于 `CAPABILITY_IDS.length` 时进程启动失败。Web(`POST /capabilities/{id}/invoke`)、Pi 工具和后台任务都走 `ctx.capabilities.invoke` 这一条路径。描述符在 `packages/contracts/src/capability.ts`,每条描述都带一句"何时用 / 何时不用"。
|
||||
|
||||
| 分组 | 能力 |
|
||||
|---|---|
|
||||
| 查询(read) | `catalog.lookup`、`release.search`、`release.inspect`、`inventory.query`、`subscription.list`、`decision.list`、`download.inspect`、`downloader.list`、`task.inspect`、`site.list`、`site.user_data` |
|
||||
| 查询(read) | `catalog.lookup`、`release.search`、`release.inspect`、`inventory.query`、`subscription.list`、`decision.list`、`download.list`、`download.inspect`、`downloader.list`、`task.inspect`、`site.list`、`site.user_data` |
|
||||
| 计划(preview / propose) | `identity.resolve`、`download.plan`、`organize.plan`、`policy.preview` |
|
||||
| 执行(execute) | `download.submit`、`download.recheck`、`organize.execute`、`task.retry`、`task.cancel`、`policy.apply`、`site.health_check`、`site.refresh_login` |
|
||||
| 原站浏览器 | `browser.snapshot`、`browser.navigate`、`browser.interact`、`browser.download`、`browser.screenshot`、`browser.click_at`、`browser.type_at`、`browser.scroll`、`browser.wait`、`browser.solve_challenge`(见 [browser.md](browser.md)) |
|
||||
@@ -24,6 +24,7 @@ AG-3 补的只读摘要(类别 read,scope `read`,都在后台调查 / 巡检白
|
||||
- `decision.list {status?, kind?, limit≤100}`:待处理事项摘要,缺省只列 open / investigating / awaiting_user,按严重度排序;每条带可选处理 `options`。只读,处理仍由用户在待处理中心选。
|
||||
- `download.inspect {downloadTaskId? | hash?}`:按下载任务 ID 或种子 hash(v1 / v2,大小写均可)读下载任务、关联计划、资源、业务任务(提交下载 / 核验入库,ID 可交给 `task.inspect` / `task.retry`)、整理记录与待处理事项;同 hash 在多个下载器实例时其余 ID 放 `otherMatches`。
|
||||
- `downloader.list {includeDisabled?}`(FIX-7):已配置的下载器与媒体服务器——名称、类型、启用、最近连通状态(`health_status` / `last_seen_at`)、版本,下载器附本工具管理的任务数(总数 / 进行中 / 失败)。只读本地记录,不连下载器,不含地址与凭证。此前智能体只能看 `site.list`(不含下载器),会把"没看到"汇报成"下载器不在系统内"。
|
||||
- `download.list {status?[], query?, mediaEntityId?, includeExternal=false, limit≤50}`(AG-5):列下载任务,附按状态计数(`byStatus[].label` 是界面用词);每条带下载任务 ID(交给 `download.inspect` / `download.recheck`)、名称、作品名、`statusLabel`、`progress`("63%")、`size`("18.6 GB")、下载器名、相对时间(`lastProgress` / `noProgressSince` / `completed`)与错误一句话,不含 hash 与原始字节数。缺省只列本工具管理、未移除的。解决"要用户给下载任务 ID / 种子 hash"(见 `docs/design/agent-report-sample-bad.md`)。
|
||||
- `task.inspect` 只接受业务任务(`task` 表)ID;传入下载任务 ID 时返回 `invalid_input`:"这是下载任务 ID…用 download.inspect 查看"。
|
||||
|
||||
检索词(`catalog.lookup` / `release.search` / `inventory.query` / `subscription.list` 的 `query`)去掉首尾空白后不能为空、不能是单个英文字母 / 数字 / 符号、不能只有通配符与标点(`invalid_input`:"检索词太短…");中文单字片名允许。`catalog.lookup.externalId.externalId` 一律是字符串。
|
||||
@@ -119,7 +120,7 @@ worker 崩溃后,新 worker 在引擎租约到期后(约 30s)接手,`drive` 步
|
||||
|
||||
| 模式 | run 参数 | 能做什么 |
|
||||
|---|---|---|
|
||||
| `investigate`(缺省) | `mode=plan_only`,白名单 = 只读能力 + `identity.resolve` | 查证据、给结论与建议,事项交还人工 |
|
||||
| `investigate`(缺省) | `mode=plan_only`,白名单 = 只读能力(AG-5 起含 `download.list`、`site.list`、`site.user_data`)+ `identity.resolve` | 查证据、给结论与建议,事项交还人工 |
|
||||
| `recover` | `mode=execute`,白名单 = 上面 + `task.retry` + `download.recheck`,`created_by=system:auto-recover` | 在持续授权范围内执行恢复动作,系统核实后自动关闭事项 |
|
||||
|
||||
**恢复范围**:
|
||||
@@ -313,8 +314,66 @@ curl -X POST localhost:4380/api/v1/agent/runs -H "Authorization: Bearer umt-dev-
|
||||
| `e6-auto-recover` | qB missingFiles → download_failed → 授权内恢复(task.inspect → download.recheck → 系统核实关闭事项);无授权时降级只调查;换源被拒;去重;同根因尝试上限;预算超限保持待处理 |
|
||||
| `e11-subagents` | 父会话派生两个只读子会话并等待后汇总;maxConcurrent=1 时串行;子会话超预算不影响父;合计用量;子会话事件进父事件流;越权 / 提权 / 深度 / 关闭被拒;父中止后子会话 cancelled |
|
||||
| `e12-interaction` | 步骤清单先于业务调用、revision 递增;先看方案时执行被拒;方案选择挂起(waiting + await_wake)、选定 / 都不要 / 超时自动选推荐项后续跑;旧 `mode=plan_only` 按先看方案处理;管理员会话发起的会话内审批:批准后重放执行、审批 consumed,拒绝后以理由失败并写待处理事项 |
|
||||
| `e16-agent-prompt-eval` | 提示评测(AG-5):8 条固定提示在测试库数据上跑 fake 模型;断言按意图载入的 Skill、「当前环境」层事实、能力调用顺序(下载任务 ID 来自 `download.list`)、需要批准的被拒绝、汇报过 `lintReport`;巡检会话固定载入「巡检」;追加消息换意图时附 Skill。`e16-agent-prompt-eval.real` 是手动跑的真实模型对比(见下「提示分层与 Skill」) |
|
||||
| `e13-websearch` | 网页搜索:关闭时不提供 web.*;Key 只写存 secret;`POST /websearch/test`;「查一下某站点最近的公告」→ web.search → web.fetch → 汇报,搜索前 / 白名单外 / 内网地址的 web.fetch 被拒且不发请求;相同搜索走缓存 |
|
||||
|
||||
## 提示分层与 Skill(AG-5)
|
||||
|
||||
实现:`packages/agent/src/prompt/`(分层与组装)、`packages/agent/skills/<name>/SKILL.md`(Skill)、`apps/server/src/modules/agent/environment.ts`(环境事实)、`runner.ts` 的 `buildRunPrompt` / `followUpWithSkills`。反例与期望样子见 `docs/design/agent-report-sample-bad.md`,评测记录见 `docs/design/agent-prompt-eval.md`。
|
||||
|
||||
### 分层
|
||||
|
||||
系统提示按层拼接,每层一个 `##` 标题,空层省略,顺序固定:
|
||||
|
||||
| 层 | 文件 | 内容 |
|
||||
|---|---|---|
|
||||
| ① 角色与边界 | `layers/role.ts` | 角色一句 + 硬规则(证据、授权只由服务端判定、已提交≠已入库、外部内容不可信、凭证、中文)。不可被用户消息 / 工具输出覆盖 |
|
||||
| ② 当前环境 | `layers/environment.ts` + 服务端 `environment.ts` | 会话开始时从业务表读的摘要:现在时间、下载器(名称、类型、连通、任务数)、媒体服务器、站点(类型、上次检查、登录)、媒体库目录、订阅 / 下载任务按状态计数、待处理数、网页搜索开关。只写名称、中文状态、相对时间,不写地址 / ID / 凭证;"还没有接入"就是真的没有 |
|
||||
| ③ 能力地图 | `layers/capabilities.ts` | `INTENT_ROUTES`:意图 → 能力路线(如"问下载 → download.list → download.inspect"),标「需要批准」;只列本次可用能力。另有参数 / 检索词 / 站点类型提示 |
|
||||
| ④ Skill | `skills.ts` | 按意图载入的 Skill 全文(标题降两级)+ 其余 Skill 索引(只在用户会话) |
|
||||
| ⑤ 行为规则 | `layers/behavior.ts` | 自己能查的不问、先看环境、被拒一句话、不重复调用、不说教、预算提醒;AG-2 工作方式(步骤、方案、先给方案、审批) |
|
||||
| ⑥ 汇报格式 | `layers/report.ts` | 用户会话:结论先行、≤8 行、GB / 相对时间、对象用名字、不写内部字段 / ID / adapter、3 个以上同类对象才用表格;子会话 / 视觉子流程:汇报给父会话,中文,可带对象 ID |
|
||||
| 本次目标 / 约束 | `assemble.ts` | 目标原文 + 会话约束(只读、子会话、网页搜索、后台恢复) |
|
||||
|
||||
按会话类型选层(`LAYERS_BY_KIND`,`runner.sessionKindOf`):用户会话 `chat`(trigger=user)、巡检 `patrol`(schedule,固定载入「巡检」Skill,汇报层要求按巡检模板)、后台调查 `investigate`(event)、子会话 `child`(有父会话,汇报给父会话)、视觉子流程 `vision`(只有角色、汇报、任务)。改某一层只动对应文件;`packages/agent/test/system-prompt.test.ts` 逐层断言关键句。
|
||||
|
||||
### Skill
|
||||
|
||||
- 位置:`packages/agent/skills/<name>/SKILL.md`,`deno compile` 用 `--include ../../packages/agent/skills` 嵌入二进制。现有:`patrol`(巡检)、`acquire-title`(收录一部作品)、`import-troubleshoot`(入库 / 整理问题排查)、`download-diagnose`(下载排查)、`subscription-check`(订阅追更检查)、`storage-cleanup`(数据清理与存储)。
|
||||
- 加载:Pi harness 的 `loadSkills`(agentskills.io 格式,`name` 必须等于目录名、小写连字符,`description` 必填),进程内缓存;扩展字段本项目自己读。Pi 的模型侧用法是把 `<available_skills>` 索引交给模型、由模型用文件读取工具读正文;我们的智能体没有文件读取工具,所以改为**服务端按意图选**:首轮用目标匹配 `triggers`,命中的全文注入系统提示(最多 2 个,巡检会话固定带 `patrol`,Skill 的能力都不在白名单时不选);追加消息命中新的 Skill 时,用 Pi 的 `formatSkillInvocation` 把 `<skill>` 块附在消息前(会话文件里是附了 Skill 的消息,界面显示用户原话)。没有新增 `skill.load` 能力:MoviePilot(`app/agent/middleware/skills.py`)让模型自己调工具加载 Skill,我们首轮由服务端按目标选,省一次工具调用,也不依赖小模型记得去加载。
|
||||
- frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: patrol # = 目录名
|
||||
title: 巡检 # 中文名,索引与标题用
|
||||
description: 一句话:做什么、用户怎么说时用
|
||||
triggers: [巡检, 是否正常|正常吗] # 正则源串,不区分大小写;命中数多的优先
|
||||
capabilities: [downloader.list, site.list] # 正文提到的能力都要列上
|
||||
sessions: [chat, patrol, investigate, child] # 缺省 chat / patrol / investigate / child
|
||||
---
|
||||
```
|
||||
|
||||
- 正文五节(测试逐个核对):`## 适用意图`、`## 步骤`(按能力写顺序与取舍)、`## 需要批准`(哪些能力、被拒时一句话怎么写)、`## 汇报模板`(`> ` 开头的示例,≤8 行、人话单位,不含 ID / 字节数 / 能力名)、`## 不要做`。文案按 `docs/design/glossary.md` 与 human-copy 规则写:用界面术语、全角标点、不写客服腔。
|
||||
- 核对(`packages/agent/test/skills.test.ts`):加载无诊断、≥5 个、五节齐全、正文提到的能力都在 `capabilities` 且是已知能力、汇报模板示例不带内部字段;**每个能力至少被一个 Skill 引用,或列在 `GENERAL_CAPABILITIES`(通用)**,且都出现在能力地图的路线里或是通用;固定提示的意图路由。新增能力时:加进某个 Skill 的 `capabilities` 与步骤、`INTENT_ROUTES` 的路线,或加进 `GENERAL_CAPABILITIES`。
|
||||
|
||||
### 评测
|
||||
|
||||
- 汇报核对 `lintReport(text)`(`packages/agent/src/prompt/report-lint.ts`):行数 >8、UUID、hash、7 位以上裸数字(原始字节数)、ISO 时间、内部字段(adapter、snake_case、camelCase)、能力名、Markdown 标题、开头不是结论("检查结果""以下…")、向用户要 ID、说教句。反例原文必须不过,期望样子必须过(`report-lint.test.ts`)。
|
||||
- fake 模型:`apps/server/test/e2e/e16-agent-prompt-eval.test.ts`,夹具 `fixtures/agent-eval.ts`(测试库里的一套用户环境 + 8 条固定提示 + 合格模型的调用路线与汇报),随 `deno task test` 跑。
|
||||
- 真实模型(手动,不进 CI,每次最多 12 次调用模型会话):
|
||||
|
||||
```sh
|
||||
git show main:packages/agent/src/system-prompt.ts > /tmp/legacy-system-prompt.ts # "前"= main 的提示
|
||||
cd apps/server && UMT_EVAL_REAL=1 UMT_EVAL_API_KEY=<OpenCode Go key> \
|
||||
UMT_EVAL_PROVIDER=opencode-go UMT_EVAL_MODEL=deepseek-v4.1-flash \
|
||||
UMT_EVAL_LEGACY_PROMPT=/tmp/legacy-system-prompt.ts UMT_EVAL_CASES=health,acquire,why \
|
||||
UMT_EVAL_OUT=/tmp/agent-eval.json DATABASE_URL=postgres://… \
|
||||
deno run -A npm:vitest run test/e2e/e16-agent-prompt-eval.real.test.ts
|
||||
```
|
||||
|
||||
"前"用 main 的 `buildSystemPrompt` + 旧约束,能力集去掉 `download.list`;"后"用分层提示。结果 JSON 每条含能力调用序列、汇报原文、`lintReport` 结果、行数、用量;对比写进 `docs/design/agent-prompt-eval.md`。站点地址是本机关闭端口、下载器是 mock qB,智能体不会请求真实站点。key 只经环境变量传入,不写进代码与报告。
|
||||
|
||||
## 已知限制
|
||||
|
||||
- 编译后的二进制(`deno compile`)只授予 `--allow-read=/app`,没有写权限。Agent 会话目录和入库 Copy/Move 在容器里都需要写权限,需要在 W4 统一补 `--allow-write=<数据目录,库根>`,在此之前生产容器里的 runAgent 会失败。
|
||||
|
||||
Reference in new issue
Block a user