[PRD] arise 运营与质量(决策快照 + 六池成本治理 + 管理命令)—— ADR-0010 完整落地 #77

Closed
opened 2026-07-27 04:45:56 +00:00 by KumaAgent · 0 comments
Member

Problem Statement

从 host 部署者角度:arise 现在已经是一个会自己决定要不要说话、会主动找人、会开后台任务钻研数据、会自我学习调参的智能体,但部署者对它完全没有可见性也没有闸门——不知道它今天花了多少钱(没有任何 token 用量记账),不知道它为什么刚才没回那条消息(决策过程只有 15 处散落的非结构化 logger.debug 文本,事后无从查起),没法在它开始烧钱时让某个能力降级或停下(六池预算/熔断全仓零实现),也没法在运行时调整任何东西(/setmodel//settool//reloadprompt//diagnose//cost//why//why_query 七个管理命令一个都不存在)。装进一个真实、会持续运行数月的部署里,这几件事不是锦上添花,是能不能放心让它跑下去的前提。

从维护者角度:CONTEXT.md/design.md 里"决策快照""成本治理""可解释性""可观测性""管理命令"整节都已经拍板(ADR-0010),Phase 4 PRD(issue #33)当时明确只交付了"决策快照的写入这一个最小必要切片",其余全部推迟。这是 design.md"上线路线"六个阶段里最后一个还没落地的阶段,也是 issue #33 排除清单里最后一项——做完这块,整条路线图第一次全部走完。

从调参角度:ADR-0011 的"阈值三层分类"明确写着标定方法是"保守默认 → 决策快照录制回放"。但决策快照至今不是结构化记录、不落盘、没有查询入口,这条标定路径实际上从来没有真正可用过——所有阈值默认值至今都还是"保守默认"这一半,另一半(按真实数据标定)缺的正是本次要建的基础设施。

Solution

一次性把 ADR-0010 运营面三块收口——它们互为前提,拆开做会让实现期反复回填基础设施(同 issue #33 当年把十条 ADR 一次性收口的理由):

  • 决策快照结构化:把现在散落在各触发路径的非结构化日志,收敛成一份结构化、可持久化、可查询的决策记录,字段覆盖门控各级得分/裁决、注入的记忆条目、情感态三轴、沉默预算余量、命中意图、addressed/skipped_message_ids
  • 成本记账与六池治理:扩展 LLMClient 契约让每次调用回传 token 用量(当前完全丢弃),按能力归入六池(reply/proactive/reflection/multimodal/embedding/delegate)记账,各池独立预算 + 超预算熔断(该能力降级或停),前台回复优先保
  • 管理命令与权限:新增 host 权限 port 回调,在其上建七个管理命令——/cost 查花费、/why 拉最近一次决策解释、/why_query 假设性查询调参、/setmodel/settool/reloadprompt/diagnose

User Stories

部署者视角:成本可见与可控

  1. 作为部署者,我希望能随时查到机器人今天/本月花了多少钱,而不是等账单来了才知道。
  2. 作为部署者,我希望这个花费能按能力拆开看(正常回复花了多少、主动开口花了多少、后台反思花了多少、深挖任务花了多少),而不是只有一个笼统的总数——不然我不知道该关掉哪个功能来省钱。
  3. 作为部署者,我希望能给每个能力单独设预算上限,某个能力超了就让它降级或停下来,而不是整个机器人一起崩。
  4. 作为部署者,我希望预算耗尽时,最后被牺牲的一定是正常回复——后台反思、主动开口、深挖任务都可以停,但用户直接跟机器人说话时它还能应答,是最低限度的体面。
  5. 作为部署者,我希望熔断触发时我能知道(有明确的可观测信号),不是机器人默默变哑巴、我还以为它坏了。
  6. 作为部署者,我希望后台任务/工具调用产生的消耗也被计进对应的池子,不会有一块消耗游离在记账之外。

部署者/用户视角:可解释性

  1. 作为部署者,我希望群友问我"它刚才为什么不理我"时,我能拉出那次的决策记录看到具体原因(是级一没过?预算耗尽?还是模型自己选择了沉默),而不是只能猜。
  2. 作为部署者,我希望这个解释是人能读懂的话,不是一坨要我自己解析的 JSON 或日志行。
  3. 作为部署者,我希望能看到那次决策注入了哪些记忆条目——机器人说错话时,我想知道它当时"记得"什么。
  4. 作为一个跟机器人私聊过敏感话题的普通用户,我不希望群管理员通过 /why 就能看到我的私人记忆内容——"能管这个机器人"和"有权看某个人的记忆"是两码事,管理员身份不该成为绕过敏感度分桶/知情-gate 的后门。
  5. 作为部署者,我希望即使我自己是管理员,/why 给我看的也是经过敏感度过滤后的内容——我要排查的是"它当时为什么这么决定",不需要、也不应该顺带拿到别人的私密信息。
  6. 作为部署者,我希望在调参时能做假设性查询——给定一个模拟的查询和情感坐标,看看候选记忆各维度分别打了多少分(recency/importance/relevance/mood_congruence),而不是只能回看真实发生过的那一次。
  7. 作为部署者,我希望 /why_query 这类调参工具自己产生的消耗(它要算 relevance 就得真的 embed 一次查询)同样被记进池子——调参工具不能是记账体系的例外,否则"今天花了多少钱"这个数就是不准的。
  8. 作为部署者,我希望能查到一次决策当时的情感态三轴数值和沉默预算余量,这两个是最常见的"为什么它今天这么安静"的答案来源。
  9. 作为维护者,我希望这些决策记录能被录制下来回放到测试里,让 ADR-0011 说的"保守默认→录制回放标定"这条阈值标定路径第一次真正可用。

部署者视角:运行时管理

  1. 作为部署者,我希望能在运行时切换机器人用的模型,不用改配置重启整个 bot。
  2. 作为部署者,我希望能按 chat 开关某些工具——某个群不想让它用某个 host 工具时,不需要为此改全局配置。
  3. 作为部署者,我希望改完 persona 之后能让它重新加载,不用重启。
  4. 作为部署者,我希望有一个自检命令,能一次看清楚各个依赖(模型/数据库/向量库)现在通不通,排障时不用一个个手动试。
  5. 作为部署者,我希望这些管理命令只有我(或我授权的人)能用,不是群里随便谁都能改机器人的模型和工具开关。
  6. 作为部署者,我希望"谁算管理员"这件事由我的 host 决定——我可能想让群主管自己的群,也可能只想让我自己管全部,core 不该替我把这个规则写死。
  7. 作为部署者,我希望非管理员误触这些命令时,机器人的反应是明确拒绝而不是装作没听见,也不该泄漏任何本来只有管理员看得到的信息。

维护者/贡献者视角:可测性

  1. 作为维护者,我希望"这次调用该记进哪个池""这个池现在超预算了吗"这类判断是纯函数,可以直接构造固定输入断言,不需要真实 LLM 调用或真实时钟。
  2. 作为维护者,我希望决策快照的渲染(结构化记录→人类可读解释)是纯函数,可以直接断言输出文本。
  3. 作为维护者,我希望权限校验是一个可注入的回调,测试里可以直接塞一个"永远返回真/假"的假实现,不需要真的构造平台管理员身份。
  4. 作为维护者,我希望管理命令的处理逻辑能脱离真实 NoneBot 事件派发直接测试,同仓库既有命令处理的测试方式一致。

验收标准视角

  1. 作为验收标准,我希望六个池的记账互不串——某一路径的消耗只出现在它自己的池里。
  2. 作为验收标准,我希望某个池熔断后,该能力确实不再发起 LLM 调用(不是发了再丢弃结果)。
  3. 作为验收标准,我希望前台回复路径在其余五池全部熔断的情况下依然可用。
  4. 作为验收标准,我希望每一条触发路径(反应式/Drive Tick/tier-2 环境信号/即刻追问/Callback 送回)产生的决策都写快照,不存在某条路径静默跳过记录。
  5. 作为验收标准,我希望权限校验失败时不产生任何状态变更,也不产生 LLM 调用。
  6. 作为验收标准,我希望决策快照里存的是记忆条目的标识而不是内容副本——快照不该成为一份绕过既有敏感度治理的记忆影子拷贝,也不该在条目本身被更新/删除后还留着过期的内容快照。
  7. 作为验收标准,我希望 /why 渲染记忆条目时按"命令是在哪个 chat 发起的"重新过一遍既有可见性闸——决策发生时对那个 chat 可见,不等于现在对发起 /why 的这个 chat 可见。
  8. 作为验收标准,我希望 /why_query 的 embedding 消耗确实出现在 embedding 池的记账里;embedding 池已熔断时,/why_query 明确失败而不是绕过熔断照常调用。

Implementation Decisions

决策快照结构化

  • 新增结构化决策快照记录(存储 port 新切片,持久化)——字段覆盖:chat_id、触发路径类型、时间、门控各级得分/阈值/裁决(既有 GateEvaluation 已经完整产出这些,目前只是被格式化成日志文本丢掉)、情感态三轴、沉默预算余量、命中的未决意图、注入的记忆条目标识、addressed_message_ids/skipped_message_ids
  • 取代现有散落的 15 处 logger.debug 决策日志——日志本身不必全删(保留作运行时可观测性),但"事后能查"这个能力由结构化记录承担,不再依赖日志文本解析。
  • 保留策略:新增静态 config 控制保留条数/时长(每次触发都写一条,无上限会无限增长)。具体数字按 ADR-0011 惯例留实现期标定。
  • 渲染为人类可读解释是纯函数(结构化记录 → 文本),/why 只负责取最近一条记录 + 调用这个渲染函数。

记忆条目只存标识 + 渲染时过敏感闸(隐私边界,不可省略)

  • 快照只存记忆条目的标识,不存内容副本。两个理由:① 内容副本等于一份绕过既有敏感度分桶/知情-gate 治理的记忆影子拷贝;② 条目本身被更新/删除后,快照里的内容副本会变成谁也管不到的过期残留。
  • /why 渲染记忆条目时,按"命令是在哪个 chat 发起的"重新过一遍既有的可见性闸events.is_event_visible(event, current_chat_id=<发起 /why 的 chat>),画像/Knowledge 等其它记忆类型同理走各自既有的同一套规则),不可见的条目降级为"有 N 条不可见条目参与了这次决策"之类的计数占位,不泄漏内容。
  • 理由:管理员身份 ≠ 记忆主人。 现有可见性闸是按 chat 作用域判定的(source_ctx == current_chat_id,或私聊场景下当事人是知情参与者),它从来不是一个"谁有权看"的授权模型。决策发生时对 chat A 可见,不等于现在对发起 /why 的 chat B 可见;即便同一个 chat,"当时注入进模型上下文"与"现在打印给管理员看"也是两件不同的事,中间还隔着条目敏感度可能已被更新的时间差。is_admin 授予的是"能不能管这个机器人",不该顺带成为绕过敏感度治理的后门。
  • 这与 PR #76 修掉的"隐私外送"是同一形状的坑(那次是把用户全部画像事实——含住址/健康/财务——拼进任务描述交给 host 抓取工具,已改为只带非敏感事实)。同一个错误不该在 /why 这条新出口上再犯一次。

成本记账与六池治理

  • 扩展 LLMClient 契约携带 token 用量AssistantTurn 目前只有 content/tool_callsAnyLLMClient.complete() 拿到 any-llm 响应后直接丢弃了 usage——这是六池治理绕不开的地基,必须先补上。新增用量字段(输入/输出 token),所有既有 LLMClient 实现(含测试用的 ScriptedLLMClient)随之适配;embedding 客户端同理。
  • 六池:reply / proactive / reflection / multimodal / embedding / delegate。归池依据是调用发生在哪条路径,不是调用了什么模型——路径信息在调用点是已知的(RuntimeContext 及各调度入口已经区分得很清楚)。host 工具产生的消耗不新开池,记进调用它的那条路径已有的池(ADR-0019 既定)。
  • 记账:按池 + 时间维度累计(支撑"今日/本月已花"查询)。token→货币的换算取决于具体 provider/模型定价,定价表由 config 声明(core 不内置任何 provider 价格表,也不联网查价)。
  • 预算与熔断:每池独立预算上限(静态 config),超预算即该能力降级或停——熔断检查在发起调用之前,不是发了再丢弃结果(AC25)。判断"该不该熔断"是纯函数(当前累计 vs 预算),可直接单测。
  • 前台回复优先保:reply 池的熔断行为与其余五池不同——其余池熔断即停,reply 池即使超预算也不停(可以降级,但不能让"用户说话它不应"成为省钱手段)。这条是 ADR-0010 的明确要求,也是本块唯一不可省略的行为约束。

管理命令与权限

  • 新增 host 权限 port 回调is_admin(chat_id, user_id) -> bool(同 get_persona(chat_id)/get_tools(context) 既有回调先例,core 不认识群主/管理员/superuser 这些平台概念,也不替 host 决定"谁算管理员")。挂进 ArisePorts
  • 既有的用户级同意命令不受影响:跨平台拉取的两个同意命令(允许了解我的动态/不再了解我的动态)是用户级授权(内容所有者本人同意),刻意不走管理命令的权限校验体系——ADR-0027 明确拒绝混用这两个授权维度。本次新增的权限校验只覆盖下述七个管理命令,不碰那两个。
  • 七个管理命令,全部经 is_admin 校验,校验失败明确拒绝(不静默忽略、不泄漏管理员才能看到的信息):
    • /cost:查今日/本月各池已花费。
    • /why:拉最近一次决策快照的可读解释。记忆条目按上述"渲染时过敏感闸"规则处理。
    • /why_query:假设性查询——给定模拟的 (query, 情感坐标),展示候选记忆的分维度得分明细(recency/importance/relevance/mood_congruence)。区别于 /why 只能回看真实发生过的一轮。候选数设静态上限。记忆条目同样过敏感闸(这条路径直接搜全库,比 /why 更需要,/why 至少还受限于"当时真的注入过")。
      • /why_query 自己的 embedding 消耗必须记进 embedding:算 relevance 就要真的 embed 一次模拟查询,这是真实的 provider 调用,不是纯本地计算。调参工具不是记账体系的例外——否则"今天花了多少钱"这个数本身就不准(呼应 user story 6/13)。
      • 同理受 embedding 池的熔断约束:该池已熔断时 /why_query 明确失败并说明原因,不绕过熔断照常调用。这是"熔断在发起调用之前"这条通用规则的直接推论,不是本命令的特例。
    • /setmodel:运行时切换模型。
    • /settool:per-chat 工具开关。
    • /reloadprompt:重新加载 persona。
    • /diagnose:依赖连通性自检(模型/数据库/向量库)。
  • 命令匹配沿用既有 on_command(..., rule=to_me(), priority=1, block=True) 形状(COMMAND_START 含空串时裸文本会命中,to_me() 收掉误触面——这个先例已经在跨平台拉取同意命令上验证过)。

明确的范围边界

  • 不新增 web/HTTP 面板——/why_query 等一律是聊天命令形态(ADR-0010 更新节既定)。
  • 不做跨表级联删除式的数据治理(ADR-0010 已明确 YAGNI 拒绝)。

Testing Decisions

好的测试只测外部可观察行为(记账结果、熔断是否真的挡住调用、命令的可见效果、渲染输出),不测内部实现细节。

  • 纯函数:归池判断、熔断判断(当前累计 vs 预算,含 reply 池的特殊行为)、决策快照渲染(结构化→文本)、/why_query 的分维度得分明细计算(复用既有 recall.py 打分纯函数,本次只测明细展开)。
  • 存储契约:决策快照 CRUD + 保留策略、成本记账累计(按池/按时间维度),仿既有存储契约测试模式(内存版 + 持久化版共用同一套断言)。
  • 熔断有效性:断言熔断后该能力确实没有发起 LLM 调用(用脚本化假 LLM 客户端的调用计数断言,不是断言"结果被丢弃")——这是 AC25 的直接验证,也是最容易写成空转测试的一处:如果只断言"没有发消息",模型自己选择沉默的路径同样满足,必须锁定调用计数。
  • 前台优先保:构造其余五池全部熔断的场景,断言反应式回复路径依然完整可用(AC26)。
  • 快照覆盖完整性:逐条触发路径(反应式/Drive Tick/tier-2 环境信号/即刻追问/Callback 送回)断言各自都写了快照(AC27)——这条建议用变异测试验一次(把某条路径的写入删掉,确认对应测试真的会挂),防止"看起来覆盖了但其实断言恒成立"。
  • 权限校验:注入假 is_admin 实现(恒真/恒假),断言恒假时命令无状态变更、无 LLM 调用(AC31),且拒绝信息不泄漏管理员才可见的内容。
  • 管理命令处理:脱离真实 NoneBot 事件派发直接测处理逻辑,仿既有命令测试模式。
  • /why 敏感闸(AC32/AC33):构造"决策发生在 chat A、/why 从 chat B 发起、注入过的条目里有对 A 可见但对 B 不可见的敏感条目"这个场景,断言渲染输出不含该条目内容。这条最容易写成空转测试——如果只断言"输出里没有那段敏感文本",在渲染压根没实现条目展示的情况下同样成立;必须同时有反向对照(非敏感条目/同 chat 场景必须正常渲染出来),并建议做一次变异验证(把敏感闸那行删掉,确认测试真的会挂)。延续 issue #69/#70 两次被抓到同型问题的教训:断言"什么都没发生"时,通过原因往往不止一个
  • /why_query 记账与熔断(AC34):断言跑一次 /why_query 后 embedding 池累计确实增加;embedding 池预设为已熔断时,断言假 embedding 客户端的调用计数为零(不是断言"没有输出结果"——那在多种原因下都成立,同上)。

Out of Scope

  • web/HTTP 管理面板:ADR-0010 更新节已明确 /why_query 等只做聊天命令形态,不新增 web 交互形态。
  • GDPR 式跨表级联删除:ADR-0010 已明确 YAGNI 拒绝("忘记我"仅清短期,长期靠自然遗忘曲线),本次不重新评估。
  • 按任务难度自动路由模型的 router:ADR-0020 已明确搁置("是一整套新的 Routing 层设计,超出范围,留待未来专门开一轮 grill")。/setmodel 只做运行时手动切换,不做自动路由。
  • 模型自主选择自己用什么模型:ADR-0020 已明确否决(LLM 不该直接支配成本/基础设施决策),本次不重新评估。
  • 深挖任务委托的"灰度可一键关"缺口issue #61):独立跟踪,不在本次范围。
  • MediaKind"file" 第四态:ADR-0028/CONTEXT.md 已决定但从未写进代码(见 issue #66 Further Notes 留痕),与本 PRD 无关,不顺手夹带。
  • 各项预算/保留策略的具体数字:按 ADR-0011 既定原则("具体数字按 QQ 场景实测标定,禁用 Hermes/OpenClaw 数字")留实现期,本 PRD 只钉死机制形状。

Further Notes

  • 本 PRD 完成后,docs/design.md"上线路线"六个阶段将首次全部落地。届时值得做一次全文档核实(同深挖任务委托那轮的做法),确认路线图各阶段的承诺与实际实现逐条对得上——历史上已经出现过至少两处"文档说有、代码没有"的落差(深挖任务委托的"灰度可一键关"、MediaKind.file),全部走完之后正是系统性核对一遍的时机。
  • /why 的敏感闸这条约束是维护者在 PRD 评审时提出的,不是从 ADR-0010 原文推出来的——ADR-0010 写"决策快照记录注入了哪些记忆条目"时,隐含假设是"给系统自己看/给写代码的人看",没有考虑过它会经由一个管理员可见的聊天命令出口暴露出去。这是"把一个内部记录接上一条新的对外出口"时容易整类漏掉的问题:原记录的可见性假设未必还成立。未来若再给决策快照加新出口(比如导出、web 面板——虽然后者已被明确排除),需要重新做一次同样的检查,不能假设"快照里已经有的字段就是可以随便给出去的"。
  • 成本记账这块有一个隐含的契约扩展面:LLMClient/EmbeddingClient 增加用量字段会影响所有既有实现和测试替身。这个改动本身很机械,但触及面广,实现期建议先单独把契约扩展做掉、确认全仓测试仍绿,再在其上做记账/预算逻辑——而不是混在一个 commit 里,否则一旦出问题很难区分是契约适配漏了还是记账逻辑错了。
  • ADR-0011"阈值三层分类"说的标定方法(保守默认→决策快照录制回放)在本次之前从未真正可用。本次交付后,此前所有 PRD 里"留实现期按真实数据标定"的数字第一次具备了被真正标定的条件——这可能是本次交付最有长期价值的副产物,但标定工作本身是持续运营行为,不属于本 PRD 的交付物。
## Problem Statement 从 host 部署者角度:arise 现在已经是一个会自己决定要不要说话、会主动找人、会开后台任务钻研数据、会自我学习调参的智能体,但部署者对它**完全没有可见性也没有闸门**——不知道它今天花了多少钱(没有任何 token 用量记账),不知道它为什么刚才没回那条消息(决策过程只有 15 处散落的非结构化 `logger.debug` 文本,事后无从查起),没法在它开始烧钱时让某个能力降级或停下(六池预算/熔断全仓零实现),也没法在运行时调整任何东西(`/setmodel`/`/settool`/`/reloadprompt`/`/diagnose`/`/cost`/`/why`/`/why_query` 七个管理命令一个都不存在)。装进一个真实、会持续运行数月的部署里,这几件事不是锦上添花,是能不能放心让它跑下去的前提。 从维护者角度:CONTEXT.md/design.md 里"决策快照""成本治理""可解释性""可观测性""管理命令"整节都已经拍板([ADR-0010](../docs/adr/0010-operations.md)),Phase 4 PRD(issue #33)当时明确只交付了"决策快照的写入这一个最小必要切片",其余全部推迟。这是 design.md"上线路线"六个阶段里**最后一个还没落地的阶段**,也是 issue #33 排除清单里最后一项——做完这块,整条路线图第一次全部走完。 从调参角度:ADR-0011 的"阈值三层分类"明确写着标定方法是"保守默认 → 决策快照录制回放"。但决策快照至今不是结构化记录、不落盘、没有查询入口,这条标定路径实际上从来没有真正可用过——所有阈值默认值至今都还是"保守默认"这一半,另一半(按真实数据标定)缺的正是本次要建的基础设施。 ## Solution 一次性把 ADR-0010 运营面三块收口——它们互为前提,拆开做会让实现期反复回填基础设施(同 issue #33 当年把十条 ADR 一次性收口的理由): - **决策快照结构化**:把现在散落在各触发路径的非结构化日志,收敛成一份结构化、可持久化、可查询的决策记录,字段覆盖门控各级得分/裁决、注入的记忆条目、情感态三轴、沉默预算余量、命中意图、`addressed`/`skipped_message_ids`。 - **成本记账与六池治理**:扩展 `LLMClient` 契约让每次调用回传 token 用量(当前完全丢弃),按能力归入六池(reply/proactive/reflection/multimodal/embedding/delegate)记账,各池独立预算 + 超预算熔断(该能力降级或停),**前台回复优先保**。 - **管理命令与权限**:新增 host 权限 port 回调,在其上建七个管理命令——`/cost` 查花费、`/why` 拉最近一次决策解释、`/why_query` 假设性查询调参、`/setmodel`、`/settool`、`/reloadprompt`、`/diagnose`。 ## User Stories **部署者视角:成本可见与可控** 1. 作为部署者,我希望能随时查到机器人今天/本月花了多少钱,而不是等账单来了才知道。 2. 作为部署者,我希望这个花费能按能力拆开看(正常回复花了多少、主动开口花了多少、后台反思花了多少、深挖任务花了多少),而不是只有一个笼统的总数——不然我不知道该关掉哪个功能来省钱。 3. 作为部署者,我希望能给每个能力单独设预算上限,某个能力超了就让它降级或停下来,而不是整个机器人一起崩。 4. 作为部署者,我希望预算耗尽时,**最后被牺牲的一定是正常回复**——后台反思、主动开口、深挖任务都可以停,但用户直接跟机器人说话时它还能应答,是最低限度的体面。 5. 作为部署者,我希望熔断触发时我能知道(有明确的可观测信号),不是机器人默默变哑巴、我还以为它坏了。 6. 作为部署者,我希望后台任务/工具调用产生的消耗也被计进对应的池子,不会有一块消耗游离在记账之外。 **部署者/用户视角:可解释性** 7. 作为部署者,我希望群友问我"它刚才为什么不理我"时,我能拉出那次的决策记录看到具体原因(是级一没过?预算耗尽?还是模型自己选择了沉默),而不是只能猜。 8. 作为部署者,我希望这个解释是人能读懂的话,不是一坨要我自己解析的 JSON 或日志行。 9. 作为部署者,我希望能看到那次决策**注入了哪些记忆条目**——机器人说错话时,我想知道它当时"记得"什么。 10. **作为一个跟机器人私聊过敏感话题的普通用户,我不希望群管理员通过 `/why` 就能看到我的私人记忆内容——"能管这个机器人"和"有权看某个人的记忆"是两码事,管理员身份不该成为绕过敏感度分桶/知情-gate 的后门。** 11. **作为部署者,我希望即使我自己是管理员,`/why` 给我看的也是经过敏感度过滤后的内容——我要排查的是"它当时为什么这么决定",不需要、也不应该顺带拿到别人的私密信息。** 12. 作为部署者,我希望在调参时能做假设性查询——给定一个模拟的查询和情感坐标,看看候选记忆各维度分别打了多少分(recency/importance/relevance/mood_congruence),而不是只能回看真实发生过的那一次。 13. **作为部署者,我希望 `/why_query` 这类调参工具自己产生的消耗(它要算 relevance 就得真的 embed 一次查询)同样被记进池子——调参工具不能是记账体系的例外,否则"今天花了多少钱"这个数就是不准的。** 14. 作为部署者,我希望能查到一次决策当时的情感态三轴数值和沉默预算余量,这两个是最常见的"为什么它今天这么安静"的答案来源。 15. 作为维护者,我希望这些决策记录能被录制下来回放到测试里,让 ADR-0011 说的"保守默认→录制回放标定"这条阈值标定路径第一次真正可用。 **部署者视角:运行时管理** 16. 作为部署者,我希望能在运行时切换机器人用的模型,不用改配置重启整个 bot。 17. 作为部署者,我希望能按 chat 开关某些工具——某个群不想让它用某个 host 工具时,不需要为此改全局配置。 18. 作为部署者,我希望改完 persona 之后能让它重新加载,不用重启。 19. 作为部署者,我希望有一个自检命令,能一次看清楚各个依赖(模型/数据库/向量库)现在通不通,排障时不用一个个手动试。 20. 作为部署者,我希望这些管理命令只有我(或我授权的人)能用,不是群里随便谁都能改机器人的模型和工具开关。 21. 作为部署者,我希望"谁算管理员"这件事由我的 host 决定——我可能想让群主管自己的群,也可能只想让我自己管全部,core 不该替我把这个规则写死。 22. 作为部署者,我希望非管理员误触这些命令时,机器人的反应是明确拒绝而不是装作没听见,也不该泄漏任何本来只有管理员看得到的信息。 **维护者/贡献者视角:可测性** 23. 作为维护者,我希望"这次调用该记进哪个池""这个池现在超预算了吗"这类判断是纯函数,可以直接构造固定输入断言,不需要真实 LLM 调用或真实时钟。 24. 作为维护者,我希望决策快照的渲染(结构化记录→人类可读解释)是纯函数,可以直接断言输出文本。 25. 作为维护者,我希望权限校验是一个可注入的回调,测试里可以直接塞一个"永远返回真/假"的假实现,不需要真的构造平台管理员身份。 26. 作为维护者,我希望管理命令的处理逻辑能脱离真实 NoneBot 事件派发直接测试,同仓库既有命令处理的测试方式一致。 **验收标准视角** 27. 作为验收标准,我希望六个池的记账互不串——某一路径的消耗只出现在它自己的池里。 28. 作为验收标准,我希望某个池熔断后,该能力确实不再发起 LLM 调用(不是发了再丢弃结果)。 29. 作为验收标准,我希望前台回复路径在其余五池全部熔断的情况下依然可用。 30. 作为验收标准,我希望每一条触发路径(反应式/Drive Tick/tier-2 环境信号/即刻追问/Callback 送回)产生的决策都写快照,不存在某条路径静默跳过记录。 31. 作为验收标准,我希望权限校验失败时不产生任何状态变更,也不产生 LLM 调用。 32. **作为验收标准,我希望决策快照里存的是记忆条目的标识而不是内容副本——快照不该成为一份绕过既有敏感度治理的记忆影子拷贝,也不该在条目本身被更新/删除后还留着过期的内容快照。** 33. **作为验收标准,我希望 `/why` 渲染记忆条目时按"命令是在哪个 chat 发起的"重新过一遍既有可见性闸——决策发生时对那个 chat 可见,不等于现在对发起 `/why` 的这个 chat 可见。** 34. **作为验收标准,我希望 `/why_query` 的 embedding 消耗确实出现在 embedding 池的记账里;embedding 池已熔断时,`/why_query` 明确失败而不是绕过熔断照常调用。** ## Implementation Decisions ### 决策快照结构化 - 新增结构化决策快照记录(存储 port 新切片,持久化)——字段覆盖:`chat_id`、触发路径类型、时间、门控各级得分/阈值/裁决(既有 `GateEvaluation` 已经完整产出这些,目前只是被格式化成日志文本丢掉)、情感态三轴、沉默预算余量、命中的未决意图、注入的记忆条目标识、`addressed_message_ids`/`skipped_message_ids`。 - 取代现有散落的 15 处 `logger.debug` 决策日志——**日志本身不必全删**(保留作运行时可观测性),但"事后能查"这个能力由结构化记录承担,不再依赖日志文本解析。 - 保留策略:新增静态 config 控制保留条数/时长(每次触发都写一条,无上限会无限增长)。具体数字按 ADR-0011 惯例留实现期标定。 - 渲染为人类可读解释是**纯函数**(结构化记录 → 文本),`/why` 只负责取最近一条记录 + 调用这个渲染函数。 #### 记忆条目只存标识 + 渲染时过敏感闸(隐私边界,不可省略) - **快照只存记忆条目的标识,不存内容副本**。两个理由:① 内容副本等于一份绕过既有敏感度分桶/知情-gate 治理的记忆影子拷贝;② 条目本身被更新/删除后,快照里的内容副本会变成谁也管不到的过期残留。 - **`/why` 渲染记忆条目时,按"命令是在哪个 chat 发起的"重新过一遍既有的可见性闸**(`events.is_event_visible(event, current_chat_id=<发起 /why 的 chat>)`,画像/Knowledge 等其它记忆类型同理走各自既有的同一套规则),不可见的条目降级为"有 N 条不可见条目参与了这次决策"之类的计数占位,不泄漏内容。 - **理由:管理员身份 ≠ 记忆主人。** 现有可见性闸是**按 chat 作用域**判定的(`source_ctx == current_chat_id`,或私聊场景下当事人是知情参与者),它从来不是一个"谁有权看"的授权模型。决策发生时对 chat A 可见,不等于现在对发起 `/why` 的 chat B 可见;即便同一个 chat,"当时注入进模型上下文"与"现在打印给管理员看"也是两件不同的事,中间还隔着条目敏感度可能已被更新的时间差。`is_admin` 授予的是"能不能管这个机器人",不该顺带成为绕过敏感度治理的后门。 - **这与 [PR #76](https://code.srcz.one/ProjectKuma/arise/pulls/76) 修掉的"隐私外送"是同一形状的坑**(那次是把用户全部画像事实——含住址/健康/财务——拼进任务描述交给 host 抓取工具,已改为只带非敏感事实)。同一个错误不该在 `/why` 这条新出口上再犯一次。 ### 成本记账与六池治理 - **扩展 `LLMClient` 契约携带 token 用量**:`AssistantTurn` 目前只有 `content`/`tool_calls`,`AnyLLMClient.complete()` 拿到 any-llm 响应后直接丢弃了 usage——这是六池治理绕不开的地基,必须先补上。新增用量字段(输入/输出 token),所有既有 `LLMClient` 实现(含测试用的 `ScriptedLLMClient`)随之适配;embedding 客户端同理。 - **六池**:reply / proactive / reflection / multimodal / embedding / delegate。归池依据是**调用发生在哪条路径**,不是调用了什么模型——路径信息在调用点是已知的(`RuntimeContext` 及各调度入口已经区分得很清楚)。host 工具产生的消耗不新开池,记进调用它的那条路径已有的池(ADR-0019 既定)。 - **记账**:按池 + 时间维度累计(支撑"今日/本月已花"查询)。token→货币的换算取决于具体 provider/模型定价,定价表由 config 声明(core 不内置任何 provider 价格表,也不联网查价)。 - **预算与熔断**:每池独立预算上限(静态 config),超预算即该能力降级或停——**熔断检查在发起调用之前**,不是发了再丢弃结果(AC25)。判断"该不该熔断"是纯函数(当前累计 vs 预算),可直接单测。 - **前台回复优先保**:reply 池的熔断行为与其余五池不同——其余池熔断即停,reply 池即使超预算也不停(可以降级,但不能让"用户说话它不应"成为省钱手段)。这条是 ADR-0010 的明确要求,也是本块唯一不可省略的行为约束。 ### 管理命令与权限 - **新增 host 权限 port 回调**:`is_admin(chat_id, user_id) -> bool`(同 `get_persona(chat_id)`/`get_tools(context)` 既有回调先例,core 不认识群主/管理员/superuser 这些平台概念,也不替 host 决定"谁算管理员")。挂进 `ArisePorts`。 - **既有的用户级同意命令不受影响**:跨平台拉取的两个同意命令(`允许了解我的动态`/`不再了解我的动态`)是**用户级**授权(内容所有者本人同意),刻意不走管理命令的权限校验体系——ADR-0027 明确拒绝混用这两个授权维度。本次新增的权限校验只覆盖下述七个管理命令,不碰那两个。 - **七个管理命令**,全部经 `is_admin` 校验,校验失败明确拒绝(不静默忽略、不泄漏管理员才能看到的信息): - `/cost`:查今日/本月各池已花费。 - `/why`:拉最近一次决策快照的可读解释。记忆条目按上述"渲染时过敏感闸"规则处理。 - `/why_query`:假设性查询——给定模拟的 `(query, 情感坐标)`,展示候选记忆的分维度得分明细(recency/importance/relevance/mood_congruence)。区别于 `/why` 只能回看真实发生过的一轮。候选数设静态上限。**记忆条目同样过敏感闸**(这条路径直接搜全库,比 `/why` 更需要,`/why` 至少还受限于"当时真的注入过")。 - **`/why_query` 自己的 embedding 消耗必须记进 `embedding` 池**:算 `relevance` 就要真的 embed 一次模拟查询,这是真实的 provider 调用,不是纯本地计算。调参工具不是记账体系的例外——否则"今天花了多少钱"这个数本身就不准(呼应 user story 6/13)。 - 同理受 embedding 池的**熔断**约束:该池已熔断时 `/why_query` 明确失败并说明原因,不绕过熔断照常调用。这是"熔断在发起调用之前"这条通用规则的直接推论,不是本命令的特例。 - `/setmodel`:运行时切换模型。 - `/settool`:per-chat 工具开关。 - `/reloadprompt`:重新加载 persona。 - `/diagnose`:依赖连通性自检(模型/数据库/向量库)。 - 命令匹配沿用既有 `on_command(..., rule=to_me(), priority=1, block=True)` 形状(`COMMAND_START` 含空串时裸文本会命中,`to_me()` 收掉误触面——这个先例已经在跨平台拉取同意命令上验证过)。 ### 明确的范围边界 - 不新增 web/HTTP 面板——`/why_query` 等一律是聊天命令形态(ADR-0010 更新节既定)。 - 不做跨表级联删除式的数据治理(ADR-0010 已明确 YAGNI 拒绝)。 ## Testing Decisions 好的测试只测外部可观察行为(记账结果、熔断是否真的挡住调用、命令的可见效果、渲染输出),不测内部实现细节。 - **纯函数**:归池判断、熔断判断(当前累计 vs 预算,含 reply 池的特殊行为)、决策快照渲染(结构化→文本)、`/why_query` 的分维度得分明细计算(复用既有 `recall.py` 打分纯函数,本次只测明细展开)。 - **存储契约**:决策快照 CRUD + 保留策略、成本记账累计(按池/按时间维度),仿既有存储契约测试模式(内存版 + 持久化版共用同一套断言)。 - **熔断有效性**:断言熔断后该能力**确实没有发起 LLM 调用**(用脚本化假 LLM 客户端的调用计数断言,不是断言"结果被丢弃")——这是 AC25 的直接验证,也是最容易写成空转测试的一处:如果只断言"没有发消息",模型自己选择沉默的路径同样满足,必须锁定调用计数。 - **前台优先保**:构造其余五池全部熔断的场景,断言反应式回复路径依然完整可用(AC26)。 - **快照覆盖完整性**:逐条触发路径(反应式/Drive Tick/tier-2 环境信号/即刻追问/Callback 送回)断言各自都写了快照(AC27)——这条建议用变异测试验一次(把某条路径的写入删掉,确认对应测试真的会挂),防止"看起来覆盖了但其实断言恒成立"。 - **权限校验**:注入假 `is_admin` 实现(恒真/恒假),断言恒假时命令无状态变更、无 LLM 调用(AC31),且拒绝信息不泄漏管理员才可见的内容。 - **管理命令处理**:脱离真实 NoneBot 事件派发直接测处理逻辑,仿既有命令测试模式。 - **`/why` 敏感闸(AC32/AC33)**:构造"决策发生在 chat A、`/why` 从 chat B 发起、注入过的条目里有对 A 可见但对 B 不可见的敏感条目"这个场景,断言渲染输出**不含**该条目内容。**这条最容易写成空转测试**——如果只断言"输出里没有那段敏感文本",在渲染压根没实现条目展示的情况下同样成立;必须同时有反向对照(非敏感条目/同 chat 场景必须正常渲染出来),并建议做一次变异验证(把敏感闸那行删掉,确认测试真的会挂)。延续 [issue #69](https://code.srcz.one/ProjectKuma/arise/issues/69)/[#70](https://code.srcz.one/ProjectKuma/arise/issues/70) 两次被抓到同型问题的教训:**断言"什么都没发生"时,通过原因往往不止一个**。 - **`/why_query` 记账与熔断(AC34)**:断言跑一次 `/why_query` 后 embedding 池累计确实增加;embedding 池预设为已熔断时,断言假 embedding 客户端的**调用计数为零**(不是断言"没有输出结果"——那在多种原因下都成立,同上)。 ## Out of Scope - **web/HTTP 管理面板**:ADR-0010 更新节已明确 `/why_query` 等只做聊天命令形态,不新增 web 交互形态。 - **GDPR 式跨表级联删除**:ADR-0010 已明确 YAGNI 拒绝("忘记我"仅清短期,长期靠自然遗忘曲线),本次不重新评估。 - **按任务难度自动路由模型的 router**:ADR-0020 已明确搁置("是一整套新的 Routing 层设计,超出范围,留待未来专门开一轮 grill")。`/setmodel` 只做运行时手动切换,不做自动路由。 - **模型自主选择自己用什么模型**:ADR-0020 已明确否决(LLM 不该直接支配成本/基础设施决策),本次不重新评估。 - **深挖任务委托的"灰度可一键关"缺口**([issue #61](https://code.srcz.one/ProjectKuma/arise/issues/61)):独立跟踪,不在本次范围。 - **`MediaKind` 的 `"file"` 第四态**:ADR-0028/CONTEXT.md 已决定但从未写进代码(见 [issue #66](https://code.srcz.one/ProjectKuma/arise/issues/66) Further Notes 留痕),与本 PRD 无关,不顺手夹带。 - **各项预算/保留策略的具体数字**:按 ADR-0011 既定原则("具体数字按 QQ 场景实测标定,禁用 Hermes/OpenClaw 数字")留实现期,本 PRD 只钉死机制形状。 ## Further Notes - 本 PRD 完成后,`docs/design.md`"上线路线"六个阶段将首次全部落地。届时值得做一次全文档核实(同深挖任务委托那轮的做法),确认路线图各阶段的承诺与实际实现逐条对得上——历史上已经出现过至少两处"文档说有、代码没有"的落差(深挖任务委托的"灰度可一键关"、`MediaKind.file`),全部走完之后正是系统性核对一遍的时机。 - **`/why` 的敏感闸这条约束是维护者在 PRD 评审时提出的,不是从 ADR-0010 原文推出来的**——ADR-0010 写"决策快照记录注入了哪些记忆条目"时,隐含假设是"给系统自己看/给写代码的人看",没有考虑过它会经由一个**管理员可见**的聊天命令出口暴露出去。这是"把一个内部记录接上一条新的对外出口"时容易整类漏掉的问题:原记录的可见性假设未必还成立。未来若再给决策快照加新出口(比如导出、web 面板——虽然后者已被明确排除),需要重新做一次同样的检查,不能假设"快照里已经有的字段就是可以随便给出去的"。 - 成本记账这块有一个隐含的契约扩展面:`LLMClient`/`EmbeddingClient` 增加用量字段会影响所有既有实现和测试替身。这个改动本身很机械,但触及面广,实现期建议先单独把契约扩展做掉、确认全仓测试仍绿,再在其上做记账/预算逻辑——而不是混在一个 commit 里,否则一旦出问题很难区分是契约适配漏了还是记账逻辑错了。 - ADR-0011"阈值三层分类"说的标定方法(保守默认→决策快照录制回放)在本次之前从未真正可用。本次交付后,此前所有 PRD 里"留实现期按真实数据标定"的数字第一次具备了被真正标定的条件——这可能是本次交付最有长期价值的副产物,但标定工作本身是持续运营行为,不属于本 PRD 的交付物。
Yushu closed this issue 2026-07-28 09:40:15 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ProjectKuma/arise#77
No description provided.