|
| 1 | +/* |
| 2 | + * Copyright 2017-2026 noear.org and authors |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * https://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | +package org.noear.solon.codecli.command.builtin; |
| 17 | + |
| 18 | +/** |
| 19 | + * Goal 提示词构建器 — 从 LoopScheduler 解耦。 |
| 20 | + * |
| 21 | + * <p>职责:根据预算剩余比率自动切换完整/精简/极简三种引导词模式,以及预算耗尽收尾引导词。 |
| 22 | + * |
| 23 | + * <p>预算阈值: |
| 24 | + * <ul> |
| 25 | + * <li>剩余 ≥ 30% → 完整 7 章节引导</li> |
| 26 | + * <li>15% ≤ 剩余 < 30% → 精简 3 章节</li> |
| 27 | + * <li>剩余 < 15% → 极简单段落</li> |
| 28 | + * </ul> |
| 29 | + * |
| 30 | + * @author noear |
| 31 | + * @since 3.9.3 |
| 32 | + */ |
| 33 | +public class LoopPromptBuilder { |
| 34 | + |
| 35 | + private final int stagnationThreshold; |
| 36 | + |
| 37 | + public LoopPromptBuilder(int stagnationThreshold) { |
| 38 | + this.stagnationThreshold = stagnationThreshold; |
| 39 | + } |
| 40 | + |
| 41 | + // ==================== 主入口 ==================== |
| 42 | + |
| 43 | + /** |
| 44 | + * 构建完整的 effective prompt(goal 引导词注入) |
| 45 | + * |
| 46 | + * <p>根据预算剩余自动切换精简模式: |
| 47 | + * <ul> |
| 48 | + * <li>预算 > 30%:完整 7 章节</li> |
| 49 | + * <li>预算 15%-30%:精简 3 章节</li> |
| 50 | + * <li>预算 < 15%:极简单段落</li> |
| 51 | + * </ul> |
| 52 | + */ |
| 53 | + public String buildEffectivePrompt(LoopTask task) { |
| 54 | + String prompt = task.getPrompt(); |
| 55 | + |
| 56 | + if (!task.isGoalMode()) { |
| 57 | + return prompt; |
| 58 | + } |
| 59 | + |
| 60 | + GoalState gs = task.getGoalState(); |
| 61 | + int iter = task.getCurrentIteration(); |
| 62 | + boolean isFirstIter = iter == 0; |
| 63 | + |
| 64 | + String budgetInfo = buildBudgetInfo(gs); |
| 65 | + |
| 66 | + // 预算感知精简模式 |
| 67 | + double budgetRatio = budgetRatio(gs); |
| 68 | + |
| 69 | + if (budgetRatio < 0.15) { |
| 70 | + return buildMinimalPrompt(prompt, gs, budgetInfo); |
| 71 | + } else if (budgetRatio < 0.30) { |
| 72 | + return buildCompactPrompt(prompt, gs, task, budgetInfo, iter, isFirstIter); |
| 73 | + } |
| 74 | + |
| 75 | + // 完整模式(7 章节) |
| 76 | + StringBuilder sb = new StringBuilder(); |
| 77 | + sb.append("\n\n"); |
| 78 | + sb.append("--- 目标延续 (Goal Continuation) ---\n"); |
| 79 | + sb.append("你正在朝向以下目标工作: ").append(gs.getCondition()).append("\n"); |
| 80 | + sb.append("你的目标是完成此任务。这是持续性的工作 — 每一轮执行都是同一个目标的延续。\n"); |
| 81 | + sb.append("\n"); |
| 82 | + |
| 83 | + // Chapter 3: Work from evidence |
| 84 | + sb.append("--- 证据驱动 (Evidence-Based) ---\n"); |
| 85 | + sb.append("不要依赖记忆或假设来判断当前状态。在采取行动前,先检查实际的文件内容、\n"); |
| 86 | + sb.append("测试结果、构建输出等客观证据。你的判断必须基于最新的事实,而非上轮的记忆。\n"); |
| 87 | + sb.append("\n"); |
| 88 | + |
| 89 | + // Chapter 5: Fidelity |
| 90 | + sb.append("--- 忠于目标 (Goal Fidelity) ---\n"); |
| 91 | + sb.append("不要缩小目标范围或降低完成标准。目标中的每一项都必须完成。\n"); |
| 92 | + sb.append("不要留占位符、TODO 或 stub。如果某个部分很难,要投入精力解决,而非跳过。\n"); |
| 93 | + sb.append("\n"); |
| 94 | + |
| 95 | + // Chapter 6: Completion audit |
| 96 | + sb.append("--- 审计完成 (Audit Check) ---\n"); |
| 97 | + sb.append("在继续之前,请完成以下步骤:\n"); |
| 98 | + sb.append("1. 回顾:目标是什么?检查已有的进展。\n"); |
| 99 | + sb.append("2. 核查:针对目标中的每一项,通过运行测试、检查文件等客观手段验证其是否已完成。\n"); |
| 100 | + sb.append(" 不要仅凭推理 — 必须有权威证据(测试通过、构建成功、文件存在且内容正确)。\n"); |
| 101 | + sb.append("3. 如果你已完成所有项,说明你是如何实现每一项的,\n"); |
| 102 | + sb.append(" 然后在回复末尾输出 [GOAL_ACHIEVED] 并调用 goal_update(complete) 标记完成。\n"); |
| 103 | + sb.append("\n"); |
| 104 | + |
| 105 | + // Chapter 7: Blocked audit |
| 106 | + sb.append("--- 阻塞审计 (Blocked Audit) ---\n"); |
| 107 | + sb.append("如果你遇到阻碍(同一困境尝试了 3 次),调用 goal_update(blocked) 声明阻塞。\n"); |
| 108 | + sb.append("不要因为工作困难、进展慢或不确定就声明阻塞 — 仅当同一问题反复尝试仍无法解决时才使用。\n"); |
| 109 | + sb.append("resume 后阻塞计数重置为 0。\n"); |
| 110 | + sb.append("\n"); |
| 111 | + |
| 112 | + // 停滞质疑(运行时兜底,仅触发时注入) |
| 113 | + if (task.getStagnationCount() >= stagnationThreshold) { |
| 114 | + sb.append("--- 进展质疑 (Stagnation Check) ---\n"); |
| 115 | + sb.append("系统检测到最近 ").append(task.getStagnationCount()) |
| 116 | + .append(" 轮执行未产生实质性进展。\n"); |
| 117 | + sb.append("请认真评估:你是否在同一问题上反复尝试但无法推进?\n"); |
| 118 | + sb.append("如果是,请调用 goal_update(blocked) 声明阻塞。\n"); |
| 119 | + sb.append("如果不是,请在下一步采取明显不同的策略。\n"); |
| 120 | + sb.append("\n"); |
| 121 | + } |
| 122 | + |
| 123 | + // Chapter 2: Budget |
| 124 | + if (gs.isBudgetCritical()) { |
| 125 | + sb.append("[紧急] 你的 Token 预算即将耗尽。请专注于高效完成目标。\n"); |
| 126 | + } |
| 127 | + |
| 128 | + // Chapter 4: Progress visibility — 上一轮摘要 |
| 129 | + if (!isFirstIter && task.getLastResult() != null) { |
| 130 | + String lastSummary = truncateForPrompt(task.getLastResult(), 300); |
| 131 | + sb.append("\n--- 上一轮执行摘要(第 ").append(iter).append(" 轮)---\n"); |
| 132 | + sb.append(lastSummary).append("\n"); |
| 133 | + sb.append("请基于以上进展继续推进,避免重复已尝试过的方案。\n"); |
| 134 | + } |
| 135 | + |
| 136 | + sb.append(budgetInfo); |
| 137 | + |
| 138 | + return prompt + sb.toString(); |
| 139 | + } |
| 140 | + |
| 141 | + /** |
| 142 | + * 构建 budget_limit 引导词(对齐 Codex budget_limit.md) |
| 143 | + */ |
| 144 | + public String buildBudgetLimitPrompt(LoopTask task, GoalState gs) { |
| 145 | + StringBuilder sb = new StringBuilder(); |
| 146 | + sb.append("\n\n--- 预算耗尽 (Budget Limit) ---\n"); |
| 147 | + sb.append("你的目标 Token 预算已耗尽。\n\n"); |
| 148 | + sb.append("目标: ").append(gs.getCondition()).append("\n\n"); |
| 149 | + |
| 150 | + sb.append("预算:\n"); |
| 151 | + long elapsed = (System.currentTimeMillis() - gs.getStartEpochMs()) / 1000; |
| 152 | + sb.append("- 耗时: ").append(formatDuration(elapsed * 1000)).append("\n"); |
| 153 | + sb.append("- 已消耗: ").append(formatTokens(gs.getConsumedTokens())); |
| 154 | + if (gs.getMaxTokens() > 0) { |
| 155 | + sb.append(" / ").append(formatTokens(gs.getMaxTokens())).append(" tokens\n"); |
| 156 | + } else { |
| 157 | + sb.append(" tokens\n"); |
| 158 | + } |
| 159 | + sb.append("\n"); |
| 160 | + |
| 161 | + sb.append("系统已将此目标标记为 budget_limited,请勿开始新的实质性工作。\n"); |
| 162 | + sb.append("请在此轮回复中:\n"); |
| 163 | + sb.append("1. 总结已完成的工作和进展\n"); |
| 164 | + sb.append("2. 列出剩余未完成的工作\n"); |
| 165 | + sb.append("3. 给出明确的下一步建议\n\n"); |
| 166 | + sb.append("不要调用 goal_update 除非目标确实已完成。\n"); |
| 167 | + |
| 168 | + return sb.toString(); |
| 169 | + } |
| 170 | + |
| 171 | + // ==================== 内部构建方法 ==================== |
| 172 | + |
| 173 | + /** |
| 174 | + * 精简模式(预算 15%-30%):3 章节 |
| 175 | + */ |
| 176 | + private String buildCompactPrompt(String prompt, GoalState gs, LoopTask task, |
| 177 | + String budgetInfo, int iter, boolean isFirstIter) { |
| 178 | + StringBuilder sb = new StringBuilder(); |
| 179 | + sb.append("\n\n"); |
| 180 | + sb.append("--- 目标延续 (Goal Continuation) ---\n"); |
| 181 | + sb.append("目标: ").append(gs.getCondition()).append("\n"); |
| 182 | + sb.append("持续工作直至完成。完成后输出 [GOAL_ACHIEVED] 并调用 goal_update(complete)。\n"); |
| 183 | + sb.append("\n"); |
| 184 | + |
| 185 | + sb.append("--- 审计完成 (Audit Check) ---\n"); |
| 186 | + sb.append("逐条验证目标完成情况。必须有客观证据(测试通过/文件存在)。不要凭推理判定完成。\n"); |
| 187 | + sb.append("\n"); |
| 188 | + |
| 189 | + if (!isFirstIter && task.getLastResult() != null) { |
| 190 | + String lastSummary = truncateForPrompt(task.getLastResult(), 200); |
| 191 | + sb.append("上一轮(第 ").append(iter).append("轮): ").append(lastSummary).append("\n"); |
| 192 | + } |
| 193 | + |
| 194 | + sb.append(budgetInfo); |
| 195 | + return prompt + sb.toString(); |
| 196 | + } |
| 197 | + |
| 198 | + /** |
| 199 | + * 极简模式(预算 < 15%):单段落 |
| 200 | + */ |
| 201 | + private String buildMinimalPrompt(String prompt, GoalState gs, |
| 202 | + String budgetInfo) { |
| 203 | + StringBuilder sb = new StringBuilder(); |
| 204 | + sb.append("\n\n"); |
| 205 | + sb.append("目标: ").append(gs.getCondition()).append(" | "); |
| 206 | + sb.append(budgetInfo.trim()).append("\n"); |
| 207 | + sb.append("持续工作直至完成。完成后输出 [GOAL_ACHIEVED] 并调用 goal_update(complete)。3 轮无法推进则调用 goal_update(blocked)。\n"); |
| 208 | + return prompt + sb.toString(); |
| 209 | + } |
| 210 | + |
| 211 | + // ==================== 静态辅助方法 ==================== |
| 212 | + |
| 213 | + /** |
| 214 | + * 计算预算剩余比率 |
| 215 | + */ |
| 216 | + static double budgetRatio(GoalState gs) { |
| 217 | + return gs.getMaxTokens() > 0 |
| 218 | + ? (double) (gs.getMaxTokens() - gs.getConsumedTokens()) / gs.getMaxTokens() |
| 219 | + : 1.0; |
| 220 | + } |
| 221 | + |
| 222 | + static String buildBudgetInfo(GoalState gs) { |
| 223 | + StringBuilder sb = new StringBuilder(); |
| 224 | + |
| 225 | + if (gs.getMaxTokens() > 0) { |
| 226 | + long remainToken = gs.getMaxTokens() - gs.getConsumedTokens(); |
| 227 | + sb.append("\n已消耗 ").append(formatTokens(gs.getConsumedTokens())) |
| 228 | + .append(" / ").append(formatTokens(gs.getMaxTokens())) |
| 229 | + .append(" (").append(budgetPercent(gs.getConsumedTokens(), gs.getMaxTokens())).append("%)"); |
| 230 | + if (remainToken > 0 && gs.isBudgetCritical()) { |
| 231 | + sb.append(" (剩余: ").append(formatTokens(remainToken)).append(")"); |
| 232 | + } |
| 233 | + if (gs.isBudgetWarning()) { |
| 234 | + sb.append("\n[预算提示] 已使用 ").append(budgetPercent(gs.getConsumedTokens(), gs.getMaxTokens())) |
| 235 | + .append("%,请评估是否需要调整策略或申请扩容"); |
| 236 | + } |
| 237 | + } else if (gs.getConsumedTokens() > 0) { |
| 238 | + sb.append("\n已消耗 Token: ").append(formatTokens(gs.getConsumedTokens())); |
| 239 | + } |
| 240 | + |
| 241 | + if (gs.getStartEpochMs() > 0) { |
| 242 | + long elapsed = System.currentTimeMillis() - gs.getStartEpochMs(); |
| 243 | + if (elapsed > 1000) { |
| 244 | + sb.append("\n耗时: ").append(formatDuration(elapsed)); |
| 245 | + } |
| 246 | + } |
| 247 | + |
| 248 | + return sb.toString(); |
| 249 | + } |
| 250 | + |
| 251 | + static String budgetPercent(long value, long total) { |
| 252 | + if (total <= 0) return "0"; |
| 253 | + return String.valueOf((int) (value * 100 / total)); |
| 254 | + } |
| 255 | + |
| 256 | + static String formatTokens(long tokens) { |
| 257 | + if (tokens < 1000) return tokens + " tokens"; |
| 258 | + if (tokens < 1_000_000) return String.format("%.1fk", tokens / 1000.0); |
| 259 | + return String.format("%.1fM", tokens / 1_000_000.0); |
| 260 | + } |
| 261 | + |
| 262 | + static String formatDuration(long ms) { |
| 263 | + if (ms < 60_000) return (ms / 1000) + "s"; |
| 264 | + if (ms < 3_600_000) return (ms / 60_000) + "m " + ((ms % 60_000) / 1000) + "s"; |
| 265 | + return (ms / 3_600_000) + "h " + ((ms % 3_600_000) / 60_000) + "m"; |
| 266 | + } |
| 267 | + |
| 268 | + static String truncateForPrompt(String text, int maxLen) { |
| 269 | + if (text == null || text.isEmpty()) return ""; |
| 270 | + if (text.length() <= maxLen) return text; |
| 271 | + int half = maxLen / 2; |
| 272 | + return text.substring(0, half) + "\n...(省略)...\n" + text.substring(text.length() - half); |
| 273 | + } |
| 274 | +} |
0 commit comments