Skip to content

Commit b73f686

Browse files
author
SolonCode
committed
添加LoopPromptBuilder类及测试,实现预算剩余比率驱动的引导词模式自动切换
1 parent 58d33d7 commit b73f686

5 files changed

Lines changed: 680 additions & 224 deletions

File tree

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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% ≤ 剩余 &lt; 30% → 精简 3 章节</li>
27+
* <li>剩余 &lt; 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>预算 &gt; 30%:完整 7 章节</li>
49+
* <li>预算 15%-30%:精简 3 章节</li>
50+
* <li>预算 &lt; 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

Comments
 (0)