画布 Agent 一轮要跑很多步模型调用,此前只有"按任务类型的分钟级超时":某一步卡住只能等 整轮判死;更根本的是每一步请求根本不带输出上限,不写 max_tokens 时上游按剩余上下文放行, 思考模型可以把单步拖到几分钟,且偶发把整个输出预算花在推理上、正文与工具调用皆空。 - 新增运行时策略 task.agentStepTimeoutSeconds 与 task.agentStepMaxOutputTokens:秒级墙钟 与单步输出上限,都支持环境变量给部署级默认值、管理端热更新;旧配置 JSON 缺字段按默认值 回填,无需数据迁移。 - 单步墙钟到点中止这一步时不再把整轮判死:以可识别标记记为该步失败,运行期关掉上游思考 重试同一步一次(model_failure_recovered / step_timeout_retried),重试仍超时才以可读原因 (model_step_timeout)结束本轮。 - 空输出催办用尽后新增一级升级:关思考 + 放大输出预算重试同一步 (model_failure_recovered / empty_output_escalated),而不是直接判死。放大档在原值上翻倍 并以 131072 封顶,策略值为 0 时用 32768 兜底,让重试仍然有界。 - 输出上限真正下发到上游:textOptions.maxOutputTokens 成为任务信封字段,Agent 的两条请求 路径(内置协议与声明式协议)组装请求体时写入 max_tokens / max_output_tokens,与既有的 input.MaxOutputTokens 取较小值,避免策略上限超过模型声明的物理上限。 - task_worker:执行时限改用带画布 Agent 分支的 taskExecutionTimeout;执行时限到点本身不再 被当成"租约丢失",否则任务会停在 running 等租约过期后被反复重跑(实测一次调用被重跑成 三次上游请求)。续租仍用上游已合入的"不继承父 context 取消"的独立 context。
52 lines
2.2 KiB
Go
52 lines
2.2 KiB
Go
package app
|
||
|
||
import (
|
||
"strings"
|
||
|
||
"infinite-canvas/backend/internal/model"
|
||
"infinite-canvas/backend/internal/repository"
|
||
)
|
||
|
||
const cloudAgentMaxEmptyOutputNudges = 2
|
||
|
||
// cloudAgentMaxEmptyOutputEscalations 是"空输出升级重试"次数:催办没用时改为关思考 + 放大预算。
|
||
const cloudAgentMaxEmptyOutputEscalations = 1
|
||
|
||
// cloudAgentEmptyModelOutput 判断模型任务是不是「上游成功、但正文与工具调用都为空」。
|
||
// 开思考时偶发只流 reasoning、可见正文和工具调用都空,解析层会归为「没有返回内容」。
|
||
func cloudAgentEmptyModelOutput(task *model.Task) bool {
|
||
if task == nil || task.Status == model.TaskStatusSucceeded {
|
||
return false
|
||
}
|
||
return strings.Contains(task.Error, "没有返回内容")
|
||
}
|
||
|
||
func (s *Service) correctCloudAgentEmptyOutput(run *model.CloudAgentExecution, state *cloudAgentRuntime) error {
|
||
return s.repo.MutateCloudAgent(run.UserID, run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
|
||
state.EmptyOutputNudged++
|
||
state.Canonical.Messages = append(state.Canonical.Messages, cloudAgentRuntimeMessage(cloudAgentRuntimeContext{Kind: cloudAgentContextEmptyOutput}))
|
||
state.ActiveTaskID = ""
|
||
state.Calls = nil
|
||
state.CallIndex = 0
|
||
return cloudAgentSave(current, state)
|
||
})
|
||
}
|
||
|
||
// correctCloudAgentEmptyOutputEscalation 是空输出的兜底:催办无效时关掉上游思考并放大输出预算,
|
||
// 重新发同一步(不追加催办消息,避免模型误以为出现了新任务)。
|
||
func (s *Service) correctCloudAgentEmptyOutputEscalation(run *model.CloudAgentExecution, state *cloudAgentRuntime) error {
|
||
return s.repo.MutateCloudAgent(run.UserID, run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
|
||
state.EmptyOutputEscalated++
|
||
state.ForceThinkingOff = true
|
||
state.BoostStepOutputBudget = true
|
||
state.ActiveTaskID = ""
|
||
state.Calls = nil
|
||
state.CallIndex = 0
|
||
state.event(run.ID, "model_failure_recovered", map[string]any{
|
||
"reason": "empty_output_escalated",
|
||
"text": "上游连续返回空内容;已关闭思考并放大输出预算重试同一步",
|
||
})
|
||
return cloudAgentSave(current, state)
|
||
})
|
||
}
|