mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 09:17:31 +00:00
fix(core): 裁剪收归基础设施,contextfull 只作为触发信号
## 问题
同一件事(上下文装不下)有三套实现、三份防护差异:
task.go 发 LLM 前 maybeHandleContextOverflow(有恢复预算)
task.go 上游报错 handleUpstreamContextFull(**直调内部,绕过预算**)
resident.go 轮首 checkContextFull(只对驻留子生效)
其中 handleUpstreamContextFull 是"另起炉灶":它自己又调一遍
handleContextOverflow,绕过 f.OverflowRecover 预算——而文档 §四.1 明确
「L4 遇 L4 不能抢占,反复触发只会排队堆积」。
## 改法
裁剪成为**基础设施**,contextfull 只是「判定已被外部短路」的一种入参:
enforceContextBudget(f, query, force)
force=false 本地水位:积累量 > ratio × 窗口
force=true 外部已确认超页(provider 返回 ErrContextFull)
之后的一切(恢复预算、裁剪、raise L4)完全相同。handleUpstreamContextFull
删除,只剩 isUpstreamContextFull 这个纯判定。
## 任务恢复入口补裁剪
中断任务跑完会把它的输入/输出提交进 a.context,恢复时重建出的前缀可能
**比被挂起时更大**。不在这里裁,这份更大的前缀会被原样发出去。
恢复用的是专用入口 enforceResumeContextBudget,两处语义与通用入口不同:
1. **query 取本帧最新有效上下文**(resumeQueryFrom 从 f.Msgs 尾部往回找),
不是 f.Input。被挂起的任务可能隔了很久,f.Input 早已与当前对话脱节;
拿旧 Input 算相关性,会裁掉"对该任务重要、但字面上不匹配旧输入"的事件。
2. **保护范围是本帧自己的现场**(len(f.Msgs)-f.PrefixLen),不是全局末尾
N 条。被挂起任务的现场可能是一整批工具结果,若落在保护范围外被归档,
恢复后的任务引用的内容已不在上下文里,任务自相矛盾。
为此新增 pruneForResume + contextTopKForProtected——不能复用 contextTopK,
它内部会把 topK 抬到 effectiveProtectedCount()+1(全局保护),两套下限
打架会让恢复场景「裁不动」(实测 protected=8、20 条上下文时 topK 被抬到 20)。
## 不变的部分
声明式裁剪(业务策略)语义原样保留:
· 中断/消息注入 —— pruneOnInput,判据 payload.context_policy / ChannelDef
· 工具内容注入 —— stepToolAfter,判据 ToolDef.ContextPolicy
两者走 memoryPass → pruneByQuery,与容量保护共用底层,但职责正交。
判据:任务恢复入口必须裁、恢复查询取本帧最新、保护本帧现场、
恢复预算对所有入口生效。变异验证:恢复不裁/保护退回全局/上游绕过预算,
三种坏法均被抓。
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -365,6 +365,17 @@ func (a *Agent) rebaseFramePrefix(f *TaskFrame) {
|
||||
|
||||
f.Msgs = append(prefix, tail...)
|
||||
f.PrefixLen = len(prefix)
|
||||
|
||||
// ★ 恢复也是**新的上下文装配点**:中断任务跑完已把它的输入/输出提交进
|
||||
// a.context(见上面注释),于是重建出的前缀可能比被挂起时更大。
|
||||
// 不在这里裁,这份"更大的前缀"会被原样发出去,下一轮再靠 stepLLM 的
|
||||
// 闸门补救——而那时已经付过一次超限请求的代价。
|
||||
//
|
||||
// 用的是**恢复专用**入口:相关性按本帧最新上下文算、保护范围是本帧自己的
|
||||
// 现场(f.Msgs[PrefixLen:]),而不是全局末尾 N 条。见 enforceResumeContextBudget。
|
||||
if handled, _ := a.enforceResumeContextBudget(f); handled && f.Terminal == terminalError {
|
||||
return // 预算耗尽:f.Err 已写明,调用方据此终止
|
||||
}
|
||||
}
|
||||
|
||||
// prepareInputTask 执行 processInput 的前半段(去重、通道解析、阶段、裁剪、
|
||||
@ -676,14 +687,24 @@ func (a *Agent) stepLLM(f *TaskFrame) stepOutcome {
|
||||
if llmErr != nil {
|
||||
// ★ 上下文超页(上游 ErrContextFull)必须在 provider fallback **之前**处理:
|
||||
// fallback 会换 provider 重发**同一个**超限请求,换谁都一样超。
|
||||
// 与本地预判(maybeHandleContextOverflow)走同一条裁剪路径。
|
||||
if a.handleUpstreamContextFull(llmErr) {
|
||||
// 已裁剪。**必须退回 StepPrepare 重建 f.Msgs**:
|
||||
// f.Msgs 只在 stepPrepare 里由 buildMessages 装配,stepLLM 不重建;
|
||||
// 而挂起帧里那份就是刚刚超限的那份,复用它等于原样再发一次。
|
||||
f.Turn++
|
||||
f.Step = StepPrepare
|
||||
return outcomeContinue
|
||||
//
|
||||
// 这里**不另立路径**:上游报错只是「判定已被短路」的一种(force=true),
|
||||
// 之后的恢复预算 / 裁剪 / raise L4 与本地预判完全相同,共用同一个函数。
|
||||
// 区别仅在于:本地预判在发请求**之前**判,这里在**收到错误之后**判。
|
||||
if isUpstreamContextFull(llmErr) {
|
||||
handled, _ := a.enforceContextBudget(f, "", true)
|
||||
if handled {
|
||||
if f.Terminal == terminalError {
|
||||
return outcomeFailed // 恢复预算耗尽,f.Err 已写明
|
||||
}
|
||||
// 已裁剪。**必须退回 StepPrepare 重建 f.Msgs**:
|
||||
// f.Msgs 只在 stepPrepare 里由 buildMessages 装配,stepLLM 不重建;
|
||||
// 而挂起帧里那份就是刚刚超限的那份,复用它等于原样再发一次。
|
||||
f.Turn++
|
||||
f.Step = StepPrepare
|
||||
return outcomeContinue
|
||||
}
|
||||
// 未处理(如轻量内核不裁剪):落到下面按普通错误处理。
|
||||
}
|
||||
if errors.Is(llmErr, context.Canceled) && a.ctx.Err() == nil {
|
||||
if f.OutputChannel == channelConsolidation {
|
||||
|
||||
Reference in New Issue
Block a user