[PRD] arise 深挖任务委托(Deep Task Delegation)落地 #52
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem Statement
从终端用户角度:现在的 arise 只能在当轮
run_limit(默认 6 步推理工具调用)内直接给出回答。一旦用户提的问题需要"钻研"——比如"帮我分析总结一下我这千把条舞萌成绩打得怎么样"——这类任务原始数据量太大,不能直接倒进当轮 prompt(会撑爆上下文、破坏冻结快照的 prefix-cache),分析所需的步骤也太多、太多样,撑不满一个前台短循环。目前 arise 对这类请求要么勉强在run_limit内做浅层分析敷衍过去,要么干脆做不了。从 host 部署者角度:CONTEXT.md/design.md 已经把"深挖任务委托"作为既定架构决策写进了文档(ADR-0020),但仓库里没有任何代码实现,也没有它依赖的"无门控送回"机制(Callback)。这个缺口挡在"能不能让 arise 真正处理复杂请求"和"现在只能处理三言两语能说清的问题"之间。
从维护者角度:ADR-0020 明确要求这类后台任务"异步执行、带工具、隔离任务上下文、不直接对用户说话",如果不从设计上把这几条约束落实为可测的边界,后续任何子智能体相关的改动都可能不小心开一条绕过人设/情感态调节的发言通道,或者让后台任务偷偷占用前台的成本/并发预算。
Solution
实现 ADR-0020 定义的深挖任务委托机制:主模型识别到用户请求需要深挖时,调用新增核心工具
delegate_task登记一个异步后台任务、紧跟一次send_message应一声("我看看啊")、当轮结束——不占用前台run_limit/并发槽位。后台一个隔离上下文的子智能体(只带任务描述 + 工具港注册的 host 工具,无人设/记忆/关系/情感态/历史)自主完成多轮工具调用钻研数据,产出蒸馏文本结果。任务完成后,结果作为普通内容"无门控"重新进入一次带全套人设/记忆上下文的正常 Runtime Loop,由主模型自己组织语气、调send_message发出——子智能体全程不直接对用户说话。用户可用自然语言("算了不用查了")触发cancel_delegate_task,真中断底层任务。由于仓库里承接"任务完成后无门控重新进入会话"这个投递语义的 Callback 机制此前完全不存在(
pending_intent.py现有实现明确只覆盖 context 模式,time 模式/Callback 从未落地),本次顺带把 Callback 建成一个通用抽象(持久化、per-chat 多条待定、无门控直接送回),深挖任务完成是它的第一个真实触发源。register_callback(用户主动设置到点提醒)这个未来触发源本身不在本次范围内——只是让这次新建的存储/送回抽象足够通用,将来加它时只需要新增一个工具 + 一个到点扫描生产者,不需要改动本次建的地基。User Stories
私聊/群聊用户视角:请求深挖
Host 部署者/运维者视角:安全与可控
维护者/贡献者视角:可测性
ScriptedLLMClient先例)驱动,断言"给定一串工具调用/返回,最终产出预期的蒸馏文本",不需要真实网络请求。delegate_task/cancel_delegate_task这两个新工具在RuntimeLoop里的处理逻辑,跟现有note_pending_intent等工具处理器一样,可以直接喂RuntimeLoop.run()一段脚本化对话来测试,不需要真的启动后台任务等待完成。pending_intent现有的纯函数测试 + 存储集成测试两层先例。验收标准视角
run_limit。send_message/self_recall/edit等任何输出类工具——结构上就不可能绕开主循环直接发言。assemble_context()(安全基线 + persona + 冻结记忆快照 + 触发注入),不是一个阉割版上下文。cancel_delegate_task在任务已经完成、结果还没来得及送回这个窄时间窗口内被调用时,也能确实压下这次送回,不会出现"说了算了、结果还是发过来了"的情况。Implementation Decisions
新增核心工具(RuntimeLoop 层,前台)
delegate_task(description):core 原生工具(同note_pending_intent/search_memory先例,非工具港 host 工具),当轮调用即在进程内登记一个后台任务(chat_id/task_id → 运行中任务句柄),随即以asyncio方式启动后台执行,不阻塞当轮工具循环,也不计入本轮run_limit/MAX_SENDS_PER_TURN(本身不是 send)。模型通常紧跟一次send_message应声,当轮随后自然结束。cancel_delegate_task(task_id?):core 原生工具,模型在对话中识别到用户要求取消时调用。若对应任务仍在跑,真中断底层asyncio任务;若任务已完成但结果尚未送回(见下 Callback 窗口),一并撤销这条待送回记录。找不到对应任务/已经送达过的任务,视为无操作(fail-closed,不报错阻断对话)。delegate_task直接返回一个可读的"已有任务在跑"结果给模型,由模型组织语言告知用户,不新起第二个任务。任务登记(进程内存态,不持久化)
asyncio.Task+ task_id)的登记表是进程内存字典,同现有_pending_flushes/_pending_followup_checks先例,不落盘。进程崩溃/重启即丢失在途任务,不重放——与 ADR-0013 崩溃恢复语义一致,不是本次新开的例外。后台子智能体执行(新模块,隔离上下文)
reflection.py/persona_drift_guard.py的既有分层先例),职责是:① 组装隔离任务上下文(纯函数:只含任务描述 + 安全基线,不含 persona/画像/关系网/情感态/历史对话);② 驱动一个独立的多轮工具调用循环,使用ports.llm_client(主力模型——当前仓库还没有独立配置的 auxiliary 模型客户端,reflection.py现状也是复用同一个llm_client,故这里天然满足 ADR-0020"用主力模型"的要求,不需要新增模型路由);③ 循环终止条件是子智能体自己表示"分析完毕"(不再调用工具,给出最终文本),产出该文本作为蒸馏结果。get_tools("background")返回的 host 工具(RuntimeContext.background这个既有契约取值首次被真正接入调用点)。子智能体工具集里不包含 core 自己的任何输出类工具(send_message/self_recall/edit/delegate_task本身等)——结构性排除,不是运行时判断。arise_run_limit——量级不同,具体数字按 ADR-0011 既定原则推迟到真实数据标定),超过硬顶即强制结束循环,把已经拿到的内容作为蒸馏结果(不是直接判定失败——尽量拿现有信息交差,同 ADR-0028"不阻塞回复"的一贯风格)。Callback(通用送回抽象,新建)
"task_completion"一个取值,字段本身开放给未来扩展如"user_reminder")、created_at、fire_at(可空——本次唯一生产者"任务完成"不填这个字段,语义上是"立即到期";为未来 time 模式预留)。per-chat 支持多条待定记录(不是单槽位),同pending_intent的 per-scope 多记录先例。nonebot-plugin-apscheduler定时任务,同 Drive Tick/反思闭环既有先例)扫描待送回的 Callback 记录:fire_at为空或已到期即视为到期。到期记录按 chat_id 尝试送回;若该 chat 当前正被前台占用(复用 Drive Tick 已有的"忙时 defer"检查,同一个_pending_flushes/等价追踪),顺延到下一次扫描,不强行插队造成并发写冲突。assemble_context()五段拼装:安全基线→persona→冻结快照→触发注入→本次内容),无门控——不经过两级门控/沉默预算/自发目标的判定链,直接组装并请求模型基于这段内容组织语气回复。这不是一次新的输出出口,而是"履约"语义(同 ADR-0020 对 Callback 既有定性)。完成后清除对应 Callback 记录。register_callback(用户主动设置到点提醒的工具)本次不实现——留给未来专门 ticket,届时只需新增一个工具把fire_at填成用户指定时间、复用本次已建好的存储/扫描/送回管线,不需要改动这次的地基。成本归因(不做完整治理)
delegate(概念上对应 ADR-0020 定义的独立成本池),但本次不实现任何预算额度/熔断//cost可见性——完整六池成本治理(含另外五个现状同样零实现的池)是独立的运营与质量收尾工作,不在本次范围(见 Out of Scope)。Testing Decisions
好的测试只测外部可观察行为(工具调用产生的效果、送回时机、并发/取消的最终状态),不测内部实现细节(如具体用哪种 asyncio 原语)。
drive_tick.py::assemble_light_context的既有测试模式。ScriptedLLMClient(仓库既有的脚本化假 LLM 测试双胞胎,见test_reflection_cycle.py/test_persona_drift_guard.py先例)驱动一串"调用工具→拿到结果→再调用→…→给出最终文本"的固定脚本,断言最终蒸馏文本、断言过程中不出现任何输出类工具调用、断言超过静态硬顶时强制收尾。test_pending_intent_storage.py先例,跑在真实测试数据库上。pending_intent.py::is_expired的既有测试风格。delegate_task/cancel_delegate_task两个新 handler 直接喂RuntimeLoop.run()脚本化对话断言效果(任务登记表状态、send_message应声内容、重复请求时的提示、找不到任务时的 fail-closed 行为)——同现有_handle_note_pending_intent等工具处理器的测试模式。assemble_context()(而不是走run_light/事件驱动的裁剪版组装),且不经过_evaluate_unified_gate。cancel_delegate_task能让这条记录不再被送回。recover()),断言进程重启后不会尝试"续跑"一个内存态任务登记表里已经不存在的任务(因为这类记录本就不持久化,不需要专门的清理逻辑,只需确认没有任何代码假设它能在重启后存活)。Out of Scope
register_callback(用户主动设置到点提醒):Callback 的第二个触发源(time 模式),本次只建通用地基,不实现这个工具本身。/cost可见性、六池预算+熔断、系统自检信号层):ADR-0010 完整运营面,独立于本次范围,Phase 4 PRD(issue #33)已经把它排除过一次,本次维持同样的边界。Further Notes